diff --git a/.coveragerc b/.coveragerc index 4ba57e0f750..3163b5f723c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,6 +20,9 @@ omit = homeassistant/components/android_ip_webcam.py homeassistant/components/*/android_ip_webcam.py + homeassistant/components/arlo.py + homeassistant/components/*/arlo.py + homeassistant/components/axis.py homeassistant/components/*/axis.py @@ -89,6 +92,9 @@ omit = homeassistant/components/qwikswitch.py homeassistant/components/*/qwikswitch.py + homeassistant/components/rachio.py + homeassistant/components/*/rachio.py + homeassistant/components/raspihats.py homeassistant/components/*/raspihats.py diff --git a/Dockerfile b/Dockerfile index 54f993b01a9..8c4cd0f5440 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,6 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_TELLSTICK no #ENV INSTALL_OPENALPR no #ENV INSTALL_FFMPEG no -#ENV INSTALL_OPENZWAVE no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP_CLIENT no diff --git a/docs/source/_templates/links.html b/docs/source/_templates/links.html index 57a2e09f99e..272809d1920 100644 --- a/docs/source/_templates/links.html +++ b/docs/source/_templates/links.html @@ -1,8 +1,6 @@ -
diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 0b07e5aa6f6..219d413db12 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -10,6 +10,7 @@ import threading from typing import Optional, List +from homeassistant import monkey_patch from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, @@ -17,7 +18,6 @@ from homeassistant.const import ( REQUIRED_PYTHON_VER_WIN, RESTART_EXIT_CODE, ) -from homeassistant.util.async import run_callback_threadsafe def attempt_use_uvloop(): @@ -310,6 +310,9 @@ def setup_and_run_hass(config_dir: str, return None if args.open_ui: + # 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: @@ -371,6 +374,13 @@ def main() -> int: """Start Home Assistant.""" validate_python() + if os.environ.get('HASS_MONKEYPATCH_ASYNCIO') == '1': + if sys.version_info[:3] >= (3, 6): + monkey_patch.disable_c_asyncio() + monkey_patch.patch_weakref_tasks() + elif sys.version_info[:3] < (3, 5, 3): + monkey_patch.patch_weakref_tasks() + attempt_use_uvloop() if sys.version_info[:3] < (3, 5, 3): diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index eeda1db51fc..5f64fd447a6 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -83,8 +83,7 @@ def async_from_config_dict(config: Dict[str, Any], conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) return None - yield from hass.loop.run_in_executor( - None, conf_util.process_ha_config_upgrade, hass) + yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass) if enable_log: async_enable_logging(hass, verbose, log_rotate_days) @@ -95,7 +94,7 @@ def async_from_config_dict(config: Dict[str, Any], 'This may cause issues.') if not loader.PREPARED: - yield from hass.loop.run_in_executor(None, loader.prepare, hass) + yield from hass.async_add_job(loader.prepare, hass) # Merge packages conf_util.merge_packages_config( @@ -184,14 +183,13 @@ def async_from_config_file(config_path: str, # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - yield from hass.loop.run_in_executor( - None, mount_local_lib_path, config_dir) + yield from hass.async_add_job(mount_local_lib_path, config_dir) async_enable_logging(hass, verbose, log_rotate_days) try: - config_dict = yield from hass.loop.run_in_executor( - None, conf_util.load_yaml_config_file, config_path) + config_dict = yield from hass.async_add_job( + conf_util.load_yaml_config_file, config_path) except HomeAssistantError as err: _LOGGER.error('Error loading %s: %s', config_path, err) return None diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index a13abfae8f9..80c5e0ad1cc 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -123,8 +123,8 @@ def async_setup(hass, config): if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, os.path.join( + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( os.path.dirname(__file__), 'services.yaml')) for service in SERVICE_TO_METHOD: @@ -158,8 +158,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.alarm_disarm, code) + return self.hass.async_add_job(self.alarm_disarm, code) def alarm_arm_home(self, code=None): """Send arm home command.""" @@ -170,8 +169,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.alarm_arm_home, code) + return self.hass.async_add_job(self.alarm_arm_home, code) def alarm_arm_away(self, code=None): """Send arm away command.""" @@ -182,8 +180,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.alarm_arm_away, code) + return self.hass.async_add_job(self.alarm_arm_away, code) def alarm_trigger(self, code=None): """Send alarm trigger command.""" @@ -194,8 +191,7 @@ class AlarmControlPanel(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.alarm_trigger, code) + return self.hass.async_add_job(self.alarm_trigger, code) @property def state_attributes(self): diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index 167b7909fe6..df815424ee9 100755 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -117,7 +117,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): def alarm_arm_home(self, code=None): """Send arm home command.""" - self._alarm.arm('home') + self._alarm.arm('stay') def alarm_arm_away(self, code=None): """Send arm away command.""" diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index 34919a9db79..6029816ba76 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -70,8 +70,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device.async_alarm_keypress(keypress) # Register Envisalink specific services - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, os.path.join( + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( os.path.dirname(__file__), 'services.yaml')) hass.services.async_register( diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 24c14e7c9a8..09db0f84346 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -128,8 +128,8 @@ def async_setup(hass, config): all_alerts[entity.entity_id] = entity # Read descriptions - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, os.path.join( + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( os.path.dirname(__file__), 'services.yaml')) descriptions = descriptions.get(DOMAIN, {}) diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py index 72db3e06dee..b2423d44623 100644 --- a/homeassistant/components/apcupsd.py +++ b/homeassistant/components/apcupsd.py @@ -13,7 +13,7 @@ from homeassistant.const import (CONF_HOST, CONF_PORT) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['apcaccess==0.0.4'] +REQUIREMENTS = ['apcaccess==0.0.10'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 8beb737ae89..b722fc6ebb4 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -83,7 +83,7 @@ class APIEventStream(HomeAssistantView): stop_obj = object() to_write = asyncio.Queue(loop=hass.loop) - restrict = request.GET.get('restrict') + restrict = request.query.get('restrict') if restrict: restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py new file mode 100644 index 00000000000..feb77209237 --- /dev/null +++ b/homeassistant/components/arlo.py @@ -0,0 +1,60 @@ +""" +This component provides basic support for Netgear Arlo IP cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/arlo/ +""" +import logging +import voluptuous as vol +from homeassistant.helpers import config_validation as cv + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +import homeassistant.loader as loader + +from requests.exceptions import HTTPError, ConnectTimeout + +REQUIREMENTS = ['pyarlo==0.0.4'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ATTRIBUTION = 'Data provided by arlo.netgear.com' + +DOMAIN = 'arlo' + +DEFAULT_BRAND = 'Netgear Arlo' + +NOTIFICATION_ID = 'arlo_notification' +NOTIFICATION_TITLE = 'Arlo Camera Setup' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up an Arlo component.""" + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + + persistent_notification = loader.get_component('persistent_notification') + try: + from pyarlo import PyArlo + + arlo = PyArlo(username, password, preload=False) + if not arlo.is_connected: + return False + hass.data['arlo'] = arlo + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex)) + persistent_notification.create( + hass, 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + return True diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 9227222d479..a99113b6f6f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -158,8 +158,8 @@ def async_setup(hass, config): yield from _async_process_config(hass, config, component) - descriptions = yield from hass.loop.run_in_executor( - None, conf_util.load_yaml_config_file, os.path.join( + descriptions = yield from hass.async_add_job( + conf_util.load_yaml_config_file, os.path.join( os.path.dirname(__file__), 'services.yaml') ) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 90a8f2adce4..fbd1570a1e0 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -12,6 +12,7 @@ import homeassistant.util.dt as dt_util from homeassistant.const import MATCH_ALL, CONF_PLATFORM from homeassistant.helpers.event import ( async_track_state_change, async_track_point_in_utc_time) +from homeassistant.helpers.deprecation import get_deprecated import homeassistant.helpers.config_validation as cv CONF_ENTITY_ID = 'entity_id' @@ -40,10 +41,11 @@ def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) - to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL + to_state = get_deprecated(config, CONF_TO, CONF_STATE, MATCH_ALL) time_delta = config.get(CONF_FOR) async_remove_state_for_cancel = None async_remove_state_for_listener = None + match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL) @callback def clear_listener(): @@ -75,13 +77,13 @@ def async_trigger(hass, config, action): } }) - if time_delta is None: - call_action() + # Ignore changes to state attributes if from/to is in use + if (not match_all and from_s is not None and to_s is not None and + from_s.last_changed == to_s.last_changed): return - # If only state attributes changed, ignore this event - if (from_s is not None and to_s is not None and - from_s.last_changed == to_s.last_changed): + if time_delta is None: + call_action() return @callback diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 1ca714026a2..8ba082e3331 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import CONF_AFTER, CONF_PLATFORM +from homeassistant.const import CONF_AT, CONF_PLATFORM, CONF_AFTER from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change @@ -22,20 +22,26 @@ _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = vol.All(vol.Schema({ vol.Required(CONF_PLATFORM): 'time', + CONF_AT: cv.time, CONF_AFTER: cv.time, CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)), CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)), CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)), }), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, - CONF_SECONDS, CONF_AFTER)) + CONF_SECONDS, CONF_AT, CONF_AFTER)) @asyncio.coroutine def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" - if CONF_AFTER in config: - after = config.get(CONF_AFTER) - hours, minutes, seconds = after.hour, after.minute, after.second + if CONF_AT in config: + at_time = config.get(CONF_AT) + hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second + elif CONF_AFTER in config: + _LOGGER.warning("'after' is deprecated for the time trigger. Please " + "rename 'after' to 'at' in your configuration file.") + at_time = config.get(CONF_AFTER) + hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second else: hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) diff --git a/homeassistant/components/binary_sensor/mystrom.py b/homeassistant/components/binary_sensor/mystrom.py index c551a8c4efe..08ab1f4a8b7 100644 --- a/homeassistant/components/binary_sensor/mystrom.py +++ b/homeassistant/components/binary_sensor/mystrom.py @@ -38,7 +38,7 @@ class MyStromView(HomeAssistantView): @asyncio.coroutine def get(self, request): """The GET request received from a myStrom button.""" - res = yield from self._handle(request.app['hass'], request.GET) + res = yield from self._handle(request.app['hass'], request.query) return res @asyncio.coroutine diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py index 9f6ad70b58f..7823f03c85e 100755 --- a/homeassistant/components/calendar/demo.py +++ b/homeassistant/components/calendar/demo.py @@ -1,82 +1,82 @@ -""" -Demo platform that has two fake binary sensors. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" -import homeassistant.util.dt as dt_util -from homeassistant.components.calendar import CalendarEventDevice -from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Demo Calendar platform.""" - calendar_data_future = DemoGoogleCalendarDataFuture() - calendar_data_current = DemoGoogleCalendarDataCurrent() - add_devices([ - DemoGoogleCalendar(hass, calendar_data_future, { - CONF_NAME: 'Future Event', - CONF_DEVICE_ID: 'future_event', - }), - - DemoGoogleCalendar(hass, calendar_data_current, { - CONF_NAME: 'Current Event', - CONF_DEVICE_ID: 'current_event', - }), - ]) - - -class DemoGoogleCalendarData(object): - """Representation of a Demo Calendar element.""" - - # pylint: disable=no-self-use - def update(self): - """Return true so entity knows we have new data.""" - return True - - -class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData): - """Representation of a Demo Calendar for a future event.""" - - def __init__(self): - """Set the event to a future event.""" - one_hour_from_now = dt_util.now() \ - + dt_util.dt.timedelta(minutes=30) - self.event = { - 'start': { - 'dateTime': one_hour_from_now.isoformat() - }, - 'end': { - 'dateTime': (one_hour_from_now + dt_util.dt. - timedelta(minutes=60)).isoformat() - }, - 'summary': 'Future Event', - } - - -class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData): - """Representation of a Demo Calendar for a current event.""" - - def __init__(self): - """Set the event data.""" - middle_of_event = dt_util.now() \ - - dt_util.dt.timedelta(minutes=30) - self.event = { - 'start': { - 'dateTime': middle_of_event.isoformat() - }, - 'end': { - 'dateTime': (middle_of_event + dt_util.dt. - timedelta(minutes=60)).isoformat() - }, - 'summary': 'Current Event', - } - - -class DemoGoogleCalendar(CalendarEventDevice): - """Representation of a Demo Calendar element.""" - - def __init__(self, hass, calendar_data, data): - """Initialize Google Calendar but without the API calls.""" - self.data = calendar_data - super().__init__(hass, data) +""" +Demo platform that has two fake binary sensors. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +import homeassistant.util.dt as dt_util +from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Demo Calendar platform.""" + calendar_data_future = DemoGoogleCalendarDataFuture() + calendar_data_current = DemoGoogleCalendarDataCurrent() + add_devices([ + DemoGoogleCalendar(hass, calendar_data_future, { + CONF_NAME: 'Future Event', + CONF_DEVICE_ID: 'future_event', + }), + + DemoGoogleCalendar(hass, calendar_data_current, { + CONF_NAME: 'Current Event', + CONF_DEVICE_ID: 'current_event', + }), + ]) + + +class DemoGoogleCalendarData(object): + """Representation of a Demo Calendar element.""" + + # pylint: disable=no-self-use + def update(self): + """Return true so entity knows we have new data.""" + return True + + +class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData): + """Representation of a Demo Calendar for a future event.""" + + def __init__(self): + """Set the event to a future event.""" + one_hour_from_now = dt_util.now() \ + + dt_util.dt.timedelta(minutes=30) + self.event = { + 'start': { + 'dateTime': one_hour_from_now.isoformat() + }, + 'end': { + 'dateTime': (one_hour_from_now + dt_util.dt. + timedelta(minutes=60)).isoformat() + }, + 'summary': 'Future Event', + } + + +class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData): + """Representation of a Demo Calendar for a current event.""" + + def __init__(self): + """Set the event data.""" + middle_of_event = dt_util.now() \ + - dt_util.dt.timedelta(minutes=30) + self.event = { + 'start': { + 'dateTime': middle_of_event.isoformat() + }, + 'end': { + 'dateTime': (middle_of_event + dt_util.dt. + timedelta(minutes=60)).isoformat() + }, + 'summary': 'Current Event', + } + + +class DemoGoogleCalendar(CalendarEventDevice): + """Representation of a Demo Calendar element.""" + + def __init__(self, hass, calendar_data, data): + """Initialize Google Calendar but without the API calls.""" + self.data = calendar_data + super().__init__(hass, data) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 26c2c251afb..362202d1bde 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -1,78 +1,78 @@ -""" -Support for Google Calendar Search binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.google_calendar/ -""" -# pylint: disable=import-error -import logging -from datetime import timedelta - -from homeassistant.components.calendar import CalendarEventDevice -from homeassistant.components.google import ( - CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE, - GoogleCalendarService) -from homeassistant.util import Throttle, dt - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_GOOGLE_SEARCH_PARAMS = { - 'orderBy': 'startTime', - 'maxResults': 1, - 'singleEvents': True, -} - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) - - -def setup_platform(hass, config, add_devices, disc_info=None): - """Set up the calendar platform for event devices.""" - if disc_info is None: - return - - if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]): - return - - calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) - add_devices([GoogleCalendarEventDevice(hass, calendar_service, - disc_info[CONF_CAL_ID], data) - for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]]) - - -# pylint: disable=too-many-instance-attributes -class GoogleCalendarEventDevice(CalendarEventDevice): - """A calendar event device.""" - - def __init__(self, hass, calendar_service, calendar, data): - """Create the Calendar event device.""" - self.data = GoogleCalendarData(calendar_service, calendar, - data.get('search', None)) - super().__init__(hass, data) - - -class GoogleCalendarData(object): - """Class to utilize calendar service object to get next event.""" - - def __init__(self, calendar_service, calendar_id, search=None): - """Set up how we are going to search the google calendar.""" - self.calendar_service = calendar_service - self.calendar_id = calendar_id - self.search = search - self.event = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data.""" - service = self.calendar_service.get() - params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) - params['timeMin'] = dt.now().isoformat('T') - params['calendarId'] = self.calendar_id - if self.search: - params['q'] = self.search - - events = service.events() # pylint: disable=no-member - result = events.list(**params).execute() - - items = result.get('items', []) - self.event = items[0] if len(items) == 1 else None - return True +""" +Support for Google Calendar Search binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.google_calendar/ +""" +# pylint: disable=import-error +import logging +from datetime import timedelta + +from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.components.google import ( + CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE, + GoogleCalendarService) +from homeassistant.util import Throttle, dt + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_GOOGLE_SEARCH_PARAMS = { + 'orderBy': 'startTime', + 'maxResults': 1, + 'singleEvents': True, +} + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) + + +def setup_platform(hass, config, add_devices, disc_info=None): + """Set up the calendar platform for event devices.""" + if disc_info is None: + return + + if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]): + return + + calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) + add_devices([GoogleCalendarEventDevice(hass, calendar_service, + disc_info[CONF_CAL_ID], data) + for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]]) + + +# pylint: disable=too-many-instance-attributes +class GoogleCalendarEventDevice(CalendarEventDevice): + """A calendar event device.""" + + def __init__(self, hass, calendar_service, calendar, data): + """Create the Calendar event device.""" + self.data = GoogleCalendarData(calendar_service, calendar, + data.get('search', None)) + super().__init__(hass, data) + + +class GoogleCalendarData(object): + """Class to utilize calendar service object to get next event.""" + + def __init__(self, calendar_service, calendar_id, search=None): + """Set up how we are going to search the google calendar.""" + self.calendar_service = calendar_service + self.calendar_id = calendar_id + self.search = search + self.event = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data.""" + service = self.calendar_service.get() + params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) + params['timeMin'] = dt.now().isoformat('T') + params['calendarId'] = self.calendar_id + if self.search: + params['q'] = self.search + + events = service.events() # pylint: disable=no-member + result = events.list(**params).execute() + + items = result.get('items', []) + self.event = items[0] if len(items) == 1 else None + return True diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 79f0757d006..7c21b99ddda 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -138,7 +138,7 @@ class Camera(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor(None, self.camera_image) + return self.hass.async_add_job(self.camera_image) @asyncio.coroutine def handle_async_mjpeg_stream(self, request): @@ -241,7 +241,7 @@ class CameraView(HomeAssistantView): return web.Response(status=status) authenticated = (request[KEY_AUTHENTICATED] or - request.GET.get('token') in camera.access_tokens) + request.query.get('token') in camera.access_tokens) if not authenticated: return web.Response(status=401) diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py new file mode 100644 index 00000000000..16688370b07 --- /dev/null +++ b/homeassistant/components/camera/arlo.py @@ -0,0 +1,92 @@ +""" +This component provides basic support for Netgear Arlo IP cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.arlo/ +""" +import asyncio +import logging +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv +from homeassistant.components.arlo import DEFAULT_BRAND + +from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.helpers.aiohttp_client import ( + async_aiohttp_proxy_stream) + +DEPENDENCIES = ['arlo', 'ffmpeg'] + +_LOGGER = logging.getLogger(__name__) + +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_FFMPEG_ARGUMENTS): + cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up an Arlo IP Camera.""" + arlo = hass.data.get('arlo') + if not arlo: + return False + + cameras = [] + for camera in arlo.cameras: + cameras.append(ArloCam(hass, camera, config)) + + async_add_devices(cameras, True) + return True + + +class ArloCam(Camera): + """An implementation of a Netgear Arlo IP camera.""" + + def __init__(self, hass, camera, device_info): + """Initialize an Arlo camera.""" + super().__init__() + + self._camera = camera + self._name = self._camera.name + self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) + + def camera_image(self): + """Return a still image reponse from the camera.""" + return self._camera.last_image + + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from the camera.""" + from haffmpeg import CameraMjpeg + video = self._camera.last_video + if not video: + return + + stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + yield from stream.open_camera( + video.video_url, extra_cmd=self._ffmpeg_arguments) + + yield from async_aiohttp_proxy_stream( + self.hass, request, stream, + 'multipart/x-mixed-replace;boundary=ffserver') + yield from stream.close() + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def model(self): + """Camera model.""" + return self._camera.model_id + + @property + def brand(self): + """Camera brand.""" + return DEFAULT_BRAND diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 83164c55230..8a9854ab97e 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -103,8 +103,8 @@ class GenericCamera(Camera): _LOGGER.error("Error getting camera image: %s", error) return self._last_image - self._last_image = yield from self.hass.loop.run_in_executor( - None, fetch) + self._last_image = yield from self.hass.async_add_job( + fetch) # async else: try: diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 1e9859fe7c2..6168eb81939 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -88,8 +88,8 @@ class MjpegCamera(Camera): # DigestAuth is not supported if self._authentication == HTTP_DIGEST_AUTHENTICATION or \ self._still_image_url is None: - image = yield from self.hass.loop.run_in_executor( - None, self.camera_image) + image = yield from self.hass.async_add_job( + self.camera_image) return image websession = async_get_clientsession(self.hass) diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index de61535b336..90dfa58d8c5 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -1,250 +1,250 @@ -""" -Support for Synology Surveillance Station Cameras. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/camera.synology/ -""" -import asyncio -import logging - -import voluptuous as vol - -import aiohttp -import async_timeout - -from homeassistant.const import ( - CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT) -from homeassistant.components.camera import ( - Camera, PLATFORM_SCHEMA) -from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_create_clientsession, - async_aiohttp_proxy_web) -import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Synology Camera' -DEFAULT_STREAM_ID = '0' -DEFAULT_TIMEOUT = 5 -CONF_CAMERA_NAME = 'camera_name' -CONF_STREAM_ID = 'stream_id' - -QUERY_CGI = 'query.cgi' -QUERY_API = 'SYNO.API.Info' -AUTH_API = 'SYNO.API.Auth' -CAMERA_API = 'SYNO.SurveillanceStation.Camera' -STREAMING_API = 'SYNO.SurveillanceStation.VideoStream' -SESSION_ID = '0' - -WEBAPI_PATH = '/webapi/' -AUTH_PATH = 'auth.cgi' -CAMERA_PATH = 'camera.cgi' -STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi' -CONTENT_TYPE_HEADER = 'Content-Type' - -SYNO_API_URL = '{0}{1}{2}' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_URL): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, -}) - - -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up a Synology IP Camera.""" - verify_ssl = config.get(CONF_VERIFY_SSL) - timeout = config.get(CONF_TIMEOUT) - websession_init = async_get_clientsession(hass, verify_ssl) - - # Determine API to use for authentication - syno_api_url = SYNO_API_URL.format( - config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI) - - query_payload = { - 'api': QUERY_API, - 'method': 'Query', - 'version': '1', - 'query': 'SYNO.' - } - try: - with async_timeout.timeout(timeout, loop=hass.loop): - query_req = yield from websession_init.get( - syno_api_url, - params=query_payload - ) - - # Skip content type check because Synology doesn't return JSON with - # right content type - query_resp = yield from query_req.json(content_type=None) - auth_path = query_resp['data'][AUTH_API]['path'] - camera_api = query_resp['data'][CAMERA_API]['path'] - camera_path = query_resp['data'][CAMERA_API]['path'] - streaming_path = query_resp['data'][STREAMING_API]['path'] - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Error on %s", syno_api_url) - return False - - # Authticate to NAS to get a session id - syno_auth_url = SYNO_API_URL.format( - config.get(CONF_URL), WEBAPI_PATH, auth_path) - - session_id = yield from get_session_id( - hass, - websession_init, - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - syno_auth_url, - timeout - ) - - # init websession - websession = async_create_clientsession( - hass, verify_ssl, cookies={'id': session_id}) - - # Use SessionID to get cameras in system - syno_camera_url = SYNO_API_URL.format( - config.get(CONF_URL), WEBAPI_PATH, camera_api) - - camera_payload = { - 'api': CAMERA_API, - 'method': 'List', - 'version': '1' - } - try: - with async_timeout.timeout(timeout, loop=hass.loop): - camera_req = yield from websession.get( - syno_camera_url, - params=camera_payload - ) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Error on %s", syno_camera_url) - return False - - camera_resp = yield from camera_req.json(content_type=None) - cameras = camera_resp['data']['cameras'] - - # add cameras - devices = [] - for camera in cameras: - if not config.get(CONF_WHITELIST): - camera_id = camera['id'] - snapshot_path = camera['snapshot_path'] - - device = SynologyCamera( - hass, websession, config, camera_id, camera['name'], - snapshot_path, streaming_path, camera_path, auth_path, timeout - ) - devices.append(device) - - async_add_devices(devices) - - -@asyncio.coroutine -def get_session_id(hass, websession, username, password, login_url, timeout): - """Get a session id.""" - auth_payload = { - 'api': AUTH_API, - 'method': 'Login', - 'version': '2', - 'account': username, - 'passwd': password, - 'session': 'SurveillanceStation', - 'format': 'sid' - } - try: - with async_timeout.timeout(timeout, loop=hass.loop): - auth_req = yield from websession.get( - login_url, - params=auth_payload - ) - auth_resp = yield from auth_req.json(content_type=None) - return auth_resp['data']['sid'] - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Error on %s", login_url) - return False - - -class SynologyCamera(Camera): - """An implementation of a Synology NAS based IP camera.""" - - def __init__(self, hass, websession, config, camera_id, - camera_name, snapshot_path, streaming_path, camera_path, - auth_path, timeout): - """Initialize a Synology Surveillance Station camera.""" - super().__init__() - self.hass = hass - self._websession = websession - self._name = camera_name - self._synology_url = config.get(CONF_URL) - self._camera_name = config.get(CONF_CAMERA_NAME) - self._stream_id = config.get(CONF_STREAM_ID) - self._camera_id = camera_id - self._snapshot_path = snapshot_path - self._streaming_path = streaming_path - self._camera_path = camera_path - self._auth_path = auth_path - self._timeout = timeout - - def camera_image(self): - """Return bytes of camera image.""" - return run_coroutine_threadsafe( - self.async_camera_image(), self.hass.loop).result() - - @asyncio.coroutine - def async_camera_image(self): - """Return a still image response from the camera.""" - image_url = SYNO_API_URL.format( - self._synology_url, WEBAPI_PATH, self._camera_path) - - image_payload = { - 'api': CAMERA_API, - 'method': 'GetSnapshot', - 'version': '1', - 'cameraId': self._camera_id - } - try: - with async_timeout.timeout(self._timeout, loop=self.hass.loop): - response = yield from self._websession.get( - image_url, - params=image_payload - ) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Error fetching %s", image_url) - return None - - image = yield from response.read() - - return image - - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): - """Return a MJPEG stream image response directly from the camera.""" - streaming_url = SYNO_API_URL.format( - self._synology_url, WEBAPI_PATH, self._streaming_path) - - streaming_payload = { - 'api': STREAMING_API, - 'method': 'Stream', - 'version': '1', - 'cameraId': self._camera_id, - 'format': 'mjpeg' - } - stream_coro = self._websession.get( - streaming_url, params=streaming_payload) - - yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) - - @property - def name(self): - """Return the name of this device.""" - return self._name +""" +Support for Synology Surveillance Station Cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.synology/ +""" +import asyncio +import logging + +import voluptuous as vol + +import aiohttp +import async_timeout + +from homeassistant.const import ( + CONF_NAME, CONF_USERNAME, CONF_PASSWORD, + CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT) +from homeassistant.components.camera import ( + Camera, PLATFORM_SCHEMA) +from homeassistant.helpers.aiohttp_client import ( + async_get_clientsession, async_create_clientsession, + async_aiohttp_proxy_web) +import homeassistant.helpers.config_validation as cv +from homeassistant.util.async import run_coroutine_threadsafe + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Synology Camera' +DEFAULT_STREAM_ID = '0' +DEFAULT_TIMEOUT = 5 +CONF_CAMERA_NAME = 'camera_name' +CONF_STREAM_ID = 'stream_id' + +QUERY_CGI = 'query.cgi' +QUERY_API = 'SYNO.API.Info' +AUTH_API = 'SYNO.API.Auth' +CAMERA_API = 'SYNO.SurveillanceStation.Camera' +STREAMING_API = 'SYNO.SurveillanceStation.VideoStream' +SESSION_ID = '0' + +WEBAPI_PATH = '/webapi/' +AUTH_PATH = 'auth.cgi' +CAMERA_PATH = 'camera.cgi' +STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi' +CONTENT_TYPE_HEADER = 'Content-Type' + +SYNO_API_URL = '{0}{1}{2}' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up a Synology IP Camera.""" + verify_ssl = config.get(CONF_VERIFY_SSL) + timeout = config.get(CONF_TIMEOUT) + websession_init = async_get_clientsession(hass, verify_ssl) + + # Determine API to use for authentication + syno_api_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI) + + query_payload = { + 'api': QUERY_API, + 'method': 'Query', + 'version': '1', + 'query': 'SYNO.' + } + try: + with async_timeout.timeout(timeout, loop=hass.loop): + query_req = yield from websession_init.get( + syno_api_url, + params=query_payload + ) + + # Skip content type check because Synology doesn't return JSON with + # right content type + query_resp = yield from query_req.json(content_type=None) + auth_path = query_resp['data'][AUTH_API]['path'] + camera_api = query_resp['data'][CAMERA_API]['path'] + camera_path = query_resp['data'][CAMERA_API]['path'] + streaming_path = query_resp['data'][STREAMING_API]['path'] + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.exception("Error on %s", syno_api_url) + return False + + # Authticate to NAS to get a session id + syno_auth_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, auth_path) + + session_id = yield from get_session_id( + hass, + websession_init, + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + syno_auth_url, + timeout + ) + + # init websession + websession = async_create_clientsession( + hass, verify_ssl, cookies={'id': session_id}) + + # Use SessionID to get cameras in system + syno_camera_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, camera_api) + + camera_payload = { + 'api': CAMERA_API, + 'method': 'List', + 'version': '1' + } + try: + with async_timeout.timeout(timeout, loop=hass.loop): + camera_req = yield from websession.get( + syno_camera_url, + params=camera_payload + ) + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.exception("Error on %s", syno_camera_url) + return False + + camera_resp = yield from camera_req.json(content_type=None) + cameras = camera_resp['data']['cameras'] + + # add cameras + devices = [] + for camera in cameras: + if not config.get(CONF_WHITELIST): + camera_id = camera['id'] + snapshot_path = camera['snapshot_path'] + + device = SynologyCamera( + hass, websession, config, camera_id, camera['name'], + snapshot_path, streaming_path, camera_path, auth_path, timeout + ) + devices.append(device) + + async_add_devices(devices) + + +@asyncio.coroutine +def get_session_id(hass, websession, username, password, login_url, timeout): + """Get a session id.""" + auth_payload = { + 'api': AUTH_API, + 'method': 'Login', + 'version': '2', + 'account': username, + 'passwd': password, + 'session': 'SurveillanceStation', + 'format': 'sid' + } + try: + with async_timeout.timeout(timeout, loop=hass.loop): + auth_req = yield from websession.get( + login_url, + params=auth_payload + ) + auth_resp = yield from auth_req.json(content_type=None) + return auth_resp['data']['sid'] + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.exception("Error on %s", login_url) + return False + + +class SynologyCamera(Camera): + """An implementation of a Synology NAS based IP camera.""" + + def __init__(self, hass, websession, config, camera_id, + camera_name, snapshot_path, streaming_path, camera_path, + auth_path, timeout): + """Initialize a Synology Surveillance Station camera.""" + super().__init__() + self.hass = hass + self._websession = websession + self._name = camera_name + self._synology_url = config.get(CONF_URL) + self._camera_name = config.get(CONF_CAMERA_NAME) + self._stream_id = config.get(CONF_STREAM_ID) + self._camera_id = camera_id + self._snapshot_path = snapshot_path + self._streaming_path = streaming_path + self._camera_path = camera_path + self._auth_path = auth_path + self._timeout = timeout + + def camera_image(self): + """Return bytes of camera image.""" + return run_coroutine_threadsafe( + self.async_camera_image(), self.hass.loop).result() + + @asyncio.coroutine + def async_camera_image(self): + """Return a still image response from the camera.""" + image_url = SYNO_API_URL.format( + self._synology_url, WEBAPI_PATH, self._camera_path) + + image_payload = { + 'api': CAMERA_API, + 'method': 'GetSnapshot', + 'version': '1', + 'cameraId': self._camera_id + } + try: + with async_timeout.timeout(self._timeout, loop=self.hass.loop): + response = yield from self._websession.get( + image_url, + params=image_payload + ) + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Error fetching %s", image_url) + return None + + image = yield from response.read() + + return image + + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Return a MJPEG stream image response directly from the camera.""" + streaming_url = SYNO_API_URL.format( + self._synology_url, WEBAPI_PATH, self._streaming_path) + + streaming_payload = { + 'api': STREAMING_API, + 'method': 'Stream', + 'version': '1', + 'cameraId': self._camera_id, + 'format': 'mjpeg' + } + stream_coro = self._websession.get( + streaming_url, params=streaming_payload) + + yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) + + @property + def name(self): + """Return the name of this device.""" + return self._name diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 2e2dfbef8ca..f9405e4b040 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -213,8 +213,8 @@ def async_setup(hass, config): component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) yield from component.async_setup(config) - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join(os.path.dirname(__file__), 'services.yaml')) @asyncio.coroutine @@ -569,8 +569,8 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.set_temperature, **kwargs)) + return self.hass.async_add_job( + ft.partial(self.set_temperature, **kwargs)) def set_humidity(self, humidity): """Set new target humidity.""" @@ -581,8 +581,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.set_humidity, humidity) + return self.hass.async_add_job(self.set_humidity, humidity) def set_fan_mode(self, fan): """Set new target fan mode.""" @@ -593,8 +592,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.set_fan_mode, fan) + return self.hass.async_add_job(self.set_fan_mode, fan) def set_operation_mode(self, operation_mode): """Set new target operation mode.""" @@ -605,8 +603,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.set_operation_mode, operation_mode) + return self.hass.async_add_job(self.set_operation_mode, operation_mode) def set_swing_mode(self, swing_mode): """Set new target swing operation.""" @@ -617,8 +614,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.set_swing_mode, swing_mode) + return self.hass.async_add_job(self.set_swing_mode, swing_mode) def turn_away_mode_on(self): """Turn away mode on.""" @@ -629,8 +625,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.turn_away_mode_on) + return self.hass.async_add_job(self.turn_away_mode_on) def turn_away_mode_off(self): """Turn away mode off.""" @@ -641,8 +636,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.turn_away_mode_off) + return self.hass.async_add_job(self.turn_away_mode_off) def set_hold_mode(self, hold_mode): """Set new target hold mode.""" @@ -653,8 +647,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.set_hold_mode, hold_mode) + return self.hass.async_add_job(self.set_hold_mode, hold_mode) def turn_aux_heat_on(self): """Turn auxillary heater on.""" @@ -665,8 +658,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.turn_aux_heat_on) + return self.hass.async_add_job(self.turn_aux_heat_on) def turn_aux_heat_off(self): """Turn auxillary heater off.""" @@ -677,8 +669,7 @@ class ClimateDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.turn_aux_heat_off) + return self.hass.async_add_job(self.turn_aux_heat_off) @property def min_temp(self): diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 7bc99de26f9..af9ad44fd7e 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -1,4 +1,4 @@ -""" +""" Tado component to create a climate device for each zone. For more details about this platform, please refer to the documentation at diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index bbee0e836a3..f913d126c4a 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -175,8 +175,8 @@ def async_setup(hass, config): if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, os.path.join( + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( os.path.dirname(__file__), 'services.yaml')) for service_name in SERVICE_TO_METHOD: @@ -263,8 +263,7 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.open_cover, **kwargs)) + return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs)) def close_cover(self, **kwargs): """Close cover.""" @@ -275,8 +274,7 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.close_cover, **kwargs)) + return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs)) def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" @@ -287,8 +285,8 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.set_cover_position, **kwargs)) + return self.hass.async_add_job( + ft.partial(self.set_cover_position, **kwargs)) def stop_cover(self, **kwargs): """Stop the cover.""" @@ -299,8 +297,7 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.stop_cover, **kwargs)) + return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs)) def open_cover_tilt(self, **kwargs): """Open the cover tilt.""" @@ -311,8 +308,8 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.open_cover_tilt, **kwargs)) + return self.hass.async_add_job( + ft.partial(self.open_cover_tilt, **kwargs)) def close_cover_tilt(self, **kwargs): """Close the cover tilt.""" @@ -323,8 +320,8 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.close_cover_tilt, **kwargs)) + return self.hass.async_add_job( + ft.partial(self.close_cover_tilt, **kwargs)) def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" @@ -335,8 +332,8 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.set_cover_tilt_position, **kwargs)) + return self.hass.async_add_job( + ft.partial(self.set_cover_tilt_position, **kwargs)) def stop_cover_tilt(self, **kwargs): """Stop the cover.""" @@ -347,5 +344,5 @@ class CoverDevice(Entity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, ft.partial(self.stop_cover_tilt, **kwargs)) + return self.hass.async_add_job( + ft.partial(self.stop_cover_tilt, **kwargs)) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 06ec7ca6211..5c79540a249 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -12,19 +12,25 @@ from homeassistant.components.cover import CoverDevice from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED) import homeassistant.helpers.config_validation as cv +import homeassistant.loader as loader REQUIREMENTS = [ 'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip' '#pymyq==0.0.8'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'myq' + +NOTIFICATION_ID = 'myq_notification' +NOTIFICATION_TITLE = 'MyQ Cover Setup' + COVER_SCHEMA = vol.Schema({ vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string }) -DEFAULT_NAME = 'myq' - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MyQ component.""" @@ -33,23 +39,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) brand = config.get(CONF_TYPE) - - logger = logging.getLogger(__name__) - + persistent_notification = loader.get_component('persistent_notification') myq = pymyq(username, password, brand) - if not myq.is_supported_brand(): - logger.error("Unsupported type. See documentation") - return - - if not myq.is_login_valid(): - logger.error("Username or Password is incorrect") - return - try: + if not myq.is_supported_brand(): + raise ValueError("Unsupported type. See documentation") + + if not myq.is_login_valid(): + raise ValueError("Username or Password is incorrect") + add_devices(MyQDevice(myq, door) for door in myq.get_garage_doors()) - except (TypeError, KeyError, NameError) as ex: - logger.error("%s", ex) + return True + + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + persistent_notification.create( + hass, 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False class MyQDevice(CoverDevice): diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 781d7a03280..b682bee3e20 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -41,7 +41,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): """Initialize the Z-Wave rollershutter.""" ZWaveDeviceEntity.__init__(self, values, DOMAIN) # pylint: disable=no-member - self._network = hass.data[zwave.ZWAVE_NETWORK] + self._network = hass.data[zwave.const.DATA_NETWORK] self._open_id = None self._close_id = None self._current_position = None diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 6582ba3f57e..acf402a0c8a 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -35,7 +35,8 @@ from homeassistant.util.yaml import dump from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC, - DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID) + DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, + CONF_ICON, ATTR_ICON) _LOGGER = logging.getLogger(__name__) @@ -150,14 +151,14 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): scanner = yield from platform.async_get_scanner( hass, {DOMAIN: p_config}) elif hasattr(platform, 'get_scanner'): - scanner = yield from hass.loop.run_in_executor( - None, platform.get_scanner, hass, {DOMAIN: p_config}) + scanner = yield from hass.async_add_job( + platform.get_scanner, hass, {DOMAIN: p_config}) elif hasattr(platform, 'async_setup_scanner'): setup = yield from platform.async_setup_scanner( hass, p_config, tracker.async_see, disc_info) elif hasattr(platform, 'setup_scanner'): - setup = yield from hass.loop.run_in_executor( - None, platform.setup_scanner, hass, p_config, tracker.see, + setup = yield from hass.async_add_job( + platform.setup_scanner, hass, p_config, tracker.see, disc_info) else: raise HomeAssistantError("Invalid device_tracker platform.") @@ -209,8 +210,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)} yield from tracker.async_see(**args) - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join(os.path.dirname(__file__), 'services.yaml') ) hass.services.async_register( @@ -322,8 +323,8 @@ class DeviceTracker(object): This method is a coroutine. """ with (yield from self._is_updating): - yield from self.hass.loop.run_in_executor( - None, update_config, self.hass.config.path(YAML_DEVICES), + yield from self.hass.async_add_job( + update_config, self.hass.config.path(YAML_DEVICES), dev_id, device) @asyncio.coroutine @@ -381,6 +382,7 @@ class Device(Entity): battery = None # type: str attributes = None # type: dict vendor = None # type: str + icon = None # type: str # Track if the last update of this device was HOME. last_update_home = False @@ -388,7 +390,7 @@ class Device(Entity): def __init__(self, hass: HomeAssistantType, consider_home: timedelta, track: bool, dev_id: str, mac: str, name: str=None, - picture: str=None, gravatar: str=None, + picture: str=None, gravatar: str=None, icon: str=None, hide_if_away: bool=False, vendor: str=None) -> None: """Initialize a device.""" self.hass = hass @@ -414,6 +416,8 @@ class Device(Entity): else: self.config_picture = picture + self.icon = icon + self.away_hide = hide_if_away self.vendor = vendor @@ -608,7 +612,7 @@ class DeviceScanner(object): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor(None, self.scan_devices) + return self.hass.async_add_job(self.scan_devices) def get_device_name(self, mac: str) -> str: """Get device name from mac.""" @@ -619,7 +623,7 @@ class DeviceScanner(object): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor(None, self.get_device_name, mac) + return self.hass.async_add_job(self.get_device_name, mac) def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): @@ -637,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType, """ dev_schema = vol.Schema({ vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON, default=False): + vol.Any(None, cv.icon), vol.Optional('track', default=False): cv.boolean, vol.Optional(CONF_MAC, default=None): vol.Any(None, vol.All(cv.string, vol.Upper)), @@ -650,8 +656,8 @@ def async_load_config(path: str, hass: HomeAssistantType, try: result = [] try: - devices = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, path) + devices = yield from hass.async_add_job( + load_yaml_config_file, path) except HomeAssistantError as err: _LOGGER.error("Unable to load %s: %s", path, str(err)) return [] @@ -728,6 +734,7 @@ def update_config(path: str, dev_id: str, device: Device): device = {device.dev_id: { ATTR_NAME: device.name, ATTR_MAC: device.mac, + ATTR_ICON: device.icon, 'picture': device.config_picture, 'track': device.track, CONF_AWAY_HIDE: device.away_hide, diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index a0405b0b690..bdd28d1d168 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -118,25 +118,29 @@ class AsusWrtDeviceScanner(DeviceScanner): self.protocol = config[CONF_PROTOCOL] self.mode = config[CONF_MODE] self.port = config[CONF_PORT] - self.ssh_args = {} if self.protocol == 'ssh': - - self.ssh_args['port'] = self.port - if self.ssh_key: - self.ssh_args['ssh_key'] = self.ssh_key - elif self.password: - self.ssh_args['password'] = self.password - else: + if not (self.ssh_key or self.password): _LOGGER.error("No password or private key specified") self.success_init = False return + + self.connection = SshConnection(self.host, self.port, + self.username, + self.password, + self.ssh_key, + self.mode == "ap") else: if not self.password: _LOGGER.error("No password specified") self.success_init = False return + self.connection = TelnetConnection(self.host, self.port, + self.username, + self.password, + self.mode == "ap") + self.lock = threading.Lock() self.last_results = {} @@ -182,105 +186,9 @@ class AsusWrtDeviceScanner(DeviceScanner): self.last_results = active_clients return True - def ssh_connection(self): - """Retrieve data from ASUSWRT via the ssh protocol.""" - from pexpect import pxssh, exceptions - - ssh = pxssh.pxssh() - try: - ssh.login(self.host, self.username, **self.ssh_args) - except exceptions.EOF as err: - _LOGGER.error("Connection refused. SSH enabled?") - return None - except pxssh.ExceptionPxssh as err: - _LOGGER.error("Unable to connect via SSH: %s", str(err)) - return None - - try: - ssh.sendline(_IP_NEIGH_CMD) - ssh.prompt() - neighbors = ssh.before.split(b'\n')[1:-1] - if self.mode == 'ap': - ssh.sendline(_ARP_CMD) - ssh.prompt() - arp_result = ssh.before.split(b'\n')[1:-1] - ssh.sendline(_WL_CMD) - ssh.prompt() - leases_result = ssh.before.split(b'\n')[1:-1] - ssh.sendline(_NVRAM_CMD) - ssh.prompt() - nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:] - else: - arp_result = [''] - nvram_result = [''] - ssh.sendline(_LEASES_CMD) - ssh.prompt() - leases_result = ssh.before.split(b'\n')[1:-1] - ssh.logout() - return AsusWrtResult(neighbors, leases_result, arp_result, - nvram_result) - except pxssh.ExceptionPxssh as exc: - _LOGGER.error("Unexpected response from router: %s", exc) - return None - - def telnet_connection(self): - """Retrieve data from ASUSWRT via the telnet protocol.""" - try: - telnet = telnetlib.Telnet(self.host) - telnet.read_until(b'login: ') - telnet.write((self.username + '\n').encode('ascii')) - telnet.read_until(b'Password: ') - telnet.write((self.password + '\n').encode('ascii')) - prompt_string = telnet.read_until(b'#').split(b'\n')[-1] - telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii')) - neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1] - if self.mode == 'ap': - telnet.write('{}\n'.format(_ARP_CMD).encode('ascii')) - arp_result = (telnet.read_until(prompt_string). - split(b'\n')[1:-1]) - telnet.write('{}\n'.format(_WL_CMD).encode('ascii')) - leases_result = (telnet.read_until(prompt_string). - split(b'\n')[1:-1]) - telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii')) - nvram_result = (telnet.read_until(prompt_string). - split(b'\n')[1].split(b'<')[1:]) - else: - arp_result = [''] - nvram_result = [''] - telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii')) - leases_result = (telnet.read_until(prompt_string). - split(b'\n')[1:-1]) - telnet.write('exit\n'.encode('ascii')) - return AsusWrtResult(neighbors, leases_result, arp_result, - nvram_result) - except EOFError: - _LOGGER.error("Unexpected response from router") - return None - except ConnectionRefusedError: - _LOGGER.error("Connection refused by router. Telnet enabled?") - return None - except socket.gaierror as exc: - _LOGGER.error("Socket exception: %s", exc) - return None - except OSError as exc: - _LOGGER.error("OSError: %s", exc) - return None - def get_asuswrt_data(self): """Retrieve data from ASUSWRT and return parsed result.""" - if self.protocol == 'ssh': - result = self.ssh_connection() - elif self.protocol == 'telnet': - result = self.telnet_connection() - else: - # autodetect protocol - result = self.ssh_connection() - if result: - self.protocol = 'ssh' - else: - result = self.telnet_connection() - if result: - self.protocol = 'telnet' + result = self.connection.get_result() if not result: return {} @@ -363,3 +271,193 @@ class AsusWrtDeviceScanner(DeviceScanner): if match.group('ip') in devices: devices[match.group('ip')]['status'] = match.group('status') return devices + + +class _Connection: + def __init__(self): + self._connected = False + + @property + def connected(self): + """Return connection state.""" + return self._connected + + def connect(self): + """Mark currenct connection state as connected.""" + self._connected = True + + def disconnect(self): + """Mark current connection state as disconnected.""" + self._connected = False + + +class SshConnection(_Connection): + """Maintains an SSH connection to an ASUS-WRT router.""" + + def __init__(self, host, port, username, password, ssh_key, ap): + """Initialize the SSH connection properties.""" + super(SshConnection, self).__init__() + + self._ssh = None + self._host = host + self._port = port + self._username = username + self._password = password + self._ssh_key = ssh_key + self._ap = ap + + def get_result(self): + """Retrieve a single AsusWrtResult through an SSH connection. + + Connect to the SSH server if not currently connected, otherwise + use the existing connection. + """ + from pexpect import pxssh, exceptions + + try: + if not self.connected: + self.connect() + self._ssh.sendline(_IP_NEIGH_CMD) + self._ssh.prompt() + neighbors = self._ssh.before.split(b'\n')[1:-1] + if self._ap: + self._ssh.sendline(_ARP_CMD) + self._ssh.prompt() + arp_result = self._ssh.before.split(b'\n')[1:-1] + self._ssh.sendline(_WL_CMD) + self._ssh.prompt() + leases_result = self._ssh.before.split(b'\n')[1:-1] + self._ssh.sendline(_NVRAM_CMD) + self._ssh.prompt() + nvram_result = self._ssh.before.split(b'\n')[1].split(b'<')[1:] + else: + arp_result = [''] + nvram_result = [''] + self._ssh.sendline(_LEASES_CMD) + self._ssh.prompt() + leases_result = self._ssh.before.split(b'\n')[1:-1] + return AsusWrtResult(neighbors, leases_result, arp_result, + nvram_result) + except exceptions.EOF as err: + _LOGGER.error("Connection refused. SSH enabled?") + self.disconnect() + return None + except pxssh.ExceptionPxssh as err: + _LOGGER.error("Unexpected SSH error: %s", str(err)) + self.disconnect() + return None + except AssertionError as err: + _LOGGER.error("Connection to router unavailable: %s", str(err)) + self.disconnect() + return None + + def connect(self): + """Connect to the ASUS-WRT SSH server.""" + from pexpect import pxssh + + self._ssh = pxssh.pxssh() + if self._ssh_key: + self._ssh.login(self._host, self._username, + ssh_key=self._ssh_key, port=self._port) + else: + self._ssh.login(self._host, self._username, + password=self._password, port=self._port) + + super(SshConnection, self).connect() + + def disconnect(self): \ + # pylint: disable=broad-except + """Disconnect the current SSH connection.""" + try: + self._ssh.logout() + except Exception: + pass + finally: + self._ssh = None + + super(SshConnection, self).disconnect() + + +class TelnetConnection(_Connection): + """Maintains a Telnet connection to an ASUS-WRT router.""" + + def __init__(self, host, port, username, password, ap): + """Initialize the Telnet connection properties.""" + super(TelnetConnection, self).__init__() + + self._telnet = None + self._host = host + self._port = port + self._username = username + self._password = password + self._ap = ap + self._prompt_string = None + + def get_result(self): + """Retrieve a single AsusWrtResult through a Telnet connection. + + Connect to the Telnet server if not currently connected, otherwise + use the existing connection. + """ + try: + if not self.connected: + self.connect() + + self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii')) + neighbors = (self._telnet.read_until(self._prompt_string). + split(b'\n')[1:-1]) + if self._ap: + self._telnet.write('{}\n'.format(_ARP_CMD).encode('ascii')) + arp_result = (self._telnet.read_until(self._prompt_string). + split(b'\n')[1:-1]) + self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii')) + leases_result = (self._telnet.read_until(self._prompt_string). + split(b'\n')[1:-1]) + self._telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii')) + nvram_result = (self._telnet.read_until(self._prompt_string). + split(b'\n')[1].split(b'<')[1:]) + else: + arp_result = [''] + nvram_result = [''] + self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii')) + leases_result = (self._telnet.read_until(self._prompt_string). + split(b'\n')[1:-1]) + return AsusWrtResult(neighbors, leases_result, arp_result, + nvram_result) + except EOFError: + _LOGGER.error("Unexpected response from router") + self.disconnect() + return None + except ConnectionRefusedError: + _LOGGER.error("Connection refused by router. Telnet enabled?") + self.disconnect() + return None + except socket.gaierror as exc: + _LOGGER.error("Socket exception: %s", exc) + self.disconnect() + return None + except OSError as exc: + _LOGGER.error("OSError: %s", exc) + self.disconnect() + return None + + def connect(self): + """Connect to the ASUS-WRT Telnet server.""" + self._telnet = telnetlib.Telnet(self._host) + self._telnet.read_until(b'login: ') + self._telnet.write((self._username + '\n').encode('ascii')) + self._telnet.read_until(b'Password: ') + self._telnet.write((self._password + '\n').encode('ascii')) + self._prompt_string = self._telnet.read_until(b'#').split(b'\n')[-1] + + super(TelnetConnection, self).connect() + + def disconnect(self): \ + # pylint: disable=broad-except + """Disconnect the current Telnet connection.""" + try: + self._telnet.write('exit\n'.encode('ascii')) + except Exception: + pass + + super(TelnetConnection, self).disconnect() diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 733127cb0f2..b88245ac9a5 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -39,7 +39,7 @@ class GPSLoggerView(HomeAssistantView): @asyncio.coroutine def get(self, request): """Handle for GPSLogger message received as GET.""" - res = yield from self._handle(request.app['hass'], request.GET) + res = yield from self._handle(request.app['hass'], request.query) return res @asyncio.coroutine @@ -75,10 +75,10 @@ class GPSLoggerView(HomeAssistantView): if 'activity' in data: attrs['activity'] = data['activity'] - yield from hass.loop.run_in_executor( - None, partial(self.see, dev_id=device, - gps=gps_location, battery=battery, - gps_accuracy=accuracy, - attributes=attrs)) + yield from hass.async_add_job( + partial(self.see, dev_id=device, + gps=gps_location, battery=battery, + gps_accuracy=accuracy, + attributes=attrs)) return 'Setting location for {}'.format(device) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 668ee6dd8a0..ced24edde48 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -41,7 +41,7 @@ class LocativeView(HomeAssistantView): @asyncio.coroutine def get(self, request): """Locative message received as GET.""" - res = yield from self._handle(request.app['hass'], request.GET) + res = yield from self._handle(request.app['hass'], request.query) return res @asyncio.coroutine @@ -79,10 +79,9 @@ class LocativeView(HomeAssistantView): gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) if direction == 'enter': - yield from hass.loop.run_in_executor( - None, partial(self.see, dev_id=device, - location_name=location_name, - gps=gps_location)) + yield from hass.async_add_job( + partial(self.see, dev_id=device, location_name=location_name, + gps=gps_location)) return 'Setting location to {}'.format(location_name) elif direction == 'exit': @@ -91,10 +90,9 @@ class LocativeView(HomeAssistantView): if current_state is None or current_state.state == location_name: location_name = STATE_NOT_HOME - yield from hass.loop.run_in_executor( - None, partial(self.see, dev_id=device, - location_name=location_name, - gps=gps_location)) + yield from hass.async_add_job( + partial(self.see, dev_id=device, + location_name=location_name, gps=gps_location)) return 'Setting location to not home' else: # Ignore the message if it is telling us to exit a zone that we diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index f22b297b78d..af543548fbd 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -60,13 +60,20 @@ class MikrotikScanner(DeviceScanner): self.success_init = False self.client = None + self.wireless_exist = None self.success_init = self.connect_to_device() if self.success_init: - _LOGGER.info("Start polling Mikrotik router...") + _LOGGER.info( + "Start polling Mikrotik (%s) router...", + self.host + ) self._update_info() else: - _LOGGER.error("Connection to Mikrotik failed") + _LOGGER.error( + "Connection to Mikrotik (%s) failed", + self.host + ) def connect_to_device(self): """Connect to Mikrotik method.""" @@ -87,6 +94,16 @@ class MikrotikScanner(DeviceScanner): routerboard_info[0].get('model', 'Router'), self.host) self.connected = True + self.wireless_exist = self.client( + cmd='/interface/wireless/getall' + ) + if not self.wireless_exist: + _LOGGER.info( + 'Mikrotik %s: Wireless adapters not found. Try to ' + 'use DHCP lease table as presence tracker source. ' + 'Please decrease lease time as much as possible.', + self.host + ) except (librouteros.exceptions.TrapError, librouteros.exceptions.ConnectionError) as api_error: @@ -108,24 +125,39 @@ class MikrotikScanner(DeviceScanner): def _update_info(self): """Retrieve latest information from the Mikrotik box.""" with self.lock: - _LOGGER.info("Loading wireless device from Mikrotik...") + if self.wireless_exist: + devices_tracker = 'wireless' + else: + devices_tracker = 'ip' - wireless_clients = self.client( - cmd='/interface/wireless/registration-table/getall' + _LOGGER.info( + "Loading %s devices from Mikrotik (%s) ...", + devices_tracker, + self.host ) - device_names = self.client(cmd='/ip/dhcp-server/lease/getall') - if device_names is None or wireless_clients is None: + device_names = self.client(cmd='/ip/dhcp-server/lease/getall') + if self.wireless_exist: + devices = self.client( + cmd='/interface/wireless/registration-table/getall' + ) + else: + devices = device_names + + if device_names is None and devices is None: return False mac_names = {device.get('mac-address'): device.get('host-name') for device in device_names if device.get('mac-address')} - self.last_results = { - device.get('mac-address'): - mac_names.get(device.get('mac-address')) - for device in wireless_clients - } + if self.wireless_exist: + self.last_results = { + device.get('mac-address'): + mac_names.get(device.get('mac-address')) + for device in devices + } + else: + self.last_results = mac_names return True diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 1449ae6dbef..b0a2015362e 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -19,7 +19,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pysnmp==4.3.5'] +REQUIREMENTS = ['pysnmp==4.3.7'] CONF_COMMUNITY = 'community' CONF_AUTHKEY = 'authkey' diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 261d8953940..6132bd565dd 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.0.0'] +REQUIREMENTS = ['netdisco==1.0.1'] DOMAIN = 'discovery' @@ -115,8 +115,7 @@ def async_setup(hass, config): @asyncio.coroutine def scan_devices(now): """Scan for devices.""" - results = yield from hass.loop.run_in_executor( - None, _discover, netdisco) + results = yield from hass.async_add_job(_discover, netdisco) for result in results: hass.async_add_job(new_service_found(*result)) diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index 22647532d9a..db8af964fed 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.0.5'] +REQUIREMENTS = ['pyeight==0.0.6'] _LOGGER = logging.getLogger(__name__) @@ -159,8 +159,8 @@ def async_setup(hass, config): CONF_BINARY_SENSORS: binary_sensors, }, config)) - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join(os.path.dirname(__file__), 'services.yaml')) @asyncio.coroutine diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 8aa41f6274a..87b6163282a 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['pyenvisalink==2.0'] +REQUIREMENTS = ['pyenvisalink==2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 500cebfe73b..54c503c1b9f 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -229,8 +229,8 @@ def async_setup(hass, config: dict): yield from asyncio.wait(update_tasks, loop=hass.loop) # Listen for fan service calls. - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, os.path.join( + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join( os.path.dirname(__file__), 'services.yaml')) for service_name in SERVICE_TO_METHOD: @@ -256,7 +256,7 @@ class FanEntity(ToggleEntity): """ if speed is SPEED_OFF: return self.async_turn_off() - return self.hass.loop.run_in_executor(None, self.set_speed, speed) + return self.hass.async_add_job(self.set_speed, speed) def set_direction(self: ToggleEntity, direction: str) -> None: """Set the direction of the fan.""" @@ -267,8 +267,7 @@ class FanEntity(ToggleEntity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.set_direction, direction) + return self.hass.async_add_job(self.set_direction, direction) def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None: """Turn on the fan.""" @@ -281,8 +280,8 @@ class FanEntity(ToggleEntity): """ if speed is SPEED_OFF: return self.async_turn_off() - return self.hass.loop.run_in_executor( - None, ft.partial(self.turn_on, speed, **kwargs)) + return self.hass.async_add_job( + ft.partial(self.turn_on, speed, **kwargs)) def oscillate(self: ToggleEntity, oscillating: bool) -> None: """Oscillate the fan.""" @@ -293,8 +292,7 @@ class FanEntity(ToggleEntity): This method must be run in the event loop and returns a coroutine. """ - return self.hass.loop.run_in_executor( - None, self.oscillate, oscillating) + return self.hass.async_add_job(self.oscillate, oscillating) @property def is_on(self): diff --git a/homeassistant/components/fan/zwave.py b/homeassistant/components/fan/zwave.py new file mode 100644 index 00000000000..fe01ae5f3a4 --- /dev/null +++ b/homeassistant/components/fan/zwave.py @@ -0,0 +1,86 @@ +""" +Z-Wave platform that handles fans. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/fan.zwave/ +""" +import logging +import math + +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 + +_LOGGER = logging.getLogger(__name__) + +SPEED_LIST = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + +SUPPORTED_FEATURES = SUPPORT_SET_SPEED + +# Value will first be divided to an integer +VALUE_TO_SPEED = { + 0: SPEED_OFF, + 1: SPEED_LOW, + 2: SPEED_MEDIUM, + 3: SPEED_HIGH, +} + +SPEED_TO_VALUE = { + SPEED_OFF: 0, + SPEED_LOW: 1, + SPEED_MEDIUM: 50, + SPEED_HIGH: 99, +} + + +def get_device(values, **kwargs): + """Create zwave entity device.""" + return ZwaveFan(values) + + +class ZwaveFan(zwave.ZWaveDeviceEntity, FanEntity): + """Representation of a Z-Wave fan.""" + + def __init__(self, values): + """Initialize the Z-Wave fan device.""" + zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN) + self.update_properties() + + def update_properties(self): + """Handle data changes for node values.""" + value = math.ceil(self.values.primary.data * 3 / 100) + self._state = VALUE_TO_SPEED[value] + + def set_speed(self, speed): + """Set the speed of the fan.""" + self.node.set_dimmer( + self.values.primary.value_id, SPEED_TO_VALUE[speed]) + + def turn_on(self, speed=None, **kwargs): + """Turn the device on.""" + if speed is None: + # Value 255 tells device to return to previous value + self.node.set_dimmer(self.values.primary.value_id, 255) + else: + self.set_speed(speed) + + def turn_off(self, **kwargs): + """Turn the device off.""" + self.node.set_dimmer(self.values.primary.value_id, 0) + + @property + def speed(self): + """Return the current speed.""" + return self._state + + @property + def speed_list(self): + """Get the list of available speeds.""" + return SPEED_LIST + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 959962f02ac..45bd651ad95 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -89,8 +89,8 @@ def async_setup(hass, config): conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST) ) - descriptions = yield from hass.loop.run_in_executor( - None, load_yaml_config_file, + descriptions = yield from hass.async_add_job( + load_yaml_config_file, os.path.join(os.path.dirname(__file__), 'services.yaml')) # Register service diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 617db06be2c..8d55ad879fa 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -268,8 +268,8 @@ class IndexView(HomeAssistantView): no_auth = 'true' icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) - template = yield from hass.loop.run_in_executor( - None, self.templates.get_template, 'index.html') + template = yield from hass.async_add_job( + self.templates.get_template, 'index.html') # pylint is wrong # pylint: disable=no-member diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index f4af26cc376..fd7f4c921cb 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,7 +3,7 @@ FINGERPRINTS = { "compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812", "core.js": "d4a7cb8c80c62b536764e0e81385f6aa", - "frontend.html": "fbb9d6bdd3d661db26cad9475a5e22f1", + "frontend.html": "ed18c05632c071eb4f7b012382d0f810", "mdi.html": "f407a5a57addbe93817ee1b244d33fbe", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8", @@ -18,6 +18,6 @@ FINGERPRINTS = { "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163", "panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4", - "panels/ha-panel-zwave.html": "19336d2c50c91dd6a122acc0606ff10d", + "panels/ha-panel-zwave.html": "780a792213e98510b475f752c40ef0f9", "websocket_test.html": "575de64b431fe11c3785bf96d7813450" } diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index c0443749f7c..936db8dccde 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -552,7 +552,7 @@ window.hassUtil.computeLocationName = function (hass) { }, }); }()); \ No newline at end of file +}()); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index edb01a40d53..db87c3e66e3 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 6858555c86f..75679e90f2a 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 6858555c86f18eb0ab176008e9aa2c3842fec7ce +Subproject commit 75679e90f2aa11bc1b42188965746217feef0ea6 diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html index 70e5cc64177..9cbcaf14c0a 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html @@ -31,6 +31,200 @@ }); this.selectedNodeAttrs = att.sort(); }, +});