diff --git a/.coveragerc b/.coveragerc index d078cd5bf8a..272ace975c4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -15,6 +15,10 @@ omit = homeassistant/components/*/modbus.py homeassistant/components/*/tellstick.py + + homeassistant/components/tellduslive.py + homeassistant/components/*/tellduslive.py + homeassistant/components/*/vera.py homeassistant/components/ecobee.py @@ -32,6 +36,9 @@ omit = homeassistant/components/rfxtrx.py homeassistant/components/*/rfxtrx.py + homeassistant/components/mysensors.py + homeassistant/components/*/mysensors.py + homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py @@ -41,7 +48,6 @@ omit = homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py - homeassistant/components/device_tracker/geofancy.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py @@ -70,6 +76,7 @@ omit = homeassistant/components/media_player/plex.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/notify/free_mobile.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py @@ -89,7 +96,6 @@ omit = homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py - homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/rpi_gpio.py diff --git a/.gitignore b/.gitignore index 8935ffedc17..3ee71808ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ Icon dist build eggs +.eggs parts bin var diff --git a/.travis.yml b/.travis.yml index a75cf6685d3..c01b0750360 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,7 @@ python: - 3.4 - 3.5 install: - # Validate requirements_all.txt on Python 3.5 - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi - - script/bootstrap_server + - "true" script: - script/cibuild matrix: diff --git a/LICENSE b/LICENSE index b3c5e1df750..42a425b4118 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Paulus Schoutsen +Copyright (c) 2016 Paulus Schoutsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a7507fd12b8..b704fc082ac 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -275,7 +275,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None): datefmt='%y-%m-%d %H:%M:%S')) logger = logging.getLogger('') logger.addHandler(err_handler) - logger.setLevel(logging.NOTSET) # this sets the minimum log level + logger.setLevel(logging.INFO) else: _LOGGER.error( diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index e0b008cab5e..10e18216ea0 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -87,13 +87,21 @@ def setup(hass, config): lambda item: util.split_entity_id(item)[0]) for domain, ent_ids in by_domain: + # We want to block for all calls and only return when all calls + # have been processed. If a service does not exist it causes a 10 + # second delay while we're blocking waiting for a response. + # But services can be registered on other HA instances that are + # listening to the bus too. So as a in between solution, we'll + # block only if the service is defined in the current HA instance. + blocking = hass.services.has_service(domain, service.service) + # Create a new dict for this call data = dict(service.data) # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - hass.services.call(domain, service.service, data, True) + hass.services.call(domain, service.service, data, blocking) hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 63bc989f3df..2658e005aea 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -68,7 +68,8 @@ class ManualAlarm(alarm.AlarmControlPanel): @property def state(self): """ Returns the state of the device. """ - if self._state in (STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) and \ + if self._state in (STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY) and \ self._pending_time and self._state_ts + self._pending_time > \ dt_util.utcnow(): return STATE_ALARM_PENDING diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index e4c498a5044..ea48209a0fd 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): alarms.extend([ VerisureAlarm(value) - for value in verisure.get_alarm_status().values() + for value in verisure.ALARM_STATUS.values() if verisure.SHOW_ALARM ]) @@ -42,7 +42,6 @@ class VerisureAlarm(alarm.AlarmControlPanel): def __init__(self, alarm_status): self._id = alarm_status.id - self._device = verisure.MY_PAGES.DEVICE_ALARM self._state = STATE_UNKNOWN @property @@ -62,36 +61,36 @@ class VerisureAlarm(alarm.AlarmControlPanel): def update(self): """ Update alarm status """ - verisure.update() + verisure.update_alarm() - if verisure.STATUS[self._device][self._id].status == 'unarmed': + if verisure.ALARM_STATUS[self._id].status == 'unarmed': self._state = STATE_ALARM_DISARMED - elif verisure.STATUS[self._device][self._id].status == 'armedhome': + elif verisure.ALARM_STATUS[self._id].status == 'armedhome': self._state = STATE_ALARM_ARMED_HOME - elif verisure.STATUS[self._device][self._id].status == 'armedaway': + elif verisure.ALARM_STATUS[self._id].status == 'armed': self._state = STATE_ALARM_ARMED_AWAY - elif verisure.STATUS[self._device][self._id].status != 'pending': + elif verisure.ALARM_STATUS[self._id].status != 'pending': _LOGGER.error( 'Unknown alarm state %s', - verisure.STATUS[self._device][self._id].status) + verisure.ALARM_STATUS[self._id].status) def alarm_disarm(self, code=None): """ Send disarm command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_DISARMED) - _LOGGER.warning('disarming') + verisure.MY_PAGES.alarm.set(code, 'DISARMED') + _LOGGER.info('verisure alarm disarming') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() def alarm_arm_home(self, code=None): """ Send arm home command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_ARMED_HOME) - _LOGGER.warning('arming home') + verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME') + _LOGGER.info('verisure alarm arming home') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() def alarm_arm_away(self, code=None): """ Send arm away command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_ARMED_AWAY) - _LOGGER.warning('arming away') + verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY') + _LOGGER.info('verisure alarm arming away') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 0b06f3c9a79..66ac9de0b43 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -11,6 +11,7 @@ import logging from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.util import template +from homeassistant.helpers.service import call_from_config DOMAIN = 'alexa' DEPENDENCIES = ['http'] @@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa' CONF_INTENTS = 'intents' CONF_CARD = 'card' CONF_SPEECH = 'speech' +CONF_ACTION = 'action' def setup(hass, config): @@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data): speech = config.get(CONF_SPEECH) card = config.get(CONF_CARD) + action = config.get(CONF_ACTION) # pylint: disable=unsubscriptable-object if speech is not None: @@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data): response.add_card(CardType[card['type']], card['title'], card['content']) + if action is not None: + call_from_config(handler.server.hass, action, True) + handler.write_json(response.as_dict()) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 23d83f554ca..9c464f6954e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/ import logging from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.util import split_entity_id -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM from homeassistant.components import logbook +from homeassistant.helpers.service import call_from_config DOMAIN = 'automation' @@ -19,8 +19,6 @@ DEPENDENCIES = ['group'] CONF_ALIAS = 'alias' CONF_SERVICE = 'service' -CONF_SERVICE_ENTITY_ID = 'entity_id' -CONF_SERVICE_DATA = 'data' CONF_CONDITION = 'condition' CONF_ACTION = 'action' @@ -96,22 +94,7 @@ def _get_action(hass, config, name): _LOGGER.info('Executing %s', name) logbook.log_entry(hass, name, 'has been triggered', DOMAIN) - domain, service = split_entity_id(config[CONF_SERVICE]) - service_data = config.get(CONF_SERVICE_DATA, {}) - - if not isinstance(service_data, dict): - _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) - service_data = {} - - if CONF_SERVICE_ENTITY_ID in config: - try: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID].split(",") - except AttributeError: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID] - - hass.services.call(domain, service, service_data) + call_from_config(hass, config) return action diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index f2baf760748..61e68aa8e8e 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -6,6 +6,7 @@ Offers numeric state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#numeric-state-trigger """ +from functools import partial import logging from homeassistant.const import CONF_VALUE_TEMPLATE @@ -20,6 +21,14 @@ CONF_ABOVE = "above" _LOGGER = logging.getLogger(__name__) +def _renderer(hass, value_template, state): + """Render state value.""" + if value_template is None: + return state.state + + return template.render(hass, value_template, {'state': state}) + + def trigger(hass, config, action): """ Listen for state changes based on `config`. """ entity_id = config.get(CONF_ENTITY_ID) @@ -38,12 +47,7 @@ def trigger(hass, config, action): CONF_BELOW, CONF_ABOVE) return False - if value_template is not None: - renderer = lambda value: template.render(hass, - value_template, - {'state': value}) - else: - renderer = lambda value: value.state + renderer = partial(_renderer, hass, value_template) # pylint: disable=unused-argument def state_automation_listener(entity, from_s, to_s): @@ -79,12 +83,7 @@ def if_action(hass, config): CONF_BELOW, CONF_ABOVE) return None - if value_template is not None: - renderer = lambda value: template.render(hass, - value_template, - {'state': value}) - else: - renderer = lambda value: value.state + renderer = partial(_renderer, hass, value_template) def if_numeric_state(): """ Test numeric state condition. """ diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 84334493d0f..0616c0a48e6 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -17,6 +17,10 @@ DEPENDENCIES = ['sun'] CONF_OFFSET = 'offset' CONF_EVENT = 'event' +CONF_BEFORE = "before" +CONF_BEFORE_OFFSET = "before_offset" +CONF_AFTER = "after" +CONF_AFTER_OFFSET = "after_offset" EVENT_SUNSET = 'sunset' EVENT_SUNRISE = 'sunrise' @@ -37,26 +41,9 @@ def trigger(hass, config, action): _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) return False - if CONF_OFFSET in config: - raw_offset = config.get(CONF_OFFSET) - - negative_offset = False - if raw_offset.startswith('-'): - negative_offset = True - raw_offset = raw_offset[1:] - - try: - (hour, minute, second) = [int(x) for x in raw_offset.split(':')] - except ValueError: - _LOGGER.error('Could not parse offset %s', raw_offset) - return False - - offset = timedelta(hours=hour, minutes=minute, seconds=second) - - if negative_offset: - offset *= -1 - else: - offset = timedelta(0) + offset = _parse_offset(config.get(CONF_OFFSET)) + if offset is False: + return False # Do something to call action if event == EVENT_SUNRISE: @@ -67,6 +54,77 @@ def trigger(hass, config, action): return True +def if_action(hass, config): + """ Wraps action method with sun based condition. """ + before = config.get(CONF_BEFORE) + after = config.get(CONF_AFTER) + + # Make sure required configuration keys are present + if before is None and after is None: + logging.getLogger(__name__).error( + "Missing if-condition configuration key %s or %s", + CONF_BEFORE, CONF_AFTER) + return None + + # Make sure configuration keys have the right value + if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \ + after not in (None, EVENT_SUNRISE, EVENT_SUNSET): + logging.getLogger(__name__).error( + "%s and %s can only be set to %s or %s", + CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET) + return None + + before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET)) + after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET)) + if before_offset is False or after_offset is False: + return None + + if before is None: + def before_func(): + """Return no point in time.""" + return None + elif before == EVENT_SUNRISE: + def before_func(): + """Return time before sunrise.""" + return sun.next_rising(hass) + before_offset + else: + def before_func(): + """Return time before sunset.""" + return sun.next_setting(hass) + before_offset + + if after is None: + def after_func(): + """Return no point in time.""" + return None + elif after == EVENT_SUNRISE: + def after_func(): + """Return time after sunrise.""" + return sun.next_rising(hass) + after_offset + else: + def after_func(): + """Return time after sunset.""" + return sun.next_setting(hass) + after_offset + + def time_if(): + """ Validate time based if-condition """ + + now = dt_util.now() + before = before_func() + after = after_func() + + if before is not None and now > now.replace(hour=before.hour, + minute=before.minute): + return False + + if after is not None and now < now.replace(hour=after.hour, + minute=after.minute): + return False + + return True + + return time_if + + def trigger_sunrise(hass, action, offset): """ Trigger action at next sun rise. """ def next_rise(): @@ -103,3 +161,26 @@ def trigger_sunset(hass, action, offset): action() track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + + +def _parse_offset(raw_offset): + if raw_offset is None: + return timedelta(0) + + negative_offset = False + if raw_offset.startswith('-'): + negative_offset = True + raw_offset = raw_offset[1:] + + try: + (hour, minute, second) = [int(x) for x in raw_offset.split(':')] + except ValueError: + _LOGGER.error('Could not parse offset %s', raw_offset) + return False + + offset = timedelta(hours=hour, minutes=minute, seconds=second) + + if negative_offset: + offset *= -1 + + return offset diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 7fc2c0d40e2..e8cf9c3b6ee 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -32,8 +32,8 @@ def trigger(hass, config, action): _error_time(config[CONF_AFTER], CONF_AFTER) return False hours, minutes, seconds = after.hour, after.minute, after.second - elif (CONF_HOURS in config or CONF_MINUTES in config - or CONF_SECONDS in config): + elif (CONF_HOURS in config or CONF_MINUTES in config or + CONF_SECONDS in config): hours = convert(config.get(CONF_HOURS), int) minutes = convert(config.get(CONF_MINUTES), int) seconds = convert(config.get(CONF_SECONDS), int) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 60963988f39..4d82d25e473 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -6,12 +6,11 @@ The rest binary sensor will consume responses sent by an exposed REST API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.rest/ """ -from datetime import timedelta import logging -import requests from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.util import template, Throttle +from homeassistant.util import template +from homeassistant.components.sensor.rest import RestData from homeassistant.components.binary_sensor import BinarySensorDevice _LOGGER = logging.getLogger(__name__) @@ -19,61 +18,33 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'REST Binary Sensor' DEFAULT_METHOD = 'GET' -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): - """ Get the REST binary sensor. """ - - use_get = False - use_post = False - + """Setup REST binary sensors.""" resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error('Response status is "%s"', response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error('Missing resource or schema in configuration. ' - 'Add http:// to your URL.') - return False - except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint: %s', - resource) + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestBinarySensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestBinarySensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): - """ Implements a REST binary sensor. """ + """REST binary sensor.""" def __init__(self, hass, rest, name, value_template): + """Initialize a REST binary sensor.""" self._hass = hass self.rest = rest self._name = name @@ -83,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice): @property def name(self): - """ The name of the binary sensor. """ + """Name of the binary sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ - if self.rest.data is False: + """Return if the binary sensor is on.""" + if self.rest.data is None: return False - else: - if self._value_template is not None: - self.rest.data = template.render_with_possible_json_value( - self._hass, self._value_template, self.rest.data, False) - return bool(int(self.rest.data)) + + if self._value_template is not None: + self.rest.data = template.render_with_possible_json_value( + self._hass, self._value_template, self.rest.data, False) + return bool(int(self.rest.data)) def update(self): - """ Gets the latest data from REST API and updates the state. """ + """Get the latest data from REST API and updates the state.""" self.rest.update() - - -# pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ - - def __init__(self, resource, verify_ssl): - self._resource = resource - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with GET method. """ - try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index b90e1ee4448..472440d7307 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -58,8 +58,8 @@ class AsusWrtDeviceScanner(object): def __init__(self, config): self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + self.username = str(config[CONF_USERNAME]) + self.password = str(config[CONF_PASSWORD]) self.lock = threading.Lock() diff --git a/homeassistant/components/device_tracker/geofancy.py b/homeassistant/components/device_tracker/geofancy.py deleted file mode 100644 index a5e6edee71a..00000000000 --- a/homeassistant/components/device_tracker/geofancy.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -homeassistant.components.device_tracker.geofancy -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Geofancy platform for the device tracker. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.geofancy/ -""" -from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR) - -DEPENDENCIES = ['http'] - -_SEE = 0 - -URL_API_GEOFANCY_ENDPOINT = "/api/geofancy" - - -def setup_scanner(hass, config, see): - """ Set up an endpoint for the Geofancy app. """ - - # Use a global variable to keep setup_scanner compact when using a callback - global _SEE - _SEE = see - - # POST would be semantically better, but that currently does not work - # since Geofancy sends the data as key1=value1&key2=value2 - # in the request body, while Home Assistant expects json there. - - hass.http.register_path( - 'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy) - - return True - - -def _handle_get_api_geofancy(handler, path_match, data): - """ Geofancy message received. """ - - if not isinstance(data, dict): - handler.write_json_message( - "Error while parsing Geofancy message.", - HTTP_INTERNAL_SERVER_ERROR) - return - if 'latitude' not in data or 'longitude' not in data: - handler.write_json_message( - "Location not specified.", - HTTP_UNPROCESSABLE_ENTITY) - return - if 'device' not in data or 'id' not in data: - handler.write_json_message( - "Device id or location id not specified.", - HTTP_UNPROCESSABLE_ENTITY) - return - - try: - gps_coords = (float(data['latitude']), float(data['longitude'])) - except ValueError: - # If invalid latitude / longitude format - handler.write_json_message( - "Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) - return - - # entity id's in Home Assistant must be alphanumerical - device_uuid = data['device'] - device_entity_id = device_uuid.replace('-', '') - - _SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id']) - - handler.write_json_message("Geofancy message processed") diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py new file mode 100644 index 00000000000..e7532d1075d --- /dev/null +++ b/homeassistant/components/device_tracker/locative.py @@ -0,0 +1,105 @@ +""" +homeassistant.components.device_tracker.locative +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Locative platform for the device tracker. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.locative/ +""" +import logging +from functools import partial + +from homeassistant.const import ( + HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) +from homeassistant.components.device_tracker import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['http'] + +URL_API_LOCATIVE_ENDPOINT = "/api/locative" + + +def setup_scanner(hass, config, see): + """ Set up an endpoint for the Locative app. """ + + # POST would be semantically better, but that currently does not work + # since Locative sends the data as key1=value1&key2=value2 + # in the request body, while Home Assistant expects json there. + + hass.http.register_path( + 'GET', URL_API_LOCATIVE_ENDPOINT, + partial(_handle_get_api_locative, hass, see)) + + return True + + +def _handle_get_api_locative(hass, see, handler, path_match, data): + """ Locative message received. """ + + if not _check_data(handler, data): + return + + device = data['device'].replace('-', '') + location_name = data['id'].lower() + direction = data['trigger'] + + if direction == 'enter': + see(dev_id=device, location_name=location_name) + handler.write_text("Setting location to {}".format(location_name)) + + elif direction == 'exit': + current_state = hass.states.get( + "{}.{}".format(DOMAIN, device)).state + + if current_state == location_name: + see(dev_id=device, location_name=STATE_NOT_HOME) + handler.write_text("Setting location to not home") + else: + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered before + # the previous zone was exited. The enter message will be sent + # first, then the exit message will be sent second. + handler.write_text( + 'Ignoring exit from {} (already in {})'.format( + location_name, current_state)) + + elif direction == 'test': + # In the app, a test message can be sent. Just return something to + # the user to let them know that it works. + handler.write_text("Received test message.") + + else: + handler.write_text( + "Received unidentified message: {}".format(direction), + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Received unidentified message from Locative: %s", + direction) + + +def _check_data(handler, data): + if 'latitude' not in data or 'longitude' not in data: + handler.write_text("Latitude and longitude not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Latitude and longitude not specified.") + return False + + if 'device' not in data: + handler.write_text("Device id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Device id not specified.") + return False + + if 'id' not in data: + handler.write_text("Location id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Location id not specified.") + return False + + if 'trigger' not in data: + handler.write_text("Trigger is not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Trigger is not specified.") + return False + + return True diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 5d20e98e992..ab1eccba769 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear==0.3'] +REQUIREMENTS = ['pynetgear==0.3.1'] def get_scanner(hass, config): diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index b98c3a1636c..e1b0e1de306 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -10,14 +10,17 @@ import json import logging import homeassistant.components.mqtt as mqtt +from homeassistant.const import (STATE_HOME, STATE_NOT_HOME) DEPENDENCIES = ['mqtt'] +CONF_TRANSITION_EVENTS = 'use_events' LOCATION_TOPIC = 'owntracks/+/+' +EVENT_TOPIC = 'owntracks/+/+/event' def setup_scanner(hass, config, see): - """ Set up a OwnTracksks tracker. """ + """ Set up an OwnTracks tracker. """ def owntracks_location_update(topic, payload, qos): """ MQTT message received. """ @@ -48,6 +51,56 @@ def setup_scanner(hass, config, see): see(**kwargs) - mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) + def owntracks_event_update(topic, payload, qos): + """ MQTT event (geofences) received. """ + + # Docs on available data: + # http://owntracks.org/booklet/tech/json/#_typetransition + try: + data = json.loads(payload) + except ValueError: + # If invalid JSON + logging.getLogger(__name__).error( + 'Unable to parse payload as JSON: %s', payload) + return + + if not isinstance(data, dict) or data.get('_type') != 'transition': + return + + # check if in "home" fence or other zone + location = '' + if data['event'] == 'enter': + + if data['desc'].lower() == 'home': + location = STATE_HOME + else: + location = data['desc'] + + elif data['event'] == 'leave': + location = STATE_NOT_HOME + else: + logging.getLogger(__name__).error( + 'Misformatted mqtt msgs, _type=transition, event=%s', + data['event']) + return + + parts = topic.split('/') + kwargs = { + 'dev_id': '{}_{}'.format(parts[1], parts[2]), + 'host_name': parts[1], + 'gps': (data['lat'], data['lon']), + 'location_name': location, + } + if 'acc' in data: + kwargs['gps_accuracy'] = data['acc'] + + see(**kwargs) + + use_events = config.get(CONF_TRANSITION_EVENTS) + + if use_events: + mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) + else: + mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) return True diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index e69de29bb2d..dc573ae0275 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -0,0 +1,33 @@ +# Describes the format for available device tracker services + +see: + description: Control tracked device + + fields: + mac: + description: MAC address of device + example: 'FF:FF:FF:FF:FF:FF' + + dev_id: + description: Id of device (find id in known_devices.yaml) + example: 'phonedave' + + host_name: + description: Hostname of device + example: 'Dave' + + location_name: + description: Name of location where device is located (not_home is away) + example: 'home' + + gps: + description: GPS coordinates where device is located (latitude, longitude) + example: '[51.509802, -0.086692]' + + gps_accuracy: + description: Accuracy of GPS coordinates + example: '80' + + battery: + description: Battery level of device + example: '100' diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 868f701673a..cd0e8239c38 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -105,8 +105,7 @@ class SnmpScanner(object): return if errstatus: _LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(), - errindex and restable[-1][int(errindex)-1] - or '?') + errindex and restable[-1][int(errindex)-1] or '?') return for resrow in restable: diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 46556b3eca4..a661dac0c1e 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -242,8 +242,8 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): _LOGGER.info("Loading wireless clients...") - url = 'http://{}/cgi-bin/luci/;stok={}/admin/wireless?form=statistics' \ - .format(self.host, self.stok) + url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?' + 'form=statistics').format(self.host, self.stok) referer = 'http://{}/webpages/index.html'.format(self.host) response = requests.post(url, diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 64845a350ca..67454d11974 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "be08c5a3ce12040bbdba2db83cb1a568" +VERSION = "63d38b69fc6582e75f892abc140a893a" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 8df0a4724a0..86e5daca0aa 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -275,7 +275,7 @@ var n=this._rootDataHost;return n?n._scopeElementClass(t,e):void 0},stamp:functi html /deep/ .fixed-right { top: 0; right: 0; - bottom: 0; + botttom: 0; } html /deep/ .fixed-bottom { @@ -286,7 +286,7 @@ var n=this._rootDataHost;return n?n._scopeElementClass(t,e):void 0},stamp:functi html /deep/ .fixed-left { top: 0; - bottom: 0; + botttom: 0; left: 0; }
\ No newline at end of file +o["default"])({SELECT_ENTITY:null,LOG_OUT:null})},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({moreInfoEntityId:u["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.getters=e.actions=void 0,e.register=o;var a=n(155),u=i(a),s=n(153),c=r(s),l=n(154),f=r(l);e.actions=c,e.getters=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(u["default"].SHOW_SIDEBAR,{show:e})}function o(t,e){t.dispatch(u["default"].NAVIGATE,{pane:e})}Object.defineProperty(e,"__esModule",{value:!0}),e.showSidebar=i,e.navigate=o;var a=n(26),u=r(a)},function(t,e){"use strict";function n(t){return[r,function(e){return e===t}]}Object.defineProperty(e,"__esModule",{value:!0}),e.isActivePane=n;var r=e.activePane=["selectedNavigationPanel"];e.showSidebar=["showSidebar"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({selectedNavigationPanel:u["default"],showSidebar:c["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.urlSync=e.getters=e.actions=void 0,e.register=o;var a=n(156),u=i(a),s=n(157),c=i(s),l=n(48),f=r(l),d=n(49),h=r(d),p=n(158),_=r(p);e.actions=f,e.getters=h,e.urlSync=_},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(6),o=r(i);e["default"]=(0,o["default"])({NOTIFICATION_CREATED:null})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(6),o=r(i);e["default"]=(0,o["default"])({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return[h(t),function(t){return!!t&&t.services.has(e)}]}function o(t){return[u.getters.byId(t),d,f["default"]]}Object.defineProperty(e,"__esModule",{value:!0}),e.byDomain=e.entityMap=e.hasData=void 0,e.hasService=i,e.canToggleEntity=o;var a=n(10),u=n(9),s=n(54),c=r(s),l=n(168),f=r(l),d=(e.hasData=(0,a.createHasDataGetter)(c["default"]),e.entityMap=(0,a.createEntityMapGetter)(c["default"])),h=e.byDomain=(0,a.createByIdGetter)(c["default"])},function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function o(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a=function(){function t(t,e){for(var n=0;n6e4}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n},function(t,e,n){function r(t,e,n){function r(){y&&clearTimeout(y),h&&clearTimeout(h),g=0,h=y=m=void 0}function s(e,n){n&&clearTimeout(n),h=y=m=void 0,e&&(g=o(),p=t.apply(v,d),y||h||(d=v=void 0))}function c(){var t=e-(o()-_);0>=t||t>e?s(m,h):y=setTimeout(c,t)}function l(){s(S,y)}function f(){if(d=arguments,_=o(),v=this,m=S&&(y||!w),b===!1)var n=w&&!y;else{h||w||(g=_);var r=b-(_-g),i=0>=r||r>b;i?(h&&(h=clearTimeout(h)),g=_,p=t.apply(v,d)):h||(h=setTimeout(l,r))}return i&&y?y=clearTimeout(y):y||e===b||(y=setTimeout(c,e)),n&&(i=!0,p=t.apply(v,d)),!i||y||h||(d=v=void 0),p}var d,h,p,_,v,y,m,g=0,b=!1,S=!0;if("function"!=typeof t)throw new TypeError(a);if(e=0>e?0:+e||0,n===!0){var w=!0;S=!1}else i(n)&&(w=!!n.leading,b="maxWait"in n&&u(+n.maxWait||0,e),S="trailing"in n?!!n.trailing:S);return f.cancel=r,f}var i=n(63),o=n(188),a="Expected a function",u=Math.max;t.exports=r},function(t,e,n){function r(t,e){var n=null==t?void 0:t[e];return i(n)?n:void 0}var i=n(191);t.exports=r},function(t,e){function n(t){return!!t&&"object"==typeof t}t.exports=n},function(t,e,n){function r(t){return i(t)&&u.call(t)==o}var i=n(63),o="[object Function]",a=Object.prototype,u=a.toString;t.exports=r},function(t,e){function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){(function(t){!function(e,n){t.exports=n()}(this,function(){"use strict";function e(){return Nn.apply(null,arguments)}function n(t){Nn=t}function r(t){return"[object Array]"===Object.prototype.toString.call(t)}function i(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function o(t,e){var n,r=[];for(n=0;n0)for(n in zn)r=zn[n],i=e[r],"undefined"!=typeof i&&(t[r]=i);return t}function p(t){h(this,t),this._d=new Date(null!=t._d?t._d.getTime():NaN),xn===!1&&(xn=!0,e.updateOffset(this),xn=!1)}function _(t){return t instanceof p||null!=t&&null!=t._isAMomentObject}function v(t){return 0>t?Math.ceil(t):Math.floor(t)}function y(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=v(e)),n}function m(t,e,n){var r,i=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),a=0;for(r=0;i>r;r++)(n&&t[r]!==e[r]||!n&&y(t[r])!==y(e[r]))&&a++;return a+o}function g(){}function b(t){return t?t.toLowerCase().replace("_","-"):t}function S(t){for(var e,n,r,i,o=0;o0;){if(r=w(i.slice(0,e).join("-")))return r;if(n&&n.length>=e&&m(i,n,!0)>=e-1)break;e--}o++}return null}function w(e){var n=null;if(!Hn[e]&&"undefined"!=typeof t&&t&&t.exports)try{n=Rn._abbr,!function(){var t=new Error('Cannot find module "./locale"');throw t.code="MODULE_NOT_FOUND",t}(),O(n)}catch(r){}return Hn[e]}function O(t,e){var n;return t&&(n="undefined"==typeof e?T(t):M(t,e),n&&(Rn=n)),Rn._abbr}function M(t,e){return null!==e?(e.abbr=t,Hn[t]=Hn[t]||new g,Hn[t].set(e),O(t),Hn[t]):(delete Hn[t],null)}function T(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return Rn;if(!r(t)){if(e=w(t))return e;t=[t]}return S(t)}function I(t,e){var n=t.toLowerCase();Yn[n]=Yn[n+"s"]=Yn[e]=t}function E(t){return"string"==typeof t?Yn[t]||Yn[t.toLowerCase()]:void 0}function D(t){var e,n,r={};for(n in t)a(t,n)&&(e=E(n),e&&(r[e]=t[n]));return r}function C(t,n){return function(r){return null!=r?(A(this,t,r),e.updateOffset(this,n),this):j(this,t)}}function j(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function A(t,e,n){return t._d["set"+(t._isUTC?"UTC":"")+e](n)}function P(t,e){var n;if("object"==typeof t)for(n in t)this.set(n,t[n]);else if(t=E(t),"function"==typeof this[t])return this[t](e);return this}function k(t,e,n){var r=""+Math.abs(t),i=e-r.length,o=t>=0;return(o?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+r}function L(t,e,n,r){var i=r;"string"==typeof r&&(i=function(){return this[r]()}),t&&(Fn[t]=i),e&&(Fn[e[0]]=function(){return k(i.apply(this,arguments),e[1],e[2])}),n&&(Fn[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),t)})}function N(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function R(t){var e,n,r=t.match(Un);for(e=0,n=r.length;n>e;e++)Fn[r[e]]?r[e]=Fn[r[e]]:r[e]=N(r[e]);return function(i){var o="";for(e=0;n>e;e++)o+=r[e]instanceof Function?r[e].call(i,t):r[e];return o}}function z(t,e){return t.isValid()?(e=x(e,t.localeData()),Bn[e]=Bn[e]||R(e),Bn[e](t)):t.localeData().invalidDate()}function x(t,e){function n(t){return e.longDateFormat(t)||t}var r=5;for(Gn.lastIndex=0;r>=0&&Gn.test(t);)t=t.replace(Gn,n),Gn.lastIndex=0,r-=1;return t}function H(t){return"function"==typeof t&&"[object Function]"===Object.prototype.toString.call(t)}function Y(t,e,n){or[t]=H(e)?e:function(t){return t&&n?n:e}}function U(t,e){return a(or,t)?or[t](e._strict,e._locale):new RegExp(G(t))}function G(t){return t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,r,i){return e||n||r||i}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function B(t,e){var n,r=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(r=function(t,n){n[e]=y(t)}),n=0;nr;r++){if(i=s([2e3,r]),n&&!this._longMonthsParse[r]&&(this._longMonthsParse[r]=new RegExp("^"+this.months(i,"").replace(".","")+"$","i"),this._shortMonthsParse[r]=new RegExp("^"+this.monthsShort(i,"").replace(".","")+"$","i")),n||this._monthsParse[r]||(o="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[r]=new RegExp(o.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[r].test(t))return r;if(n&&"MMM"===e&&this._shortMonthsParse[r].test(t))return r;if(!n&&this._monthsParse[r].test(t))return r}}function $(t,e){var n;return"string"==typeof e&&(e=t.localeData().monthsParse(e),"number"!=typeof e)?t:(n=Math.min(t.date(),q(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t)}function Z(t){return null!=t?($(this,t),e.updateOffset(this,!0),this):j(this,"Month")}function X(){return q(this.year(),this.month())}function Q(t){var e,n=t._a;return n&&-2===l(t).overflow&&(e=n[sr]<0||n[sr]>11?sr:n[cr]<1||n[cr]>q(n[ur],n[sr])?cr:n[lr]<0||n[lr]>24||24===n[lr]&&(0!==n[fr]||0!==n[dr]||0!==n[hr])?lr:n[fr]<0||n[fr]>59?fr:n[dr]<0||n[dr]>59?dr:n[hr]<0||n[hr]>999?hr:-1,l(t)._overflowDayOfYear&&(ur>e||e>cr)&&(e=cr),l(t).overflow=e),t}function tt(t){e.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}function et(t,e){var n=!0;return u(function(){return n&&(tt(t+"\n"+(new Error).stack),n=!1),e.apply(this,arguments)},e)}function nt(t,e){vr[t]||(tt(e),vr[t]=!0)}function rt(t){var e,n,r=t._i,i=yr.exec(r);if(i){for(l(t).iso=!0,e=0,n=mr.length;n>e;e++)if(mr[e][1].exec(r)){t._f=mr[e][0];break}for(e=0,n=gr.length;n>e;e++)if(gr[e][1].exec(r)){t._f+=(i[6]||" ")+gr[e][0];break}r.match(nr)&&(t._f+="Z"),wt(t)}else t._isValid=!1}function it(t){var n=br.exec(t._i);return null!==n?void(t._d=new Date(+n[1])):(rt(t),void(t._isValid===!1&&(delete t._isValid,e.createFromInputFallback(t))))}function ot(t,e,n,r,i,o,a){var u=new Date(t,e,n,r,i,o,a);return 1970>t&&u.setFullYear(t),u}function at(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function ut(t){return st(t)?366:365}function st(t){return t%4===0&&t%100!==0||t%400===0}function ct(){return st(this.year())}function lt(t,e,n){var r,i=n-e,o=n-t.day();return o>i&&(o-=7),i-7>o&&(o+=7),r=jt(t).add(o,"d"),{week:Math.ceil(r.dayOfYear()/7),year:r.year()}}function ft(t){return lt(t,this._week.dow,this._week.doy).week}function dt(){return this._week.dow}function ht(){return this._week.doy}function pt(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function _t(t){var e=lt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function vt(t,e,n,r,i){var o,a=6+i-r,u=at(t,0,1+a),s=u.getUTCDay();return i>s&&(s+=7),n=null!=n?1*n:i,o=1+a+7*(e-1)-s+n,{year:o>0?t:t-1,dayOfYear:o>0?o:ut(t-1)+o}}function yt(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function mt(t,e,n){return null!=t?t:null!=e?e:n}function gt(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function bt(t){var e,n,r,i,o=[];if(!t._d){for(r=gt(t),t._w&&null==t._a[cr]&&null==t._a[sr]&&St(t),t._dayOfYear&&(i=mt(t._a[ur],r[ur]),t._dayOfYear>ut(i)&&(l(t)._overflowDayOfYear=!0),n=at(i,0,t._dayOfYear),t._a[sr]=n.getUTCMonth(),t._a[cr]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=o[e]=r[e];for(;7>e;e++)t._a[e]=o[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[lr]&&0===t._a[fr]&&0===t._a[dr]&&0===t._a[hr]&&(t._nextDay=!0,t._a[lr]=0),t._d=(t._useUTC?at:ot).apply(null,o),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[lr]=24)}}function St(t){var e,n,r,i,o,a,u;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(o=1,a=4,n=mt(e.GG,t._a[ur],lt(jt(),1,4).year),r=mt(e.W,1),i=mt(e.E,1)):(o=t._locale._week.dow,a=t._locale._week.doy,n=mt(e.gg,t._a[ur],lt(jt(),o,a).year),r=mt(e.w,1),null!=e.d?(i=e.d,o>i&&++r):i=null!=e.e?e.e+o:o),u=vt(n,r,i,a,o),t._a[ur]=u.year,t._dayOfYear=u.dayOfYear}function wt(t){if(t._f===e.ISO_8601)return void rt(t);t._a=[],l(t).empty=!0;var n,r,i,o,a,u=""+t._i,s=u.length,c=0;for(i=x(t._f,t._locale).match(Un)||[],n=0;n0&&l(t).unusedInput.push(a),u=u.slice(u.indexOf(r)+r.length),c+=r.length),Fn[o]?(r?l(t).empty=!1:l(t).unusedTokens.push(o),V(o,r,t)):t._strict&&!r&&l(t).unusedTokens.push(o);l(t).charsLeftOver=s-c,u.length>0&&l(t).unusedInput.push(u),l(t).bigHour===!0&&t._a[lr]<=12&&t._a[lr]>0&&(l(t).bigHour=void 0),t._a[lr]=Ot(t._locale,t._a[lr],t._meridiem),bt(t),Q(t)}function Ot(t,e,n){var r;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(r=t.isPM(n),r&&12>e&&(e+=12),r||12!==e||(e=0),e):e}function Mt(t){var e,n,r,i,o;if(0===t._f.length)return l(t).invalidFormat=!0,void(t._d=new Date(NaN));for(i=0;io)&&(r=o,n=e));u(t,n||e)}function Tt(t){if(!t._d){var e=D(t._i);t._a=[e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],bt(t)}}function It(t){var e=new p(Q(Et(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function Et(t){var e=t._i,n=t._f;return t._locale=t._locale||T(t._l),null===e||void 0===n&&""===e?d({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),_(e)?new p(Q(e)):(r(n)?Mt(t):n?wt(t):i(e)?t._d=e:Dt(t),t))}function Dt(t){var n=t._i;void 0===n?t._d=new Date:i(n)?t._d=new Date(+n):"string"==typeof n?it(t):r(n)?(t._a=o(n.slice(0),function(t){return parseInt(t,10)}),bt(t)):"object"==typeof n?Tt(t):"number"==typeof n?t._d=new Date(n):e.createFromInputFallback(t)}function Ct(t,e,n,r,i){var o={};return"boolean"==typeof n&&(r=n,n=void 0),o._isAMomentObject=!0,o._useUTC=o._isUTC=i,o._l=n,o._i=t,o._f=e,o._strict=r,It(o)}function jt(t,e,n,r){return Ct(t,e,n,r,!1)}function At(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return jt();for(n=e[0],i=1;it&&(t=-t,n="-"),n+k(~~(t/60),2)+e+k(~~t%60,2)})}function zt(t){var e=(t||"").match(nr)||[],n=e[e.length-1]||[],r=(n+"").match(Tr)||["-",0,0],i=+(60*r[1])+y(r[2]);return"+"===r[0]?i:-i}function xt(t,n){var r,o;return n._isUTC?(r=n.clone(),o=(_(t)||i(t)?+t:+jt(t))-+r,r._d.setTime(+r._d+o),e.updateOffset(r,!1),r):jt(t).local()}function Ht(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Yt(t,n){var r,i=this._offset||0;return null!=t?("string"==typeof t&&(t=zt(t)),Math.abs(t)<16&&(t=60*t),!this._isUTC&&n&&(r=Ht(this)),this._offset=t,this._isUTC=!0,null!=r&&this.add(r,"m"),i!==t&&(!n||this._changeInProgress?ne(this,Zt(t-i,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,e.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?i:Ht(this)}function Ut(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Gt(t){return this.utcOffset(0,t)}function Bt(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ht(this),"m")),this}function Ft(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(zt(this._i)),this}function Vt(t){return t=t?jt(t).utcOffset():0,(this.utcOffset()-t)%60===0}function qt(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Wt(){if("undefined"!=typeof this._isDSTShifted)return this._isDSTShifted;var t={};if(h(t,this),t=Et(t),t._a){var e=t._isUTC?s(t._a):jt(t._a);this._isDSTShifted=this.isValid()&&m(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Kt(){return!this._isUTC}function Jt(){return this._isUTC}function $t(){return this._isUTC&&0===this._offset}function Zt(t,e){var n,r,i,o=t,u=null;return Nt(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(u=Ir.exec(t))?(n="-"===u[1]?-1:1,o={y:0,d:y(u[cr])*n,h:y(u[lr])*n,m:y(u[fr])*n,s:y(u[dr])*n,ms:y(u[hr])*n}):(u=Er.exec(t))?(n="-"===u[1]?-1:1,o={y:Xt(u[2],n),M:Xt(u[3],n),d:Xt(u[4],n),h:Xt(u[5],n),m:Xt(u[6],n),s:Xt(u[7],n),w:Xt(u[8],n)}):null==o?o={}:"object"==typeof o&&("from"in o||"to"in o)&&(i=te(jt(o.from),jt(o.to)),o={},o.ms=i.milliseconds,o.M=i.months),r=new Lt(o),Nt(t)&&a(t,"_locale")&&(r._locale=t._locale),r}function Xt(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Qt(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function te(t,e){var n;return e=xt(e,t),t.isBefore(e)?n=Qt(t,e):(n=Qt(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n}function ee(t,e){return function(n,r){var i,o;return null===r||isNaN(+r)||(nt(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period)."),o=n,n=r,r=o),n="string"==typeof n?+n:n,i=Zt(n,r),ne(this,i,t),this}}function ne(t,n,r,i){var o=n._milliseconds,a=n._days,u=n._months;i=null==i?!0:i,o&&t._d.setTime(+t._d+o*r),a&&A(t,"Date",j(t,"Date")+a*r),u&&$(t,j(t,"Month")+u*r),i&&e.updateOffset(t,a||u)}function re(t,e){var n=t||jt(),r=xt(n,this).startOf("day"),i=this.diff(r,"days",!0),o=-6>i?"sameElse":-1>i?"lastWeek":0>i?"lastDay":1>i?"sameDay":2>i?"nextDay":7>i?"nextWeek":"sameElse";return this.format(e&&e[o]||this.localeData().calendar(o,this,jt(n)))}function ie(){return new p(this)}function oe(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=_(t)?t:jt(t),+this>+t):(n=_(t)?+t:+jt(t),n<+this.clone().startOf(e))}function ae(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=_(t)?t:jt(t),+t>+this):(n=_(t)?+t:+jt(t),+this.clone().endOf(e)e-o?(n=t.clone().add(i-1,"months"),r=(e-o)/(o-n)):(n=t.clone().add(i+1,"months"),r=(e-o)/(n-o)),-(i+r)}function fe(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function de(){var t=this.clone().utc();return 0e;e++)if(this._weekdaysParse[e]||(n=jt([2e3,1]).day(e),r="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(r.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e}function Ge(t){var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=ze(t,this.localeData()),this.add(t-e,"d")):e}function Be(t){var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function Fe(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)}function Ve(t,e){L(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function qe(t,e){return e._meridiemParse}function We(t){return"p"===(t+"").toLowerCase().charAt(0)}function Ke(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Je(t,e){e[hr]=y(1e3*("0."+t))}function $e(){return this._isUTC?"UTC":""}function Ze(){return this._isUTC?"Coordinated Universal Time":""}function Xe(t){return jt(1e3*t)}function Qe(){return jt.apply(null,arguments).parseZone()}function tn(t,e,n){var r=this._calendar[t];return"function"==typeof r?r.call(e,n):r}function en(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])}function nn(){return this._invalidDate}function rn(t){return this._ordinal.replace("%d",t)}function on(t){return t}function an(t,e,n,r){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,r):i.replace(/%d/i,t)}function un(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)}function sn(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function cn(t,e,n,r){var i=T(),o=s().set(r,e);return i[n](o,t)}function ln(t,e,n,r,i){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return cn(t,e,n,i);var o,a=[];for(o=0;r>o;o++)a[o]=cn(t,o,n,i);return a}function fn(t,e){return ln(t,e,"months",12,"month")}function dn(t,e){return ln(t,e,"monthsShort",12,"month")}function hn(t,e){return ln(t,e,"weekdays",7,"day")}function pn(t,e){return ln(t,e,"weekdaysShort",7,"day")}function _n(t,e){return ln(t,e,"weekdaysMin",7,"day")}function vn(){var t=this._data;return this._milliseconds=$r(this._milliseconds),this._days=$r(this._days),this._months=$r(this._months),t.milliseconds=$r(t.milliseconds),t.seconds=$r(t.seconds),t.minutes=$r(t.minutes),t.hours=$r(t.hours),t.months=$r(t.months),t.years=$r(t.years),this}function yn(t,e,n,r){var i=Zt(e,n);return t._milliseconds+=r*i._milliseconds,t._days+=r*i._days,t._months+=r*i._months,t._bubble()}function mn(t,e){return yn(this,t,e,1)}function gn(t,e){return yn(this,t,e,-1)}function bn(t){return 0>t?Math.floor(t):Math.ceil(t)}function Sn(){var t,e,n,r,i,o=this._milliseconds,a=this._days,u=this._months,s=this._data;return o>=0&&a>=0&&u>=0||0>=o&&0>=a&&0>=u||(o+=864e5*bn(On(u)+a),a=0,u=0),s.milliseconds=o%1e3,t=v(o/1e3),s.seconds=t%60,e=v(t/60),s.minutes=e%60,n=v(e/60),s.hours=n%24,a+=v(n/24),i=v(wn(a)),u+=i,a-=bn(On(i)),r=v(u/12),u%=12,s.days=a,s.months=u,s.years=r,this}function wn(t){return 4800*t/146097}function On(t){return 146097*t/4800}function Mn(t){var e,n,r=this._milliseconds;if(t=E(t),"month"===t||"year"===t)return e=this._days+r/864e5,n=this._months+wn(e),"month"===t?n:n/12;switch(e=this._days+Math.round(On(this._months)),t){case"week":return e/7+r/6048e5;case"day":return e+r/864e5;case"hour":return 24*e+r/36e5;case"minute":return 1440*e+r/6e4;case"second":return 86400*e+r/1e3;case"millisecond":return Math.floor(864e5*e)+r;default:throw new Error("Unknown unit "+t)}}function Tn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*y(this._months/12)}function In(t){return function(){return this.as(t)}}function En(t){return t=E(t),this[t+"s"]()}function Dn(t){return function(){return this._data[t]}}function Cn(){return v(this.days()/7)}function jn(t,e,n,r,i){return i.relativeTime(e||1,!!n,t,r)}function An(t,e,n){var r=Zt(t).abs(),i=di(r.as("s")),o=di(r.as("m")),a=di(r.as("h")),u=di(r.as("d")),s=di(r.as("M")),c=di(r.as("y")),l=i0,l[4]=n,jn.apply(null,l)}function Pn(t,e){return void 0===hi[t]?!1:void 0===e?hi[t]:(hi[t]=e,!0)}function kn(t){var e=this.localeData(),n=An(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function Ln(){var t,e,n,r=pi(this._milliseconds)/1e3,i=pi(this._days),o=pi(this._months);t=v(r/60),e=v(t/60),r%=60,t%=60,n=v(o/12),o%=12;var a=n,u=o,s=i,c=e,l=t,f=r,d=this.asSeconds();return d?(0>d?"-":"")+"P"+(a?a+"Y":"")+(u?u+"M":"")+(s?s+"D":"")+(c||l||f?"T":"")+(c?c+"H":"")+(l?l+"M":"")+(f?f+"S":""):"P0D"}var Nn,Rn,zn=e.momentProperties=[],xn=!1,Hn={},Yn={},Un=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Gn=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Bn={},Fn={},Vn=/\d/,qn=/\d\d/,Wn=/\d{3}/,Kn=/\d{4}/,Jn=/[+-]?\d{6}/,$n=/\d\d?/,Zn=/\d{1,3}/,Xn=/\d{1,4}/,Qn=/[+-]?\d{1,6}/,tr=/\d+/,er=/[+-]?\d+/,nr=/Z|[+-]\d\d:?\d\d/gi,rr=/[+-]?\d+(\.\d{1,3})?/,ir=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,or={},ar={},ur=0,sr=1,cr=2,lr=3,fr=4,dr=5,hr=6;L("M",["MM",2],"Mo",function(){return this.month()+1}),L("MMM",0,0,function(t){return this.localeData().monthsShort(this,t)}),L("MMMM",0,0,function(t){return this.localeData().months(this,t)}),I("month","M"),Y("M",$n),Y("MM",$n,qn),Y("MMM",ir),Y("MMMM",ir),B(["M","MM"],function(t,e){e[sr]=y(t)-1}),B(["MMM","MMMM"],function(t,e,n,r){var i=n._locale.monthsParse(t,r,n._strict);null!=i?e[sr]=i:l(n).invalidMonth=t});var pr="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),_r="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),vr={}; +e.suppressDeprecationWarnings=!1;var yr=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,mr=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],gr=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],br=/^\/?Date\((\-?\d+)/i;e.createFromInputFallback=et("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),L(0,["YY",2],0,function(){return this.year()%100}),L(0,["YYYY",4],0,"year"),L(0,["YYYYY",5],0,"year"),L(0,["YYYYYY",6,!0],0,"year"),I("year","y"),Y("Y",er),Y("YY",$n,qn),Y("YYYY",Xn,Kn),Y("YYYYY",Qn,Jn),Y("YYYYYY",Qn,Jn),B(["YYYYY","YYYYYY"],ur),B("YYYY",function(t,n){n[ur]=2===t.length?e.parseTwoDigitYear(t):y(t)}),B("YY",function(t,n){n[ur]=e.parseTwoDigitYear(t)}),e.parseTwoDigitYear=function(t){return y(t)+(y(t)>68?1900:2e3)};var Sr=C("FullYear",!1);L("w",["ww",2],"wo","week"),L("W",["WW",2],"Wo","isoWeek"),I("week","w"),I("isoWeek","W"),Y("w",$n),Y("ww",$n,qn),Y("W",$n),Y("WW",$n,qn),F(["w","ww","W","WW"],function(t,e,n,r){e[r.substr(0,1)]=y(t)});var wr={dow:0,doy:6};L("DDD",["DDDD",3],"DDDo","dayOfYear"),I("dayOfYear","DDD"),Y("DDD",Zn),Y("DDDD",Wn),B(["DDD","DDDD"],function(t,e,n){n._dayOfYear=y(t)}),e.ISO_8601=function(){};var Or=et("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var t=jt.apply(null,arguments);return this>t?this:t}),Mr=et("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var t=jt.apply(null,arguments);return t>this?this:t});Rt("Z",":"),Rt("ZZ",""),Y("Z",nr),Y("ZZ",nr),B(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=zt(t)});var Tr=/([\+\-]|\d\d)/gi;e.updateOffset=function(){};var Ir=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Er=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Zt.fn=Lt.prototype;var Dr=ee(1,"add"),Cr=ee(-1,"subtract");e.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var jr=et("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});L(0,["gg",2],0,function(){return this.weekYear()%100}),L(0,["GG",2],0,function(){return this.isoWeekYear()%100}),je("gggg","weekYear"),je("ggggg","weekYear"),je("GGGG","isoWeekYear"),je("GGGGG","isoWeekYear"),I("weekYear","gg"),I("isoWeekYear","GG"),Y("G",er),Y("g",er),Y("GG",$n,qn),Y("gg",$n,qn),Y("GGGG",Xn,Kn),Y("gggg",Xn,Kn),Y("GGGGG",Qn,Jn),Y("ggggg",Qn,Jn),F(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,r){e[r.substr(0,2)]=y(t)}),F(["gg","GG"],function(t,n,r,i){n[i]=e.parseTwoDigitYear(t)}),L("Q",0,0,"quarter"),I("quarter","Q"),Y("Q",Vn),B("Q",function(t,e){e[sr]=3*(y(t)-1)}),L("D",["DD",2],"Do","date"),I("date","D"),Y("D",$n),Y("DD",$n,qn),Y("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),B(["D","DD"],cr),B("Do",function(t,e){e[cr]=y(t.match($n)[0],10)});var Ar=C("Date",!0);L("d",0,"do","day"),L("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),L("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),L("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),L("e",0,0,"weekday"),L("E",0,0,"isoWeekday"),I("day","d"),I("weekday","e"),I("isoWeekday","E"),Y("d",$n),Y("e",$n),Y("E",$n),Y("dd",ir),Y("ddd",ir),Y("dddd",ir),F(["dd","ddd","dddd"],function(t,e,n){var r=n._locale.weekdaysParse(t);null!=r?e.d=r:l(n).invalidWeekday=t}),F(["d","e","E"],function(t,e,n,r){e[r]=y(t)});var Pr="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),kr="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Lr="Su_Mo_Tu_We_Th_Fr_Sa".split("_");L("H",["HH",2],0,"hour"),L("h",["hh",2],0,function(){return this.hours()%12||12}),Ve("a",!0),Ve("A",!1),I("hour","h"),Y("a",qe),Y("A",qe),Y("H",$n),Y("h",$n),Y("HH",$n,qn),Y("hh",$n,qn),B(["H","HH"],lr),B(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),B(["h","hh"],function(t,e,n){e[lr]=y(t),l(n).bigHour=!0});var Nr=/[ap]\.?m?\.?/i,Rr=C("Hours",!0);L("m",["mm",2],0,"minute"),I("minute","m"),Y("m",$n),Y("mm",$n,qn),B(["m","mm"],fr);var zr=C("Minutes",!1);L("s",["ss",2],0,"second"),I("second","s"),Y("s",$n),Y("ss",$n,qn),B(["s","ss"],dr);var xr=C("Seconds",!1);L("S",0,0,function(){return~~(this.millisecond()/100)}),L(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),L(0,["SSS",3],0,"millisecond"),L(0,["SSSS",4],0,function(){return 10*this.millisecond()}),L(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),L(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),L(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),L(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),L(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),I("millisecond","ms"),Y("S",Zn,Vn),Y("SS",Zn,qn),Y("SSS",Zn,Wn);var Hr;for(Hr="SSSS";Hr.length<=9;Hr+="S")Y(Hr,tr);for(Hr="S";Hr.length<=9;Hr+="S")B(Hr,Je);var Yr=C("Milliseconds",!1);L("z",0,0,"zoneAbbr"),L("zz",0,0,"zoneName");var Ur=p.prototype;Ur.add=Dr,Ur.calendar=re,Ur.clone=ie,Ur.diff=ce,Ur.endOf=Se,Ur.format=he,Ur.from=pe,Ur.fromNow=_e,Ur.to=ve,Ur.toNow=ye,Ur.get=P,Ur.invalidAt=Ce,Ur.isAfter=oe,Ur.isBefore=ae,Ur.isBetween=ue,Ur.isSame=se,Ur.isValid=Ee,Ur.lang=jr,Ur.locale=me,Ur.localeData=ge,Ur.max=Mr,Ur.min=Or,Ur.parsingFlags=De,Ur.set=P,Ur.startOf=be,Ur.subtract=Cr,Ur.toArray=Te,Ur.toObject=Ie,Ur.toDate=Me,Ur.toISOString=de,Ur.toJSON=de,Ur.toString=fe,Ur.unix=Oe,Ur.valueOf=we,Ur.year=Sr,Ur.isLeapYear=ct,Ur.weekYear=Pe,Ur.isoWeekYear=ke,Ur.quarter=Ur.quarters=Re,Ur.month=Z,Ur.daysInMonth=X,Ur.week=Ur.weeks=pt,Ur.isoWeek=Ur.isoWeeks=_t,Ur.weeksInYear=Ne,Ur.isoWeeksInYear=Le,Ur.date=Ar,Ur.day=Ur.days=Ge,Ur.weekday=Be,Ur.isoWeekday=Fe,Ur.dayOfYear=yt,Ur.hour=Ur.hours=Rr,Ur.minute=Ur.minutes=zr,Ur.second=Ur.seconds=xr,Ur.millisecond=Ur.milliseconds=Yr,Ur.utcOffset=Yt,Ur.utc=Gt,Ur.local=Bt,Ur.parseZone=Ft,Ur.hasAlignedHourOffset=Vt,Ur.isDST=qt,Ur.isDSTShifted=Wt,Ur.isLocal=Kt,Ur.isUtcOffset=Jt,Ur.isUtc=$t,Ur.isUTC=$t,Ur.zoneAbbr=$e,Ur.zoneName=Ze,Ur.dates=et("dates accessor is deprecated. Use date instead.",Ar),Ur.months=et("months accessor is deprecated. Use month instead",Z),Ur.years=et("years accessor is deprecated. Use year instead",Sr),Ur.zone=et("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Ut);var Gr=Ur,Br={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Fr={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Vr="Invalid date",qr="%d",Wr=/\d{1,2}/,Kr={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Jr=g.prototype;Jr._calendar=Br,Jr.calendar=tn,Jr._longDateFormat=Fr,Jr.longDateFormat=en,Jr._invalidDate=Vr,Jr.invalidDate=nn,Jr._ordinal=qr,Jr.ordinal=rn,Jr._ordinalParse=Wr,Jr.preparse=on,Jr.postformat=on,Jr._relativeTime=Kr,Jr.relativeTime=an,Jr.pastFuture=un,Jr.set=sn,Jr.months=W,Jr._months=pr,Jr.monthsShort=K,Jr._monthsShort=_r,Jr.monthsParse=J,Jr.week=ft,Jr._week=wr,Jr.firstDayOfYear=ht,Jr.firstDayOfWeek=dt,Jr.weekdays=xe,Jr._weekdays=Pr,Jr.weekdaysMin=Ye,Jr._weekdaysMin=Lr,Jr.weekdaysShort=He,Jr._weekdaysShort=kr,Jr.weekdaysParse=Ue,Jr.isPM=We,Jr._meridiemParse=Nr,Jr.meridiem=Ke,O("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===y(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),e.lang=et("moment.lang is deprecated. Use moment.locale instead.",O),e.langData=et("moment.langData is deprecated. Use moment.localeData instead.",T);var $r=Math.abs,Zr=In("ms"),Xr=In("s"),Qr=In("m"),ti=In("h"),ei=In("d"),ni=In("w"),ri=In("M"),ii=In("y"),oi=Dn("milliseconds"),ai=Dn("seconds"),ui=Dn("minutes"),si=Dn("hours"),ci=Dn("days"),li=Dn("months"),fi=Dn("years"),di=Math.round,hi={s:45,m:45,h:22,d:26,M:11},pi=Math.abs,_i=Lt.prototype;_i.abs=vn,_i.add=mn,_i.subtract=gn,_i.as=Mn,_i.asMilliseconds=Zr,_i.asSeconds=Xr,_i.asMinutes=Qr,_i.asHours=ti,_i.asDays=ei,_i.asWeeks=ni,_i.asMonths=ri,_i.asYears=ii,_i.valueOf=Tn,_i._bubble=Sn,_i.get=En,_i.milliseconds=oi,_i.seconds=ai,_i.minutes=ui,_i.hours=si,_i.days=ci,_i.weeks=Cn,_i.months=li,_i.years=fi,_i.humanize=kn,_i.toISOString=Ln,_i.toString=Ln,_i.toJSON=Ln,_i.locale=me,_i.localeData=ge,_i.toIsoString=et("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Ln),_i.lang=jr,L("X",0,0,"unix"),L("x",0,0,"valueOf"),Y("x",er),Y("X",rr),B("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),B("x",function(t,e,n){n._d=new Date(y(t))}),e.version="2.10.6",n(jt),e.fn=Gr,e.min=Pt,e.max=kt,e.utc=s,e.unix=Xe,e.months=fn,e.isDate=i,e.locale=O,e.invalid=d,e.duration=Zt,e.isMoment=_,e.weekdays=hn,e.parseZone=Qe,e.localeData=T,e.isDuration=Nt,e.monthsShort=dn,e.weekdaysMin=_n,e.defineLocale=M,e.weekdaysShort=pn,e.normalizeUnits=E,e.relativeTimeThreshold=Pn;var vi=e;return vi})}).call(e,n(65)(t))},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var a=n(162),u=n(182),s=i(u),c=n(184),l=i(c),f=n(186),d=i(f),h=n(15),p=r(h),_=n(24),v=r(_),y=n(9),m=r(y),g=n(44),b=r(g),S=n(142),w=r(S),O=n(25),M=r(O),T=n(147),I=r(T),E=n(47),D=r(E),C=n(50),j=r(C),A=n(27),P=r(A),k=n(13),L=r(k),N=n(28),R=r(N),z=n(30),x=r(z),H=n(179),Y=r(H),U=n(10),G=r(U),B=function F(){o(this,F);var t=(0,s["default"])();Object.defineProperties(this,{demo:{value:!1,enumerable:!0},localStoragePreferences:{value:a.localStoragePreferences,enumerable:!0},reactor:{value:t,enumerable:!0},util:{value:d["default"],enumerable:!0},startLocalStoragePreferencesSync:{value:a.localStoragePreferences.startSync.bind(a.localStoragePreferences,t)},startUrlSync:{value:j.urlSync.startSync.bind(null,t)},stopUrlSync:{value:j.urlSync.stopSync.bind(null,t)}}),(0,l["default"])(this,t,{auth:p,config:v,entity:m,entityHistory:b,errorLog:w,event:M,logbook:I,moreInfo:D,navigation:j,notification:P,service:L,stream:R,sync:x,voice:Y,restApi:G})};e["default"]=B},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(74),e["default"]=new o["default"]({is:"ha-badges-card",properties:{states:{type:Array}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(21),c=r(s);n(35),n(34),n(19);var l=u["default"].moreInfoActions;e["default"]=new o["default"]({is:"ha-domain-card",properties:{domain:{type:String},states:{type:Array},groupEntity:{type:Object}},computeDomainTitle:function(t){return t.replace(/_/g," ")},entityTapped:function(t){if(!t.target.classList.contains("paper-toggle-button")&&!t.target.classList.contains("paper-icon-button")){t.stopPropagation();var e=t.model.item.entityId;this.async(function(){return l.selectEntity(e)},1)}},showGroupToggle:function(t,e){return!t||!e||"on"!==t.state&&"off"!==t.state?!1:e.reduce(function(t,e){return t+(0,c["default"])(e.entityId)},0)>1}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(35),e["default"]=new o["default"]({is:"ha-introduction-card",properties:{showInstallInstruction:{type:Boolean,value:!1},showHideInstruction:{type:Boolean,value:!0}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(40),u=r(a);e["default"]=new o["default"]({is:"display-time",properties:{dateObj:{type:Object}},computeTime:function(t){return t?(0,u["default"])(t):""}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].entityGetters;e["default"]=new u["default"]({is:"entity-list",behaviors:[c["default"]],properties:{entities:{type:Array,bindNuclear:[l.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.entityId}).toArray()}]}},entitySelected:function(t){t.preventDefault(),this.fire("entity-selected",{entityId:t.model.entity.entityId})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a);n(17);var s=u["default"].reactor,c=u["default"].entityGetters,l=u["default"].moreInfoActions;e["default"]=new o["default"]({is:"ha-entity-marker",properties:{entityId:{type:String,value:""},state:{type:Object,computed:"computeState(entityId)"},icon:{type:Object,computed:"computeIcon(state)"},image:{type:Object,computed:"computeImage(state)"},value:{type:String,computed:"computeValue(state)"}},listeners:{click:"badgeTap"},badgeTap:function(t){var e=this;t.stopPropagation(),this.entityId&&this.async(function(){return l.selectEntity(e.entityId)},1)},computeState:function(t){return t&&s.evaluate(c.byId(t))},computeIcon:function(t){return!t&&"home"},computeImage:function(t){return t&&t.attributes.entity_picture},computeValue:function(t){return t&&t.entityDisplay.split(" ").map(function(t){return t.substr(0,1)}).join("")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(121),u=r(a);e["default"]=new o["default"]({is:"ha-state-icon",properties:{stateObj:{type:Object}},computeIcon:function(t){return(0,u["default"])(t)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(22),c=r(s),l=n(21),f=r(l);n(17);var d=u["default"].moreInfoActions,h=u["default"].serviceActions;e["default"]=new o["default"]({is:"ha-state-label-badge",properties:{state:{type:Object,observer:"stateChanged"}},listeners:{click:"badgeTap"},badgeTap:function(t){var e=this;return t.stopPropagation(),(0,f["default"])(this.state.entityId)?void("scene"===this.state.domain?h.callTurnOn(this.state.entityId):"off"===this.state.state?h.callTurnOn(this.state.entityId):h.callTurnOff(this.state.entityId)):void this.async(function(){return d.selectEntity(e.state.entityId)},1)},computeClasses:function(t){switch(t.domain){case"scene":return"green";case"binary_sensor":case"script":return"on"===t.state?"blue":"grey";case"updater":return"blue";default:return""}},computeValue:function(t){switch(t.domain){case"binary_sensor":case"device_tracker":case"updater":case"sun":case"scene":case"script":case"alarm_control_panel":return;case"sensor":return t.state;default:return t.state}},computeIcon:function(t){switch(t.domain){case"alarm_control_panel":return"pending"===t.state?"mdi:clock-fast":"armed_away"===t.state?"mdi:nature":"armed_home"===t.state?"mdi:home-variant":(0,c["default"])(t.domain,t.state);case"binary_sensor":case"device_tracker":case"scene":case"updater":case"script":return(0,c["default"])(t.domain,t.state);case"sun":return"above_horizon"===t.state?(0,c["default"])(t.domain):"mdi:brightness-3";default:return}},computeImage:function(t){return t.attributes.entity_picture},computeLabel:function(t){switch(t.domain){case"scene":case"script":return t.domain;case"device_tracker":return"not_home"===t.state?"Away":t.state;case"alarm_control_panel":return"pending"===t.state?"pend":"armed_away"===t.state||"armed_home"===t.state?"armed":"disarm";default:return t.attributes.unit_of_measurement}},computeDescription:function(t){return t.entityDisplay},stateChanged:function(){this.updateStyles()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(73),e["default"]=new o["default"]({is:"state-badge",properties:{stateObj:{type:Object,observer:"updateIconColor"}},updateIconColor:function(t){"light"===t.domain&&"on"===t.state&&t.attributes.rgb_color&&t.attributes.rgb_color.reduce(function(t,e){return t+e},0)<730?this.$.icon.style.color="rgb("+t.attributes.rgb_color.join(",")+")":this.$.icon.style.color=null}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].eventGetters;e["default"]=new u["default"]({is:"events-list",behaviors:[c["default"]],properties:{events:{type:Array,bindNuclear:[l.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.event}).toArray()}]}},eventSelected:function(t){t.preventDefault(),this.fire("event-selected",{eventType:t.model.event.event})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"ha-color-picker",properties:{color:{type:Object},width:{type:Number},height:{type:Number}},listeners:{mousedown:"onMouseDown",mouseup:"onMouseUp",touchstart:"onTouchStart",touchend:"onTouchEnd"},onMouseDown:function(t){this.onMouseMove(t),this.addEventListener("mousemove",this.onMouseMove)},onMouseUp:function(){this.removeEventListener("mousemove",this.onMouseMove)},onTouchStart:function(t){this.onTouchMove(t),this.addEventListener("touchmove",this.onTouchMove)},onTouchEnd:function(){this.removeEventListener("touchmove",this.onTouchMove)},onTouchMove:function(t){var e=this;this.mouseMoveIsThrottled&&(this.mouseMoveIsThrottled=!1,this.processColorSelect(t.touches[0]),this.async(function(){return e.mouseMoveIsThrottled=!0},100))},onMouseMove:function(t){var e=this;this.mouseMoveIsThrottled&&(this.mouseMoveIsThrottled=!1,this.processColorSelect(t),this.async(function(){return e.mouseMoveIsThrottled=!0},100))},processColorSelect:function(t){var e=this.canvas.getBoundingClientRect();t.clientX=e.left+e.width||t.clientY=e.top+e.height||this.onColorSelect(t.clientX-e.left,t.clientY-e.top)},onColorSelect:function(t,e){var n=this.context.getImageData(t,e,1,1).data;this.setColor({r:n[0],g:n[1],b:n[2]})},setColor:function(t){this.color=t,this.fire("colorselected",{rgb:this.color})},ready:function(){var t=this;this.setColor=this.setColor.bind(this),this.mouseMoveIsThrottled=!0,this.canvas=this.children[0],this.context=this.canvas.getContext("2d"),this.debounce("drawGradient",function(){var e=getComputedStyle(t),n=parseInt(e.width,10),r=parseInt(e.height,10);t.width=n,t.height=r;var i=t.context.createLinearGradient(0,0,n,0);i.addColorStop(0,"rgb(255,0,0)"),i.addColorStop(.16,"rgb(255,0,255)"),i.addColorStop(.32,"rgb(0,0,255)"),i.addColorStop(.48,"rgb(0,255,255)"),i.addColorStop(.64,"rgb(0,255,0)"),i.addColorStop(.8,"rgb(255,255,0)"),i.addColorStop(1,"rgb(255,0,0)"),t.context.fillStyle=i,t.context.fillRect(0,0,n,r);var o=t.context.createLinearGradient(0,0,0,r);o.addColorStop(0,"rgba(255,255,255,1)"),o.addColorStop(.5,"rgba(255,255,255,0)"),o.addColorStop(.5,"rgba(0,0,0,0)"),o.addColorStop(1,"rgba(0,0,0,1)"),t.context.fillStyle=o,t.context.fillRect(0,0,n,r)},100)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(17),e["default"]=new o["default"]({is:"ha-demo-badge"})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(82),e["default"]=new o["default"]({is:"ha-logbook",properties:{entries:{type:Object,value:[]}},noEntries:function(t){return!t.length}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(86);var l=o["default"].configGetters,f=o["default"].navigationGetters,d=o["default"].authActions,h=o["default"].navigationActions;e["default"]=new u["default"]({is:"ha-sidebar",behaviors:[c["default"]],properties:{menuShown:{type:Boolean},menuSelected:{type:String},selected:{type:String,bindNuclear:f.activePane,observer:"selectedChanged"},hasHistoryComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("history")},hasLogbookComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("logbook")}},selectedChanged:function(t){for(var e=this.querySelectorAll(".menu [data-panel]"),n=0;nd;d++)f._columns[d]=[];var h=0;return n&&a(),s.keySeq().sortBy(function(t){return i(t)}).forEach(function(t){if("a"===t)return void(f._demo=!0);var n=i(t);n>=0&&10>n?f._badges.push.apply(f._badges,r(s.get(t)).sortBy(o).toArray()):"group"===t?s.get(t).filter(function(t){return!t.attributes.auto}).sortBy(o).forEach(function(t){var n=l.expandGroup(t,e);n.forEach(function(t){return c[t.entityId]=!0}),u(t.entityDisplay,n.toArray(),t)}):u(t,r(s.get(t)).sortBy(o).toArray())}),f},computeShouldRenderColumn:function(t,e){return 0===t||e.length},computeShowIntroduction:function(t,e,n){return 0===t&&(e||n._demo)},computeShowHideInstruction:function(t,e){return t.size>0&&!0&&!e._demo},computeGroupEntityOfCard:function(t,e){return e in t&&t[e].groupEntity},computeStatesOfCard:function(t,e){return e in t&&t[e].entities}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(33),n(70),n(36);var s=o["default"].moreInfoActions;e["default"]=new u["default"]({is:"logbook-entry",entityClicked:function(t){t.preventDefault(),s.selectEntity(this.entryObj.entityId)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(33);var l=o["default"].serviceGetters;e["default"]=new u["default"]({is:"services-list",behaviors:[c["default"]],properties:{serviceDomains:{type:Array,bindNuclear:l.entityMap}},computeDomains:function(t){return t.valueSeq().map(function(t){return t.domain}).sort().toJS()},computeServices:function(t,e){return t.get(e).get("services").keySeq().toArray()},serviceClicked:function(t){t.preventDefault(),this.fire("service-selected",{domain:t.model.domain,service:t.model.service})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){var e=parseFloat(t);return!isNaN(e)&&isFinite(e)?e:null}Object.defineProperty(e,"__esModule",{value:!0});var o=n(199),a=r(o),u=n(1),s=r(u);e["default"]=new s["default"]({is:"state-history-chart-line",properties:{data:{type:Object,observer:"dataChanged"},unit:{type:String},isSingleDevice:{type:Boolean,value:!1},isAttached:{type:Boolean,value:!1,observer:"dataChanged"},chartEngine:{type:Object}},created:function(){this.style.display="block"},attached:function(){this.isAttached=!0},dataChanged:function(){this.drawChart()},drawChart:function(){if(this.isAttached){this.chartEngine||(this.chartEngine=new window.google.visualization.LineChart(this));var t=this.unit,e=this.data;if(0!==e.length){var n={legend:{position:"top"},interpolateNulls:!0,titlePosition:"none",vAxes:{0:{title:t}},hAxis:{format:"H:mm"},chartArea:{left:"60",width:"95%"},explorer:{actions:["dragToZoom","rightClickToReset","dragToPan"],keepInBounds:!0,axis:"horizontal",maxZoomIn:.1}};this.isSingleDevice&&(n.legend.position="none",n.vAxes[0].title=null,n.chartArea.left=40,n.chartArea.height="80%",n.chartArea.top=5,n.enableInteractivity=!1);var r=new Date(Math.min.apply(null,e.map(function(t){return t[0].lastChangedAsDate}))),o=new Date(r);o.setDate(o.getDate()+1),o>new Date&&(o=new Date);var u=e.map(function(t){function e(t,e){c&&e&&s.push([t[0]].concat(c.slice(1).map(function(t,n){return e[n]?t:null}))),s.push(t),c=t}var n=t[t.length-1],r=n.domain,a=n.entityDisplay,u=new window.google.visualization.DataTable;u.addColumn({type:"datetime",id:"Time"});var s=[],c=void 0;if("thermostat"===r){var l=t.reduce(function(t,e){return t||e.attributes.target_temp_high!==e.attributes.target_temp_low},!1);u.addColumn("number",a+" current temperature");var f=void 0;l?!function(){u.addColumn("number",a+" target temperature high"),u.addColumn("number",a+" target temperature low");var t=[!1,!0,!0];f=function(n){var r=i(n.attributes.current_temperature),o=i(n.attributes.target_temp_high),a=i(n.attributes.target_temp_low);e([n.lastChangedAsDate,r,o,a],t)}}():!function(){u.addColumn("number",a+" target temperature");var t=[!1,!0];f=function(n){var r=i(n.attributes.current_temperature),o=i(n.attributes.temperature);e([n.lastChangedAsDate,r,o],t)}}(),t.forEach(f)}else!function(){u.addColumn("number",a);var n="sensor"!==r&&[!0];t.forEach(function(t){var r=i(t.state);e([t.lastChangedAsDate,r],n)})}();return e([o].concat(c.slice(1)),!1),u.addRows(s),u}),s=void 0;s=1===u.length?u[0]:u.slice(1).reduce(function(t,e){return window.google.visualization.data.join(t,e,"full",[[0,0]],(0,a["default"])(1,t.getNumberOfColumns()),(0,a["default"])(1,e.getNumberOfColumns()))},u[0]),this.chartEngine.draw(s,n)}}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"state-history-chart-timeline",properties:{data:{type:Object,observer:"dataChanged"},isAttached:{type:Boolean,value:!1,observer:"dataChanged"}},attached:function(){this.isAttached=!0},dataChanged:function(){this.drawChart()},drawChart:function(){function t(t,e,n,r){var o=e.replace(/_/g," ");i.addRow([t,o,n,r])}if(this.isAttached){for(var e=o["default"].dom(this),n=this.data;e.node.lastChild;)e.node.removeChild(e.node.lastChild);if(n&&0!==n.length){var r=new window.google.visualization.Timeline(this),i=new window.google.visualization.DataTable;i.addColumn({type:"string",id:"Entity"}),i.addColumn({type:"string",id:"State"}),i.addColumn({type:"date",id:"Start"}),i.addColumn({type:"date",id:"End"});var a=new Date(n.reduce(function(t,e){return Math.min(t,e[0].lastChangedAsDate)},new Date)),u=new Date(a);u.setDate(u.getDate()+1),u>new Date&&(u=new Date);var s=0;n.forEach(function(e){if(0!==e.length){var n=e[0].entityDisplay,r=void 0,i=null,o=null;e.forEach(function(e){null!==i&&e.state!==i?(r=e.lastChangedAsDate,t(n,i,o,r),i=e.state,o=r):null===i&&(i=e.state,o=e.lastChangedAsDate)}),t(n,i,o,u),s++}}),r.draw(i,{height:55+42*s,timeline:{showRowLabels:n.length>1},hAxis:{format:"H:mm"}})}}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].streamGetters,f=o["default"].streamActions;e["default"]=new u["default"]({is:"stream-status",behaviors:[c["default"]],properties:{isStreaming:{type:Boolean,bindNuclear:l.isStreamingEvents},hasError:{type:Boolean,bindNuclear:l.hasStreamingEventsError}},toggleChanged:function(){this.isStreaming?f.stop():f.start()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].voiceActions,f=o["default"].voiceGetters;e["default"]=new u["default"]({is:"ha-voice-command-dialog",behaviors:[c["default"]],properties:{dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},finalTranscript:{type:String,bindNuclear:f.finalTranscript},interimTranscript:{type:String,bindNuclear:f.extraInterimTranscript},isTransmitting:{type:Boolean,bindNuclear:f.isTransmitting},isListening:{type:Boolean,bindNuclear:f.isListening},showListenInterface:{type:Boolean,computed:"computeShowListenInterface(isListening, isTransmitting)",observer:"showListenInterfaceChanged"},_boundOnBackdropTap:{type:Function,value:function(){return this._onBackdropTap.bind(this)}}},computeShowListenInterface:function(t,e){return t||e},dialogOpenChanged:function(t){t?this.$.dialog.backdropElement.addEventListener("click",this._boundOnBackdropTap):!t&&this.isListening&&l.stop()},showListenInterfaceChanged:function(t){!t&&this.dialogOpen?this.dialogOpen=!1:t&&(this.dialogOpen=!0)},_onBackdropTap:function(){this.$.dialog.backdropElement.removeEventListener("click",this._boundOnBackdropTap),this.isListening&&l.stop()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(19),n(37),n(104);var l=o["default"].configGetters,f=o["default"].entityHistoryGetters,d=o["default"].entityHistoryActions,h=o["default"].moreInfoGetters,p=o["default"].moreInfoActions,_=["camera","configurator","scene"];e["default"]=new u["default"]({is:"more-info-dialog",behaviors:[c["default"]],properties:{stateObj:{type:Object,bindNuclear:h.currentEntity,observer:"stateObjChanged"},stateHistory:{type:Object,bindNuclear:[h.currentEntityHistory,function(t){return t?[t]:!1}]},isLoadingHistoryData:{type:Boolean,computed:"computeIsLoadingHistoryData(_delayedDialogOpen, _isLoadingHistoryData)"},_isLoadingHistoryData:{type:Boolean,bindNuclear:f.isLoadingEntityHistory},hasHistoryComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("history"),observer:"fetchHistoryData"},shouldFetchHistory:{type:Boolean,bindNuclear:h.isCurrentEntityHistoryStale,observer:"fetchHistoryData"},showHistoryComponent:{type:Boolean,value:!1},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},_delayedDialogOpen:{ +type:Boolean,value:!1}},computeIsLoadingHistoryData:function(t,e){return!t||e},fetchHistoryData:function(){this.stateObj&&this.hasHistoryComponent&&this.shouldFetchHistory&&d.fetchRecent(this.stateObj.entityId)},stateObjChanged:function(t){var e=this;return t?(this.showHistoryComponent=this.hasHistoryComponent&&-1===_.indexOf(this.stateObj.domain),void this.async(function(){e.fetchHistoryData(),e.dialogOpen=!0},10)):void(this.dialogOpen=!1)},dialogOpenChanged:function(t){var e=this;t?this.async(function(){return e._delayedDialogOpen=!0},10):!t&&this.stateObj&&(p.deselectEntity(),this._delayedDialogOpen=!1)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(4),c=r(s),l=n(41),f=r(l);n(80),n(99),n(97),n(96),n(98),n(91),n(92),n(94),n(95),n(93),n(100),n(88),n(87);var d=u["default"].navigationActions,h=u["default"].navigationGetters,p=u["default"].startUrlSync,_=u["default"].stopUrlSync;e["default"]=new o["default"]({is:"home-assistant-main",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},activePane:{type:String,bindNuclear:h.activePane,observer:"activePaneChanged"},isSelectedStates:{type:Boolean,bindNuclear:h.isActivePane("states")},isSelectedHistory:{type:Boolean,bindNuclear:h.isActivePane("history")},isSelectedMap:{type:Boolean,bindNuclear:h.isActivePane("map")},isSelectedLogbook:{type:Boolean,bindNuclear:h.isActivePane("logbook")},isSelectedDevEvent:{type:Boolean,bindNuclear:h.isActivePane("devEvent")},isSelectedDevState:{type:Boolean,bindNuclear:h.isActivePane("devState")},isSelectedDevTemplate:{type:Boolean,bindNuclear:h.isActivePane("devTemplate")},isSelectedDevService:{type:Boolean,bindNuclear:h.isActivePane("devService")},isSelectedDevInfo:{type:Boolean,bindNuclear:h.isActivePane("devInfo")},showSidebar:{type:Boolean,bindNuclear:h.showSidebar}},listeners:{"open-menu":"openMenu","close-menu":"closeMenu"},openMenu:function(){this.narrow?this.$.drawer.openDrawer():d.showSidebar(!0)},closeMenu:function(){this.$.drawer.closeDrawer(),this.showSidebar&&d.showSidebar(!1)},activePaneChanged:function(){this.narrow&&this.$.drawer.closeDrawer()},attached:function(){(0,f["default"])(),p()},computeForceNarrow:function(t,e){return t||!e},detached:function(){_()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(4),c=r(s),l=n(42),f=r(l),d=n(41),h=r(d),p=u["default"].authGetters;e["default"]=new o["default"]({is:"login-form",behaviors:[c["default"]],properties:{errorMessage:{type:String,bindNuclear:p.attemptErrorMessage},isInvalid:{type:Boolean,bindNuclear:p.isInvalidAttempt},isValidating:{type:Boolean,observer:"isValidatingChanged",bindNuclear:p.isValidating},loadingResources:{type:Boolean,value:!1},forceShowLoading:{type:Boolean,value:!1},showLoading:{type:Boolean,computed:"computeShowSpinner(forceShowLoading, isValidating)"}},listeners:{keydown:"passwordKeyDown","loginButton.click":"validatePassword"},observers:["validatingChanged(isValidating, isInvalid)"],attached:function(){(0,h["default"])()},computeShowSpinner:function(t,e){return t||e},validatingChanged:function(t,e){t||e||(this.$.passwordInput.value="")},isValidatingChanged:function(t){var e=this;t||this.async(function(){return e.$.passwordInput.focus()},10)},passwordKeyDown:function(t){13===t.keyCode?(this.validatePassword(),t.preventDefault()):this.isInvalid&&(this.isInvalid=!1)},validatePassword:function(){this.$.hideKeyboardOnFocus.focus(),(0,f["default"])(this.$.passwordInput.value,this.$.rememberLogin.checked)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(83);var s=o["default"].reactor,c=o["default"].serviceActions,l=o["default"].serviceGetters;e["default"]=new u["default"]({is:"partial-dev-call-service",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},domain:{type:String,value:""},service:{type:String,value:""},serviceData:{type:String,value:""},description:{type:String,computed:"computeDescription(domain, service)"}},computeDescription:function(t,e){return s.evaluate([l.entityMap,function(n){return n.has(t)&&n.get(t).get("services").has(e)?JSON.stringify(n.get(t).get("services").get(e).toJS(),null,2):"No description available"}])},serviceSelected:function(t){this.domain=t.detail.domain,this.service=t.detail.service},callService:function(){var t=void 0;try{t=this.serviceData?JSON.parse(this.serviceData):{}}catch(e){return void alert("Error parsing JSON: "+e)}c.callService(this.domain,this.service,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(76);var s=o["default"].eventActions;e["default"]=new u["default"]({is:"partial-dev-fire-event",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},eventType:{type:String,value:""},eventData:{type:String,value:""}},eventSelected:function(t){this.eventType=t.detail.eventType},fireEvent:function(){var t=void 0;try{t=this.eventData?JSON.parse(this.eventData):{}}catch(e){return void alert("Error parsing JSON: "+e)}s.fireEvent(this.eventType,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7);var l=o["default"].configGetters,f=o["default"].errorLogActions;e["default"]=new u["default"]({is:"partial-dev-info",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:l.serverVersion},polymerVersion:{type:String,value:u["default"].version},nuclearVersion:{type:String,value:"1.2.1"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(t){var e=this;t&&t.preventDefault(),this.errorLog="Loading error log…",f.fetchErrorLog().then(function(t){return e.errorLog=t||"No errors have been reported."})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(71);var s=o["default"].reactor,c=o["default"].entityGetters,l=o["default"].entityActions;e["default"]=new u["default"]({is:"partial-dev-set-state",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},entityId:{type:String,value:""},state:{type:String,value:""},stateAttributes:{type:String,value:""}},setStateData:function(t){var e=t?JSON.stringify(t,null," "):"";this.$.inputData.value=e,this.$.inputDataWrapper.update(this.$.inputData)},entitySelected:function(t){var e=s.evaluate(c.byId(t.detail.entityId));this.entityId=e.entityId,this.state=e.state,this.stateAttributes=JSON.stringify(e.attributes,null," ")},handleSetState:function(){var t=void 0;try{t=this.stateAttributes?JSON.parse(this.stateAttributes):{}}catch(e){return void alert("Error parsing JSON: "+e)}l.save({entityId:this.entityId,state:this.state,attributes:t})},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7);var l=o["default"].templateActions;e["default"]=new u["default"]({is:"partial-dev-template",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},error:{type:Boolean,value:!1},rendering:{type:Boolean,value:!1},template:{type:String,value:'{%- if is_state("device_tracker.paulus", "home") and \n is_state("device_tracker.anne_therese", "home") -%}\n\n You are both home, you silly\n\n{%- else -%}\n\n Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}\n\n{%- endif %}\n\nFor loop example:\n\n{% for state in states.sensor -%}\n {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}\n {{ state.name | lower }} is {{state.state}} {{- state.attributes.unit_of_measurement}}\n{%- endfor -%}.',observer:"templateChanged"},processed:{type:String,value:""}},computeFormClasses:function(t){return"content fit layout "+(t?"vertical":"horizontal")},computeRenderedClasses:function(t){return t?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate,500)},renderTemplate:function(){var t=this;this.rendering=!0,l.render(this.template).then(function(e){t.processed=e,t.rendering=!1},function(e){t.processed=e.message,t.error=!0,t.rendering=!1})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(37);var l=o["default"].entityHistoryGetters,f=o["default"].entityHistoryActions;e["default"]=new u["default"]({is:"partial-history",behaviors:[c["default"]],properties:{narrow:{type:Boolean},showMenu:{type:Boolean,value:!1},isDataLoaded:{type:Boolean,bindNuclear:l.hasDataForCurrentDate,observer:"isDataLoadedChanged"},stateHistory:{type:Object,bindNuclear:l.entityHistoryForCurrentDate},isLoadingData:{type:Boolean,bindNuclear:l.isLoadingEntityHistory},selectedDate:{type:String,value:null,bindNuclear:l.currentDate}},isDataLoadedChanged:function(t){t||this.async(function(){return f.fetchSelectedDate()},1)},handleRefreshClick:function(){f.fetchSelectedDate()},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:f.changeCurrentDate})},detached:function(){this.datePicker.destroy()},computeContentClasses:function(t){return"flex content "+(t?"narrow":"wide")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(79),n(18);var l=o["default"].logbookGetters,f=o["default"].logbookActions;e["default"]=new u["default"]({is:"partial-logbook",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},selectedDate:{type:String,bindNuclear:l.currentDate},isLoading:{type:Boolean,bindNuclear:l.isLoadingEntries},isStale:{type:Boolean,bindNuclear:l.isCurrentStale,observer:"isStaleChanged"},entries:{type:Array,bindNuclear:[l.currentEntries,function(t){return t.reverse().toArray()}]},datePicker:{type:Object}},isStaleChanged:function(t){var e=this;t&&this.async(function(){return f.fetchDate(e.selectedDate)},1)},handleRefresh:function(){f.fetchDate(this.selectedDate)},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:f.changeCurrentDate})},detached:function(){this.datePicker.destroy()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(72);var l=o["default"].configGetters,f=o["default"].entityGetters;window.L.Icon.Default.imagePath="/static/images/leaflet",e["default"]=new u["default"]({is:"partial-map",behaviors:[c["default"]],properties:{locationGPS:{type:Number,bindNuclear:l.locationGPS},locationName:{type:String,bindNuclear:l.locationName},locationEntities:{type:Array,bindNuclear:[f.visibleEntityMap,function(t){return t.valueSeq().filter(function(t){return t.attributes.latitude&&"home"!==t.state}).toArray()}]},zoneEntities:{type:Array,bindNuclear:[f.entityMap,function(t){return t.valueSeq().filter(function(t){return"zone"===t.domain}).toArray()}]},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1}},attached:function(){var t=this;window.L.Browser.mobileWebkit&&this.async(function(){var e=t.$.map,n=e.style.display;e.style.display="none",t.async(function(){e.style.display=n},1)},1)},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},toggleMenu:function(){this.fire("open-menu")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(81);var l=o["default"].configGetters,f=o["default"].entityGetters,d=o["default"].voiceGetters,h=o["default"].streamGetters,p=o["default"].syncGetters,_=o["default"].syncActions,v=o["default"].voiceActions;e["default"]=new u["default"]({is:"partial-zone",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},isFetching:{type:Boolean,bindNuclear:p.isFetching},isStreaming:{type:Boolean,bindNuclear:h.isStreamingEvents},canListen:{type:Boolean,bindNuclear:[d.isVoiceSupported,l.isComponentLoaded("conversation"),function(t,e){return t&&e}]},introductionLoaded:{type:Boolean,bindNuclear:l.isComponentLoaded("introduction")},locationName:{type:String,bindNuclear:l.locationName},showMenu:{type:Boolean,value:!1,observer:"windowChange"},states:{type:Object,bindNuclear:f.visibleEntityMap},columns:{type:Number,value:1}},created:function(){var t=this;this.windowChange=this.windowChange.bind(this);for(var e=[],n=0;5>n;n++)e.push(300+300*n);this.mqls=e.map(function(e){var n=window.matchMedia("(min-width: "+e+"px)");return n.addListener(t.windowChange),n})},detached:function(){var t=this;this.mqls.forEach(function(e){return e.removeListener(t.windowChange)})},windowChange:function(){var t=this.mqls.reduce(function(t,e){return t+e.matches},0);this.columns=Math.max(1,t-this.showMenu)},handleRefresh:function(){_.fetchAll()},handleListenClick:function(){v.listen()},computeDomains:function(t){return t.keySeq().toArray()},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},computeStatesOfDomain:function(t,e){return t.get(e).toArray()},computeRefreshButtonClass:function(t){return t?"ha-spin":void 0},computeShowIntroduction:function(t,e){return t||0===e.size},toggleMenu:function(){this.fire("open-menu")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].notificationGetters;e["default"]=new u["default"]({is:"notification-manager",behaviors:[c["default"]],properties:{text:{type:String,bindNuclear:l.lastNotificationMessage,observer:"showNotification"}},showNotification:function(t){t&&this.$.toast.show()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=o["default"].serviceActions;e["default"]=new u["default"]({is:"more-info-alarm_control_panel",handleDisarmTap:function(){this.callService("alarm_disarm",{code:this.enteredCode})},handleHomeTap:function(){this.callService("alarm_arm_home",{code:this.enteredCode})},handleAwayTap:function(){this.callService("alarm_arm_away",{code:this.enteredCode})},properties:{stateObj:{type:Object,observer:"stateObjChanged"},enteredCode:{type:String,value:""},disarmButtonVisible:{type:Boolean,value:!1},armHomeButtonVisible:{type:Boolean,value:!1},armAwayButtonVisible:{type:Boolean,value:!1},codeInputVisible:{type:Boolean,value:!1},codeInputEnabled:{type:Boolean,value:!1},codeFormat:{type:String,value:""},codeValid:{type:Boolean,computed:"validateCode(enteredCode, codeFormat)"}},validateCode:function(t,e){var n=new RegExp(e);return null===e?!0:n.test(t)},stateObjChanged:function(t){var e=this;t&&(this.codeFormat=t.attributes.code_format,this.codeInputVisible=null!==this.codeFormat,this.codeInputEnabled="armed_home"===t.state||"armed_away"===t.state||"disarmed"===t.state||"pending"===t.state||"triggered"===t.state,this.disarmButtonVisible="armed_home"===t.state||"armed_away"===t.state||"pending"===t.state||"triggered"===t.state,this.armHomeButtonVisible="disarmed"===t.state,this.armAwayButtonVisible="disarmed"===t.state),this.async(function(){return e.fire("iron-resize")},500)},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,s.callService("alarm_control_panel",t,n)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-camera",properties:{stateObj:{type:Object},dialogOpen:{type:Boolean}},imageLoaded:function(){this.fire("iron-resize")},computeCameraImageUrl:function(t){return t?"/api/camera_proxy_stream/"+this.stateObj.entityId:"data:image/gif;base64,R0lGODlhAQABAAAAACw="}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(18);var l=o["default"].streamGetters,f=o["default"].syncActions,d=o["default"].serviceActions;e["default"]=new u["default"]({is:"more-info-configurator",behaviors:[c["default"]],properties:{stateObj:{type:Object},action:{type:String,value:"display"},isStreaming:{type:Boolean,bindNuclear:l.isStreamingEvents},isConfigurable:{type:Boolean,computed:"computeIsConfigurable(stateObj)"},isConfiguring:{type:Boolean,value:!1},submitCaption:{type:String,computed:"computeSubmitCaption(stateObj)"},fieldInput:{type:Object,value:{}}},computeIsConfigurable:function(t){return"configure"===t.state},computeSubmitCaption:function(t){return t.attributes.submit_caption||"Set configuration"},fieldChanged:function(t){var e=t.target;this.fieldInput[e.id]=e.value},submitClicked:function(){var t=this;this.isConfiguring=!0;var e={configure_id:this.stateObj.attributes.configure_id,fields:this.fieldInput};d.callService("configurator","configure",e).then(function(){t.isConfiguring=!1,t.isStreaming||f.fetchAll()},function(){t.isConfiguring=!1})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(122),u=r(a);n(105),n(106),n(110),n(103),n(111),n(109),n(107),n(108),n(102),n(112),n(101),e["default"]=new o["default"]({is:"more-info-content",properties:{stateObj:{type:Object,observer:"stateObjChanged"},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"}},dialogOpenChanged:function(t){var e=o["default"].dom(this);e.lastChild&&(e.lastChild.dialogOpen=t)},stateObjChanged:function(t,e){var n=o["default"].dom(this);if(!t)return void(n.lastChild&&n.removeChild(n.lastChild));var r=(0,u["default"])(t);if(e&&(0,u["default"])(e)===r)n.lastChild.dialogOpen=this.dialogOpen,n.lastChild.stateObj=t;else{n.lastChild&&n.removeChild(n.lastChild);var i=document.createElement("more-info-"+r);i.stateObj=t,i.dialogOpen=this.dialogOpen,n.appendChild(i)}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=["entity_picture","friendly_name","icon","unit_of_measurement"];e["default"]=new o["default"]({is:"more-info-default",properties:{stateObj:{type:Object}},computeDisplayAttributes:function(t){return t?Object.keys(t.attributes).filter(function(t){return-1===a.indexOf(t)}):[]},getAttributeValue:function(t,e){return t.attributes[e]}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(19);var l=o["default"].entityGetters,f=o["default"].moreInfoGetters;e["default"]=new u["default"]({is:"more-info-group",behaviors:[c["default"]],properties:{stateObj:{type:Object},states:{type:Array,bindNuclear:[f.currentEntity,l.entityMap,function(t,e){return t?t.attributes.entity_id.map(e.get.bind(e)):[]}]}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){f.callService("light","turn_on",{entity_id:t,rgb_color:[e.r,e.g,e.b]})}Object.defineProperty(e,"__esModule",{value:!0});var o=n(2),a=r(o),u=n(1),s=r(u),c=n(20),l=r(c);n(77);var f=a["default"].serviceActions,d=["brightness","rgb_color","color_temp"];e["default"]=new s["default"]({is:"more-info-light",properties:{stateObj:{type:Object,observer:"stateObjChanged"},brightnessSliderValue:{type:Number,value:0},ctSliderValue:{type:Number,value:0}},stateObjChanged:function(t){var e=this;t&&"on"===t.state&&(this.brightnessSliderValue=t.attributes.brightness,this.ctSliderValue=t.attributes.color_temp),this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return(0,l["default"])(t,d)},brightnessSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||(0===e?f.callTurnOff(this.stateObj.entityId):f.callService("light","turn_on",{entity_id:this.stateObj.entityId,brightness:e}))},ctSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||f.callService("light","turn_on",{entity_id:this.stateObj.entityId,color_temp:e})},colorPicked:function(t){var e=this;return this.skipColorPicked?void(this.colorChanged=!0):(this.color=t.detail.rgb,i(this.stateObj.entityId,this.color),this.colorChanged=!1,this.skipColorPicked=!0,void(this.colorDebounce=setTimeout(function(){e.colorChanged&&i(e.stateObj.entityId,e.color),e.skipColorPicked=!1},500)))}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(20),c=r(s),l=o["default"].serviceActions,f=["volume_level"];e["default"]=new u["default"]({is:"more-info-media_player",properties:{stateObj:{type:Object,observer:"stateObjChanged"},isOff:{type:Boolean,value:!1},isPlaying:{type:Boolean,value:!1},isMuted:{type:Boolean,value:!1},volumeSliderValue:{type:Number,value:0},supportsPause:{type:Boolean,value:!1},supportsVolumeSet:{type:Boolean,value:!1},supportsVolumeMute:{type:Boolean,value:!1},supportsPreviousTrack:{type:Boolean,value:!1},supportsNextTrack:{type:Boolean,value:!1},supportsTurnOn:{type:Boolean,value:!1},supportsTurnOff:{type:Boolean,value:!1},supportsVolumeButtons:{type:Boolean,value:!1},hasMediaControl:{type:Boolean,value:!1}},stateObjChanged:function(t){var e=this;if(t){var n=["playing","paused","unknown"];this.isOff="off"===t.state,this.isPlaying="playing"===t.state,this.hasMediaControl=-1!==n.indexOf(t.state),this.volumeSliderValue=100*t.attributes.volume_level,this.isMuted=t.attributes.is_volume_muted,this.supportsPause=0!==(1&t.attributes.supported_media_commands),this.supportsVolumeSet=0!==(4&t.attributes.supported_media_commands),this.supportsVolumeMute=0!==(8&t.attributes.supported_media_commands),this.supportsPreviousTrack=0!==(16&t.attributes.supported_media_commands),this.supportsNextTrack=0!==(32&t.attributes.supported_media_commands),this.supportsTurnOn=0!==(128&t.attributes.supported_media_commands),this.supportsTurnOff=0!==(256&t.attributes.supported_media_commands),this.supportsVolumeButtons=0!==(1024&t.attributes.supported_media_commands)}this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return(0,c["default"])(t,f)},computeIsOff:function(t){return"off"===t.state},computeMuteVolumeIcon:function(t){return t?"mdi:volume-off":"mdi:volume-high"},computeHideVolumeButtons:function(t,e){return!e||t},computeShowPlaybackControls:function(t,e){return!t&&e},computePlaybackControlIcon:function(){return this.isPlaying?this.supportsPause?"mdi:pause":"mdi:stop":"mdi:play"},computeHidePowerButton:function(t,e,n){return t?!e:!n},handleTogglePower:function(){this.callService(this.isOff?"turn_on":"turn_off")},handlePrevious:function(){this.callService("media_previous_track")},handlePlaybackControl:function(){this.callService("media_play_pause")},handleNext:function(){this.callService("media_next_track")},handleVolumeTap:function(){this.supportsVolumeMute&&this.callService("volume_mute",{is_volume_muted:!this.isMuted})},handleVolumeUp:function(){var t=this.$.volumeUp;this.handleVolumeWorker("volume_up",t,!0)},handleVolumeDown:function(){var t=this.$.volumeDown;this.handleVolumeWorker("volume_down",t,!0)},handleVolumeWorker:function(t,e,n){var r=this;(n||void 0!==e&&e.pointerDown)&&(this.callService(t),this.async(function(){return r.handleVolumeWorker(t,e,!1)},500))},volumeSliderChanged:function(t){var e=parseFloat(t.target.value),n=e>0?e/100:0;this.callService("volume_set",{volume_level:n})},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,l.callService("media_player",t,n)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-script",properties:{stateObj:{type:Object}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(40),c=r(s),l=u["default"].util.parseDateTime;e["default"]=new o["default"]({is:"more-info-sun",properties:{stateObj:{type:Object},risingDate:{type:Object,computed:"computeRising(stateObj)"},settingDate:{type:Object,computed:"computeSetting(stateObj)"}},computeRising:function(t){return l(t.attributes.next_rising)},computeSetting:function(t){return l(t.attributes.next_setting)},computeOrder:function(t,e){return t>e?["set","ris"]:["ris","set"]},itemCaption:function(t){return"ris"===t?"Rising ":"Setting "},itemDate:function(t){return"ris"===t?this.risingDate:this.settingDate},itemValue:function(t){return(0,c["default"])(this.itemDate(t))}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(20),c=r(s),l=o["default"].serviceActions,f=["away_mode"];e["default"]=new u["default"]({is:"more-info-thermostat",properties:{stateObj:{type:Object,observer:"stateObjChanged"},tempMin:{type:Number},tempMax:{type:Number},targetTemperatureSliderValue:{type:Number},awayToggleChecked:{type:Boolean}},stateObjChanged:function(t){this.targetTemperatureSliderValue=t.attributes.temperature,this.awayToggleChecked="on"===t.attributes.away_mode,this.tempMin=t.attributes.min_temp,this.tempMax=t.attributes.max_temp},computeClassNames:function(t){return(0,c["default"])(t,f)},targetTemperatureSliderChanged:function(t){l.callService("thermostat","set_temperature",{entity_id:this.stateObj.entityId,temperature:t.target.value})},toggleChanged:function(t){var e=t.target.checked;e&&"off"===this.stateObj.attributes.away_mode?this.service_set_away(!0):e||"on"!==this.stateObj.attributes.away_mode||this.service_set_away(!1)},service_set_away:function(t){var e=this;l.callService("thermostat","set_away_mode",{away_mode:t,entity_id:this.stateObj.entityId}).then(function(){return e.stateObjChanged(e.stateObj)})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-updater",properties:{}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),n(38),e["default"]=new o["default"]({is:"state-card-configurator",properties:{stateObj:{type:Object}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8);var a=["playing","paused"];e["default"]=new o["default"]({is:"state-card-media_player",properties:{stateObj:{type:Object},isPlaying:{type:Boolean,computed:"computeIsPlaying(stateObj)"}},computeIsPlaying:function(t){return-1!==a.indexOf(t.state)},computePrimaryText:function(t,e){return e?t.attributes.media_title:t.stateDisplay},computeSecondaryText:function(t){var e=void 0;return"music"===t.attributes.media_content_type?t.attributes.media_artist:"tvshow"===t.attributes.media_content_type?(e=t.attributes.media_series_title,t.attributes.media_season&&t.attributes.media_episode&&(e+=" S"+t.attributes.media_season+"E"+t.attributes.media_episode),e):t.attributes.app_name?t.attributes.app_name:""}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(8);var s=o["default"].serviceActions;e["default"]=new u["default"]({is:"state-card-rollershutter",properties:{stateObj:{type:Object}},computeIsFullyOpen:function(t){return 100===t.attributes.current_position},computeIsFullyClosed:function(t){return 0===t.attributes.current_position},onMoveUpTap:function(){s.callService("rollershutter","move_up",{entity_id:this.stateObj.entityId})},onMoveDownTap:function(){s.callService("rollershutter","move_down",{entity_id:this.stateObj.entityId})},onStopTap:function(){s.callService("rollershutter","stop",{entity_id:this.stateObj.entityId})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a);n(8);var s=u["default"].serviceActions;e["default"]=new o["default"]({is:"state-card-scene",properties:{stateObj:{type:Object}},activateScene:function(){s.callTurnOn(this.stateObj.entityId)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),e["default"]=new o["default"]({is:"state-card-thermostat",properties:{stateObj:{type:Object}},computeTargetTemperature:function(t){return t.attributes.temperature+" "+t.attributes.unit_of_measurement}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),n(34),e["default"]=new o["default"]({is:"state-card-toggle"})},function(t,e){"use strict";function n(t){return{attached:function(){var e=this;this.__unwatchFns=Object.keys(this.properties).reduce(function(n,r){if(!("bindNuclear"in e.properties[r]))return n;var i=e.properties[r].bindNuclear;if(!i)throw new Error("Undefined getter specified for key "+r);return e[r]=t.evaluate(i),n.concat(t.observe(i,function(t){e[r]=t}))},[])},detached:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return-1!==u.indexOf(t.domain)?t.domain:(0,a["default"])(t.entityId)?"toggle":"display"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(21),a=r(o),u=["thermostat","configurator","scene","media_player","rollershutter"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(!t)return a["default"];if(t.attributes.icon)return t.attributes.icon;var e=t.attributes.unit_of_measurement;return!e||"sensor"!==t.domain||e!==f.UNIT_TEMP_C&&e!==f.UNIT_TEMP_F?(0,s["default"])(t.domain,t.state):"mdi:thermometer"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(39),a=r(o),u=n(22),s=r(u),c=n(2),l=r(c),f=l["default"].util.temperatureUnits},function(t,e){"use strict";function n(t){return-1!==r.indexOf(t.domain)?t.domain:"default"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n;var r=["light","group","sun","configurator","thermostat","script","media_player","camera","updater","alarm_control_panel"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(187),i=n(15),o=function(t,e,n){var o=arguments.length<=3||void 0===arguments[3]?null:arguments[3],a=t.evaluate(i.getters.authInfo),u=a.host+"/api/"+n;return new r.Promise(function(t,n){var r=new XMLHttpRequest; +r.open(e,u,!0),r.setRequestHeader("X-HA-access",a.authToken),r.onload=function(){var e=void 0;try{e="application/json"===r.getResponseHeader("content-type")?JSON.parse(r.responseText):r.responseText}catch(i){e=r.responseText}r.status>199&&r.status<300?t(e):n(e)},r.onerror=function(){return n({})},o?r.send(JSON.stringify(o)):r.send()})};e["default"]=o},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2],r=n.useStreaming,i=void 0===r?t.evaluate(c.getters.isSupported):r,o=n.rememberAuth,a=void 0===o?!1:o,s=n.host,d=void 0===s?"":s;t.dispatch(u["default"].VALIDATING_AUTH_TOKEN,{authToken:e,host:d}),l.actions.fetchAll(t).then(function(){t.dispatch(u["default"].VALID_AUTH_TOKEN,{authToken:e,host:d,rememberAuth:a}),i?c.actions.start(t,{syncOnInitialConnect:!1}):l.actions.start(t,{skipInitialSync:!0})},function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],n=e.message,r=void 0===n?f:n;t.dispatch(u["default"].INVALID_AUTH_TOKEN,{errorMessage:r})})}function o(t){(0,s.callApi)(t,"POST","log_out"),t.dispatch(u["default"].LOG_OUT,{})}Object.defineProperty(e,"__esModule",{value:!0}),e.validate=i,e.logOut=o;var a=n(14),u=r(a),s=n(5),c=n(28),l=n(30),f="Unexpected result from API"},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=e.isValidating=["authAttempt","isValidating"],r=(e.isInvalidAttempt=["authAttempt","isInvalid"],e.attemptErrorMessage=["authAttempt","errorMessage"],e.rememberAuth=["rememberAuth"],e.attemptAuthInfo=[["authAttempt","authToken"],["authAttempt","host"],function(t,e){return{authToken:t,host:e}}]),i=e.currentAuthToken=["authCurrent","authToken"],o=e.currentAuthInfo=[i,["authCurrent","host"],function(t,e){return{authToken:t,host:e}}];e.authToken=[n,["authAttempt","authToken"],["authCurrent","authToken"],function(t,e,n){return t?e:n}],e.authInfo=[n,r,o,function(t,e,n){return t?e:n}]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(null==t)throw new TypeError("Cannot destructure undefined")}function o(t,e){var n=e.authToken,r=e.host;return(0,s.toImmutable)({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function a(t,e){return i(e),f.getInitialState()}function u(t,e){var n=e.errorMessage;return t.withMutations(function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)})}Object.defineProperty(e,"__esModule",{value:!0});var s=n(3),c=n(14),l=r(c),f=new s.Store({getInitialState:function(){return(0,s.toImmutable)({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(l["default"].VALIDATING_AUTH_TOKEN,o),this.on(l["default"].VALID_AUTH_TOKEN,a),this.on(l["default"].INVALID_AUTH_TOKEN,u)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.authToken,r=e.host;return(0,a.toImmutable)({authToken:n,host:r})}function o(){return c.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(14),s=r(u),c=new a.Store({getInitialState:function(){return(0,a.toImmutable)({authToken:null,host:""})},initialize:function(){this.on(s["default"].VALID_AUTH_TOKEN,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=c},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.rememberAuth;return n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(3),a=n(14),u=r(a),s=new o.Store({getInitialState:function(){return!0},initialize:function(){this.on(u["default"].VALID_AUTH_TOKEN,i)}});e["default"]=s},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(c["default"].SERVER_CONFIG_LOADED,e)}function o(t){(0,u.callApi)(t,"GET","config").then(function(e){return i(t,e)})}function a(t,e){t.dispatch(c["default"].COMPONENT_LOADED,{component:e})}Object.defineProperty(e,"__esModule",{value:!0}),e.configLoaded=i,e.fetchAll=o,e.componentLoaded=a;var u=n(5),s=n(23),c=r(s)},function(t,e){"use strict";function n(t){return[["serverComponent"],function(e){return e.contains(t)}]}Object.defineProperty(e,"__esModule",{value:!0}),e.isComponentLoaded=n,e.locationGPS=[["serverConfig","latitude"],["serverConfig","longitude"],function(t,e){return{latitude:t,longitude:e}}],e.locationName=["serverConfig","location_name"],e.serverVersion=["serverConfig","serverVersion"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.component;return t.push(n)}function o(t,e){var n=e.components;return(0,u.toImmutable)(n)}function a(){return l.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var u=n(3),s=n(23),c=r(s),l=new u.Store({getInitialState:function(){return(0,u.toImmutable)([])},initialize:function(){this.on(c["default"].COMPONENT_LOADED,i),this.on(c["default"].SERVER_CONFIG_LOADED,o),this.on(c["default"].LOG_OUT,a)}});e["default"]=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.latitude,r=e.longitude,i=e.location_name,o=e.temperature_unit,u=e.time_zone,s=e.version;return(0,a.toImmutable)({latitude:n,longitude:r,location_name:i,temperature_unit:o,time_zone:u,serverVersion:s})}function o(){return c.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(23),s=r(u),c=new a.Store({getInitialState:function(){return(0,a.toImmutable)({latitude:null,longitude:null,location_name:"Home",temperature_unit:"°C",time_zone:"UTC",serverVersion:"unknown"})},initialize:function(){this.on(s["default"].SERVER_CONFIG_LOADED,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=c},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){t.dispatch(f["default"].ENTITY_HISTORY_DATE_SELECTED,{date:e})}function a(t){var e=arguments.length<=1||void 0===arguments[1]?null:arguments[1];t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),(0,c.callApi)(t,"GET",n).then(function(e){return t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})},function(){return t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_ERROR,{})})}function u(t,e){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_START,{date:e}),(0,c.callApi)(t,"GET","history/period/"+e).then(function(n){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})},function(){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_ERROR,{})})}function s(t){var e=t.evaluate(h.currentDate);return u(t,e)}Object.defineProperty(e,"__esModule",{value:!0}),e.changeCurrentDate=o,e.fetchRecent=a,e.fetchDate=u,e.fetchSelectedDate=s;var c=n(5),l=n(11),f=i(l),d=n(43),h=r(d)},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.date;return(0,s["default"])(n)}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(31),s=r(u),c=n(11),l=r(c),f=new a.Store({getInitialState:function(){var t=new Date;return t.setDate(t.getDate()-1),(0,s["default"])(t)},initialize:function(){this.on(l["default"].ENTITY_HISTORY_DATE_SELECTED,i),this.on(l["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,(0,a.toImmutable)({})):t.withMutations(function(t){r.forEach(function(e){return t.setIn([n,e[0].entity_id],(0,a.toImmutable)(e.map(l["default"].fromJSON)))})})}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c=n(16),l=r(c),f=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(3),o=n(11),a=r(o),u=new i.Store({getInitialState:function(){return!1},initialize:function(){this.on(a["default"].ENTITY_HISTORY_FETCH_START,function(){return!0}),this.on(a["default"].ENTITY_HISTORY_FETCH_SUCCESS,function(){return!1}),this.on(a["default"].ENTITY_HISTORY_FETCH_ERROR,function(){return!1}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_START,function(){return!0}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,function(){return!1}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_ERROR,function(){return!1}),this.on(a["default"].LOG_OUT,function(){return!1})}});e["default"]=u},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.stateHistory;return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,(0,a.toImmutable)(e.map(l["default"].fromJSON)))})})}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c=n(16),l=r(c),f=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,r)}),history.length>1&&t.set(c,r)})}function o(){return l.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c="ALL_ENTRY_FETCH",l=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(10),o=n(16),a=r(o),u=(0,i.createApiActions)(a["default"]);e["default"]=u},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e.visibleEntityMap=e.byId=e.entityMap=e.hasData=void 0;var i=n(10),o=n(16),a=r(o),u=(e.hasData=(0,i.createHasDataGetter)(a["default"]),e.entityMap=(0,i.createEntityMapGetter)(a["default"]));e.byId=(0,i.createByIdGetter)(a["default"]),e.visibleEntityMap=[u,function(t){return t.filter(function(t){return!t.attributes.hidden})}]},function(t,e,n){"use strict";function r(t){return(0,i.callApi)(t,"GET","error_log")}Object.defineProperty(e,"__esModule",{value:!0}),e.fetchErrorLog=r;var i=n(5)},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}Object.defineProperty(e,"__esModule",{value:!0}),e.actions=void 0;var i=n(141),o=r(i);e.actions=o},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=n(10),a=n(27),u=n(45),s=r(u),c=(0,o.createApiActions)(s["default"]);c.fireEvent=function(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2];return(0,i.callApi)(t,"POST","events/"+e,n).then(function(){a.actions.createNotification(t,"Event "+e+" successful fired!")})},e["default"]=c},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e.byId=e.entityMap=e.hasData=void 0;var i=n(10),o=n(45),a=r(o);e.hasData=(0,i.createHasDataGetter)(a["default"]),e.entityMap=(0,i.createEntityMapGetter)(a["default"]),e.byId=(0,i.createByIdGetter)(a["default"])},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(s["default"].LOGBOOK_DATE_SELECTED,{date:e})}function o(t,e){t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_START,{date:e}),(0,a.callApi)(t,"GET","logbook/"+e).then(function(n){return t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})},function(){return t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_ERROR,{})})}Object.defineProperty(e,"__esModule",{value:!0}),e.changeCurrentDate=i,e.fetchDate=o;var a=n(5),u=n(12),s=r(u)},function(t,e,n){"use strict";function r(t){return!t||(new Date).getTime()-t>o}Object.defineProperty(e,"__esModule",{value:!0}),e.isLoadingEntries=e.currentEntries=e.isCurrentStale=e.currentDate=void 0;var i=n(3),o=6e4,a=e.currentDate=["currentLogbookDate"];e.isCurrentStale=[a,["logbookEntriesUpdated"],function(t,e){return r(e.get(t))}],e.currentEntries=[a,["logbookEntries"],function(t,e){return e.get(t)||(0,i.toImmutable)([])}],e.isLoadingEntries=["isLoadingLogbookEntries"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({currentLogbookDate:u["default"],isLoadingLogbookEntries:c["default"],logbookEntries:f["default"],logbookEntriesUpdated:h["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.getters=e.actions=void 0,e.register=o;var a=n(149),u=i(a),s=n(150),c=i(s),l=n(151),f=i(l),d=n(152),h=i(d),p=n(145),_=r(p),v=n(146),y=r(v);e.actions=_,e.getters=y},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function a(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var u=function(){function t(t,e){for(var n=0;nt;t+=2){var e=rt[t],n=rt[t+1];e(n),rt[t]=void 0,rt[t+1]=void 0}$=0}function y(){try{var t=n(203);return q=t.runOnLoop||t.runOnContext,d()}catch(e){return _()}}function m(){}function g(){return new TypeError("You cannot resolve a promise with itself")}function b(){return new TypeError("A promises callback cannot return that same promise.")}function S(t){try{return t.then}catch(e){return ut.error=e,ut}}function w(t,e,n,r){try{t.call(e,n,r)}catch(i){return i}}function O(t,e,n){Z(function(t){var r=!1,i=w(n,e,function(n){r||(r=!0,e!==n?I(t,n):D(t,n))},function(e){r||(r=!0,C(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&i&&(r=!0,C(t,i))},t)}function M(t,e){e._state===ot?D(t,e._result):e._state===at?C(t,e._result):j(e,void 0,function(e){I(t,e)},function(e){C(t,e)})}function T(t,e){if(e.constructor===t.constructor)M(t,e);else{var n=S(e);n===ut?C(t,ut.error):void 0===n?D(t,e):u(n)?O(t,e,n):D(t,e)}}function I(t,e){t===e?C(t,g()):a(e)?T(t,e):D(t,e)}function E(t){t._onerror&&t._onerror(t._result),A(t)}function D(t,e){t._state===it&&(t._result=e,t._state=ot,0!==t._subscribers.length&&Z(A,t))}function C(t,e){t._state===it&&(t._state=at,t._result=e,Z(E,t))}function j(t,e,n,r){var i=t._subscribers,o=i.length;t._onerror=null,i[o]=e,i[o+ot]=n,i[o+at]=r,0===o&&t._state&&Z(A,t)}function A(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,i,o=t._result,a=0;aa;a++)j(r.resolve(t[a]),void 0,e,n);return i}function H(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(m);return I(n,t),n}function Y(t){var e=this,n=new e(m);return C(n,t),n}function U(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function G(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function B(t){this._id=pt++,this._state=void 0,this._result=void 0,this._subscribers=[],m!==t&&(u(t)||U(),this instanceof B||G(),N(this,t))}function F(){var t;if("undefined"!=typeof i)t=i;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=_t)}var V;V=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var q,W,K,J=V,$=0,Z=({}.toString,function(t,e){rt[$]=t,rt[$+1]=e,$+=2,2===$&&(W?W(v):K())}),X="undefined"!=typeof window?window:void 0,Q=X||{},tt=Q.MutationObserver||Q.WebKitMutationObserver,et="undefined"!=typeof t&&"[object process]"==={}.toString.call(t),nt="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,rt=new Array(1e3);K=et?f():tt?h():nt?p():void 0===X?y():_();var it=void 0,ot=1,at=2,ut=new P,st=new P;R.prototype._validateInput=function(t){return J(t)},R.prototype._validationError=function(){return new Error("Array Methods must be provided an Array")},R.prototype._init=function(){this._result=new Array(this.length)};var ct=R;R.prototype._enumerate=function(){for(var t=this,e=t.length,n=t.promise,r=t._input,i=0;n._state===it&&e>i;i++)t._eachEntry(r[i],i)},R.prototype._eachEntry=function(t,e){var n=this,r=n._instanceConstructor;s(t)?t.constructor===r&&t._state!==it?(t._onerror=null,n._settledAt(t._state,e,t._result)):n._willSettleAt(r.resolve(t),e):(n._remaining--,n._result[e]=t)},R.prototype._settledAt=function(t,e,n){var r=this,i=r.promise;i._state===it&&(r._remaining--,t===at?C(i,n):r._result[e]=n),0===r._remaining&&D(i,r._result)},R.prototype._willSettleAt=function(t,e){var n=this;j(t,void 0,function(t){n._settledAt(ot,e,t)},function(t){n._settledAt(at,e,t)})};var lt=z,ft=x,dt=H,ht=Y,pt=0,_t=B;B.all=lt,B.race=ft,B.resolve=dt,B.reject=ht,B._setScheduler=c,B._setAsap=l,B._asap=Z,B.prototype={constructor:B,then:function(t,e){var n=this,r=n._state;if(r===ot&&!t||r===at&&!e)return this;var i=new this.constructor(m),o=n._result;if(r){var a=arguments[r-1];Z(function(){L(r,i,a,o)})}else j(n,i,t,e);return i},"catch":function(t){return this.then(null,t)}};var vt=F,yt={Promise:_t,polyfill:vt};n(201).amd?(r=function(){return yt}.call(e,n,e,o),!(void 0!==r&&(o.exports=r))):"undefined"!=typeof o&&o.exports?o.exports=yt:"undefined"!=typeof this&&(this.ES6Promise=yt),vt()}).call(this)}).call(e,n(202),function(){return this}(),n(65)(t))},function(t,e,n){var r=n(60),i=r(Date,"now"),o=i||function(){return(new Date).getTime()};t.exports=o},function(t,e){function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e,n){var r=n(60),i=n(189),o=n(61),a="[object Array]",u=Object.prototype,s=u.toString,c=r(Array,"isArray"),l=c||function(t){return o(t)&&i(t.length)&&s.call(t)==a};t.exports=l},function(t,e,n){function r(t){return null==t?!1:i(t)?l.test(s.call(t)):o(t)&&a.test(t)}var i=n(62),o=n(61),a=/^\[object .+?Constructor\]$/,u=Object.prototype,s=Function.prototype.toString,c=u.hasOwnProperty,l=RegExp("^"+s.call(c).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=r},function(t,e){function n(t){return function(e){return null==e?void 0:e[t]}}t.exports=n},function(t,e,n){var r=n(192),i=r("length");t.exports=i},function(t,e,n){function r(t){return null!=t&&o(i(t))}var i=n(193),o=n(197);t.exports=r},function(t,e){function n(t,e){return t="number"==typeof t||r.test(t)?+t:-1,e=null==e?i:e,t>-1&&t%1==0&&e>t}var r=/^\d+$/,i=9007199254740991;t.exports=n},function(t,e,n){function r(t,e,n){if(!a(n))return!1;var r=typeof e;if("number"==r?i(n)&&o(e,n.length):"string"==r&&e in n){var u=n[e];return t===t?t===u:u!==u}return!1}var i=n(194),o=n(195),a=n(198);t.exports=r},function(t,e){function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e){function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){function r(t,e,n){n&&i(t,e,n)&&(e=n=void 0),t=+t||0,n=null==n?1:+n||0,null==e?(e=t,t=0):e=+e||0;for(var r=-1,u=a(o((e-t)/(n||1)),0),s=Array(u);++r1)for(var n=1;n \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 50aadaf880a..7def0c85efb 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 50aadaf880a9cb36bf144540171ff5fa029e9eaf +Subproject commit 7def0c85efbfe7a11a64560c21cb83059a5c7a3b diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 7a4e87de5a8..35c215a8630 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -21,7 +21,7 @@ from urllib.parse import urlparse, parse_qs import homeassistant.core as ha from homeassistant.const import ( - SERVER_PORT, CONTENT_TYPE_JSON, + SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING, HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED, @@ -198,12 +198,12 @@ class RequestHandler(SimpleHTTPRequestHandler): "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) return - self.authenticated = (self.server.api_password is None - or self.headers.get(HTTP_HEADER_HA_AUTH) == - self.server.api_password - or data.get(DATA_API_PASSWORD) == - self.server.api_password - or self.verify_session()) + self.authenticated = (self.server.api_password is None or + self.headers.get(HTTP_HEADER_HA_AUTH) == + self.server.api_password or + data.get(DATA_API_PASSWORD) == + self.server.api_password or + self.verify_session()) if '_METHOD' in data: method = data.pop('_METHOD') @@ -293,6 +293,17 @@ class RequestHandler(SimpleHTTPRequestHandler): json.dumps(data, indent=4, sort_keys=True, cls=rem.JSONEncoder).encode("UTF-8")) + def write_text(self, message, status_code=HTTP_OK): + """ Helper method to return a text message to the caller. """ + self.send_response(status_code) + self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN) + + self.set_session_cookie_header() + + self.end_headers() + + self.wfile.write(message.encode("UTF-8")) + def write_file(self, path, cache_headers=True): """ Returns a file to the user. """ try: diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 2286dd2d659..7cbba00afbb 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -7,7 +7,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/influxdb/ """ import logging - import homeassistant.util as util from homeassistant.helpers import validate_config from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF, @@ -77,6 +76,10 @@ def setup(hass, config): _state = 0 else: _state = state.state + try: + _state = float(_state) + except ValueError: + pass measurement = state.attributes.get('unit_of_measurement', state.domain) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 1b80035fb0d..93321b5fd10 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -50,6 +50,7 @@ FLASH_LONG = "long" # Apply an effect to the light, can be EFFECT_COLORLOOP ATTR_EFFECT = "effect" EFFECT_COLORLOOP = "colorloop" +EFFECT_RANDOM = "random" EFFECT_WHITE = "white" LIGHT_PROFILES_FILE = "light_profiles.csv" @@ -228,7 +229,8 @@ def setup(hass, config): if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG): params[ATTR_FLASH] = dat[ATTR_FLASH] - if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE): + if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE, + EFFECT_RANDOM): params[ATTR_EFFECT] = dat[ATTR_EFFECT] for light in target_lights: diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 7c3af9f968d..b1843f4bca8 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -10,17 +10,18 @@ import json import logging import os import socket +import random from datetime import timedelta from urllib.parse import urlparse from homeassistant.loader import get_component import homeassistant.util as util import homeassistant.util.color as color_util -from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME +from homeassistant.const import CONF_HOST, CONF_FILENAME, DEVICE_DEFAULT_NAME from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT, - ATTR_EFFECT, EFFECT_COLORLOOP, ATTR_RGB_COLOR) + ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_RGB_COLOR) REQUIREMENTS = ['phue==0.8'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -34,9 +35,9 @@ _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -def _find_host_from_config(hass): +def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): """ Attempt to detect host based on existing configuration. """ - path = hass.config.path(PHUE_CONFIG_FILE) + path = hass.config.path(filename) if not os.path.isfile(path): return None @@ -53,13 +54,14 @@ def _find_host_from_config(hass): def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Gets the Hue lights. """ + filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE) if discovery_info is not None: host = urlparse(discovery_info[1]).hostname else: host = config.get(CONF_HOST, None) if host is None: - host = _find_host_from_config(hass) + host = _find_host_from_config(hass, filename) if host is None: _LOGGER.error('No host found in configuration') @@ -69,17 +71,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if host in _CONFIGURING: return - setup_bridge(host, hass, add_devices_callback) + setup_bridge(host, hass, add_devices_callback, filename) -def setup_bridge(host, hass, add_devices_callback): +def setup_bridge(host, hass, add_devices_callback, filename): """ Setup a phue bridge based on host parameter. """ import phue try: bridge = phue.Bridge( host, - config_file_path=hass.config.path(PHUE_CONFIG_FILE)) + config_file_path=hass.config.path(filename)) except ConnectionRefusedError: # Wrong host was given _LOGGER.exception("Error connecting to the Hue bridge at %s", host) @@ -88,7 +90,7 @@ def setup_bridge(host, hass, add_devices_callback): except phue.PhueRegistrationException: _LOGGER.warning("Connected to Hue at %s but not registered.", host) - request_configuration(host, hass, add_devices_callback) + request_configuration(host, hass, add_devices_callback, filename) return @@ -120,10 +122,17 @@ def setup_bridge(host, hass, add_devices_callback): new_lights = [] + api_name = api.get('config').get('name') + if api_name == 'RaspBee-GW': + bridge_type = 'deconz' + else: + bridge_type = 'hue' + for light_id, info in api_states.items(): if light_id not in lights: lights[light_id] = HueLight(int(light_id), info, - bridge, update_lights) + bridge, update_lights, + bridge_type=bridge_type) new_lights.append(lights[light_id]) else: lights[light_id].info = info @@ -134,7 +143,7 @@ def setup_bridge(host, hass, add_devices_callback): update_lights() -def request_configuration(host, hass, add_devices_callback): +def request_configuration(host, hass, add_devices_callback, filename): """ Request configuration steps from the user. """ configurator = get_component('configurator') @@ -148,7 +157,7 @@ def request_configuration(host, hass, add_devices_callback): # pylint: disable=unused-argument def hue_configuration_callback(data): """ Actions to do when our configuration callback is called. """ - setup_bridge(host, hass, add_devices_callback) + setup_bridge(host, hass, add_devices_callback, filename) _CONFIGURING[host] = configurator.request_config( hass, "Philips Hue", hue_configuration_callback, @@ -162,11 +171,14 @@ def request_configuration(host, hass, add_devices_callback): class HueLight(Light): """ Represents a Hue light """ - def __init__(self, light_id, info, bridge, update_lights): + # pylint: disable=too-many-arguments + def __init__(self, light_id, info, bridge, update_lights, + bridge_type='hue'): self.light_id = light_id self.info = info self.bridge = bridge self.update_lights = update_lights + self.bridge_type = bridge_type @property def unique_id(self): @@ -226,14 +238,17 @@ class HueLight(Light): command['alert'] = 'lselect' elif flash == FLASH_SHORT: command['alert'] = 'select' - else: + elif self.bridge_type == 'hue': command['alert'] = 'none' effect = kwargs.get(ATTR_EFFECT) if effect == EFFECT_COLORLOOP: command['effect'] = 'colorloop' - else: + elif effect == EFFECT_RANDOM: + command['hue'] = random.randrange(0, 65535) + command['sat'] = random.randrange(150, 254) + elif self.bridge_type == 'hue': command['effect'] = 'none' self.bridge.set_light(self.light_id, command) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 6132c10a99c..22bd2575242 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -13,8 +13,9 @@ from homeassistant.components.light import Light from homeassistant.util import slugify from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \ - ATTR_NAME, EVENT_BUTTON_PRESSED +from homeassistant.components.rfxtrx import ( + ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, + ATTR_NAME, EVENT_BUTTON_PRESSED) DEPENDENCIES = ['rfxtrx'] diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 8a0c5b8fded..9908737b7b1 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -42,6 +42,7 @@ turn_on: description: Light effect values: - colorloop + - random turn_off: description: Turn a light off diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 829d3cfccdb..42a5e7b7899 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -7,16 +7,15 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.vera/ """ import logging -import time from requests.exceptions import RequestException from homeassistant.components.switch.vera import VeraSwitch from homeassistant.components.light import ATTR_BRIGHTNESS -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON + +REQUIREMENTS = ['pyvera==0.2.3'] _LOGGER = logging.getLogger(__name__) @@ -36,10 +35,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device_data = config.get('device_data', {}) - controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: - devices = controller.get_devices([ + devices = vera_controller.get_devices([ 'Switch', 'On/Off Switch', 'Dimmable Switch']) @@ -50,11 +58,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): lights = [] for device in devices: - extra_data = device_data.get(device.deviceId, {}) + extra_data = device_data.get(device.device_id, {}) exclude = extra_data.get('exclude', False) if exclude is not True: - lights.append(VeraLight(device, extra_data)) + lights.append(VeraLight(device, vera_controller, extra_data)) add_devices_callback(lights) @@ -77,5 +85,5 @@ class VeraLight(VeraSwitch): else: self.vera_device.switch_on() - self.last_command_send = time.time() - self.is_on_status = True + self._state = STATE_ON + self.update_ha_state() diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 5c2e7076955..16159404dec 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -28,7 +28,7 @@ QUERY_EVENTS_BETWEEN = """ SELECT * FROM events WHERE time_fired > ? AND time_fired < ? """ -EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY' +EVENT_LOGBOOK_ENTRY = 'logbook_entry' GROUP_BY_MINUTES = 15 @@ -204,7 +204,7 @@ def humanify(events): event.time_fired, "Home Assistant", action, domain=HA_DOMAIN) - elif event.event_type == EVENT_LOGBOOK_ENTRY: + elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY: domain = event.data.get(ATTR_DOMAIN) entity_id = event.data.get(ATTR_ENTITY_ID) if domain is None and entity_id is not None: diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 9a5d1c59d1a..a0d769e3d82 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -76,8 +76,12 @@ def setup(hass, config=None): logfilter[LOGGER_LOGS] = logs + logger = logging.getLogger('') + logger.setLevel(logging.NOTSET) + # Set log filter for all log handler for handler in logging.root.handlers: + handler.setLevel(logging.NOTSET) handler.addFilter(HomeAssistantLogFilter(logfilter)) return True diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8204052b4a9..1b6b9fbfa44 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -72,6 +72,7 @@ SUPPORT_YOUTUBE = 64 SUPPORT_TURN_ON = 128 SUPPORT_TURN_OFF = 256 SUPPORT_PLAY_MEDIA = 512 +SUPPORT_VOLUME_STEP = 1024 YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 87117cfd367..c0717edc860 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -20,7 +20,7 @@ from homeassistant.components.media_player import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) -REQUIREMENTS = ['pychromecast==0.6.13'] +REQUIREMENTS = ['pychromecast==0.6.14'] CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index a61dac88150..27b5aa3863c 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -142,10 +142,14 @@ class MpdDevice(MediaPlayerDevice): def media_title(self): """ Title of current playing media. """ name = self.currentsong.get('name', None) - title = self.currentsong['title'] + title = self.currentsong.get('title', None) - if name is None: + if name is None and title is None: + return "None" + elif name is None: return title + elif title is None: + return name else: return '{}: {}'.format(name, title) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 6925f942be4..52dd399cedf 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -35,7 +35,7 @@ SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK def config_from_file(filename, config=None): - ''' Small configuration file management function''' + """ Small configuration file management function. """ if config: # We're writing configuration try: @@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # pylint: disable=too-many-branches def setup_plexserver(host, token, hass, add_devices_callback): - ''' Setup a plexserver based on host parameter''' + """ Setup a plexserver based on host parameter. """ import plexapi.server import plexapi.exceptions diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index d3139d52c01..4fd13e8da42 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -22,9 +22,9 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) -SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF +SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ + SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF # pylint: disable=unused-argument @@ -202,11 +202,10 @@ class SqueezeBoxDevice(MediaPlayerDevice): """ Image url of current playing media. """ if 'artwork_url' in self._status: return self._status['artwork_url'] - return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\ - .format( - server=self._lms.host, - port=self._lms.http_port, - player=self._id) + return ('http://{server}:{port}/music/current/cover.jpg?' + 'player={player}').format(server=self._lms.host, + port=self._lms.http_port, + player=self._id) @property def media_title(self): diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py new file mode 100644 index 00000000000..09bb12ec332 --- /dev/null +++ b/homeassistant/components/media_player/universal.py @@ -0,0 +1,438 @@ +""" +homeassistant.components.media_player.universal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Combines multiple media players into one for a universal controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.universal/ +""" + +# pylint: disable=import-error +from copy import copy +import logging + +from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.service import call_from_config + +from homeassistant.const import ( + STATE_IDLE, STATE_ON, STATE_OFF, CONF_NAME, + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, + SERVICE_VOLUME_MUTE, + SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK) + +from homeassistant.components.media_player import ( + MediaPlayerDevice, DOMAIN, + SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SERVICE_PLAY_MEDIA, SERVICE_YOUTUBE_VIDEO, + ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED, + ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, + ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_TRACK, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_ALBUM_ARTIST, + ATTR_MEDIA_SEASON, ATTR_MEDIA_EPISODE, ATTR_MEDIA_CHANNEL, + ATTR_MEDIA_PLAYLIST, ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_SEEK_POSITION) + +ATTR_ACTIVE_CHILD = 'active_child' + +CONF_ATTRS = 'attributes' +CONF_CHILDREN = 'children' +CONF_COMMANDS = 'commands' +CONF_PLATFORM = 'platform' +CONF_SERVICE = 'service' +CONF_SERVICE_DATA = 'service_data' +CONF_STATE = 'state' + +OFF_STATES = [STATE_IDLE, STATE_OFF] +REQUIREMENTS = [] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ sets up the universal media players """ + if not validate_config(config): + return + + player = UniversalMediaPlayer(hass, + config[CONF_NAME], + config[CONF_CHILDREN], + config[CONF_COMMANDS], + config[CONF_ATTRS]) + + add_devices([player]) + + +def validate_config(config): + """ validate universal media player configuration """ + del config[CONF_PLATFORM] + + # validate name + if CONF_NAME not in config: + _LOGGER.error('Universal Media Player configuration requires name') + return False + + validate_children(config) + validate_commands(config) + validate_attributes(config) + + del_keys = [] + for key in config: + if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]: + _LOGGER.warning( + 'Universal Media Player (%s) unrecognized parameter %s', + config[CONF_NAME], key) + del_keys.append(key) + for key in del_keys: + del config[key] + + return True + + +def validate_children(config): + """ validate children """ + if CONF_CHILDREN not in config: + _LOGGER.info( + 'No children under Universal Media Player (%s)', config[CONF_NAME]) + config[CONF_CHILDREN] = [] + elif not isinstance(config[CONF_CHILDREN], list): + _LOGGER.warning( + 'Universal Media Player (%s) children not list in config. ' + 'They will be ignored.', + config[CONF_NAME]) + config[CONF_CHILDREN] = [] + + +def validate_commands(config): + """ validate commands """ + if CONF_COMMANDS not in config: + config[CONF_COMMANDS] = {} + elif not isinstance(config[CONF_COMMANDS], dict): + _LOGGER.warning( + 'Universal Media Player (%s) specified commands not dict in ' + 'config. They will be ignored.', + config[CONF_NAME]) + config[CONF_COMMANDS] = {} + + +def validate_attributes(config): + """ validate attributes """ + if CONF_ATTRS not in config: + config[CONF_ATTRS] = {} + elif not isinstance(config[CONF_ATTRS], dict): + _LOGGER.warning( + 'Universal Media Player (%s) specified attributes ' + 'not dict in config. They will be ignored.', + config[CONF_NAME]) + config[CONF_ATTRS] = {} + + for key, val in config[CONF_ATTRS].items(): + attr = val.split('|', 1) + if len(attr) == 1: + attr.append(None) + config[CONF_ATTRS][key] = attr + + +class UniversalMediaPlayer(MediaPlayerDevice): + """ Represents a universal media player in HA """ + # pylint: disable=too-many-public-methods + + def __init__(self, hass, name, children, commands, attributes): + # pylint: disable=too-many-arguments + self.hass = hass + self._name = name + self._children = children + self._cmds = commands + self._attrs = attributes + self._child_state = None + + def on_dependency_update(*_): + """ update ha state when dependencies update """ + self.update_ha_state(True) + + depend = copy(children) + for entity in attributes.values(): + depend.append(entity[0]) + + track_state_change(hass, depend, on_dependency_update) + + def _entity_lkp(self, entity_id, state_attr=None): + """ Looks up an entity state from hass """ + state_obj = self.hass.states.get(entity_id) + + if state_obj is None: + return + + if state_attr: + return state_obj.attributes.get(state_attr) + return state_obj.state + + def _override_or_child_attr(self, attr_name): + """ returns either the override or the active child for attr_name """ + if attr_name in self._attrs: + return self._entity_lkp(self._attrs[attr_name][0], + self._attrs[attr_name][1]) + + return self._child_attr(attr_name) + + def _child_attr(self, attr_name): + """ returns the active child's attr """ + active_child = self._child_state + return active_child.attributes.get(attr_name) if active_child else None + + def _call_service(self, service_name, service_data=None, + allow_override=False): + """ calls either a specified or active child's service """ + if allow_override and service_name in self._cmds: + call_from_config( + self.hass, self._cmds[service_name], blocking=True) + return + + if service_data is None: + service_data = {} + + active_child = self._child_state + service_data[ATTR_ENTITY_ID] = active_child.entity_id + + self.hass.services.call(DOMAIN, service_name, service_data, + blocking=True) + + @property + def should_poll(self): + """ Indicates whether HA should poll for updates """ + return False + + @property + def master_state(self): + """ gets the master state from entity or none """ + if CONF_STATE in self._attrs: + master_state = self._entity_lkp(self._attrs[CONF_STATE][0], + self._attrs[CONF_STATE][1]) + return master_state if master_state else STATE_OFF + else: + return None + + def _cache_active_child_state(self): + """ The state of the active child or None """ + for child_name in self._children: + child_state = self.hass.states.get(child_name) + if child_state and child_state.state not in OFF_STATES: + self._child_state = child_state + return + self._child_state = None + + @property + def name(self): + """ name of universal player """ + return self._name + + @property + def state(self): + """ + Current state of media player + + Off if master state is off + ELSE Status of first active child + ELSE master state or off + """ + master_state = self.master_state # avoid multiple lookups + if master_state == STATE_OFF: + return STATE_OFF + + active_child = self._child_state + if active_child: + return active_child.state + + return master_state if master_state else STATE_OFF + + @property + def volume_level(self): + """ Volume level of entity specified in attributes or active child """ + return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL) + + @property + def is_volume_muted(self): + """ boolean if volume is muted """ + return self._override_or_child_attr(ATTR_MEDIA_VOLUME_MUTED) \ + in [True, STATE_ON] + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return self._child_attr(ATTR_MEDIA_CONTENT_ID) + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return self._child_attr(ATTR_MEDIA_CONTENT_TYPE) + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + return self._child_attr(ATTR_MEDIA_DURATION) + + @property + def media_image_url(self): + """ Image url of current playing media. """ + return self._child_attr(ATTR_ENTITY_PICTURE) + + @property + def media_title(self): + """ Title of current playing media. """ + return self._child_attr(ATTR_MEDIA_TITLE) + + @property + def media_artist(self): + """ Artist of current playing media. (Music track only) """ + return self._child_attr(ATTR_MEDIA_ARTIST) + + @property + def media_album_name(self): + """ Album name of current playing media. (Music track only) """ + return self._child_attr(ATTR_MEDIA_ALBUM_NAME) + + @property + def media_album_artist(self): + """ Album arist of current playing media. (Music track only) """ + return self._child_attr(ATTR_MEDIA_ALBUM_ARTIST) + + @property + def media_track(self): + """ Track number of current playing media. (Music track only) """ + return self._child_attr(ATTR_MEDIA_TRACK) + + @property + def media_series_title(self): + """ Series title of current playing media. (TV Show only)""" + return self._child_attr(ATTR_MEDIA_SERIES_TITLE) + + @property + def media_season(self): + """ Season of current playing media. (TV Show only) """ + return self._child_attr(ATTR_MEDIA_SEASON) + + @property + def media_episode(self): + """ Episode of current playing media. (TV Show only) """ + return self._child_attr(ATTR_MEDIA_EPISODE) + + @property + def media_channel(self): + """ Channel currently playing. """ + return self._child_attr(ATTR_MEDIA_CHANNEL) + + @property + def media_playlist(self): + """ Title of Playlist currently playing. """ + return self._child_attr(ATTR_MEDIA_PLAYLIST) + + @property + def app_id(self): + """ ID of the current running app. """ + return self._child_attr(ATTR_APP_ID) + + @property + def app_name(self): + """ Name of the current running app. """ + return self._child_attr(ATTR_APP_NAME) + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + flags = self._child_attr(ATTR_SUPPORTED_MEDIA_COMMANDS) or 0 + + if SERVICE_TURN_ON in self._cmds: + flags |= SUPPORT_TURN_ON + if SERVICE_TURN_OFF in self._cmds: + flags |= SUPPORT_TURN_OFF + + if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, + SERVICE_VOLUME_DOWN]]): + flags |= SUPPORT_VOLUME_STEP + flags &= ~SUPPORT_VOLUME_SET + + if SERVICE_VOLUME_MUTE in self._cmds and \ + ATTR_MEDIA_VOLUME_MUTED in self._attrs: + flags |= SUPPORT_VOLUME_MUTE + + return flags + + @property + def device_state_attributes(self): + """ Extra attributes a device wants to expose. """ + active_child = self._child_state + return {ATTR_ACTIVE_CHILD: active_child.entity_id} \ + if active_child else {} + + def turn_on(self): + """ turn the media player on. """ + self._call_service(SERVICE_TURN_ON, allow_override=True) + + def turn_off(self): + """ turn the media player off. """ + self._call_service(SERVICE_TURN_OFF, allow_override=True) + + def mute_volume(self, is_volume_muted): + """ mute the volume. """ + data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted} + self._call_service(SERVICE_VOLUME_MUTE, data, allow_override=True) + + def set_volume_level(self, volume_level): + """ set volume level, range 0..1. """ + data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} + self._call_service(SERVICE_VOLUME_SET, data) + + def media_play(self): + """ Send play commmand. """ + self._call_service(SERVICE_MEDIA_PLAY) + + def media_pause(self): + """ Send pause command. """ + self._call_service(SERVICE_MEDIA_PAUSE) + + def media_previous_track(self): + """ Send previous track command. """ + self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK) + + def media_next_track(self): + """ Send next track command. """ + self._call_service(SERVICE_MEDIA_NEXT_TRACK) + + def media_seek(self, position): + """ Send seek command. """ + data = {ATTR_MEDIA_SEEK_POSITION: position} + self._call_service(SERVICE_MEDIA_SEEK, data) + + def play_youtube(self, media_id): + """ Plays a YouTube media. """ + data = {'media_id': media_id} + self._call_service(SERVICE_YOUTUBE_VIDEO, data) + + def play_media(self, media_type, media_id): + """ Plays a piece of media. """ + data = {'media_type': media_type, 'media_id': media_id} + self._call_service(SERVICE_PLAY_MEDIA, data) + + def volume_up(self): + """ volume_up media player. """ + self._call_service(SERVICE_VOLUME_UP, allow_override=True) + + def volume_down(self): + """ volume_down media player. """ + self._call_service(SERVICE_VOLUME_DOWN, allow_override=True) + + def media_play_pause(self): + """ media_play_pause media player. """ + self._call_service(SERVICE_MEDIA_PLAY_PAUSE) + + def update(self): + """ event to trigger a state update in HA """ + for child_name in self._children: + child_state = self.hass.states.get(child_name) + if child_state and child_state.state not in OFF_STATES: + self._child_state = child_state + return + self._child_state = None diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 37a7a63c72b..86dce3d511b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -30,7 +30,7 @@ DEFAULT_QOS = 0 DEFAULT_RETAIN = False SERVICE_PUBLISH = 'publish' -EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' +EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' REQUIREMENTS = ['paho-mqtt==1.1'] @@ -149,9 +149,9 @@ class MQTT(object): } if client_id is None: - self._mqttc = mqtt.Client() + self._mqttc = mqtt.Client(protocol=mqtt.MQTTv311) else: - self._mqttc = mqtt.Client(client_id) + self._mqttc = mqtt.Client(client_id, protocol=mqtt.MQTTv311) self._mqttc.user_data_set(self.userdata) diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py new file mode 100644 index 00000000000..a90e4b0d42a --- /dev/null +++ b/homeassistant/components/mqtt_eventstream.py @@ -0,0 +1,114 @@ +""" +homeassistant.components.mqtt_eventstream +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Connect two Home Assistant instances via mqtt. + +Configuration: + +To use the mqtt_eventstream component you will need to add the following to +your configuration.yaml file. + +If you do not specify a publish_topic you will not forward events to the queue. +If you do not specify a subscribe_topic then you will not receive events from +the remote server. + +mqtt_eventstream: + publish_topic: MyServerName + subscribe_topic: OtherHaServerName +""" +import json +from homeassistant.core import EventOrigin, State +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN +from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH +from homeassistant.const import ( + MATCH_ALL, + EVENT_TIME_CHANGED, + EVENT_CALL_SERVICE, + EVENT_SERVICE_EXECUTED, + EVENT_STATE_CHANGED, +) +import homeassistant.loader as loader +from homeassistant.remote import JSONEncoder + +# The domain of your component. Should be equal to the name of your component +DOMAIN = "mqtt_eventstream" + +# List of component names (string) your component depends upon +DEPENDENCIES = ['mqtt'] + + +def setup(hass, config): + """ Setup our mqtt_eventstream component. """ + mqtt = loader.get_component('mqtt') + pub_topic = config[DOMAIN].get('publish_topic', None) + sub_topic = config[DOMAIN].get('subscribe_topic', None) + + def _event_publisher(event): + """ Handle events by publishing them on the mqtt queue. """ + if event.origin != EventOrigin.local: + return + if event.event_type == EVENT_TIME_CHANGED: + return + + # Filter out the events that were triggered by publishing + # to the MQTT topic, or you will end up in an infinite loop. + if event.event_type == EVENT_CALL_SERVICE: + if ( + event.data.get('domain') == MQTT_DOMAIN and + event.data.get('service') == MQTT_SVC_PUBLISH and + event.data.get('topic') == pub_topic + ): + return + + # Filter out all the "event service executed" events because they + # are only used internally by core as callbacks for blocking + # during the interval while a service is being executed. + # They will serve no purpose to the external system, + # and thus are unnecessary traffic. + # And at any rate it would cause an infinite loop to publish them + # because publishing to an MQTT topic itself triggers one. + if event.event_type == EVENT_SERVICE_EXECUTED: + return + + event_info = {'event_type': event.event_type, 'event_data': event.data} + msg = json.dumps(event_info, cls=JSONEncoder) + mqtt.publish(hass, pub_topic, msg) + + # Only listen for local events if you are going to publish them + if pub_topic: + hass.bus.listen(MATCH_ALL, _event_publisher) + + # Process events from a remote server that are received on a queue + def _event_receiver(topic, payload, qos): + """ + Receive events published by the other HA instance and fire + them on this hass instance. + """ + event = json.loads(payload) + event_type = event.get('event_type') + event_data = event.get('event_data') + + # Special case handling for event STATE_CHANGED + # We will try to convert state dicts back to State objects + # Copied over from the _handle_api_post_events_event method + # of the api component. + if event_type == EVENT_STATE_CHANGED and event_data: + for key in ('old_state', 'new_state'): + state = State.from_dict(event_data.get(key)) + + if state: + event_data[key] = state + + hass.bus.fire( + event_type, + event_data=event_data, + origin=EventOrigin.remote + ) + + # Only subscribe if you specified a topic + if sub_topic: + mqtt.subscribe(hass, sub_topic, _event_receiver) + + hass.states.set('{domain}.initialized'.format(domain=DOMAIN), True) + # return boolean to indicate that initialization was successful + return True diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py new file mode 100644 index 00000000000..7fb1a7cb1d7 --- /dev/null +++ b/homeassistant/components/mysensors.py @@ -0,0 +1,230 @@ +""" +homeassistant.components.mysensors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +MySensors component that connects to a MySensors gateway via pymysensors +API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.mysensors.html + + +New features: + +New MySensors component. +Updated MySensors Sensor platform. +New MySensors Switch platform. Currently only in optimistic mode (compare +with MQTT). +Multiple gateways are now supported. + +Configuration.yaml: + +mysensors: + gateways: + - port: '/dev/ttyUSB0' + persistence_file: 'path/mysensors.json' + - port: '/dev/ttyACM1' + persistence_file: 'path/mysensors2.json' + debug: true + persistence: true + version: '1.5' +""" +import logging + +from homeassistant.helpers import validate_config +import homeassistant.bootstrap as bootstrap + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, + TEMP_CELCIUS,) + +CONF_GATEWAYS = 'gateways' +CONF_PORT = 'port' +CONF_DEBUG = 'debug' +CONF_PERSISTENCE = 'persistence' +CONF_PERSISTENCE_FILE = 'persistence_file' +CONF_VERSION = 'version' +DEFAULT_VERSION = '1.4' + +DOMAIN = 'mysensors' +DEPENDENCIES = [] +REQUIREMENTS = [ + 'https://github.com/theolind/pymysensors/archive/' + '005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4'] +_LOGGER = logging.getLogger(__name__) +ATTR_NODE_ID = 'node_id' +ATTR_CHILD_ID = 'child_id' +ATTR_PORT = 'port' + +GATEWAYS = None +SCAN_INTERVAL = 30 + +DISCOVER_SENSORS = "mysensors.sensors" +DISCOVER_SWITCHES = "mysensors.switches" + +# Maps discovered services to their platforms +DISCOVERY_COMPONENTS = [ + ('sensor', DISCOVER_SENSORS), + ('switch', DISCOVER_SWITCHES), +] + + +def setup(hass, config): + """Setup the MySensors component.""" + # pylint: disable=too-many-locals + + if not validate_config(config, + {DOMAIN: [CONF_GATEWAYS]}, + _LOGGER): + return False + + import mysensors.mysensors as mysensors + + version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION)) + is_metric = (hass.config.temperature_unit == TEMP_CELCIUS) + + def setup_gateway(port, persistence, persistence_file, version): + """Return gateway after setup of the gateway.""" + gateway = mysensors.SerialGateway(port, event_callback=None, + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + gateway.metric = is_metric + gateway.debug = config[DOMAIN].get(CONF_DEBUG, False) + gateway = GatewayWrapper(gateway, version) + # pylint: disable=attribute-defined-outside-init + gateway.event_callback = gateway.callback_factory() + + def gw_start(event): + """Callback to trigger start of gateway and any persistence.""" + gateway.start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: gateway.stop()) + if persistence: + for node_id in gateway.sensors: + gateway.event_callback('persistence', node_id) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start) + + return gateway + + # Setup all ports from config + global GATEWAYS + GATEWAYS = {} + conf_gateways = config[DOMAIN][CONF_GATEWAYS] + if isinstance(conf_gateways, dict): + conf_gateways = [conf_gateways] + persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) + + for index, gway in enumerate(conf_gateways): + port = gway[CONF_PORT] + persistence_file = gway.get( + CONF_PERSISTENCE_FILE, + hass.config.path('mysensors{}.pickle'.format(index + 1))) + GATEWAYS[port] = setup_gateway( + port, persistence, persistence_file, version) + + for (component, discovery_service) in DISCOVERY_COMPONENTS: + # Ensure component is loaded + if not bootstrap.setup_component(hass, component, config): + return False + # Fire discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_service, + ATTR_DISCOVERED: {}}) + + return True + + +def pf_callback_factory( + s_types, v_types, devices, add_devices, entity_class): + """Return a new callback for the platform.""" + def mysensors_callback(gateway, node_id): + """Callback for mysensors platform.""" + if gateway.sensors[node_id].sketch_name is None: + _LOGGER.info('No sketch_name: node %s', node_id) + return + # previously discovered, just update state with latest info + if node_id in devices: + for entity in devices[node_id]: + entity.update_ha_state(True) + return + + # First time we see this node, detect sensors + for child in gateway.sensors[node_id].children.values(): + name = '{} {}.{}'.format( + gateway.sensors[node_id].sketch_name, node_id, child.id) + + for value_type in child.values.keys(): + if child.type not in s_types or value_type not in v_types: + continue + + devices[node_id].append( + entity_class(gateway, node_id, child.id, name, value_type)) + if devices[node_id]: + _LOGGER.info('adding new devices: %s', devices[node_id]) + add_devices(devices[node_id]) + for entity in devices[node_id]: + entity.update_ha_state(True) + return mysensors_callback + + +class GatewayWrapper(object): + """Gateway wrapper class, by subclassing serial gateway.""" + + def __init__(self, gateway, version): + """Setup class attributes on instantiation. + + Args: + gateway (mysensors.SerialGateway): Gateway to wrap. + version (str): Version of mysensors API. + + Attributes: + _wrapped_gateway (mysensors.SerialGateway): Wrapped gateway. + version (str): Version of mysensors API. + platform_callbacks (list): Callback functions, one per platform. + const (module): Mysensors API constants. + __initialised (bool): True if GatewayWrapper is initialised. + """ + self._wrapped_gateway = gateway + self.version = version + self.platform_callbacks = [] + self.const = self.get_const() + self.__initialised = True + + def __getattr__(self, name): + """See if this object has attribute name.""" + # Do not use hasattr, it goes into infinite recurrsion + if name in self.__dict__: + # this object has it + return getattr(self, name) + # proxy to the wrapped object + return getattr(self._wrapped_gateway, name) + + def __setattr__(self, name, value): + """See if this object has attribute name then set to value.""" + if '_GatewayWrapper__initialised' not in self.__dict__: + return object.__setattr__(self, name, value) + elif name in self.__dict__: + object.__setattr__(self, name, value) + else: + object.__setattr__(self._wrapped_gateway, name, value) + + def get_const(self): + """Get mysensors API constants.""" + if self.version == '1.5': + import mysensors.const_15 as const + else: + import mysensors.const_14 as const + return const + + def callback_factory(self): + """Return a new callback function.""" + def node_update(update_type, node_id): + """Callback for node updates from the MySensors gateway.""" + _LOGGER.info('update %s: node %s', update_type, node_id) + for callback in self.platform_callbacks: + callback(self, node_id) + + return node_update diff --git a/homeassistant/components/notify/free_mobile.py b/homeassistant/components/notify/free_mobile.py new file mode 100644 index 00000000000..3589dc58658 --- /dev/null +++ b/homeassistant/components/notify/free_mobile.py @@ -0,0 +1,51 @@ +""" +homeassistant.components.notify.free_mobile +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Free Mobile SMS platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.free_mobile/ +""" +import logging +from homeassistant.helpers import validate_config +from homeassistant.components.notify import ( + DOMAIN, BaseNotificationService) +from homeassistant.const import CONF_USERNAME, CONF_ACCESS_TOKEN + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['freesms==0.1.0'] + + +def get_service(hass, config): + """ Get the Free Mobile SMS notification service. """ + + if not validate_config({DOMAIN: config}, + {DOMAIN: [CONF_USERNAME, + CONF_ACCESS_TOKEN]}, + _LOGGER): + return None + + return FreeSMSNotificationService(config[CONF_USERNAME], + config[CONF_ACCESS_TOKEN]) + + +# pylint: disable=too-few-public-methods +class FreeSMSNotificationService(BaseNotificationService): + """ Implements notification service for the Free Mobile SMS service. """ + + def __init__(self, username, access_token): + from freesms import FreeClient + self.free_client = FreeClient(username, access_token) + + def send_message(self, message="", **kwargs): + """ Send a message to the Free Mobile user cell. """ + resp = self.free_client.send_sms(message) + + if resp.status_code == 400: + _LOGGER.error("At least one parameter is missing") + elif resp.status_code == 402: + _LOGGER.error("Too much SMS send in a few time") + elif resp.status_code == 403: + _LOGGER.error("Wrong Username/Password") + elif resp.status_code == 500: + _LOGGER.error("Server error, try later") diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py index 4ee9ead9152..56075a6dd09 100644 --- a/homeassistant/components/notify/syslog.py +++ b/homeassistant/components/notify/syslog.py @@ -7,59 +7,62 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.syslog/ """ import logging -import syslog from homeassistant.helpers import validate_config from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) _LOGGER = logging.getLogger(__name__) -FACILITIES = {'kernel': syslog.LOG_KERN, - 'user': syslog.LOG_USER, - 'mail': syslog.LOG_MAIL, - 'daemon': syslog.LOG_DAEMON, - 'auth': syslog.LOG_KERN, - 'LPR': syslog.LOG_LPR, - 'news': syslog.LOG_NEWS, - 'uucp': syslog.LOG_UUCP, - 'cron': syslog.LOG_CRON, - 'syslog': syslog.LOG_SYSLOG, - 'local0': syslog.LOG_LOCAL0, - 'local1': syslog.LOG_LOCAL1, - 'local2': syslog.LOG_LOCAL2, - 'local3': syslog.LOG_LOCAL3, - 'local4': syslog.LOG_LOCAL4, - 'local5': syslog.LOG_LOCAL5, - 'local6': syslog.LOG_LOCAL6, - 'local7': syslog.LOG_LOCAL7} - -OPTIONS = {'pid': syslog.LOG_PID, - 'cons': syslog.LOG_CONS, - 'ndelay': syslog.LOG_NDELAY, - 'nowait': syslog.LOG_NOWAIT, - 'perror': syslog.LOG_PERROR} - -PRIORITIES = {5: syslog.LOG_EMERG, - 4: syslog.LOG_ALERT, - 3: syslog.LOG_CRIT, - 2: syslog.LOG_ERR, - 1: syslog.LOG_WARNING, - 0: syslog.LOG_NOTICE, - -1: syslog.LOG_INFO, - -2: syslog.LOG_DEBUG} def get_service(hass, config): - """ Get the mail notification service. """ - + """Get the syslog notification service.""" if not validate_config({DOMAIN: config}, {DOMAIN: ['facility', 'option', 'priority']}, _LOGGER): return None - _facility = FACILITIES.get(config['facility'], 40) - _option = OPTIONS.get(config['option'], 10) - _priority = PRIORITIES.get(config['priority'], -1) + import syslog + + _facility = { + 'kernel': syslog.LOG_KERN, + 'user': syslog.LOG_USER, + 'mail': syslog.LOG_MAIL, + 'daemon': syslog.LOG_DAEMON, + 'auth': syslog.LOG_KERN, + 'LPR': syslog.LOG_LPR, + 'news': syslog.LOG_NEWS, + 'uucp': syslog.LOG_UUCP, + 'cron': syslog.LOG_CRON, + 'syslog': syslog.LOG_SYSLOG, + 'local0': syslog.LOG_LOCAL0, + 'local1': syslog.LOG_LOCAL1, + 'local2': syslog.LOG_LOCAL2, + 'local3': syslog.LOG_LOCAL3, + 'local4': syslog.LOG_LOCAL4, + 'local5': syslog.LOG_LOCAL5, + 'local6': syslog.LOG_LOCAL6, + 'local7': syslog.LOG_LOCAL7, + }.get(config['facility'], 40) + + _option = { + 'pid': syslog.LOG_PID, + 'cons': syslog.LOG_CONS, + 'ndelay': syslog.LOG_NDELAY, + 'nowait': syslog.LOG_NOWAIT, + 'perror': syslog.LOG_PERROR + }.get(config['option'], 10) + + _priority = { + 5: syslog.LOG_EMERG, + 4: syslog.LOG_ALERT, + 3: syslog.LOG_CRIT, + 2: syslog.LOG_ERR, + 1: syslog.LOG_WARNING, + 0: syslog.LOG_NOTICE, + -1: syslog.LOG_INFO, + -2: syslog.LOG_DEBUG + }.get(config['priority'], -1) return SyslogNotificationService(_facility, _option, _priority) @@ -76,6 +79,7 @@ class SyslogNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """ Send a message to a user. """ + import syslog title = kwargs.get(ATTR_TITLE) diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 126d8c9f40e..802634715e9 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -16,7 +16,7 @@ import json import atexit from homeassistant.core import Event, EventOrigin, State -import homeassistant.util.dt as date_util +import homeassistant.util.dt as dt_util from homeassistant.remote import JSONEncoder from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, @@ -62,8 +62,8 @@ def row_to_state(row): try: return State( row[1], row[2], json.loads(row[3]), - date_util.utc_from_timestamp(row[4]), - date_util.utc_from_timestamp(row[5])) + dt_util.utc_from_timestamp(row[4]), + dt_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to state: %s", row) @@ -74,7 +74,7 @@ def row_to_event(row): """ Convert a databse row to an event. """ try: return Event(row[1], json.loads(row[2]), EventOrigin(row[3]), - date_util.utc_from_timestamp(row[5])) + dt_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to event: %s", row) @@ -116,10 +116,10 @@ class RecorderRun(object): self.start = _INSTANCE.recording_start self.closed_incorrect = False else: - self.start = date_util.utc_from_timestamp(row[1]) + self.start = dt_util.utc_from_timestamp(row[1]) if row[2] is not None: - self.end = date_util.utc_from_timestamp(row[2]) + self.end = dt_util.utc_from_timestamp(row[2]) self.closed_incorrect = bool(row[3]) @@ -169,8 +169,8 @@ class Recorder(threading.Thread): self.queue = queue.Queue() self.quit_object = object() self.lock = threading.Lock() - self.recording_start = date_util.utcnow() - self.utc_offset = date_util.now().utcoffset().total_seconds() + self.recording_start = dt_util.utcnow() + self.utc_offset = dt_util.now().utcoffset().total_seconds() def start_recording(event): """ Start recording. """ @@ -217,10 +217,11 @@ class Recorder(threading.Thread): def shutdown(self, event): """ Tells the recorder to shut down. """ self.queue.put(self.quit_object) + self.block_till_done() def record_state(self, entity_id, state, event_id): """ Save a state to the database. """ - now = date_util.utcnow() + now = dt_util.utcnow() # State got deleted if state is None: @@ -247,7 +248,7 @@ class Recorder(threading.Thread): """ Save an event to the database. """ info = ( event.event_type, json.dumps(event.data, cls=JSONEncoder), - str(event.origin), date_util.utcnow(), event.time_fired, + str(event.origin), dt_util.utcnow(), event.time_fired, self.utc_offset ) @@ -307,7 +308,7 @@ class Recorder(threading.Thread): def save_migration(migration_id): """ Save and commit a migration to the database. """ cur.execute('INSERT INTO schema_version VALUES (?, ?)', - (migration_id, date_util.utcnow())) + (migration_id, dt_util.utcnow())) self.conn.commit() _LOGGER.info("Database migrated to version %d", migration_id) @@ -420,18 +421,18 @@ class Recorder(threading.Thread): self.query( """INSERT INTO recorder_runs (start, created, utc_offset) VALUES (?, ?, ?)""", - (self.recording_start, date_util.utcnow(), self.utc_offset)) + (self.recording_start, dt_util.utcnow(), self.utc_offset)) def _close_run(self): """ Save end time for current run. """ self.query( "UPDATE recorder_runs SET end=? WHERE start=?", - (date_util.utcnow(), self.recording_start)) + (dt_util.utcnow(), self.recording_start)) def _adapt_datetime(datetimestamp): """ Turn a datetime into an integer for in the DB. """ - return date_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() + return dt_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() def _verify_instance(): diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 7c96230ccd4..ce1a3242542 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -73,8 +73,9 @@ def _process_config(scene_config): for entity_id in c_entities: if isinstance(c_entities[entity_id], dict): - state = c_entities[entity_id].pop('state', None) - attributes = c_entities[entity_id] + entity_attrs = c_entities[entity_id].copy() + state = entity_attrs.pop('state', None) + attributes = entity_attrs else: state = c_entities[entity_id] attributes = {} diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 04770ced241..88071c0b5fb 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -9,7 +9,8 @@ https://home-assistant.io/components/sensor/ import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.components import wink, zwave, isy994, verisure, ecobee +from homeassistant.components import ( + wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors) DOMAIN = 'sensor' SCAN_INTERVAL = 30 @@ -22,7 +23,9 @@ DISCOVERY_PLATFORMS = { zwave.DISCOVER_SENSORS: 'zwave', isy994.DISCOVER_SENSORS: 'isy994', verisure.DISCOVER_SENSORS: 'verisure', - ecobee.DISCOVER_SENSORS: 'ecobee' + ecobee.DISCOVER_SENSORS: 'ecobee', + tellduslive.DISCOVER_SENSORS: 'tellduslive', + mysensors.DISCOVER_SENSORS: 'mysensors', } diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index 298c9b8cb79..dd9281c484a 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -11,8 +11,8 @@ import logging import requests -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, \ - DEVICE_DEFAULT_NAME +from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + DEVICE_DEFAULT_NAME) from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity from homeassistant.util import template, Throttle diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 608dc2f19fd..4e6b8a5c342 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -53,6 +53,11 @@ class EliqSensor(Entity): """ Returns the name. """ return self._name + @property + def icon(self): + """ Returns icon. """ + return "mdi:speedometer" + @property def unit_of_measurement(self): """ Unit of measurement of this entity, if any. """ @@ -65,5 +70,8 @@ class EliqSensor(Entity): def update(self): """ Gets the latest data. """ - response = self.api.get_data_now(channelid=self.channel_id) - self._state = int(response.power) + try: + response = self.api.get_data_now(channelid=self.channel_id) + self._state = int(response.power) + except TypeError: # raised by eliqonline library on any HTTP error + pass diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index fb61173d8ee..3562af1949d 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -7,150 +7,177 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mysensors/ """ import logging +from collections import defaultdict from homeassistant.helpers.entity import Entity from homeassistant.const import ( - ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, + ATTR_BATTERY_LEVEL, TEMP_CELCIUS, TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) -CONF_PORT = "port" -CONF_DEBUG = "debug" -CONF_PERSISTENCE = "persistence" -CONF_PERSISTENCE_FILE = "persistence_file" -CONF_VERSION = "version" - -ATTR_NODE_ID = "node_id" -ATTR_CHILD_ID = "child_id" +import homeassistant.components.mysensors as mysensors _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/' - 'd4b809c2167650691058d1e29bfd2c4b1792b4b0.zip' - '#pymysensors==0.3'] +DEPENDENCIES = [] def setup_platform(hass, config, add_devices, discovery_info=None): - """ Setup the mysensors platform. """ + """Setup the mysensors platform for sensors.""" + # Only act if loaded via mysensors by discovery event. + # Otherwise gateway is not setup. + if discovery_info is None: + return - import mysensors.mysensors as mysensors - import mysensors.const_14 as const + for gateway in mysensors.GATEWAYS.values(): + # Define the S_TYPES and V_TYPES that the platform should handle as + # states. + s_types = [ + gateway.const.Presentation.S_TEMP, + gateway.const.Presentation.S_HUM, + gateway.const.Presentation.S_BARO, + gateway.const.Presentation.S_WIND, + gateway.const.Presentation.S_RAIN, + gateway.const.Presentation.S_UV, + gateway.const.Presentation.S_WEIGHT, + gateway.const.Presentation.S_POWER, + gateway.const.Presentation.S_DISTANCE, + gateway.const.Presentation.S_LIGHT_LEVEL, + gateway.const.Presentation.S_IR, + gateway.const.Presentation.S_WATER, + gateway.const.Presentation.S_AIR_QUALITY, + gateway.const.Presentation.S_CUSTOM, + gateway.const.Presentation.S_DUST, + gateway.const.Presentation.S_SCENE_CONTROLLER, + ] + not_v_types = [ + gateway.const.SetReq.V_ARMED, + gateway.const.SetReq.V_LIGHT, + gateway.const.SetReq.V_LOCK_STATUS, + ] + if float(gateway.version) >= 1.5: + s_types.extend([ + gateway.const.Presentation.S_COLOR_SENSOR, + gateway.const.Presentation.S_MULTIMETER, + ]) + not_v_types.extend([gateway.const.SetReq.V_STATUS, ]) + v_types = [member for member in gateway.const.SetReq + if member.value not in not_v_types] - devices = {} # keep track of devices added to HA - # Just assume celcius means that the user wants metric for now. - # It may make more sense to make this a global config option in the future. - is_metric = (hass.config.temperature_unit == TEMP_CELCIUS) - - def sensor_update(update_type, nid): - """ Callback for sensor updates from the MySensors gateway. """ - _LOGGER.info("sensor_update %s: node %s", update_type, nid) - sensor = gateway.sensors[nid] - if sensor.sketch_name is None: - return - if nid not in devices: - devices[nid] = {} - - node = devices[nid] - new_devices = [] - for child_id, child in sensor.children.items(): - if child_id not in node: - node[child_id] = {} - for value_type, value in child.values.items(): - if value_type not in node[child_id]: - name = '{} {}.{}'.format(sensor.sketch_name, nid, child.id) - node[child_id][value_type] = \ - MySensorsNodeValue( - nid, child_id, name, value_type, is_metric, const) - new_devices.append(node[child_id][value_type]) - else: - node[child_id][value_type].update_sensor( - value, sensor.battery_level) - - if new_devices: - _LOGGER.info("adding new devices: %s", new_devices) - add_devices(new_devices) - - port = config.get(CONF_PORT) - if port is None: - _LOGGER.error("Missing required key 'port'") - return False - - persistence = config.get(CONF_PERSISTENCE, True) - persistence_file = config.get(CONF_PERSISTENCE_FILE, - hass.config.path('mysensors.pickle')) - version = config.get(CONF_VERSION, '1.4') - - gateway = mysensors.SerialGateway(port, sensor_update, - persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - gateway.metric = is_metric - gateway.debug = config.get(CONF_DEBUG, False) - gateway.start() - - if persistence: - for nid in gateway.sensors: - sensor_update('sensor_update', nid) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: gateway.stop()) + devices = defaultdict(list) + gateway.platform_callbacks.append(mysensors.pf_callback_factory( + s_types, v_types, devices, add_devices, MySensorsSensor)) -class MySensorsNodeValue(Entity): - """ Represents the value of a MySensors child node. """ - # pylint: disable=too-many-arguments, too-many-instance-attributes - def __init__(self, node_id, child_id, name, value_type, metric, const): - self._name = name +class MySensorsSensor(Entity): + """Represent the value of a MySensors child node.""" + + # pylint: disable=too-many-arguments + + def __init__(self, gateway, node_id, child_id, name, value_type): + """Setup class attributes on instantiation. + + Args: + gateway (GatewayWrapper): Gateway object. + node_id (str): Id of node. + child_id (str): Id of child. + name (str): Entity name. + value_type (str): Value type of child. Value is entity state. + + Attributes: + gateway (GatewayWrapper): Gateway object. + node_id (str): Id of node. + child_id (str): Id of child. + _name (str): Entity name. + value_type (str): Value type of child. Value is entity state. + battery_level (int): Node battery level. + _values (dict): Child values. Non state values set as state attributes. + """ + self.gateway = gateway self.node_id = node_id self.child_id = child_id - self.battery_level = 0 + self._name = name self.value_type = value_type - self.metric = metric - self._value = '' - self.const = const + self.battery_level = 0 + self._values = {} @property def should_poll(self): - """ MySensor gateway pushes its state to HA. """ + """MySensor gateway pushes its state to HA.""" return False @property def name(self): - """ The name of this sensor. """ + """The name of this entity.""" return self._name @property def state(self): - """ Returns the state of the device. """ - return self._value + """Return the state of the device.""" + if not self._values: + return '' + return self._values[self.value_type] @property def unit_of_measurement(self): - """ Unit of measurement of this entity. """ - if self.value_type == self.const.SetReq.V_TEMP: - return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT - elif self.value_type == self.const.SetReq.V_HUM or \ - self.value_type == self.const.SetReq.V_DIMMER or \ - self.value_type == self.const.SetReq.V_LIGHT_LEVEL: + """Unit of measurement of this entity.""" + # pylint:disable=too-many-return-statements + if self.value_type == self.gateway.const.SetReq.V_TEMP: + return TEMP_CELCIUS if self.gateway.metric else TEMP_FAHRENHEIT + elif self.value_type == self.gateway.const.SetReq.V_HUM or \ + self.value_type == self.gateway.const.SetReq.V_DIMMER or \ + self.value_type == self.gateway.const.SetReq.V_PERCENTAGE or \ + self.value_type == self.gateway.const.SetReq.V_LIGHT_LEVEL: return '%' + elif self.value_type == self.gateway.const.SetReq.V_WATT: + return 'W' + elif self.value_type == self.gateway.const.SetReq.V_KWH: + return 'kWh' + elif self.value_type == self.gateway.const.SetReq.V_VOLTAGE: + return 'V' + elif self.value_type == self.gateway.const.SetReq.V_CURRENT: + return 'A' + elif self.value_type == self.gateway.const.SetReq.V_IMPEDANCE: + return 'ohm' + elif self.gateway.const.SetReq.V_UNIT_PREFIX in self._values: + return self._values[self.gateway.const.SetReq.V_UNIT_PREFIX] return None + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + device_attr = dict(self._values) + device_attr.pop(self.value_type, None) + return device_attr + @property def state_attributes(self): - """ Returns the state attributes. """ - return { - ATTR_NODE_ID: self.node_id, - ATTR_CHILD_ID: self.child_id, + """Return the state attributes.""" + data = { + mysensors.ATTR_PORT: self.gateway.port, + mysensors.ATTR_NODE_ID: self.node_id, + mysensors.ATTR_CHILD_ID: self.child_id, ATTR_BATTERY_LEVEL: self.battery_level, } - def update_sensor(self, value, battery_level): - """ Update a sensor with the latest value from the controller. """ - _LOGGER.info("%s value = %s", self._name, value) - if self.value_type == self.const.SetReq.V_TRIPPED or \ - self.value_type == self.const.SetReq.V_ARMED: - self._value = STATE_ON if int(value) == 1 else STATE_OFF - else: - self._value = value - self.battery_level = battery_level - self.update_ha_state() + device_attr = self.device_state_attributes + + if device_attr is not None: + data.update(device_attr) + + return data + + def update(self): + """Update the controller with the latest values from a sensor.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + for value_type, value in child.values.items(): + _LOGGER.info( + "%s: value_type %s, value = %s", self._name, value_type, value) + if value_type == self.gateway.const.SetReq.V_TRIPPED: + self._values[value_type] = STATE_ON if int( + value) == 1 else STATE_OFF + else: + self._values[value_type] = value + + self.battery_level = node.battery_level diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 14e8d60c7c6..84784a19546 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -13,14 +13,14 @@ from homeassistant.util import Throttle from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyowm==2.2.1'] +REQUIREMENTS = ['pyowm==2.3.0'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'weather': ['Condition', ''], 'temperature': ['Temperature', ''], 'wind_speed': ['Wind speed', 'm/s'], 'humidity': ['Humidity', '%'], - 'pressure': ['Pressure', 'hPa'], + 'pressure': ['Pressure', 'mbar'], 'clouds': ['Cloud coverage', '%'], 'rain': ['Rain', 'mm'], 'snow': ['Snow', 'mm'] diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4dcd036df5e..fdbc1ab26e3 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -10,7 +10,7 @@ from datetime import timedelta import logging import requests -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import (CONF_VALUE_TEMPLATE, STATE_UNKNOWN) from homeassistant.util import template, Throttle from homeassistant.helpers.entity import Entity @@ -26,48 +26,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the REST sensor. """ - - use_get = False - use_post = False - resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error('Response status is "%s"', response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error('Missing resource or schema in configuration. ' - 'Add http:// to your URL.') - return False - except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint. ' - 'Please check the URL in the configuration file.') + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestSensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get('unit_of_measurement'), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestSensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get('unit_of_measurement'), config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments @@ -78,7 +51,7 @@ class RestSensor(Entity): self._hass = hass self.rest = rest self._name = name - self._state = 'n/a' + self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template self.update() @@ -103,57 +76,33 @@ class RestSensor(Entity): self.rest.update() value = self.rest.data - if 'error' in value: - self._state = value['error'] - else: - if self._value_template is not None: - value = template.render_with_possible_json_value( - self._hass, self._value_template, value, 'N/A') - self._state = value + if value is None: + value = STATE_UNKNOWN + elif self._value_template is not None: + value = template.render_with_possible_json_value( + self._hass, self._value_template, value, STATE_UNKNOWN) + + self._state = value # pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ +class RestData(object): + """Class for handling the data retrieval.""" - def __init__(self, resource, verify_ssl): - self._resource = resource + def __init__(self, method, resource, data, verify_ssl): + self._request = requests.Request(method, resource, data=data).prepare() self._verify_ssl = verify_ssl - self.data = dict() + self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """ Gets the latest data from REST service with GET method. """ try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) - if 'error' in self.data: - del self.data['error'] + with requests.Session() as sess: + response = sess.send(self._request, timeout=10, + verify=self._verify_ssl) + self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint.") - self.data['error'] = 'N/A' - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = dict() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - if 'error' in self.data: - del self.data['error'] - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint.") - self.data['error'] = 'N/A' + except requests.exceptions.RequestException: + _LOGGER.error("Error fetching data: %s", self._request) + self.data = None diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 110d58283c3..ecd56ad05d7 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -14,25 +14,25 @@ from homeassistant.const import STATE_ON, STATE_OFF REQUIREMENTS = ['psutil==3.2.2'] SENSOR_TYPES = { - 'disk_use_percent': ['Disk Use', '%'], - 'disk_use': ['Disk Use', 'GiB'], - 'disk_free': ['Disk Free', 'GiB'], - 'memory_use_percent': ['RAM Use', '%'], - 'memory_use': ['RAM Use', 'MiB'], - 'memory_free': ['RAM Free', 'MiB'], - 'processor_use': ['CPU Use', '%'], - 'process': ['Process', ''], - 'swap_use_percent': ['Swap Use', '%'], - 'swap_use': ['Swap Use', 'GiB'], - 'swap_free': ['Swap Free', 'GiB'], - 'network_out': ['Sent', 'MiB'], - 'network_in': ['Recieved', 'MiB'], - 'packets_out': ['Packets sent', ''], - 'packets_in': ['Packets recieved', ''], - 'ipv4_address': ['IPv4 address', ''], - 'ipv6_address': ['IPv6 address', ''], - 'last_boot': ['Last Boot', ''], - 'since_last_boot': ['Since Last Boot', ''] + 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'], + 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'], + 'disk_free': ['Disk Free', 'GiB', 'mdi:harddisk'], + 'memory_use_percent': ['RAM Use', '%', 'mdi:memory'], + 'memory_use': ['RAM Use', 'MiB', 'mdi:memory'], + 'memory_free': ['RAM Free', 'MiB', 'mdi:memory'], + 'processor_use': ['CPU Use', '%', 'mdi:memory'], + 'process': ['Process', '', 'mdi:memory'], + 'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'], + 'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'], + 'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'], + 'network_out': ['Sent', 'MiB', 'mdi:server-network'], + 'network_in': ['Recieved', 'MiB', 'mdi:server-network'], + 'packets_out': ['Packets sent', '', 'mdi:server-network'], + 'packets_in': ['Packets recieved', '', 'mdi:server-network'], + 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'], + 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'], + 'last_boot': ['Last Boot', '', 'mdi:clock'], + 'since_last_boot': ['Since Last Boot', '', 'mdi:clock'] } _LOGGER = logging.getLogger(__name__) @@ -69,6 +69,10 @@ class SystemMonitorSensor(Entity): def name(self): return self._name.rstrip() + @property + def icon(self): + return SENSOR_TYPES[self.type][2] + @property def state(self): """ Returns the state of the device. """ diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py new file mode 100644 index 00000000000..ae05ce47e19 --- /dev/null +++ b/homeassistant/components/sensor/tellduslive.py @@ -0,0 +1,111 @@ +""" +homeassistant.components.sensor.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Shows sensor values from Tellstick Net/Telstick Live. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.tellduslive/ + +""" +import logging + +from datetime import datetime + +from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL +from homeassistant.helpers.entity import Entity +from homeassistant.components import tellduslive + +ATTR_LAST_UPDATED = "time_last_updated" + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['tellduslive'] + +SENSOR_TYPE_TEMP = "temp" +SENSOR_TYPE_HUMIDITY = "humidity" +SENSOR_TYPE_RAINRATE = "rrate" +SENSOR_TYPE_RAINTOTAL = "rtot" +SENSOR_TYPE_WINDDIRECTION = "wdir" +SENSOR_TYPE_WINDAVERAGE = "wavg" +SENSOR_TYPE_WINDGUST = "wgust" +SENSOR_TYPE_WATT = "watt" + +SENSOR_TYPES = { + SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], + SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], + SENSOR_TYPE_WATT: ['Watt', 'W', ""], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up Tellstick sensors. """ + sensors = tellduslive.NETWORK.get_sensors() + devices = [] + + for component in sensors: + for sensor in component["data"]: + # one component can have more than one sensor + # (e.g. both humidity and temperature) + devices.append(TelldusLiveSensor(component["id"], + component["name"], + sensor["name"])) + add_devices(devices) + + +class TelldusLiveSensor(Entity): + """ Represents a Telldus Live sensor. """ + + def __init__(self, sensor_id, sensor_name, sensor_type): + self._sensor_id = sensor_id + self._sensor_type = sensor_type + self._state = None + self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0] + self._last_update = None + self._battery_level = None + self.update() + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def state_attributes(self): + attrs = dict() + if self._battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self._battery_level + if self._last_update is not None: + attrs[ATTR_LAST_UPDATED] = self._last_update + return attrs + + @property + def unit_of_measurement(self): + return SENSOR_TYPES[self._sensor_type][1] + + @property + def icon(self): + return SENSOR_TYPES[self._sensor_type][2] + + def update(self): + values = tellduslive.NETWORK.get_sensor_value(self._sensor_id, + self._sensor_type) + self._state, self._battery_level, self._last_update = values + + self._state = float(self._state) + if self._sensor_type == SENSOR_TYPE_TEMP: + self._state = round(self._state, 1) + elif self._sensor_type == SENSOR_TYPE_HUMIDITY: + self._state = int(round(self._state)) + + self._battery_level = round(self._battery_level * 100 / 255) # percent + + self._last_update = str(datetime.fromtimestamp(self._last_update)) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 7fb72fd91b7..b381974ab31 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -13,11 +13,9 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, - TEMP_CELCIUS, TEMP_FAHRENHEIT) + TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['pyvera==0.2.3'] _LOGGER = logging.getLogger(__name__) @@ -37,7 +35,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + categories = ['Temperature Sensor', 'Light Sensor', 'Sensor'] devices = [] try: @@ -49,11 +56,12 @@ def get_devices(hass, config): vera_sensors = [] for device in devices: - extra_data = device_data.get(device.deviceId, {}) + extra_data = device_data.get(device.device_id, {}) exclude = extra_data.get('exclude', False) if exclude is not True: - vera_sensors.append(VeraSensor(device, extra_data)) + vera_sensors.append( + VeraSensor(device, vera_controller, extra_data)) return vera_sensors @@ -66,8 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSensor(Entity): """ Represents a Vera Sensor. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device + self.controller = controller self.extra_data = extra_data if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') @@ -76,8 +85,14 @@ class VeraSensor(Entity): self.current_value = '' self._temperature_units = None + self.controller.register(vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + self.update_ha_state(True) + def __str__(self): - return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state) + return "%s %s %s" % (self.name, self.vera_device.device_id, self.state) @property def state(self): @@ -100,26 +115,30 @@ class VeraSensor(Entity): attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' if self.vera_device.is_armable: - armed = self.vera_device.refresh_value('Armed') + armed = self.vera_device.get_value('Armed') attr[ATTR_ARMED] = 'True' if armed == '1' else 'False' if self.vera_device.is_trippable: - last_tripped = self.vera_device.refresh_value('LastTrip') + last_tripped = self.vera_device.get_value('LastTrip') if last_tripped is not None: utc_time = dt_util.utc_from_timestamp(int(last_tripped)) attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str( utc_time) else: attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.refresh_value('Tripped') + tripped = self.vera_device.get_value('Tripped') attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False' attr['Vera Device Id'] = self.vera_device.vera_device_id return attr + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + def update(self): if self.vera_device.category == "Temperature Sensor": - self.vera_device.refresh_value('CurrentTemperature') current_temp = self.vera_device.get_value('CurrentTemperature') vera_temp_units = self.vera_device.veraController.temperature_units @@ -137,10 +156,9 @@ class VeraSensor(Entity): self.current_value = current_temp elif self.vera_device.category == "Light Sensor": - self.vera_device.refresh_value('CurrentLevel') self.current_value = self.vera_device.get_value('CurrentLevel') elif self.vera_device.category == "Sensor": - tripped = self.vera_device.refresh_value('Tripped') + tripped = self.vera_device.get_value('Tripped') self.current_value = 'Tripped' if tripped == '1' else 'Not Tripped' else: self.current_value = 'Unknown' diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py index e946be9a3f4..e7c6a30b558 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -27,14 +27,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.extend([ VerisureThermometer(value) - for value in verisure.get_climate_status().values() + for value in verisure.CLIMATE_STATUS.values() if verisure.SHOW_THERMOMETERS and hasattr(value, 'temperature') and value.temperature ]) sensors.extend([ VerisureHygrometer(value) - for value in verisure.get_climate_status().values() + for value in verisure.CLIMATE_STATUS.values() if verisure.SHOW_HYGROMETERS and hasattr(value, 'humidity') and value.humidity ]) @@ -47,20 +47,19 @@ class VerisureThermometer(Entity): def __init__(self, climate_status): self._id = climate_status.id - self._device = verisure.MY_PAGES.DEVICE_CLIMATE @property def name(self): """ Returns the name of the device. """ return '{} {}'.format( - verisure.STATUS[self._device][self._id].location, + verisure.CLIMATE_STATUS[self._id].location, "Temperature") @property def state(self): """ Returns the state of the device. """ # remove ° character - return verisure.STATUS[self._device][self._id].temperature[:-1] + return verisure.CLIMATE_STATUS[self._id].temperature[:-1] @property def unit_of_measurement(self): @@ -69,7 +68,7 @@ class VerisureThermometer(Entity): def update(self): ''' update sensor ''' - verisure.update() + verisure.update_climate() class VerisureHygrometer(Entity): @@ -77,20 +76,19 @@ class VerisureHygrometer(Entity): def __init__(self, climate_status): self._id = climate_status.id - self._device = verisure.MY_PAGES.DEVICE_CLIMATE @property def name(self): """ Returns the name of the device. """ return '{} {}'.format( - verisure.STATUS[self._device][self._id].location, + verisure.CLIMATE_STATUS[self._id].location, "Humidity") @property def state(self): """ Returns the state of the device. """ # remove % character - return verisure.STATUS[self._device][self._id].humidity[:-1] + return verisure.CLIMATE_STATUS[self._id].humidity[:-1] @property def unit_of_measurement(self): @@ -99,4 +97,4 @@ class VerisureHygrometer(Entity): def update(self): ''' update sensor ''' - verisure.update() + verisure.update_climate() diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py new file mode 100644 index 00000000000..24f565feb48 --- /dev/null +++ b/homeassistant/components/sensor/yr.py @@ -0,0 +1,194 @@ +""" +homeassistant.components.sensor.yr +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Yr.no weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.yr/ +""" +import logging + +import requests + +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.helpers.entity import Entity +from homeassistant.util import location, dt as dt_util + +_LOGGER = logging.getLogger(__name__) + + +REQUIREMENTS = ['xmltodict'] + +# Sensor types are defined like so: +SENSOR_TYPES = { + 'symbol': ['Symbol', ''], + 'precipitation': ['Condition', 'mm'], + 'temperature': ['Temperature', '°C'], + 'windSpeed': ['Wind speed', 'm/s'], + 'pressure': ['Pressure', 'mbar'], + 'windDirection': ['Wind direction', '°'], + 'humidity': ['Humidity', '%'], + 'fog': ['Fog', '%'], + 'cloudiness': ['Cloudiness', '%'], + 'lowClouds': ['Low clouds', '%'], + 'mediumClouds': ['Medium clouds', '%'], + 'highClouds': ['High clouds', '%'], + 'dewpointTemperature': ['Dewpoint temperature', '°C'], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the Yr.no sensor. """ + + if None in (hass.config.latitude, hass.config.longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return False + + elevation = config.get('elevation') + + if elevation is None: + elevation = location.elevation(hass.config.latitude, + hass.config.longitude) + + coordinates = dict(lat=hass.config.latitude, + lon=hass.config.longitude, msl=elevation) + + weather = YrData(coordinates) + + dev = [] + if 'monitored_conditions' in config: + for variable in config['monitored_conditions']: + if variable not in SENSOR_TYPES: + _LOGGER.error('Sensor type: "%s" does not exist', variable) + else: + dev.append(YrSensor(variable, weather)) + + # add symbol as default sensor + if len(dev) == 0: + dev.append(YrSensor("symbol", weather)) + add_devices(dev) + + +# pylint: disable=too-many-instance-attributes +class YrSensor(Entity): + """ Implements an Yr.no sensor. """ + + def __init__(self, sensor_type, weather): + self.client_name = 'yr' + self._name = SENSOR_TYPES[sensor_type][0] + self.type = sensor_type + self._state = None + self._weather = weather + self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._update = None + + self.update() + + @property + def name(self): + return '{} {}'.format(self.client_name, self._name) + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def state_attributes(self): + """ Returns state attributes. """ + data = { + 'about': "Weather forecast from yr.no, delivered by the" + " Norwegian Meteorological Institute and the NRK" + } + if self.type == 'symbol': + symbol_nr = self._state + data[ATTR_ENTITY_PICTURE] = \ + "http://api.met.no/weatherapi/weathericon/1.1/" \ + "?symbol={0};content_type=image/png".format(symbol_nr) + + return data + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self._unit_of_measurement + + def update(self): + """ Gets the latest data from yr.no and updates the states. """ + + now = dt_util.utcnow() + # check if data should be updated + if self._update is not None and now <= self._update: + return + + self._weather.update() + + # find sensor + for time_entry in self._weather.data['product']['time']: + valid_from = dt_util.str_to_datetime( + time_entry['@from'], "%Y-%m-%dT%H:%M:%SZ") + valid_to = dt_util.str_to_datetime( + time_entry['@to'], "%Y-%m-%dT%H:%M:%SZ") + + loc_data = time_entry['location'] + + if self.type not in loc_data or now >= valid_to: + continue + + self._update = valid_to + + if self.type == 'precipitation' and valid_from < now: + self._state = loc_data[self.type]['@value'] + break + elif self.type == 'symbol' and valid_from < now: + self._state = loc_data[self.type]['@number'] + break + elif self.type == ('temperature', 'pressure', 'humidity', + 'dewpointTemperature'): + self._state = loc_data[self.type]['@value'] + break + elif self.type == 'windSpeed': + self._state = loc_data[self.type]['@mps'] + break + elif self.type == 'windDirection': + self._state = float(loc_data[self.type]['@deg']) + break + elif self.type in ('fog', 'cloudiness', 'lowClouds', + 'mediumClouds', 'highClouds'): + self._state = loc_data[self.type]['@percent'] + break + + +# pylint: disable=too-few-public-methods +class YrData(object): + """ Gets the latest data and updates the states. """ + + def __init__(self, coordinates): + self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ + 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) + + self._nextrun = None + self.data = {} + self.update() + + def update(self): + """ Gets the latest data from yr.no """ + # check if new will be available + if self._nextrun is not None and dt_util.utcnow() <= self._nextrun: + return + try: + with requests.Session() as sess: + response = sess.get(self._url) + except requests.RequestException: + return + if response.status_code != 200: + return + data = response.text + + import xmltodict + self.data = xmltodict.parse(data)['weatherdata'] + model = self.data['meta']['model'] + if '@nextrun' not in model: + model = model[0] + self._nextrun = dt_util.str_to_datetime(model['@nextrun'], + "%Y-%m-%dT%H:%M:%SZ") diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index 1ed831b286d..869f4dbe810 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -74,6 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): value.type == zwave.TYPE_DECIMAL): add_devices([ZWaveMultilevelSensor(value)]) + elif value.command_class == zwave.COMMAND_CLASS_ALARM: + add_devices([ZWaveAlarmSensor(value)]) + class ZWaveSensor(Entity): """ Represents a Z-Wave sensor. """ @@ -216,3 +219,19 @@ class ZWaveMultilevelSensor(ZWaveSensor): return TEMP_FAHRENHEIT else: return unit + + +class ZWaveAlarmSensor(ZWaveSensor): + """ A Z-wave sensor that sends Alarm alerts + + Examples include certain Multisensors that have motion and + vibration capabilities. Z-Wave defines various alarm types + such as Smoke, Flood, Burglar, CarbonMonoxide, etc. + + This wraps these alarms and allows you to use them to + trigger things, etc. + + COMMAND_CLASS_ALARM is what we get here. + """ + # Empty subclass for now. Allows for later customizations + pass diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 2e1c0c9b377..663ba03aa90 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -8,10 +8,9 @@ https://home-assistant.io/components/sun/ """ import logging from datetime import timedelta -import urllib import homeassistant.util as util -import homeassistant.util.dt as dt_util +from homeassistant.util import location as location_util, dt as dt_util from homeassistant.helpers.event import ( track_point_in_utc_time, track_utc_time_change) from homeassistant.helpers.entity import Entity @@ -19,7 +18,6 @@ from homeassistant.helpers.entity import Entity REQUIREMENTS = ['astral==0.8.1'] DOMAIN = "sun" ENTITY_ID = "sun.sun" -ENTITY_ID_ELEVATION = "sun.elevation" CONF_ELEVATION = 'elevation' @@ -34,21 +32,21 @@ _LOGGER = logging.getLogger(__name__) def is_on(hass, entity_id=None): - """ Returns if the sun is currently up based on the statemachine. """ + """Test if the sun is currently up based on the statemachine.""" entity_id = entity_id or ENTITY_ID return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON) def next_setting(hass, entity_id=None): - """ Returns the local datetime object of the next sun setting. """ + """Local datetime object of the next sun setting.""" utc_next = next_setting_utc(hass, entity_id) return dt_util.as_local(utc_next) if utc_next else None def next_setting_utc(hass, entity_id=None): - """ Returns the UTC datetime object of the next sun setting. """ + """UTC datetime object of the next sun setting.""" entity_id = entity_id or ENTITY_ID state = hass.states.get(ENTITY_ID) @@ -63,14 +61,14 @@ def next_setting_utc(hass, entity_id=None): def next_rising(hass, entity_id=None): - """ Returns the local datetime object of the next sun rising. """ + """Local datetime object of the next sun rising.""" utc_next = next_rising_utc(hass, entity_id) return dt_util.as_local(utc_next) if utc_next else None def next_rising_utc(hass, entity_id=None): - """ Returns the UTC datetime object of the next sun rising. """ + """UTC datetime object of the next sun rising.""" entity_id = entity_id or ENTITY_ID state = hass.states.get(ENTITY_ID) @@ -85,7 +83,7 @@ def next_rising_utc(hass, entity_id=None): def setup(hass, config): - """ Tracks the state of the sun. """ + """Track the state of the sun in HA.""" if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False @@ -111,21 +109,13 @@ def setup(hass, config): platform_config = config.get(DOMAIN, {}) elevation = platform_config.get(CONF_ELEVATION) + if elevation is None: + elevation = location_util.elevation(latitude, longitude) - from astral import Location, GoogleGeocoder + from astral import Location location = Location(('', '', latitude, longitude, hass.config.time_zone, - elevation or 0)) - - if elevation is None: - google = GoogleGeocoder() - try: - google._get_elevation(location) # pylint: disable=protected-access - _LOGGER.info( - 'Retrieved elevation from Google: %s', location.elevation) - except urllib.error.URLError: - # If no internet connection available etc. - pass + elevation)) sun = Sun(hass, location) sun.point_in_time_listener(dt_util.utcnow()) @@ -134,7 +124,7 @@ def setup(hass, config): class Sun(Entity): - """ Represents the Sun. """ + """Represents the Sun.""" entity_id = ENTITY_ID @@ -167,12 +157,12 @@ class Sun(Entity): @property def next_change(self): - """ Returns the datetime when the next change to the state is. """ + """Datetime when the next change to the state is.""" return min(self.next_rising, self.next_setting) @property def solar_elevation(self): - """ Returns the angle the sun is above the horizon""" + """Angle the sun is above the horizon.""" from astral import Astral return Astral().solar_elevation( dt_util.utcnow(), diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index e7b3c629f39..a05a673c3dd 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.components import ( - group, discovery, wink, isy994, verisure, zwave) + group, discovery, wink, isy994, verisure, zwave, tellduslive, mysensors) DOMAIN = 'switch' SCAN_INTERVAL = 30 @@ -40,6 +40,8 @@ DISCOVERY_PLATFORMS = { isy994.DISCOVER_SWITCHES: 'isy994', verisure.DISCOVER_SWITCHES: 'verisure', zwave.DISCOVER_SWITCHES: 'zwave', + tellduslive.DISCOVER_SWITCHES: 'tellduslive', + mysensors.DISCOVER_SWITCHES: 'mysensors', } PROP_TO_ATTR = { diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 91171be3680..a90ed61c3e2 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -10,6 +10,8 @@ import logging import subprocess from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.util import template _LOGGER = logging.getLogger(__name__) @@ -24,20 +26,30 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): for dev_name, properties in switches.items(): devices.append( CommandSwitch( + hass, properties.get('name', dev_name), properties.get('oncmd', 'true'), - properties.get('offcmd', 'true'))) + properties.get('offcmd', 'true'), + properties.get('statecmd', False), + properties.get(CONF_VALUE_TEMPLATE, False))) add_devices_callback(devices) class CommandSwitch(SwitchDevice): """ Represents a switch that can be togggled using shell commands. """ - def __init__(self, name, command_on, command_off): + + # pylint: disable=too-many-arguments + def __init__(self, hass, name, command_on, command_off, + command_state, value_template): + + self._hass = hass self._name = name self._state = False self._command_on = command_on self._command_off = command_off + self._command_state = command_state + self._value_template = value_template @staticmethod def _switch(command): @@ -51,10 +63,27 @@ class CommandSwitch(SwitchDevice): return success + @staticmethod + def _query_state_value(command): + """ Execute state command for return value. """ + _LOGGER.info('Running state command: %s', command) + + try: + return_value = subprocess.check_output(command, shell=True) + return return_value.strip().decode('utf-8') + except subprocess.CalledProcessError: + _LOGGER.error('Command failed: %s', command) + + @staticmethod + def _query_state_code(command): + """ Execute state command for return code. """ + _LOGGER.info('Running state command: %s', command) + return subprocess.call(command, shell=True) == 0 + @property def should_poll(self): - """ No polling needed. """ - return False + """ Only poll if we have statecmd. """ + return self._command_state is not None @property def name(self): @@ -66,14 +95,34 @@ class CommandSwitch(SwitchDevice): """ True if device is on. """ return self._state + def _query_state(self): + """ Query for state. """ + if not self._command_state: + _LOGGER.error('No state command specified') + return + if self._value_template: + return CommandSwitch._query_state_value(self._command_state) + return CommandSwitch._query_state_code(self._command_state) + + def update(self): + """ Update device state. """ + if self._command_state: + payload = str(self._query_state()) + if self._value_template: + payload = template.render_with_possible_json_value( + self._hass, self._value_template, payload) + self._state = (payload.lower() == "true") + def turn_on(self, **kwargs): """ Turn the device on. """ - if CommandSwitch._switch(self._command_on): + if (CommandSwitch._switch(self._command_on) and + not self._command_state): self._state = True - self.update_ha_state() + self.update_ha_state() def turn_off(self, **kwargs): """ Turn the device off. """ - if CommandSwitch._switch(self._command_off): + if (CommandSwitch._switch(self._command_off) and + not self._command_state): self._state = False - self.update_ha_state() + self.update_ha_state() diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py new file mode 100644 index 00000000000..d8d7d4d2473 --- /dev/null +++ b/homeassistant/components/switch/mysensors.py @@ -0,0 +1,164 @@ +""" +homeassistant.components.switch.mysensors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for MySensors switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.mysensors.html +""" +import logging +from collections import defaultdict + +from homeassistant.components.switch import SwitchDevice + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + STATE_ON, STATE_OFF) + +import homeassistant.components.mysensors as mysensors + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = [] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the mysensors platform for switches.""" + # Only act if loaded via mysensors by discovery event. + # Otherwise gateway is not setup. + if discovery_info is None: + return + + for gateway in mysensors.GATEWAYS.values(): + # Define the S_TYPES and V_TYPES that the platform should handle as + # states. + s_types = [ + gateway.const.Presentation.S_DOOR, + gateway.const.Presentation.S_MOTION, + gateway.const.Presentation.S_SMOKE, + gateway.const.Presentation.S_LIGHT, + gateway.const.Presentation.S_LOCK, + ] + v_types = [ + gateway.const.SetReq.V_ARMED, + gateway.const.SetReq.V_LIGHT, + gateway.const.SetReq.V_LOCK_STATUS, + ] + if float(gateway.version) >= 1.5: + s_types.extend([ + gateway.const.Presentation.S_BINARY, + gateway.const.Presentation.S_SPRINKLER, + gateway.const.Presentation.S_WATER_LEAK, + gateway.const.Presentation.S_SOUND, + gateway.const.Presentation.S_VIBRATION, + gateway.const.Presentation.S_MOISTURE, + ]) + v_types.extend([gateway.const.SetReq.V_STATUS, ]) + + devices = defaultdict(list) + gateway.platform_callbacks.append(mysensors.pf_callback_factory( + s_types, v_types, devices, add_devices, MySensorsSwitch)) + + +class MySensorsSwitch(SwitchDevice): + """Represent the value of a MySensors child node.""" + + # pylint: disable=too-many-arguments + + def __init__(self, gateway, node_id, child_id, name, value_type): + """Setup class attributes on instantiation. + + Args: + gateway (GatewayWrapper): Gateway object. + node_id (str): Id of node. + child_id (str): Id of child. + name (str): Entity name. + value_type (str): Value type of child. Value is entity state. + + Attributes: + gateway (GatewayWrapper): Gateway object + node_id (str): Id of node. + child_id (str): Id of child. + _name (str): Entity name. + value_type (str): Value type of child. Value is entity state. + battery_level (int): Node battery level. + _values (dict): Child values. Non state values set as state attributes. + """ + self.gateway = gateway + self.node_id = node_id + self.child_id = child_id + self._name = name + self.value_type = value_type + self.battery_level = 0 + self._values = {} + + @property + def should_poll(self): + """MySensor gateway pushes its state to HA.""" + return False + + @property + def name(self): + """The name of this entity.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + device_attr = dict(self._values) + device_attr.pop(self.value_type, None) + return device_attr + + @property + def state_attributes(self): + """Return the state attributes.""" + data = { + mysensors.ATTR_PORT: self.gateway.port, + mysensors.ATTR_NODE_ID: self.node_id, + mysensors.ATTR_CHILD_ID: self.child_id, + ATTR_BATTERY_LEVEL: self.battery_level, + } + + device_attr = self.device_state_attributes + + if device_attr is not None: + data.update(device_attr) + + return data + + @property + def is_on(self): + """Return True if switch is on.""" + if self.value_type in self._values: + return self._values[self.value_type] == STATE_ON + return False + + def turn_on(self): + """Turn the switch on.""" + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 1) + self._values[self.value_type] = STATE_ON + self.update_ha_state() + + def turn_off(self): + """Turn the switch off.""" + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 0) + self._values[self.value_type] = STATE_OFF + self.update_ha_state() + + def update(self): + """Update the controller with the latest value from a sensor.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + for value_type, value in child.values.items(): + _LOGGER.info( + "%s: value_type %s, value = %s", self._name, value_type, value) + if value_type == self.gateway.const.SetReq.V_ARMED or \ + value_type == self.gateway.const.SetReq.V_STATUS or \ + value_type == self.gateway.const.SetReq.V_LIGHT or \ + value_type == self.gateway.const.SetReq.V_LOCK_STATUS: + self._values[value_type] = ( + STATE_ON if int(value) == 1 else STATE_OFF) + else: + self._values[value_type] = value + self.battery_level = node.battery_level diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 2435829637e..5c4b9b37e1e 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -18,7 +18,7 @@ DEFAULT_BODY_ON = "ON" DEFAULT_BODY_OFF = "OFF" -# pylint: disable=unused-argument +# pylint: disable=unused-argument, def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Get REST switch. """ @@ -32,11 +32,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): requests.get(resource, timeout=10) except requests.exceptions.MissingSchema: _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// to your URL.") + "Add http:// or https:// to your URL") return False except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint. " - "Please check the IP address in the configuration file.") + _LOGGER.error("No route to resource/endpoint: %s", resource) return False add_devices_callback([RestSwitch( diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 69e08e7d129..84f4df82b1f 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -13,8 +13,9 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.util import slugify from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \ - ATTR_NAME, EVENT_BUTTON_PRESSED +from homeassistant.components.rfxtrx import ( + ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, + ATTR_NAME, EVENT_BUTTON_PRESSED) DEPENDENCIES = ['rfxtrx'] diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/switch/tellduslive.py new file mode 100644 index 00000000000..d515dcb50a2 --- /dev/null +++ b/homeassistant/components/switch/tellduslive.py @@ -0,0 +1,73 @@ +""" +homeassistant.components.switch.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Tellstick switches using Tellstick Net and +the Telldus Live online service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.tellduslive/ + +""" +import logging + +from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN) +from homeassistant.components import tellduslive +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['tellduslive'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Find and return Tellstick switches. """ + switches = tellduslive.NETWORK.get_switches() + add_devices([TelldusLiveSwitch(switch["name"], + switch["id"]) + for switch in switches if switch["type"] == "device"]) + + +class TelldusLiveSwitch(ToggleEntity): + """ Represents a Tellstick switch. """ + + def __init__(self, name, switch_id): + self._name = name + self._id = switch_id + self._state = STATE_UNKNOWN + self.update() + + @property + def should_poll(self): + """ Tells Home Assistant to poll this entity. """ + return False + + @property + def name(self): + """ Returns the name of the switch if any. """ + return self._name + + def update(self): + from tellive.live import const + state = tellduslive.NETWORK.get_switch_state(self._id) + if state == const.TELLSTICK_TURNON: + self._state = STATE_ON + elif state == const.TELLSTICK_TURNOFF: + self._state = STATE_OFF + else: + self._state = STATE_UNKNOWN + + @property + def is_on(self): + """ True if switch is on. """ + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """ Turns the switch on. """ + if tellduslive.NETWORK.turn_switch_on(self._id): + self._state = STATE_ON + self.update_ha_state() + + def turn_off(self, **kwargs): + """ Turns the switch off. """ + if tellduslive.NETWORK.turn_switch_off(self._id): + self._state = STATE_OFF + self.update_ha_state() diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 14983919c64..4094fe61f4f 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -7,17 +7,21 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.vera/ """ import logging -import time from requests.exceptions import RequestException import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) +from homeassistant.components.switch import SwitchDevice -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_TRIPPED, + ATTR_ARMED, + ATTR_LAST_TRIP_TIME, + EVENT_HOMEASSISTANT_STOP, + STATE_ON, + STATE_OFF) + +REQUIREMENTS = ['pyvera==0.2.3'] _LOGGER = logging.getLogger(__name__) @@ -37,7 +41,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: devices = vera_controller.get_devices([ @@ -49,11 +62,12 @@ def get_devices(hass, config): vera_switches = [] for device in devices: - extra_data = device_data.get(device.deviceId, {}) + extra_data = device_data.get(device.device_id, {}) exclude = extra_data.get('exclude', False) if exclude is not True: - vera_switches.append(VeraSwitch(device, extra_data)) + vera_switches.append( + VeraSwitch(device, vera_controller, extra_data)) return vera_switches @@ -63,19 +77,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(get_devices(hass, config)) -class VeraSwitch(ToggleEntity): +class VeraSwitch(SwitchDevice): """ Represents a Vera Switch. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device self.extra_data = extra_data + self.controller = controller if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') else: self._name = self.vera_device.name - self.is_on_status = False - # for debouncing status check after command is sent - self.last_command_send = 0 + self._state = STATE_OFF + + self.controller.register(vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + if self.vera_device.is_switched_on(): + self._state = STATE_ON + else: + self._state = STATE_OFF + self.update_ha_state() @property def name(self): @@ -90,18 +113,18 @@ class VeraSwitch(ToggleEntity): attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' if self.vera_device.is_armable: - armed = self.vera_device.refresh_value('Armed') + armed = self.vera_device.get_value('Armed') attr[ATTR_ARMED] = 'True' if armed == '1' else 'False' if self.vera_device.is_trippable: - last_tripped = self.vera_device.refresh_value('LastTrip') + last_tripped = self.vera_device.get_value('LastTrip') if last_tripped is not None: utc_time = dt_util.utc_from_timestamp(int(last_tripped)) attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str( utc_time) else: attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.refresh_value('Tripped') + tripped = self.vera_device.get_value('Tripped') attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False' attr['Vera Device Id'] = self.vera_device.vera_device_id @@ -109,25 +132,21 @@ class VeraSwitch(ToggleEntity): return attr def turn_on(self, **kwargs): - self.last_command_send = time.time() self.vera_device.switch_on() - self.is_on_status = True + self._state = STATE_ON + self.update_ha_state() def turn_off(self, **kwargs): - self.last_command_send = time.time() self.vera_device.switch_off() - self.is_on_status = False + self._state = STATE_OFF + self.update_ha_state() + + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False @property def is_on(self): """ True if device is on. """ - return self.is_on_status - - def update(self): - # We need to debounce the status call after turning switch on or off - # because the vera has some lag in updating the device status - try: - if (self.last_command_send + 5) < time.time(): - self.is_on_status = self.vera_device.is_switched_on() - except RequestException: - _LOGGER.warning('Could not update status for %s', self.name) + return self._state == STATE_ON diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py index a2893df76dd..c698a33ce18 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -25,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): switches.extend([ VerisureSmartplug(value) - for value in verisure.get_smartplug_status().values() + for value in verisure.SMARTPLUG_STATUS.values() if verisure.SHOW_SMARTPLUGS ]) @@ -36,31 +36,29 @@ class VerisureSmartplug(SwitchDevice): """ Represents a Verisure smartplug. """ def __init__(self, smartplug_status): self._id = smartplug_status.id - self.status_on = verisure.MY_PAGES.SMARTPLUG_ON - self.status_off = verisure.MY_PAGES.SMARTPLUG_OFF @property def name(self): """ Get the name (location) of the smartplug. """ - return verisure.get_smartplug_status()[self._id].location + return verisure.SMARTPLUG_STATUS[self._id].location @property def is_on(self): """ Returns True if on """ - plug_status = verisure.get_smartplug_status()[self._id].status - return plug_status == self.status_on + plug_status = verisure.SMARTPLUG_STATUS[self._id].status + return plug_status == 'on' def turn_on(self): """ Set smartplug status on. """ - verisure.MY_PAGES.set_smartplug_status( - self._id, - self.status_on) + verisure.MY_PAGES.smartplug.set(self._id, 'on') + verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'on') + verisure.update_smartplug() def turn_off(self): """ Set smartplug status off. """ - verisure.MY_PAGES.set_smartplug_status( - self._id, - self.status_off) + verisure.MY_PAGES.smartplug.set(self._id, 'off') + verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'off') + verisure.update_smartplug() def update(self): - verisure.update() + verisure.update_smartplug() diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index bad471ce437..ed56305542d 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -9,11 +9,14 @@ https://home-assistant.io/components/switch.wemo/ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['pywemo==0.3.3'] +REQUIREMENTS = ['pywemo==0.3.8'] _LOGGER = logging.getLogger(__name__) +_WEMO_SUBSCRIPTION_REGISTRY = None + # pylint: disable=unused-argument, too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -21,6 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import pywemo import pywemo.discovery as discovery + global _WEMO_SUBSCRIPTION_REGISTRY + if _WEMO_SUBSCRIPTION_REGISTRY is None: + _WEMO_SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() + _WEMO_SUBSCRIPTION_REGISTRY.start() + + def stop_wemo(event): + """ Shutdown Wemo subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + _WEMO_SUBSCRIPTION_REGISTRY.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) + if discovery_info is not None: location = discovery_info[2] mac = discovery_info[3] @@ -47,6 +62,22 @@ class WemoSwitch(SwitchDevice): self.insight_params = None self.maker_params = None + _WEMO_SUBSCRIPTION_REGISTRY.register(wemo) + _WEMO_SUBSCRIPTION_REGISTRY.on( + wemo, None, self._update_callback) + + def _update_callback(self, _device, _params): + """ Called by the wemo device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', + _device) + self.update_ha_state(True) + + @property + def should_poll(self): + """ No polling needed with subscriptions """ + return False + @property def unique_id(self): """ Returns the id of this WeMo switch """ diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py new file mode 100644 index 00000000000..5c314032b27 --- /dev/null +++ b/homeassistant/components/tellduslive.py @@ -0,0 +1,209 @@ +""" +homeassistant.components.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tellduslive Component + +This component adds support for the Telldus Live service. +Telldus Live is the online service used with Tellstick Net devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.tellduslive/ + +Developer access to the Telldus Live service is neccessary +API keys can be aquired from https://api.telldus.com/keys/index + +Tellstick Net devices can be auto discovered using the method described in: +https://developer.telldus.com/doxygen/html/TellStickNet.html + +It might be possible to communicate with the Tellstick Net device +directly, bypassing the Tellstick Live service. +This however is poorly documented and yet not fully supported (?) according to +http://developer.telldus.se/ticket/114 and +https://developer.telldus.com/doxygen/html/TellStickNet.html + +API requests to certain methods, as described in +https://api.telldus.com/explore/sensor/info +are limited to one request every 10 minutes + +""" + +from datetime import timedelta +import logging + +from homeassistant.loader import get_component +from homeassistant import bootstrap +from homeassistant.util import Throttle +from homeassistant.helpers import validate_config +from homeassistant.const import ( + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED) + + +DOMAIN = "tellduslive" +DISCOVER_SWITCHES = "tellduslive.switches" +DISCOVER_SENSORS = "tellduslive.sensors" + +CONF_PUBLIC_KEY = "public_key" +CONF_PRIVATE_KEY = "private_key" +CONF_TOKEN = "token" +CONF_TOKEN_SECRET = "token_secret" + +REQUIREMENTS = ['tellive-py==0.5.2'] +_LOGGER = logging.getLogger(__name__) + +NETWORK = None + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) + + +class TelldusLiveData(object): + """ Gets the latest data and update the states. """ + + def __init__(self, hass, config): + + public_key = config[DOMAIN].get(CONF_PUBLIC_KEY) + private_key = config[DOMAIN].get(CONF_PRIVATE_KEY) + token = config[DOMAIN].get(CONF_TOKEN) + token_secret = config[DOMAIN].get(CONF_TOKEN_SECRET) + + from tellive.client import LiveClient + from tellive.live import TelldusLive + + self._sensors = [] + self._switches = [] + + self._client = LiveClient(public_key=public_key, + private_key=private_key, + access_token=token, + access_secret=token_secret) + self._api = TelldusLive(self._client) + + def update(self, hass, config): + """ Send discovery event if component not yet discovered """ + self._update_sensors() + self._update_switches() + for component_name, found_devices, discovery_type in \ + (('sensor', self._sensors, DISCOVER_SENSORS), + ('switch', self._switches, DISCOVER_SWITCHES)): + if len(found_devices): + component = get_component(component_name) + bootstrap.setup_component(hass, component.DOMAIN, config) + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, + {ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: {}}) + + def _request(self, what, **params): + """ Sends a request to the tellstick live API """ + + from tellive.live import const + + supported_methods = const.TELLSTICK_TURNON \ + | const.TELLSTICK_TURNOFF \ + | const.TELLSTICK_TOGGLE + + default_params = {'supportedMethods': supported_methods, + "includeValues": 1, + "includeScale": 1} + + params.update(default_params) + + # room for improvement: the telllive library doesn't seem to + # re-use sessions, instead it opens a new session for each request + # this needs to be fixed + response = self._client.request(what, params) + return response + + def check_request(self, what, **params): + """ Make request, check result if successful """ + response = self._request(what, **params) + return response['status'] == "success" + + def validate_session(self): + """ Make a dummy request to see if the session is valid """ + try: + response = self._request("user/profile") + return 'email' in response + except RuntimeError: + return False + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _update_sensors(self): + """ Get the latest sensor data from Telldus Live """ + _LOGGER.info("Updating sensors from Telldus Live") + self._sensors = self._request("sensors/list")["sensor"] + + def _update_switches(self): + """ Get the configured switches from Telldus Live""" + _LOGGER.info("Updating switches from Telldus Live") + self._switches = self._request("devices/list")["device"] + # filter out any group of switches + self._switches = [switch for switch in self._switches + if switch["type"] == "device"] + + def get_sensors(self): + """ Get the configured sensors """ + self._update_sensors() + return self._sensors + + def get_switches(self): + """ Get the configured switches """ + self._update_switches() + return self._switches + + def get_sensor_value(self, sensor_id, sensor_name): + """ Get the latest (possibly cached) sensor value """ + self._update_sensors() + for component in self._sensors: + if component["id"] == sensor_id: + for sensor in component["data"]: + if sensor["name"] == sensor_name: + return (sensor["value"], + component["battery"], + component["lastUpdated"]) + + def get_switch_state(self, switch_id): + """ returns state of switch. """ + _LOGGER.info("Updating switch state from Telldus Live") + response = self._request("device/info", id=switch_id)["state"] + return int(response) + + def turn_switch_on(self, switch_id): + """ turn switch off """ + return self.check_request("device/turnOn", id=switch_id) + + def turn_switch_off(self, switch_id): + """ turn switch on """ + return self.check_request("device/turnOff", id=switch_id) + + +def setup(hass, config): + """ Setup the tellduslive component """ + + # fixme: aquire app key and provide authentication + # using username + password + if not validate_config(config, + {DOMAIN: [CONF_PUBLIC_KEY, + CONF_PRIVATE_KEY, + CONF_TOKEN, + CONF_TOKEN_SECRET]}, + _LOGGER): + _LOGGER.error( + "Configuration Error: " + "Please make sure you have configured your keys " + "that can be aquired from https://api.telldus.com/keys/index") + return False + + global NETWORK + NETWORK = TelldusLiveData(hass, config) + + if not NETWORK.validate_session(): + _LOGGER.error( + "Authentication Error: " + "Please make sure you have configured your keys " + "that can be aquired from https://api.telldus.com/keys/index") + return False + + NETWORK.update(hass, config) + + return True diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index 30221689274..0b4e14f36b7 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -46,8 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return data = ecobee.NETWORK hold_temp = discovery_info['hold_temp'] - _LOGGER.info("Loading ecobee thermostat component with hold_temp set to " - + str(hold_temp)) + _LOGGER.info( + "Loading ecobee thermostat component with hold_temp set to %s", + hold_temp) add_devices(Thermostat(data, index, hold_temp) for index in range(len(data.ecobee.thermostats))) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 4139c5d8aa7..5475e1ce306 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -6,8 +6,9 @@ Adds support for Honeywell Round Connected and Honeywell Evohome thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.honeywell/ """ -import socket import logging +import socket + from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) @@ -15,6 +16,8 @@ REQUIREMENTS = ['evohomeclient==0.2.4'] _LOGGER = logging.getLogger(__name__) +CONF_AWAY_TEMP = "away_temperature" + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -23,17 +26,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - + try: + away_temp = float(config.get(CONF_AWAY_TEMP, 16)) + except ValueError: + _LOGGER.error("value entered for item %s should convert to a number", + CONF_AWAY_TEMP) + return False if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", CONF_USERNAME, CONF_PASSWORD) return False evo_api = EvohomeClient(username, password) + try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0)]) + add_devices([RoundThermostat(evo_api, + zone['id'], + i == 0, + away_temp)]) except socket.error: _LOGGER.error( "Connection error logging into the honeywell evohome web service" @@ -44,7 +56,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ - def __init__(self, device, zone_id, master): + # pylint: disable=too-many-instance-attributes + def __init__(self, device, zone_id, master, away_temp): self.device = device self._current_temperature = None self._target_temperature = None @@ -52,6 +65,8 @@ class RoundThermostat(ThermostatDevice): self._id = zone_id self._master = master self._is_dhw = False + self._away_temp = away_temp + self._away = False self.update() @property @@ -80,6 +95,25 @@ class RoundThermostat(ThermostatDevice): """ Set new target temperature """ self.device.set_temperature(self._name, temperature) + @property + def is_away_mode_on(self): + """ Returns if away mode is on. """ + return self._away + + def turn_away_mode_on(self): + """ Turns away on. + Evohome does have a proprietary away mode, but it doesn't really work + the way it should. For example: If you set a temperature manually + it doesn't get overwritten when away mode is switched on. + """ + self._away = True + self.device.set_temperature(self._name, self._away_temp) + + def turn_away_mode_off(self): + """ Turns away off. """ + self._away = False + self.device.cancel_temp_override(self._name) + def update(self): try: # Only refresh if this is the "master" device, diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 837acbd18ae..164835f07c7 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -7,6 +7,8 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/verisure/ """ import logging +import time + from datetime import timedelta from homeassistant import bootstrap @@ -26,15 +28,14 @@ DISCOVER_SWITCHES = 'verisure.switches' DISCOVER_ALARMS = 'verisure.alarm_control_panel' DEPENDENCIES = ['alarm_control_panel'] -REQUIREMENTS = [ - 'https://github.com/persandstrom/python-verisure/archive/' - '9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6' -] +REQUIREMENTS = ['vsure==0.4.5'] _LOGGER = logging.getLogger(__name__) MY_PAGES = None -STATUS = {} +ALARM_STATUS = {} +SMARTPLUG_STATUS = {} +CLIMATE_STATUS = {} VERISURE_LOGIN_ERROR = None VERISURE_ERROR = None @@ -47,7 +48,7 @@ SHOW_SMARTPLUGS = True # if wrong password was given don't try again WRONG_PASSWORD_GIVEN = False -MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=5) +MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=1) def setup(hass, config): @@ -60,10 +61,6 @@ def setup(hass, config): from verisure import MyPages, LoginError, Error - STATUS[MyPages.DEVICE_ALARM] = {} - STATUS[MyPages.DEVICE_CLIMATE] = {} - STATUS[MyPages.DEVICE_SMARTPLUG] = {} - global SHOW_THERMOMETERS, SHOW_HYGROMETERS, SHOW_ALARM, SHOW_SMARTPLUGS SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1')) SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1')) @@ -84,7 +81,9 @@ def setup(hass, config): _LOGGER.error('Could not log in to verisure mypages, %s', ex) return False - update() + update_alarm() + update_climate() + update_smartplug() # Load components for the devices in the ISY controller that we support for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), @@ -101,24 +100,10 @@ def setup(hass, config): return True -def get_alarm_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_ALARM] - - -def get_climate_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_CLIMATE] - - -def get_smartplug_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_SMARTPLUG] - - def reconnect(): """ Reconnect to verisure mypages. """ try: + time.sleep(1) MY_PAGES.login() except VERISURE_LOGIN_ERROR as ex: _LOGGER.error("Could not login to Verisure mypages, %s", ex) @@ -129,19 +114,31 @@ def reconnect(): @Throttle(MIN_TIME_BETWEEN_REQUESTS) -def update(): +def update_alarm(): + """ Updates the status of alarms. """ + update_component(MY_PAGES.alarm.get, ALARM_STATUS) + + +@Throttle(MIN_TIME_BETWEEN_REQUESTS) +def update_climate(): + """ Updates the status of climate sensors. """ + update_component(MY_PAGES.climate.get, CLIMATE_STATUS) + + +@Throttle(MIN_TIME_BETWEEN_REQUESTS) +def update_smartplug(): + """ Updates the status of smartplugs. """ + update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS) + + +def update_component(get_function, status): """ Updates the status of verisure components. """ if WRONG_PASSWORD_GIVEN: _LOGGER.error('Wrong password') return - try: - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_ALARM): - STATUS[MY_PAGES.DEVICE_ALARM][overview.id] = overview - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_CLIMATE): - STATUS[MY_PAGES.DEVICE_CLIMATE][overview.id] = overview - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_SMARTPLUG): - STATUS[MY_PAGES.DEVICE_SMARTPLUG][overview.id] = overview - except ConnectionError as ex: + for overview in get_function(): + status[overview.id] = overview + except (ConnectionError, VERISURE_ERROR) as ex: _LOGGER.error('Caught connection error %s, tries to reconnect', ex) reconnect() diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index b52e430600a..6b4de82a70a 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -26,6 +26,9 @@ CONF_POLLING_INTERVAL = "polling_interval" DEFAULT_ZWAVE_CONFIG_PATH = os.path.join(sys.prefix, 'share', 'python-openzwave', 'config') +SERVICE_ADD_NODE = "add_node" +SERVICE_REMOVE_NODE = "remove_node" + DISCOVER_SENSORS = "zwave.sensors" DISCOVER_SWITCHES = "zwave.switch" DISCOVER_LIGHTS = "zwave.light" @@ -37,6 +40,7 @@ COMMAND_CLASS_SENSOR_BINARY = 48 COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_METER = 50 COMMAND_CLASS_BATTERY = 128 +COMMAND_CLASS_ALARM = 113 # 0x71 GENRE_WHATEVER = None GENRE_USER = "User" @@ -53,7 +57,8 @@ DISCOVERY_COMPONENTS = [ DISCOVER_SENSORS, [COMMAND_CLASS_SENSOR_BINARY, COMMAND_CLASS_SENSOR_MULTILEVEL, - COMMAND_CLASS_METER], + COMMAND_CLASS_METER, + COMMAND_CLASS_ALARM], TYPE_WHATEVER, GENRE_USER), ('light', @@ -176,6 +181,14 @@ def setup(hass, config): dispatcher.connect( value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) + def add_node(event): + """ Switch into inclusion mode """ + NETWORK.controller.begin_command_add_device() + + def remove_node(event): + """ Switch into exclusion mode""" + NETWORK.controller.begin_command_remove_device() + def stop_zwave(event): """ Stop Z-wave. """ NETWORK.stop() @@ -190,6 +203,11 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zwave) + # register add / remove node services for zwave sticks without + # hardware inclusion button + hass.services.register(DOMAIN, SERVICE_ADD_NODE, add_node) + hass.services.register(DOMAIN, SERVICE_REMOVE_NODE, remove_node) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) return True diff --git a/homeassistant/const.py b/homeassistant/const.py index dd8db09d392..97e26f8d33a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """ Constants used by Home Assistant components. """ -__version__ = "0.10.1" +__version__ = "0.11.0.dev0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' @@ -24,6 +24,7 @@ CONF_USERNAME = "username" CONF_PASSWORD = "password" CONF_API_KEY = "api_key" CONF_ACCESS_TOKEN = "access_token" +CONF_FILENAME = "filename" CONF_VALUE_TEMPLATE = "value_template" diff --git a/homeassistant/core.py b/homeassistant/core.py index 8ea55c653e3..55ceddb37c7 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1,6 +1,5 @@ """ -homeassistant -~~~~~~~~~~~~~ +Core components of Home Assistant. Home Assistant is a Home Automation framework for observing the state of entities and react to changes. @@ -53,9 +52,10 @@ _MockHA = namedtuple("MockHomeAssistant", ['bus']) class HomeAssistant(object): - """ Core class to route all communication to right components. """ + """Root object of the Home Assistant home automation.""" def __init__(self): + """Initialize new Home Assistant object.""" self.pool = pool = create_worker_pool() self.bus = EventBus(pool) self.services = ServiceRegistry(self.bus, pool) @@ -63,7 +63,7 @@ class HomeAssistant(object): self.config = Config() def start(self): - """ Start home assistant. """ + """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) @@ -71,12 +71,11 @@ class HomeAssistant(object): self.bus.fire(EVENT_HOMEASSISTANT_START) def block_till_stopped(self): - """ Will register service homeassistant/stop and - will block until called. """ + """Register service homeassistant/stop and will block until called.""" request_shutdown = threading.Event() def stop_homeassistant(*args): - """ Stops Home Assistant. """ + """Stop Home Assistant.""" request_shutdown.set() self.services.register( @@ -98,7 +97,7 @@ class HomeAssistant(object): self.stop() def stop(self): - """ Stops Home Assistant and shuts down all threads. """ + """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") self.bus.fire(EVENT_HOMEASSISTANT_STOP) @@ -150,8 +149,7 @@ class HomeAssistant(object): class JobPriority(util.OrderedEnum): - """ Provides priorities for bus events. """ - # pylint: disable=no-init,too-few-public-methods + """Provides job priorities for event bus jobs.""" EVENT_CALLBACK = 0 EVENT_SERVICE = 1 @@ -161,7 +159,7 @@ class JobPriority(util.OrderedEnum): @staticmethod def from_event_type(event_type): - """ Returns a priority based on event type. """ + """Return a priority based on event type.""" if event_type == EVENT_TIME_CHANGED: return JobPriority.EVENT_TIME elif event_type == EVENT_STATE_CHANGED: @@ -175,8 +173,7 @@ class JobPriority(util.OrderedEnum): class EventOrigin(enum.Enum): - """ Distinguish between origin of event. """ - # pylint: disable=no-init,too-few-public-methods + """Represents origin of an event.""" local = "LOCAL" remote = "REMOTE" @@ -185,14 +182,15 @@ class EventOrigin(enum.Enum): return self.value -# pylint: disable=too-few-public-methods class Event(object): - """ Represents an event within the Bus. """ + # pylint: disable=too-few-public-methods + """Represents an event within the Bus.""" __slots__ = ['event_type', 'data', 'origin', 'time_fired'] def __init__(self, event_type, data=None, origin=EventOrigin.local, time_fired=None): + """Initialize a new event.""" self.event_type = event_type self.data = data or {} self.origin = origin @@ -200,7 +198,7 @@ class Event(object): time_fired or dt_util.utcnow()) def as_dict(self): - """ Returns a dict representation of this Event. """ + """Create a dict representation of this Event.""" return { 'event_type': self.event_type, 'data': dict(self.data), @@ -227,26 +225,23 @@ class Event(object): class EventBus(object): - """ Class that allows different components to communicate via services - and events. - """ + """Allows firing of and listening for events.""" def __init__(self, pool=None): + """Initialize a new event bus.""" self._listeners = {} self._lock = threading.Lock() self._pool = pool or create_worker_pool() @property def listeners(self): - """ Dict with events that is being listened for and the number - of listeners. - """ + """Dict with events and the number of listeners.""" with self._lock: return {key: len(self._listeners[key]) for key in self._listeners} def fire(self, event_type, event_data=None, origin=EventOrigin.local): - """ Fire an event. """ + """Fire an event.""" if not self._pool.running: raise HomeAssistantError('Home Assistant has shut down.') @@ -271,7 +266,7 @@ class EventBus(object): self._pool.add_job(job_priority, (func, event)) def listen(self, event_type, listener): - """ Listen for all events or events of a specific type. + """Listen for all events or events of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. @@ -283,7 +278,7 @@ class EventBus(object): self._listeners[event_type] = [listener] def listen_once(self, event_type, listener): - """ Listen once for event of a specific type. + """Listen once for event of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. @@ -292,7 +287,7 @@ class EventBus(object): """ @ft.wraps(listener) def onetime_listener(event): - """ Removes listener from eventbus and then fires listener. """ + """Remove listener from eventbus and then fires listener.""" if hasattr(onetime_listener, 'run'): return # Set variable so that we will never run twice. @@ -311,7 +306,7 @@ class EventBus(object): return onetime_listener def remove_listener(self, event_type, listener): - """ Removes a listener of a specific event_type. """ + """Remove a listener of a specific event_type.""" with self._lock: try: self._listeners[event_type].remove(listener) @@ -343,6 +338,7 @@ class State(object): # pylint: disable=too-many-arguments def __init__(self, entity_id, state, attributes=None, last_changed=None, last_updated=None): + """Initialize a new state.""" if not ENTITY_ID_PATTERN.match(entity_id): raise InvalidEntityFormatError(( "Invalid entity id encountered: {}. " @@ -363,31 +359,33 @@ class State(object): @property def domain(self): - """ Returns domain of this state. """ + """Domain of this state.""" return util.split_entity_id(self.entity_id)[0] @property def object_id(self): - """ Returns object_id of this state. """ + """Object id of this state.""" return util.split_entity_id(self.entity_id)[1] @property def name(self): - """ Name to represent this state. """ + """Name of this state.""" return ( self.attributes.get(ATTR_FRIENDLY_NAME) or self.object_id.replace('_', ' ')) def copy(self): - """ Creates a copy of itself. """ + """Return a copy of the state.""" return State(self.entity_id, self.state, dict(self.attributes), self.last_changed, self.last_updated) def as_dict(self): - """ Converts State to a dict to be used within JSON. - Ensures: state == State.from_dict(state.as_dict()) """ + """Return a dict representation of the State. + To be used for JSON serialization. + Ensures: state == State.from_dict(state.as_dict()) + """ return {'entity_id': self.entity_id, 'state': self.state, 'attributes': self.attributes, @@ -396,11 +394,11 @@ class State(object): @classmethod def from_dict(cls, json_dict): - """ Static method to create a state from a dict. - Ensures: state == State.from_json_dict(state.to_json_dict()) """ + """Initialize a state from a dict. - if not (json_dict and - 'entity_id' in json_dict and + Ensures: state == State.from_json_dict(state.to_json_dict()) + """ + if not (json_dict and 'entity_id' in json_dict and 'state' in json_dict): return None @@ -433,15 +431,16 @@ class State(object): class StateMachine(object): - """ Helper class that tracks the state of different entities. """ + """Helper class that tracks the state of different entities.""" def __init__(self, bus): + """Initialize state machine.""" self._states = {} self._bus = bus self._lock = threading.Lock() def entity_ids(self, domain_filter=None): - """ List of entity ids that are being tracked. """ + """List of entity ids that are being tracked.""" if domain_filter is None: return list(self._states.keys()) @@ -451,35 +450,43 @@ class StateMachine(object): if state.domain == domain_filter] def all(self): - """ Returns a list of all states. """ + """Create a list of all states.""" with self._lock: return [state.copy() for state in self._states.values()] def get(self, entity_id): - """ Returns the state of the specified entity. """ + """Retrieve state of entity_id or None if not found.""" state = self._states.get(entity_id.lower()) # Make a copy so people won't mutate the state return state.copy() if state else None def is_state(self, entity_id, state): - """ Returns True if entity exists and is specified state. """ + """Test if entity exists and is specified state.""" entity_id = entity_id.lower() return (entity_id in self._states and self._states[entity_id].state == state) - def remove(self, entity_id): - """ Removes an entity from the state machine. + def is_state_attr(self, entity_id, name, value): + """Test if entity exists and has a state attribute set to value.""" + entity_id = entity_id.lower() - Returns boolean to indicate if an entity was removed. """ + return (entity_id in self._states and + self._states[entity_id].attributes.get(name, None) == value) + + def remove(self, entity_id): + """Remove the state of an entity. + + Returns boolean to indicate if an entity was removed. + """ entity_id = entity_id.lower() with self._lock: return self._states.pop(entity_id, None) is not None def set(self, entity_id, new_state, attributes=None): - """ Set the state of an entity, add entity if it does not exist. + """Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. @@ -514,9 +521,7 @@ class StateMachine(object): self._bus.fire(EVENT_STATE_CHANGED, event_data) def track_change(self, entity_ids, action, from_state=None, to_state=None): - """ - DEPRECATED AS OF 8/4/2015 - """ + """DEPRECATED AS OF 8/4/2015.""" _LOGGER.warning( 'hass.states.track_change is deprecated. ' 'Use homeassistant.helpers.event.track_state_change instead.') @@ -527,33 +532,36 @@ class StateMachine(object): # pylint: disable=too-few-public-methods class Service(object): - """ Represents a service. """ + """Represents a callable service.""" __slots__ = ['func', 'description', 'fields'] def __init__(self, func, description, fields): + """Initialize a service.""" self.func = func self.description = description or '' self.fields = fields or {} def as_dict(self): - """ Return dictionary representation of this service. """ + """Return dictionary representation of this service.""" return { 'description': self.description, 'fields': self.fields, } def __call__(self, call): + """Execute the service.""" self.func(call) # pylint: disable=too-few-public-methods class ServiceCall(object): - """ Represents a call to a service. """ + """Represents a call to a service.""" __slots__ = ['domain', 'service', 'data'] def __init__(self, domain, service, data=None): + """Initialize a service call.""" self.domain = domain self.service = service self.data = data or {} @@ -567,9 +575,10 @@ class ServiceCall(object): class ServiceRegistry(object): - """ Offers services over the eventbus. """ + """Offers services over the eventbus.""" def __init__(self, bus, pool=None): + """Initialize a service registry.""" self._services = {} self._lock = threading.Lock() self._pool = pool or create_worker_pool() @@ -579,14 +588,14 @@ class ServiceRegistry(object): @property def services(self): - """ Dict with per domain a list of available services. """ + """Dict with per domain a list of available services.""" with self._lock: return {domain: {key: value.as_dict() for key, value in self._services[domain].items()} for domain in self._services} def has_service(self, domain, service): - """ Returns True if specified service exists. """ + """Test if specified service exists.""" return service in self._services.get(domain, []) def register(self, domain, service, service_func, description=None): @@ -611,7 +620,8 @@ class ServiceRegistry(object): def call(self, domain, service, service_data=None, blocking=False): """ - Calls specified service. + Call a service. + Specify blocking=True to wait till service is executed. Waits a maximum of SERVICE_CALL_LIMIT. @@ -635,10 +645,7 @@ class ServiceRegistry(object): executed_event = threading.Event() def service_executed(call): - """ - Called when a service is executed. - Will set the event if matches our service call. - """ + """Callback method that is called when service is executed.""" if call.data[ATTR_SERVICE_CALL_ID] == call_id: executed_event.set() @@ -653,7 +660,7 @@ class ServiceRegistry(object): return success def _event_to_service_call(self, event): - """ Calls a service from an event. """ + """Callback for SERVICE_CALLED events from the event bus.""" service_data = dict(event.data) domain = service_data.pop(ATTR_DOMAIN, None) service = service_data.pop(ATTR_SERVICE, None) @@ -670,7 +677,7 @@ class ServiceRegistry(object): (service_handler, service_call))) def _execute_service(self, service_and_call): - """ Executes a service and fires a SERVICE_EXECUTED event. """ + """Execute a service and fires a SERVICE_EXECUTED event.""" service, call = service_and_call service(call) @@ -680,16 +687,17 @@ class ServiceRegistry(object): {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) def _generate_unique_id(self): - """ Generates a unique service call id. """ + """Generate a unique service call id.""" self._cur_id += 1 return "{}-{}".format(id(self), self._cur_id) class Config(object): - """ Configuration settings for Home Assistant. """ + """Configuration settings for Home Assistant.""" # pylint: disable=too-many-instance-attributes def __init__(self): + """Initialize a new config object.""" self.latitude = None self.longitude = None self.temperature_unit = None @@ -709,15 +717,15 @@ class Config(object): self.config_dir = get_default_config_dir() def distance(self, lat, lon): - """ Calculate distance from Home Assistant in meters. """ + """Calculate distance from Home Assistant in meters.""" return location.distance(self.latitude, self.longitude, lat, lon) def path(self, *path): - """ Returns path to the file within the config dir. """ + """Generate path to the file within the config dir.""" return os.path.join(self.config_dir, *path) def temperature(self, value, unit): - """ Converts temperature to user preferred unit if set. """ + """Convert temperature to user preferred unit if set.""" if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and self.temperature_unit and unit != self.temperature_unit): return value, unit @@ -732,7 +740,7 @@ class Config(object): self.temperature_unit) def as_dict(self): - """ Converts config to a dictionary. """ + """Create a dict representation of this dict.""" time_zone = self.time_zone or dt_util.UTC return { @@ -747,7 +755,7 @@ class Config(object): def create_timer(hass, interval=TIMER_INTERVAL): - """ Creates a timer. Timer will start on HOMEASSISTANT_START. """ + """Create a timer that will start on HOMEASSISTANT_START.""" # We want to be able to fire every time a minute starts (seconds=0). # We want this so other modules can use that to make sure they fire # every minute. @@ -810,12 +818,12 @@ def create_timer(hass, interval=TIMER_INTERVAL): def create_worker_pool(worker_count=None): - """ Creates a worker pool to be used. """ + """Create a worker pool.""" if worker_count is None: worker_count = MIN_WORKER_THREAD def job_handler(job): - """ Called whenever a job is available to do. """ + """Called whenever a job is available to do.""" try: func, arg = job func(arg) @@ -825,8 +833,7 @@ def create_worker_pool(worker_count=None): _LOGGER.exception("BusHandler:Exception doing job") def busy_callback(worker_count, current_jobs, pending_jobs_count): - """ Callback to be called when the pool queue gets too big. """ - + """Callback to be called when the pool queue gets too big.""" _LOGGER.warning( "WorkerPool:All %d threads are busy and %d jobs pending", worker_count, pending_jobs_count) diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 021146d1c32..95dfe7dd65e 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -36,7 +36,7 @@ def extract_entity_ids(hass, service): service_ent_id = service.data[ATTR_ENTITY_ID] if isinstance(service_ent_id, str): - return group.expand_entity_ids(hass, [service_ent_id.lower()]) + return group.expand_entity_ids(hass, [service_ent_id]) return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)] diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py new file mode 100644 index 00000000000..15cfe9b97c6 --- /dev/null +++ b/homeassistant/helpers/service.py @@ -0,0 +1,43 @@ +"""Service calling related helpers.""" +import logging + +from homeassistant.util import split_entity_id +from homeassistant.const import ATTR_ENTITY_ID + +CONF_SERVICE = 'service' +CONF_SERVICE_ENTITY_ID = 'entity_id' +CONF_SERVICE_DATA = 'data' + +_LOGGER = logging.getLogger(__name__) + + +def call_from_config(hass, config, blocking=False): + """Call a service based on a config hash.""" + if not isinstance(config, dict) or CONF_SERVICE not in config: + _LOGGER.error('Missing key %s: %s', CONF_SERVICE, config) + return + + try: + domain, service = split_entity_id(config[CONF_SERVICE]) + except ValueError: + _LOGGER.error('Invalid service specified: %s', config[CONF_SERVICE]) + return + + service_data = config.get(CONF_SERVICE_DATA) + + if service_data is None: + service_data = {} + elif isinstance(service_data, dict): + service_data = dict(service_data) + else: + _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) + service_data = {} + + entity_id = config.get(CONF_SERVICE_ENTITY_ID) + if isinstance(entity_id, str): + service_data[ATTR_ENTITY_ID] = [ent.strip() for ent in + entity_id.split(",")] + elif entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(domain, service, service_data, blocking) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 24a37c5b5ea..c8f6f05661a 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,9 +1,6 @@ -""" -homeassistant.helpers.state -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Helpers that help with state related things. -""" +"""Helpers that help with state related things.""" +from collections import defaultdict +import json import logging from homeassistant.core import State @@ -25,32 +22,36 @@ class TrackStates(object): that have changed since the start time to the return list when with-block is exited. """ + def __init__(self, hass): + """Initialize a TrackStates block.""" self.hass = hass self.states = [] def __enter__(self): + """Record time from which to track changes.""" self.now = dt_util.utcnow() return self.states def __exit__(self, exc_type, exc_value, traceback): + """Add changes states to changes list.""" self.states.extend(get_changed_since(self.hass.states.all(), self.now)) def get_changed_since(states, utc_point_in_time): - """ - Returns all states that have been changed since utc_point_in_time. - """ + """List of states that have been changed since utc_point_in_time.""" point_in_time = dt_util.strip_microseconds(utc_point_in_time) return [state for state in states if state.last_updated >= point_in_time] def reproduce_state(hass, states, blocking=False): - """ Takes in a state and will try to have the entity reproduce it. """ + """Reproduce given state.""" if isinstance(states, State): states = [states] + to_call = defaultdict(list) + for state in states: current_state = hass.states.get(state.entity_id) @@ -76,7 +77,18 @@ def reproduce_state(hass, states, blocking=False): state) continue - service_data = dict(state.attributes) - service_data[ATTR_ENTITY_ID] = state.entity_id + if state.domain == 'group': + service_domain = 'homeassistant' + else: + service_domain = state.domain - hass.services.call(state.domain, service, service_data, blocking) + # We group service calls for entities by service call + # json used to create a hashable version of dict with maybe lists in it + key = (service_domain, service, + json.dumps(state.attributes, sort_keys=True)) + to_call[key].append(state.entity_id) + + for (service_domain, service, service_data), entity_ids in to_call.items(): + data = json.loads(service_data) + data[ATTR_ENTITY_ID] = entity_ids + hass.services.call(service_domain, service, data, blocking) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 26dca7ab0c6..06c9b4c6862 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -53,8 +53,12 @@ def color_xy_brightness_to_RGB(vX, vY, brightness): return (0, 0, 0) Y = brightness - X = (Y / vY) * vX - Z = (Y / vY) * (1 - vX - vY) + if vY != 0: + X = (Y / vY) * vX + Z = (Y / vY) * (1 - vX - vY) + else: + X = 0 + Z = 0 # Convert to RGB using Wide RGB D65 conversion. r = X * 1.612 - Y * 0.203 - Z * 0.302 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 35795a7ae7f..a2c796c20eb 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -108,14 +108,14 @@ def datetime_to_date_str(dattim): return dattim.strftime(DATE_STR_FORMAT) -def str_to_datetime(dt_str): +def str_to_datetime(dt_str, dt_format=DATETIME_STR_FORMAT): """ Converts a string to a UTC datetime object. @rtype: datetime """ try: return dt.datetime.strptime( - dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc) + dt_str, dt_format).replace(tzinfo=pytz.utc) except ValueError: # If dt_str did not match our format return None diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 398a0a0c56c..185745d9207 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -4,6 +4,8 @@ import collections import requests from vincenty import vincenty +ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' + LocationInfo = collections.namedtuple( "LocationInfo", @@ -34,3 +36,20 @@ def detect_location_info(): def distance(lat1, lon1, lat2, lon2): """ Calculate the distance in meters between two points. """ return vincenty((lat1, lon1), (lat2, lon2)) * 1000 + + +def elevation(latitude, longitude): + """ Return elevation for given latitude and longitude. """ + + req = requests.get(ELEVATION_URL, params={ + 'locations': '{},{}'.format(latitude, longitude), + 'sensor': 'false', + }) + + if req.status_code != 200: + return 0 + + try: + return int(float(req.json()['results'][0]['elevation'])) + except (ValueError, KeyError): + return 0 diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index d0a07507bdf..bc7431ebf6d 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -43,7 +43,8 @@ def render(hass, template, variables=None, **kwargs): try: return ENV.from_string(template, { 'states': AllStates(hass), - 'is_state': hass.states.is_state + 'is_state': hass.states.is_state, + 'is_state_attr': hass.states.is_state_attr }).render(kwargs).strip() except jinja2.TemplateError as err: raise TemplateError(err) diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 5ee64771657..00000000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -testpaths = tests diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 14dfca13f23..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -requests>=2,<3 -pyyaml>=3.11,<4 -pytz>=2015.4 -pip>=7.0.0 -vincenty==0.1.3 \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index bb41f38c6f0..1bc00b4c46d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -16,7 +16,7 @@ fuzzywuzzy==0.8.0 pyicloud==0.7.2 # homeassistant.components.device_tracker.netgear -pynetgear==0.3 +pynetgear==0.3.1 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.4.3 @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 +pyvera==0.2.3 # homeassistant.components.wink # homeassistant.components.light.wink @@ -69,7 +69,7 @@ https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d python-wink==0.3.1 # homeassistant.components.media_player.cast -pychromecast==0.6.13 +pychromecast==0.6.14 # homeassistant.components.media_player.kodi jsonrpc-requests==0.1 @@ -89,6 +89,12 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 # homeassistant.components.mqtt paho-mqtt==1.1 +# homeassistant.components.mysensors +https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4 + +# homeassistant.components.notify.free_mobile +freesms==0.1.0 + # homeassistant.components.notify.pushbullet pushbullet.py==0.9.0 @@ -131,11 +137,8 @@ eliqonline==1.0.11 # homeassistant.components.sensor.forecast python-forecastio==1.3.3 -# homeassistant.components.sensor.mysensors -https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3 - # homeassistant.components.sensor.openweathermap -pyowm==2.2.1 +pyowm==2.3.0 # homeassistant.components.sensor.rpi_gpio # homeassistant.components.switch.rpi_gpio @@ -157,6 +160,9 @@ transmissionrpc==0.11 # homeassistant.components.sensor.twitch python-twitch==1.2.0 +# homeassistant.components.sensor.yr +xmltodict + # homeassistant.components.sun astral==0.8.1 @@ -170,7 +176,10 @@ hikvision==0.4 orvibo==1.1.0 # homeassistant.components.switch.wemo -pywemo==0.3.3 +pywemo==0.3.8 + +# homeassistant.components.tellduslive +tellive-py==0.5.2 # homeassistant.components.thermostat.heatmiser heatmiserV3==0.9.1 @@ -185,7 +194,7 @@ python-nest==2.6.0 radiotherm==1.2 # homeassistant.components.verisure -https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6 +vsure==0.4.5 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 00000000000..616c49c5ae4 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,6 @@ +flake8>=2.5.1 +pylint>=1.5.3 +coveralls>=1.1 +pytest>=2.6.4 +pytest-cov>=2.2.0 +betamax>=0.5.1 \ No newline at end of file diff --git a/script/bootstrap_server b/script/bootstrap_server index a5533b0596d..f71abda0e65 100755 --- a/script/bootstrap_server +++ b/script/bootstrap_server @@ -6,7 +6,7 @@ python3 -m pip install -r requirements_all.txt REQ_STATUS=$? echo "Installing development dependencies.." -python3 -m pip install flake8 pylint coveralls pytest pytest-cov +python3 -m pip install -r requirements_test.txt REQ_DEV_STATUS=$? diff --git a/script/cibuild b/script/cibuild index 778cbe0db52..11d91415405 100755 --- a/script/cibuild +++ b/script/cibuild @@ -5,7 +5,29 @@ cd "$(dirname "$0")/.." -if [ "$TRAVIS_PYTHON_VERSION" != "3.4" ]; then +if [ "$TRAVIS_PYTHON_VERSION" = "3.5" ]; then + echo "Verifying requirements_all.txt..." + python3 setup.py -q develop 2> /dev/null + tput setaf 1 + script/gen_requirements_all.py validate + VERIFY_REQUIREMENTS_STATUS=$? + tput sgr0 +else + VERIFY_REQUIREMENTS_STATUS=0 +fi + +if [ "$VERIFY_REQUIREMENTS_STATUS" != "0" ]; then + exit $VERIFY_REQUIREMENTS_STATUS +fi + +script/bootstrap_server > /dev/null +DEP_INSTALL_STATUS=$? + +if [ "$DEP_INSTALL_STATUS" != "0" ]; then + exit $DEP_INSTALL_STATUS +fi + +if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then NO_LINT=1 fi diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d2626a2701a..a134afaa359 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -115,7 +115,6 @@ def main(): if sys.argv[-1] == 'validate': if validate_file(data): - print("requirements_all.txt is up to date.") sys.exit(0) print("******* ERROR") print("requirements_all.txt is not up to date") diff --git a/script/lint b/script/lint index 75667ef88a4..d99d030c86d 100755 --- a/script/lint +++ b/script/lint @@ -3,13 +3,16 @@ cd "$(dirname "$0")/.." echo "Checking style with flake8..." +tput setaf 1 flake8 --exclude www_static homeassistant - FLAKE8_STATUS=$? +tput sgr0 echo "Checking style with pylint..." +tput setaf 1 pylint homeassistant PYLINT_STATUS=$? +tput sgr0 if [ $FLAKE8_STATUS -eq 0 ] then diff --git a/script/test b/script/test index 25873492001..6a78ce42d41 100755 --- a/script/test +++ b/script/test @@ -5,13 +5,6 @@ cd "$(dirname "$0")/.." -if [ "$NO_LINT" = "1" ]; then - LINT_STATUS=0 -else - script/lint - LINT_STATUS=$? -fi - echo "Running tests..." if [ "$1" = "coverage" ]; then @@ -22,6 +15,13 @@ else TEST_STATUS=$? fi +if [ "$NO_LINT" = "1" ]; then + LINT_STATUS=0 +else + script/lint + LINT_STATUS=$? +fi + if [ $LINT_STATUS -eq 0 ] then exit $TEST_STATUS diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000000..aab4b18bc12 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ +[wheel] +universal = 1 + +[pytest] +testpaths = tests + +[pep257] +ignore = D203,D105 diff --git a/setup.py b/setup.py index 5bdc07700d9..9cc79615c71 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ REQUIRES = [ 'pytz>=2015.4', 'pip>=7.0.0', 'vincenty==0.1.3', - 'jinja2>=2.8' + 'jinja2>=2.8', ] setup( @@ -33,6 +33,7 @@ setup( zip_safe=False, platforms='any', install_requires=REQUIRES, + test_suite='tests', keywords=['home', 'automation'], entry_points={ 'console_scripts': [ @@ -46,5 +47,5 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python :: 3.4', 'Topic :: Home Automation' - ] + ], ) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb2d..37d3307a4ae 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +import betamax + +with betamax.Betamax.configure() as config: + config.cassette_library_dir = 'tests/cassettes' diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json new file mode 100644 index 00000000000..6bd1601260d --- /dev/null +++ b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json @@ -0,0 +1 @@ +{"http_interactions": [{"recorded_at": "2015-12-28T01:34:34", "request": {"method": "GET", "headers": {"Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["python-requests/2.9.1"], "Connection": ["keep-alive"]}, "body": {"string": "", "encoding": "utf-8"}, "uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"}, "response": {"headers": {"Content-Length": ["3598"], "X-forecast-models": ["proff,ecdet"], "Via": ["1.1 varnish"], "Content-Encoding": ["gzip"], "Date": ["Mon, 28 Dec 2015 01:34:34 GMT"], "X-Varnish": ["2670913442 2670013167"], "Expires": ["Mon, 28 Dec 2015 02:01:51 GMT"], "Server": ["Apache"], "Age": ["1574"], "Content-Type": ["text/xml; charset=utf-8"], "X-Backend-Host": ["snipe_loc"], "X-slicenumber": ["30"], "Accept-Ranges": ["bytes"], "Last-Modified": ["Mon, 28 Dec 2015 01:08:20 GMT"], "Vary": ["Accept-Encoding"], "Connection": ["keep-alive"]}, "url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "body": {"base64_string": "H4sIAAAAAAAAA+2dW4/bNhOG7/srBN+vLFKiDotNb5o0LZBD8WU/tOidYytZoT7BlrNJf31lrw+yNKRNiocRdoFcJJFFS5T18OU7w+HdYz4qH/LVZFSOvO+z6Xx9+31dvBo8lOXydjh8fHz0H0N/sfo6pEFAhn+9f/dp/JDPRjfFfF2O5uN84FWfv50vPoxm+Xo5GudPx98txqOyWMyPLY2WhT/LS3++GO6/svqf4XT/sS+LVT4ercsh8bPhetfCwBuvqg/mk1cDGhB2Q+gNTe8DchuktzT4e/DzT57n3VVNjnZ/2/1jMcmn3ry6kleDN7/4b9989ANC2cAr89WsmNcaSu4JvQ2C6s/fA2+1mefzydkXVcez25DdBqQ6Ps+/l9Vnzq8jPZ7+ZbWYnR8Lj8fKxe5IfBOQmyCuf+nweNXD4z3cLVeLyWZceuPpaL1+NVguinn5unoyg+OHy2KWe9uHVf5YVnd56LerrqJ95NDstuXDo/BG07IoN5Oq9WDgTUeHf4TUT5Mwqv5rMf+6/z9CEp/SJKo3tLvKfLasHnC5WeVeUfXr/f39wNvMi/LVYJxP18VmPfC+jaabqonITwbDxumPxXzyuqjubHc92wYmk4E3yb9W3xj4dLB/xB/AEz8t83yyO+nLl4E3W1b9GPjhwPucjzZVd5W723pq4FNZTKd5q5WHzayYFOWPwyXGic8Ol1/d1jifl61zlqt8vT7c7nJ1+PjDH6PjnZIgTIGbHU8Xm0kxr07fnfvhw8Dbf0n1zakftE74svi6++SvH9/WPhoAn5wuHn/Ztv7U8ruPf15qepZPis2sds77N69////7s69J2/1VfH2onfPb729/u3Bhk/xx99u+b/5OXvN+Jjek0czdkR2nF2n7cqi8KYG7N6X63YyLZVE+Nfh077PZ8bb3nXe3/jH7vJju+uiP0aqc/tj1d/U73sw+56vqK830TXJPyUvfcH83MbdvYkSEZX4mQ9isYpQ8YJM6YMmhgV+no433rTrnImNZsuW6DsbGAG9EjM1SPzLEWLDpaxjb/horjKU+Mfau8NWI8XdFniMNgkQORh4svVLp5tYviM8a4odPZ42+S5wVP/3uHJI84z6HDBXJQxmSx7Smld9cz/K0I8tjsh0OdLCcSerlNAZO0MNysGnELCeNy7Wje4y/LYhZzh/hnnGv1ByPRq+cjiAgawhMa0UaeSvsDkbTtWAlftwRrEmwbUKPSG6PJCKwJmlDGnrawAo2jRqs1L4MMf+y9HeyjaZv5KTyHjl9k8qEcZ8DQwR0Csy8RbYyibYe8d4WlkB6V98jSaofjxOks8SYtww2jRrpLvSP8dcFPdL5w92z7xuScvsmRYRZIqebSbgTwfKYzbpiNtNlLzNJe7liYbuL9GCWQtdiArNR5DM9nI0dyBLj7wt6lvDHICx9I+syJwrSOXyKg7l7DvxY6ukIAqYHkhkZUXwyQ2xK55TqYjrk/gjTMijAQz1MJylwLSaYzqDEG6WYoTE7hK+BzL8v6JnOHe+ee99k3Khq/QgKzkrlZRC2zdg6cPZazNJzzB7jgZ++jf65WjprMp0hFSnCbARxWRNmQ8AtuYzZMAO8akupGcy2LLHxuuBGCX8IQtQ3ctI52PvHctJ5P813iHROcDXDlcwcNya43gWkJ7TmOl+L9HA7DHRCOgurX4Em5dwew4RIj4ETNCVoAH1/mehR6C7ZzhDRhQLo2SftCka7Z943GTfGmuFKaIZWUYgom4U10/l64Rx2pGyU6lo2EgDBTFfCWY2yTZfAsxfaSx1oEizJuxin4Fj6RlY3Zwq6eU8qh8+BE2LNcCU2Q2s2hEQnPlEgOulI9DjZOt0aiE4zSSsE9CtcEp0084s9S0QPfBfyB2EG77uqs8sGRwxZ8cKx7rl3DS+6muHKcJZNiEuSWlTvesTSjohNt4JNE2Il1440E3s9bYiFPm8ipgfegoJoDkzlaYgECZo0XoTTbzR9IyeaqdJqwNS12cyLrWa4UpwhBSYiepqcVnZbFM1prCvDGQKio0UrtogO3oIS0Y2ZzQL988yzeIWj3bPvG35oFVmGs6Q1kTqxJtJMU0hP2pqAoNYvyGqympulkewoEiwZvDWOfNrMT/wwVFFEOO5g6RPZrOZAQS3vw8juOM4PqCLLapbKtqMkqPkff14P8qgjyDNda7wplKt2odicqaihLZKDt4DLAOHLnpfEXf4wh7Fv7LnMYcALqJ4dQUBZIlehiFaQClQo29VmznbpHS5yM0jQf8EM34NKLM+YOuSoEivvC3rMwkMQpr6RTc6IlJKaQ4fPYdvbcFD17AgCpidySc00qlUQtSic42S7SlwP0uWWA4aRsQqitogO3oI80EljkmVHACHM27WagSAY6xB2jWXGwjHVsyMIGEuaaU3eBcimRAmyrOvKkUhTTjPNgFQFEWRpky1e7yAL3oIKZM1UKxIrEixpuwgn4Fi6xkap5gOo3D0HOKR6dgQB0GPAfBXwPCRhrey+BNDjrqu7NaY0S+bbhcC8ol9AB29BHujUUEqzWP4gzNtFM/1+9n3DCa6eHUFAWWhZmYiyu4VxspubhN2TmmNNkA2b2s+7FNNLep/UDN6CitdsZv8ooSRBk7eLcP6Npm9kc5qpJtlsmehc0x9Vut1NIId0kp3SNK4menfZnEW6iE4lK2j0P3qobWW3meChWP30M7Xs8FZJ55ZFDnL7QsI1WwiubAdZWMVs657KbsjUPTk4Y7qyHZik/nyhlQVacUZ2Gy+LGVoRpa0xjpCzjyuOyCX9DjKlNVxJ0KqrtmJEV3UyqMStMJBvLMQEtYyXVqZWi4lH9n6GURSlFTQhtAMrnrgl2Kw9qZ0l0kRhR7buxXFTomtx64uy8tSUlamCMMJxvbfmlYqwcsgqrrRFZVpBCYMiz4rS2kp8i8pKW7C3UlZy2TuZubqvtnAF3oJS3VdDOZLCob2fvlWgxCvqqoBIfBOQ6g8gcttHUGBLroBIyFT2SOxerjpN9ZWrlsRWiiyzezdySHIr0bPRi6l9b8Fh3t77YmpSqFZm3/o+3ceObknd9hEEyIKyhoV7UyWnhA8ZpdV1MQoL9JXjkFvFnZmr/ayGrLTpJnkXkZVAuYQ4KnJcMcT308cKlPbUy9qWuy1itcVu+wgCYkmWNqZBViPW1YnA5Hxlh9pG1JqIBZUs69eKaPiKLqyg01KozcBuepdH+J6aWbFSvZysXS/HGrG4MheVm8Xkli5QGtQqnkkQq/P+n4GuUsExEBcVrkVLjblZJFUjViS9MR1YwR3Fkt8rRvh+2llMaVYYO1px9dTZlCt1KS43C9ohQVjgK1PBloYK57q2oq+wJVenIE2MbVtsbcUVdAsq0NJe3+vSIG/jbTG0TlTFgz+gzpEHT7lyl+IytAi0/6AIW4ycKmZZFFuM6SpLGEkmvCf9X/gP3oI8tZpBVztjfD/trFBtdXvoar3Uobt5mpficrUk54gkztzsPZBqSs+iTDKVFJul5So9qxlxtTLC99TQUpsekrAttcxmZx27mSt1UflZ0jtfxUo7X3UNGWZUn8J6YZXarNAYqwTDej+tLLWtnYBZoQ1UhVx9G/bawyJpVCuffD2quu5snYW6ZBVUdvIFVQ7jhPxR3carYsjAihVQFdrOeT/2MkfWhsiMK9kwIUlUNtTYOicdnStdpd4ptFOyMBUr6r3fDt6CPK6ai5vsjOzPyrlybLiHXIkb4nKuoGxoYdJ7eloEfX0G6W4X427ZDURTOXMKRRgubJvZe8MdugUVwz2xP8r31LxKlPJHEWCLK3dRmVhMbhMGEmSntTpXUyvonpO18/n1UEuydgOUKO+WWu3sjAsppNAtqFBLe6XDK8b4ftpYe3krSa0D6xxBK+KK3gibndWePAiglTCfSjOLnCdkKTArS3QZ70xyeWEElXPVxaw2fS4zK5FWWlFzq0RPMUxozE/hDfA23hVD1numZL07XRQdcRVvhMzXgjKpRTX9YrWafmnX7a5CXUpLNiErhnpID7WY4qroGICQDRs+0V975oohvq++lorUOryO7rjF07wRLl8rlfO14kAFW9F5RtZxhvkuL0vv86pYX54gBrpsrUQydGhwYTTY9GVsRZAzZwNbTTPByijfU18rVfO1AGzZWWYYCQQvMkdLqmhWnJwMreuBFXaumZVmzoBlLnyYQk7TZWDFmXQpBz3AMpb5Lhze+2lpMaX1Omnb0rLDK8ZVugyXmSXJq0TFgA/Pp4Uq+krbfm7Qts+uyjhk0HZ6Zuo40Galdk/R0DIUOuSP7zbeF0PEUpkZsr0N5oJYHJHLcFlZUCEWUQllcqo8IzEjDDsSqwKlNvsdT+EZ1ToOCsSCz0GU7SAc4t2bWYlFkdWqlvV6Vfz77zSvQcvQ1JxxlS7DZWRBVQ1E1IpPKfAS1GJodBaTTM8ySi2w7cvYAmsjW9k7NzJV4k80yju3stghR1SyYJYmaP1vVNQz4Q2lmzCB0kXmZEnlZqXbAK78zDDuSiyNqVlyRhZGYqkILQYtAECUnyUc4l2bWUxxaqiydiduTw3tICvmKt2412ZWnJxEloz5HnVdaKgtWghF6J4Ds3RNDQ2F1/ljvI0XBpWb1UrPsuNmxVydG2Nzs6SSSdNtTpD8hmBdK84kma4yyrKrDTESC6wFbSfDwcUI797LshkwdFRHOeaq3BiXkQWVqhQAi4WnaeH1GivovtAw1lWPVHYre4zEcqWwmCmFJRrfnftYqksNVRSWVmDdDZerxWQzLqsGh4/5qHzIV9uH8bP3038Aw70aUvUAAA==", "encoding": "utf-8"}, "status": {"code": 200, "message": "OK"}}}], "recorded_with": "betamax/0.5.1"} \ No newline at end of file diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json new file mode 100644 index 00000000000..4ff2ff18df5 --- /dev/null +++ b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json @@ -0,0 +1 @@ +{"http_interactions": [{"recorded_at": "2015-12-28T01:34:34", "request": {"method": "GET", "headers": {"Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["python-requests/2.9.1"], "Connection": ["keep-alive"]}, "body": {"string": "", "encoding": "utf-8"}, "uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"}, "response": {"headers": {"Content-Length": ["3598"], "X-forecast-models": ["proff,ecdet"], "Via": ["1.1 varnish"], "Content-Encoding": ["gzip"], "Date": ["Mon, 28 Dec 2015 01:34:33 GMT"], "X-Varnish": ["2670913258 2670013167"], "Expires": ["Mon, 28 Dec 2015 02:01:51 GMT"], "Server": ["Apache"], "Age": ["1573"], "Content-Type": ["text/xml; charset=utf-8"], "X-Backend-Host": ["snipe_loc"], "X-slicenumber": ["30"], "Accept-Ranges": ["bytes"], "Last-Modified": ["Mon, 28 Dec 2015 01:08:20 GMT"], "Vary": ["Accept-Encoding"], "Connection": ["keep-alive"]}, "url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "body": {"base64_string": "H4sIAAAAAAAAA+2dW4/bNhOG7/srBN+vLFKiDotNb5o0LZBD8WU/tOidYytZoT7BlrNJf31lrw+yNKRNiocRdoFcJJFFS5T18OU7w+HdYz4qH/LVZFSOvO+z6Xx9+31dvBo8lOXydjh8fHz0H0N/sfo6pEFAhn+9f/dp/JDPRjfFfF2O5uN84FWfv50vPoxm+Xo5GudPx98txqOyWMyPLY2WhT/LS3++GO6/svqf4XT/sS+LVT4ercsh8bPhetfCwBuvqg/mk1cDGhB2Q+gNTe8DchuktzT4e/DzT57n3VVNjnZ/2/1jMcmn3ry6kleDN7/4b9989ANC2cAr89WsmNcaSu4JvQ2C6s/fA2+1mefzydkXVcez25DdBqQ6Ps+/l9Vnzq8jPZ7+ZbWYnR8Lj8fKxe5IfBOQmyCuf+nweNXD4z3cLVeLyWZceuPpaL1+NVguinn5unoyg+OHy2KWe9uHVf5YVnd56LerrqJ95NDstuXDo/BG07IoN5Oq9WDgTUeHf4TUT5Mwqv5rMf+6/z9CEp/SJKo3tLvKfLasHnC5WeVeUfXr/f39wNvMi/LVYJxP18VmPfC+jaabqonITwbDxumPxXzyuqjubHc92wYmk4E3yb9W3xj4dLB/xB/AEz8t83yyO+nLl4E3W1b9GPjhwPucjzZVd5W723pq4FNZTKd5q5WHzayYFOWPwyXGic8Ol1/d1jifl61zlqt8vT7c7nJ1+PjDH6PjnZIgTIGbHU8Xm0kxr07fnfvhw8Dbf0n1zakftE74svi6++SvH9/WPhoAn5wuHn/Ztv7U8ruPf15qepZPis2sds77N69////7s69J2/1VfH2onfPb729/u3Bhk/xx99u+b/5OXvN+Jjek0czdkR2nF2n7cqi8KYG7N6X63YyLZVE+Nfh077PZ8bb3nXe3/jH7vJju+uiP0aqc/tj1d/U73sw+56vqK830TXJPyUvfcH83MbdvYkSEZX4mQ9isYpQ8YJM6YMmhgV+no433rTrnImNZsuW6DsbGAG9EjM1SPzLEWLDpaxjb/horjKU+Mfau8NWI8XdFniMNgkQORh4svVLp5tYviM8a4odPZ42+S5wVP/3uHJI84z6HDBXJQxmSx7Smld9cz/K0I8tjsh0OdLCcSerlNAZO0MNysGnELCeNy7Wje4y/LYhZzh/hnnGv1ByPRq+cjiAgawhMa0UaeSvsDkbTtWAlftwRrEmwbUKPSG6PJCKwJmlDGnrawAo2jRqs1L4MMf+y9HeyjaZv5KTyHjl9k8qEcZ8DQwR0Csy8RbYyibYe8d4WlkB6V98jSaofjxOks8SYtww2jRrpLvSP8dcFPdL5w92z7xuScvsmRYRZIqebSbgTwfKYzbpiNtNlLzNJe7liYbuL9GCWQtdiArNR5DM9nI0dyBLj7wt6lvDHICx9I+syJwrSOXyKg7l7DvxY6ukIAqYHkhkZUXwyQ2xK55TqYjrk/gjTMijAQz1MJylwLSaYzqDEG6WYoTE7hK+BzL8v6JnOHe+ee99k3Khq/QgKzkrlZRC2zdg6cPZazNJzzB7jgZ++jf65WjprMp0hFSnCbARxWRNmQ8AtuYzZMAO8akupGcy2LLHxuuBGCX8IQtQ3ctI52PvHctJ5P813iHROcDXDlcwcNya43gWkJ7TmOl+L9HA7DHRCOgurX4Em5dwew4RIj4ETNCVoAH1/mehR6C7ZzhDRhQLo2SftCka7Z943GTfGmuFKaIZWUYgom4U10/l64Rx2pGyU6lo2EgDBTFfCWY2yTZfAsxfaSx1oEizJuxin4Fj6RlY3Zwq6eU8qh8+BE2LNcCU2Q2s2hEQnPlEgOulI9DjZOt0aiE4zSSsE9CtcEp0084s9S0QPfBfyB2EG77uqs8sGRwxZ8cKx7rl3DS+6muHKcJZNiEuSWlTvesTSjohNt4JNE2Il1440E3s9bYiFPm8ipgfegoJoDkzlaYgECZo0XoTTbzR9IyeaqdJqwNS12cyLrWa4UpwhBSYiepqcVnZbFM1prCvDGQKio0UrtogO3oIS0Y2ZzQL988yzeIWj3bPvG35oFVmGs6Q1kTqxJtJMU0hP2pqAoNYvyGqympulkewoEiwZvDWOfNrMT/wwVFFEOO5g6RPZrOZAQS3vw8juOM4PqCLLapbKtqMkqPkff14P8qgjyDNda7wplKt2odicqaihLZKDt4DLAOHLnpfEXf4wh7Fv7LnMYcALqJ4dQUBZIlehiFaQClQo29VmznbpHS5yM0jQf8EM34NKLM+YOuSoEivvC3rMwkMQpr6RTc6IlJKaQ4fPYdvbcFD17AgCpidySc00qlUQtSic42S7SlwP0uWWA4aRsQqitogO3oI80EljkmVHACHM27WagSAY6xB2jWXGwjHVsyMIGEuaaU3eBcimRAmyrOvKkUhTTjPNgFQFEWRpky1e7yAL3oIKZM1UKxIrEixpuwgn4Fi6xkap5gOo3D0HOKR6dgQB0GPAfBXwPCRhrey+BNDjrqu7NaY0S+bbhcC8ol9AB29BHujUUEqzWP4gzNtFM/1+9n3DCa6eHUFAWWhZmYiyu4VxspubhN2TmmNNkA2b2s+7FNNLep/UDN6CitdsZv8ooSRBk7eLcP6Npm9kc5qpJtlsmehc0x9Vut1NIId0kp3SNK4menfZnEW6iE4lK2j0P3qobWW3meChWP30M7Xs8FZJ55ZFDnL7QsI1WwiubAdZWMVs657KbsjUPTk4Y7qyHZik/nyhlQVacUZ2Gy+LGVoRpa0xjpCzjyuOyCX9DjKlNVxJ0KqrtmJEV3UyqMStMJBvLMQEtYyXVqZWi4lH9n6GURSlFTQhtAMrnrgl2Kw9qZ0l0kRhR7buxXFTomtx64uy8tSUlamCMMJxvbfmlYqwcsgqrrRFZVpBCYMiz4rS2kp8i8pKW7C3UlZy2TuZubqvtnAF3oJS3VdDOZLCob2fvlWgxCvqqoBIfBOQ6g8gcttHUGBLroBIyFT2SOxerjpN9ZWrlsRWiiyzezdySHIr0bPRi6l9b8Fh3t77YmpSqFZm3/o+3ceObknd9hEEyIKyhoV7UyWnhA8ZpdV1MQoL9JXjkFvFnZmr/ayGrLTpJnkXkZVAuYQ4KnJcMcT308cKlPbUy9qWuy1itcVu+wgCYkmWNqZBViPW1YnA5Hxlh9pG1JqIBZUs69eKaPiKLqyg01KozcBuepdH+J6aWbFSvZysXS/HGrG4MheVm8Xkli5QGtQqnkkQq/P+n4GuUsExEBcVrkVLjblZJFUjViS9MR1YwR3Fkt8rRvh+2llMaVYYO1px9dTZlCt1KS43C9ohQVjgK1PBloYK57q2oq+wJVenIE2MbVtsbcUVdAsq0NJe3+vSIG/jbTG0TlTFgz+gzpEHT7lyl+IytAi0/6AIW4ycKmZZFFuM6SpLGEkmvCf9X/gP3oI8tZpBVztjfD/trFBtdXvoar3Uobt5mpficrUk54gkztzsPZBqSs+iTDKVFJul5So9qxlxtTLC99TQUpsekrAttcxmZx27mSt1UflZ0jtfxUo7X3UNGWZUn8J6YZXarNAYqwTDej+tLLWtnYBZoQ1UhVx9G/bawyJpVCuffD2quu5snYW6ZBVUdvIFVQ7jhPxR3carYsjAihVQFdrOeT/2MkfWhsiMK9kwIUlUNtTYOicdnStdpd4ptFOyMBUr6r3fDt6CPK6ai5vsjOzPyrlybLiHXIkb4nKuoGxoYdJ7eloEfX0G6W4X427ZDURTOXMKRRgubJvZe8MdugUVwz2xP8r31LxKlPJHEWCLK3dRmVhMbhMGEmSntTpXUyvonpO18/n1UEuydgOUKO+WWu3sjAsppNAtqFBLe6XDK8b4ftpYe3krSa0D6xxBK+KK3gibndWePAiglTCfSjOLnCdkKTArS3QZ70xyeWEElXPVxaw2fS4zK5FWWlFzq0RPMUxozE/hDfA23hVD1numZL07XRQdcRVvhMzXgjKpRTX9YrWafmnX7a5CXUpLNiErhnpID7WY4qroGICQDRs+0V975oohvq++lorUOryO7rjF07wRLl8rlfO14kAFW9F5RtZxhvkuL0vv86pYX54gBrpsrUQydGhwYTTY9GVsRZAzZwNbTTPByijfU18rVfO1AGzZWWYYCQQvMkdLqmhWnJwMreuBFXaumZVmzoBlLnyYQk7TZWDFmXQpBz3AMpb5Lhze+2lpMaX1Omnb0rLDK8ZVugyXmSXJq0TFgA/Pp4Uq+krbfm7Qts+uyjhk0HZ6Zuo40Galdk/R0DIUOuSP7zbeF0PEUpkZsr0N5oJYHJHLcFlZUCEWUQllcqo8IzEjDDsSqwKlNvsdT+EZ1ToOCsSCz0GU7SAc4t2bWYlFkdWqlvV6Vfz77zSvQcvQ1JxxlS7DZWRBVQ1E1IpPKfAS1GJodBaTTM8ySi2w7cvYAmsjW9k7NzJV4k80yju3stghR1SyYJYmaP1vVNQz4Q2lmzCB0kXmZEnlZqXbAK78zDDuSiyNqVlyRhZGYqkILQYtAECUnyUc4l2bWUxxaqiydiduTw3tICvmKt2412ZWnJxEloz5HnVdaKgtWghF6J4Ds3RNDQ2F1/ljvI0XBpWb1UrPsuNmxVydG2Nzs6SSSdNtTpD8hmBdK84kma4yyrKrDTESC6wFbSfDwcUI797LshkwdFRHOeaq3BiXkQWVqhQAi4WnaeH1GivovtAw1lWPVHYre4zEcqWwmCmFJRrfnftYqksNVRSWVmDdDZerxWQzLqsGh4/5qHzIV9uH8bP3038Aw70aUvUAAA==", "encoding": "utf-8"}, "status": {"code": 200, "message": "OK"}}}], "recorded_with": "betamax/0.5.1"} \ No newline at end of file diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index de8b2f8121b..db4782cfd46 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -139,3 +139,228 @@ class TestAutomationSun(unittest.TestCase): fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + + def test_if_action_before(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_before_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + 'before_offset': '+1:00:00' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'after_offset': '+1:00:00' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_before_and_after_during(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015', + sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'before': 'sunset' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after_different_tz(self): + import pytz + + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '17:30:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunset', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + # Before + now = datetime(2015, 9, 16, 17, tzinfo=pytz.timezone('US/Mountain')) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + # After + now = datetime(2015, 9, 16, 18, tzinfo=pytz.timezone('US/Mountain')) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py new file mode 100644 index 00000000000..619fe929ac7 --- /dev/null +++ b/tests/components/device_tracker/test_locative.py @@ -0,0 +1,205 @@ +""" +tests.components.device_tracker.locative +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests the locative device tracker component. +""" + +import unittest +from unittest.mock import patch + +import requests + +from homeassistant import bootstrap, const +import homeassistant.components.device_tracker as device_tracker +import homeassistant.components.http as http + +from tests.common import get_test_home_assistant + +SERVER_PORT = 8126 +HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) + +hass = None + + +def _url(data={}): + """ Helper method to generate urls. """ + data = "&".join(["{}={}".format(name, value) for name, value in data.items()]) + return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data) + + +@patch('homeassistant.components.http.util.get_local_ip', + return_value='127.0.0.1') +def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name + """ Initalizes a Home Assistant server. """ + global hass + + hass = get_test_home_assistant() + + # Set up server + bootstrap.setup_component(hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_SERVER_PORT: SERVER_PORT + } + }) + + # Set up API + bootstrap.setup_component(hass, 'api') + + # Set up device tracker + bootstrap.setup_component(hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + 'platform': 'locative' + } + }) + + hass.start() + + +def tearDownModule(): # pylint: disable=invalid-name + """ Stops the Home Assistant server. """ + hass.stop() + +# Stub out update_config or else Travis CI raises an exception +@patch('homeassistant.components.device_tracker.update_config') +class TestLocative(unittest.TestCase): + """ Test Locative """ + + def test_missing_data(self, update_config): + data = { + 'latitude': 1.0, + 'longitude': 1.1, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # No data + req = requests.get(_url({})) + self.assertEqual(422, req.status_code) + + # No latitude + copy = data.copy() + del copy['latitude'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No device + copy = data.copy() + del copy['device'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No location + copy = data.copy() + del copy['id'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No trigger + copy = data.copy() + del copy['trigger'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # Test message + copy = data.copy() + copy['trigger'] = 'test' + req = requests.get(_url(copy)) + self.assertEqual(200, req.status_code) + + # Unknown trigger + copy = data.copy() + copy['trigger'] = 'foobar' + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + + def test_enter_and_exit(self, update_config): + """ Test when there is a known zone """ + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # Enter the Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'home') + + data['id'] = 'HOME' + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'not_home') + + data['id'] = 'hOmE' + data['trigger'] = 'enter' + + # Enter Home again + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'home') + + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'not_home') + + data['id'] = 'work' + data['trigger'] = 'enter' + + # Enter Work + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'work') + + + def test_exit_after_enter(self, update_config): + """ Test when an exit message comes after an enter message """ + + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # Enter Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'home') + + data['id'] = 'Work' + + # Enter Work + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'work') + + data['id'] = 'Home' + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'work') diff --git a/tests/components/media_player/__init__.py b/tests/components/media_player/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/test_media_player.py b/tests/components/media_player/test_init.py similarity index 100% rename from tests/components/test_media_player.py rename to tests/components/media_player/test_init.py diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py new file mode 100644 index 00000000000..eca863b935e --- /dev/null +++ b/tests/components/media_player/test_universal.py @@ -0,0 +1,342 @@ +""" +tests.component.media_player.test_universal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests universal media_player component. +""" +from copy import copy +import unittest + +import homeassistant.core as ha +from homeassistant.const import ( + STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED) +import homeassistant.components.switch as switch +import homeassistant.components.media_player as media_player +import homeassistant.components.media_player.universal as universal + + +class MockMediaPlayer(media_player.MediaPlayerDevice): + """ Mock media player for testing """ + + def __init__(self, hass, name): + self.hass = hass + self._name = name + self.entity_id = media_player.ENTITY_ID_FORMAT.format(name) + self._state = STATE_OFF + self._volume_level = 0 + self._is_volume_muted = False + self._media_title = None + self._supported_media_commands = 0 + + @property + def name(self): + """ name of player """ + return self._name + + @property + def state(self): + """ state of the player """ + return self._state + + @property + def volume_level(self): + """ volume level of player """ + return self._volume_level + + @property + def is_volume_muted(self): + """ if the media player is muted """ + return self._is_volume_muted + + @property + def supported_media_commands(self): + """ supported media commands flag """ + return self._supported_media_commands + + def turn_on(self): + """ mock turn_on function """ + self._state = STATE_UNKNOWN + + def turn_off(self): + """ mock turn_off function """ + self._state = STATE_OFF + + def mute_volume(self): + """ mock mute function """ + self._is_volume_muted = ~self._is_volume_muted + + def set_volume_level(self, volume): + """ mock set volume level """ + self._volume_level = volume + + def media_play(self): + """ mock play """ + self._state = STATE_PLAYING + + def media_pause(self): + """ mock pause """ + self._state = STATE_PAUSED + + +class TestMediaPlayer(unittest.TestCase): + """ Test the media_player module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + + self.mock_mp_1 = MockMediaPlayer(self.hass, 'mock1') + self.mock_mp_1.update_ha_state() + + self.mock_mp_2 = MockMediaPlayer(self.hass, 'mock2') + self.mock_mp_2.update_ha_state() + + self.mock_mute_switch_id = switch.ENTITY_ID_FORMAT.format('mute') + self.hass.states.set(self.mock_mute_switch_id, STATE_OFF) + + self.mock_state_switch_id = switch.ENTITY_ID_FORMAT.format('state') + self.hass.states.set(self.mock_state_switch_id, STATE_OFF) + + self.config_children_only = \ + {'name': 'test', 'platform': 'universal', + 'children': [media_player.ENTITY_ID_FORMAT.format('mock1'), + media_player.ENTITY_ID_FORMAT.format('mock2')]} + self.config_children_and_attr = \ + {'name': 'test', 'platform': 'universal', + 'children': [media_player.ENTITY_ID_FORMAT.format('mock1'), + media_player.ENTITY_ID_FORMAT.format('mock2')], + 'attributes': { + 'is_volume_muted': self.mock_mute_switch_id, + 'state': self.mock_state_switch_id}} + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_check_config_children_only(self): + """ Check config with only children """ + config_start = copy(self.config_children_only) + del config_start['platform'] + config_start['commands'] = {} + config_start['attributes'] = {} + + response = universal.validate_config(self.config_children_only) + + self.assertTrue(response) + self.assertEqual(config_start, self.config_children_only) + + def test_check_config_children_and_attr(self): + """ Check config with children and attributes """ + config_start = copy(self.config_children_and_attr) + del config_start['platform'] + config_start['commands'] = {} + + response = universal.validate_config(self.config_children_and_attr) + + self.assertTrue(response) + self.assertEqual(config_start, self.config_children_and_attr) + + def test_master_state(self): + """ test master state property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(None, ump.master_state) + + def test_master_state_with_attrs(self): + """ test master state property """ + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(STATE_OFF, ump.master_state) + + self.hass.states.set(self.mock_state_switch_id, STATE_ON) + + self.assertEqual(STATE_ON, ump.master_state) + + def test_master_state_with_bad_attrs(self): + """ test master state property """ + config = self.config_children_and_attr + config['attributes']['state'] = 'bad.entity_id' + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(STATE_OFF, ump.master_state) + + def test_active_child_state(self): + """ test active child state property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.assertEqual(None, ump._child_state) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + ump.update() + self.assertEqual(self.mock_mp_1.entity_id, + ump._child_state.entity_id) + + self.mock_mp_2._state = STATE_PLAYING + self.mock_mp_2.update_ha_state() + ump.update() + self.assertEqual(self.mock_mp_1.entity_id, + ump._child_state.entity_id) + + self.mock_mp_1._state = STATE_OFF + self.mock_mp_1.update_ha_state() + ump.update() + self.assertEqual(self.mock_mp_2.entity_id, + ump._child_state.entity_id) + + def test_name(self): + """ test name property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(config['name'], ump.name) + + def test_state_children_only(self): + """ test media player state with only children """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.assertTrue(ump.state, STATE_OFF) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + ump.update() + self.assertEqual(STATE_PLAYING, ump.state) + + def test_state_with_children_and_attrs(self): + """ test media player with children and master state """ + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.assertEqual(ump.state, STATE_OFF) + + self.hass.states.set(self.mock_state_switch_id, STATE_ON) + ump.update() + self.assertEqual(ump.state, STATE_ON) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + ump.update() + self.assertEqual(ump.state, STATE_PLAYING) + + self.hass.states.set(self.mock_state_switch_id, STATE_OFF) + ump.update() + self.assertEqual(ump.state, STATE_OFF) + + def test_volume_level(self): + """ test volume level property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.assertEqual(None, ump.volume_level) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + ump.update() + self.assertEqual(0, ump.volume_level) + + self.mock_mp_1._volume_level = 1 + self.mock_mp_1.update_ha_state() + ump.update() + self.assertEqual(1, ump.volume_level) + + def test_is_volume_muted_children_only(self): + """ test is volume muted property w/ children only """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.assertFalse(ump.is_volume_muted) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + ump.update() + self.assertFalse(ump.is_volume_muted) + + self.mock_mp_1._is_volume_muted = True + self.mock_mp_1.update_ha_state() + ump.update() + self.assertTrue(ump.is_volume_muted) + + def test_is_volume_muted_children_and_attr(self): + """ test is volume muted property w/ children and attrs """ + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertFalse(ump.is_volume_muted) + + self.hass.states.set(self.mock_mute_switch_id, STATE_ON) + self.assertTrue(ump.is_volume_muted) + + def test_supported_media_commands_children_only(self): + """ test supported media commands with only children """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.assertEqual(0, ump.supported_media_commands) + + self.mock_mp_1._supported_media_commands = 512 + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + ump.update() + self.assertEqual(512, ump.supported_media_commands) + + def test_supported_media_commands_children_and_cmds(self): + """ test supported media commands with children and attrs """ + config = self.config_children_and_attr + universal.validate_config(config) + config['commands']['turn_on'] = 'test' + config['commands']['turn_off'] = 'test' + config['commands']['volume_up'] = 'test' + config['commands']['volume_down'] = 'test' + config['commands']['volume_mute'] = 'test' + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update() + + self.mock_mp_1._supported_media_commands = \ + universal.SUPPORT_VOLUME_SET + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + ump.update() + + check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ + | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE + + self.assertEqual(check_flags, ump.supported_media_commands) diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py new file mode 100644 index 00000000000..780176dd1b8 --- /dev/null +++ b/tests/components/sensor/test_yr.py @@ -0,0 +1,79 @@ +""" +tests.components.sensor.test_yr +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests Yr sensor. +""" +from datetime import datetime +from unittest.mock import patch + +import pytest + +import homeassistant.core as ha +import homeassistant.components.sensor as sensor +import homeassistant.util.dt as dt_util + + +@pytest.mark.usefixtures('betamax_session') +class TestSensorYr: + """ Test the Yr sensor. """ + + def setup_method(self, method): + self.hass = ha.HomeAssistant() + self.hass.config.latitude = 32.87336 + self.hass.config.longitude = 117.22743 + + def teardown_method(self, method): + """ Stop down stuff we started. """ + self.hass.stop() + + def test_default_setup(self, betamax_session): + now = datetime(2016, 1, 5, 1, tzinfo=dt_util.UTC) + + with patch('homeassistant.components.sensor.yr.requests.Session', + return_value=betamax_session): + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'elevation': 0, + } + }) + + state = self.hass.states.get('sensor.yr_symbol') + + assert state.state.isnumeric() + assert state.attributes.get('unit_of_measurement') is None + + def test_custom_setup(self, betamax_session): + with patch('homeassistant.components.sensor.yr.requests.Session', + return_value=betamax_session): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'elevation': 0, + 'monitored_conditions': { + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed' + } + } + }) + + state = self.hass.states.get('sensor.yr_pressure') + assert 'hPa', state.attributes.get('unit_of_measurement') + + state = self.hass.states.get('sensor.yr_wind_direction') + assert '°', state.attributes.get('unit_of_measurement') + + state = self.hass.states.get('sensor.yr_humidity') + assert '%', state.attributes.get('unit_of_measurement') + + state = self.hass.states.get('sensor.yr_fog') + assert '%', state.attributes.get('unit_of_measurement') + + state = self.hass.states.get('sensor.yr_wind_speed') + assert 'm/s', state.attributes.get('unit_of_measurement') diff --git a/tests/components/switch/test_command_switch.py b/tests/components/switch/test_command_switch.py new file mode 100644 index 00000000000..3684f78fff4 --- /dev/null +++ b/tests/components/switch/test_command_switch.py @@ -0,0 +1,158 @@ +""" +tests.components.switch.test_command_switch +~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests command switch. +""" +import json +import os +import tempfile +import unittest + +from homeassistant import core +from homeassistant.const import STATE_ON, STATE_OFF +import homeassistant.components.switch as switch + + +class TestCommandSwitch(unittest.TestCase): + """ Test the command switch. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = core.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_state_none(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + + def test_state_value(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + 'value_template': '{{ value=="1" }}' + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + + def test_state_json_value(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + oncmd = json.dumps({'status': 'ok'}) + offcmd = json.dumps({'status': 'nope'}) + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo \'{}\' > {}'.format(oncmd, path), + 'offcmd': 'echo \'{}\' > {}'.format(offcmd, path), + 'value_template': '{{ value_json.status=="ok" }}' + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + def test_state_code(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index 741cfff4bb8..42acf5b3f62 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -27,12 +27,13 @@ API_URL = "http://127.0.0.1:{}{}".format(SERVER_PORT, alexa.API_ENDPOINT) HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD} hass = None +calls = [] @patch('homeassistant.components.http.util.get_local_ip', return_value='127.0.0.1') def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name - """ Initalizes a Home Assistant server. """ + """Initalize a Home Assistant server for testing this module.""" global hass hass = ha.HomeAssistant() @@ -42,6 +43,8 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, http.CONF_SERVER_PORT: SERVER_PORT}}) + hass.services.register('test', 'alexa', lambda call: calls.append(call)) + bootstrap.setup_component(hass, alexa.DOMAIN, { 'alexa': { 'intents': { @@ -61,7 +64,20 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name 'GetZodiacHoroscopeIntent': { 'speech': { 'type': 'plaintext', - 'text': 'You told us your sign is {{ ZodiacSign }}.' + 'text': 'You told us your sign is {{ ZodiacSign }}.', + } + }, + 'CallServiceIntent': { + 'speech': { + 'type': 'plaintext', + 'text': 'Service called', + }, + 'action': { + 'service': 'test.alexa', + 'data': { + 'hello': 1 + }, + 'entity_id': 'switch.test', } } } @@ -231,6 +247,39 @@ class TestAlexa(unittest.TestCase): text = req.json().get('response', {}).get('outputSpeech', {}).get('text') self.assertEqual('You are both home, you silly', text) + def test_intent_request_calling_service(self): + data = { + 'version': '1.0', + 'session': { + 'new': False, + 'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000', + 'application': { + 'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe' + }, + 'attributes': {}, + 'user': { + 'userId': 'amzn1.account.AM3B00000000000000000000000' + } + }, + 'request': { + 'type': 'IntentRequest', + 'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000', + 'timestamp': '2015-05-13T12:34:56Z', + 'intent': { + 'name': 'CallServiceIntent', + } + } + } + call_count = len(calls) + req = _req(data) + self.assertEqual(200, req.status_code) + self.assertEqual(call_count + 1, len(calls)) + call = calls[-1] + self.assertEqual('test', call.domain) + self.assertEqual('alexa', call.service) + self.assertEqual(['switch.test'], call.data.get('entity_id')) + self.assertEqual(1, call.data.get('hello')) + def test_session_ended_request(self): data = { 'version': '1.0', diff --git a/tests/components/test_history.py b/tests/components/test_history.py index fdd8270a661..f9e773c499a 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -5,11 +5,10 @@ tests.test_component_history Tests the history component. """ # pylint: disable=protected-access,too-many-public-methods -import time +from datetime import timedelta import os import unittest from unittest.mock import patch -from datetime import timedelta import homeassistant.core as ha import homeassistant.util.dt as dt_util @@ -25,21 +24,24 @@ class TestComponentHistory(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ Init needed objects. """ self.hass = get_test_home_assistant(1) - self.init_rec = False def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ self.hass.stop() - if self.init_rec: - recorder._INSTANCE.block_till_done() - os.remove(self.hass.config.path(recorder.DB_FILE)) + db_path = self.hass.config.path(recorder.DB_FILE) + if os.path.isfile(db_path): + os.remove(db_path) def init_recorder(self): recorder.setup(self.hass, {}) self.hass.start() + self.wait_recording_done() + + def wait_recording_done(self): + """ Block till recording is done. """ + self.hass.pool.block_till_done() recorder._INSTANCE.block_till_done() - self.init_rec = True def test_setup(self): """ Test setup method of history. """ @@ -56,12 +58,11 @@ class TestComponentHistory(unittest.TestCase): for i in range(7): self.hass.states.set(entity_id, "State {}".format(i)) + self.wait_recording_done() + if i > 1: states.append(self.hass.states.get(entity_id)) - self.hass.pool.block_till_done() - recorder._INSTANCE.block_till_done() - self.assertEqual( list(reversed(states)), history.last_5_states(entity_id)) @@ -70,22 +71,9 @@ class TestComponentHistory(unittest.TestCase): self.init_recorder() states = [] - for i in range(5): - state = ha.State( - 'test.point_in_time_{}'.format(i % 5), - "State {}".format(i), - {'attribute_test': i}) - - mock_state_change_event(self.hass, state) - self.hass.pool.block_till_done() - - states.append(state) - - recorder._INSTANCE.block_till_done() - - point = dt_util.utcnow() + timedelta(seconds=1) - - with patch('homeassistant.util.dt.utcnow', return_value=point): + now = dt_util.utcnow() + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=now): for i in range(5): state = ha.State( 'test.point_in_time_{}'.format(i % 5), @@ -93,16 +81,32 @@ class TestComponentHistory(unittest.TestCase): {'attribute_test': i}) mock_state_change_event(self.hass, state) - self.hass.pool.block_till_done() + + states.append(state) + + self.wait_recording_done() + + future = now + timedelta(seconds=1) + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=future): + for i in range(5): + state = ha.State( + 'test.point_in_time_{}'.format(i % 5), + "State {}".format(i), + {'attribute_test': i}) + + mock_state_change_event(self.hass, state) + + self.wait_recording_done() # Get states returns everything before POINT self.assertEqual(states, - sorted(history.get_states(point), + sorted(history.get_states(future), key=lambda state: state.entity_id)) # Test get_state here because we have a DB setup self.assertEqual( - states[0], history.get_state(point, states[0].entity_id)) + states[0], history.get_state(future, states[0].entity_id)) def test_state_changes_during_period(self): self.init_recorder() @@ -110,19 +114,20 @@ class TestComponentHistory(unittest.TestCase): def set_state(state): self.hass.states.set(entity_id, state) - self.hass.pool.block_till_done() - recorder._INSTANCE.block_till_done() - + self.wait_recording_done() return self.hass.states.get(entity_id) - set_state('idle') - set_state('YouTube') - start = dt_util.utcnow() point = start + timedelta(seconds=1) end = point + timedelta(seconds=1) - with patch('homeassistant.util.dt.utcnow', return_value=point): + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=start): + set_state('idle') + set_state('YouTube') + + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=point): states = [ set_state('idle'), set_state('Netflix'), @@ -130,10 +135,11 @@ class TestComponentHistory(unittest.TestCase): set_state('YouTube'), ] - with patch('homeassistant.util.dt.utcnow', return_value=end): + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=end): set_state('Netflix') set_state('Plex') - self.assertEqual( - {entity_id: states}, - history.state_changes_during_period(start, end, entity_id)) + hist = history.state_changes_during_period(start, end, entity_id) + + self.assertEqual(states, hist[entity_id]) diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 4ff334c1b1e..cb170a5c24b 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -6,20 +6,22 @@ Tests core compoments. """ # pylint: disable=protected-access,too-many-public-methods import unittest +from unittest.mock import patch import homeassistant.core as ha -import homeassistant.loader as loader from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF) import homeassistant.components as comps +from tests.common import get_test_home_assistant + class TestComponentsCore(unittest.TestCase): """ Tests homeassistant.components module. """ def setUp(self): # pylint: disable=invalid-name """ Init needed objects. """ - self.hass = ha.HomeAssistant() + self.hass = get_test_home_assistant() self.assertTrue(comps.setup(self.hass, {})) self.hass.states.set('light.Bowl', STATE_ON) @@ -58,3 +60,24 @@ class TestComponentsCore(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(runs)) + + @patch('homeassistant.core.ServiceRegistry.call') + def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): + self.hass.services.register('light', SERVICE_TURN_ON, lambda x: x) + + # We can't test if our service call results in services being called + # because by mocking out the call service method, we mock out all + # So we mimick how the service registry calls services + service_call = ha.ServiceCall('homeassistant', 'turn_on', { + 'entity_id': ['light.test', 'sensor.bla', 'light.bla'] + }) + self.hass.services._services['homeassistant']['turn_on'](service_call) + + self.assertEqual(2, mock_call.call_count) + self.assertEqual( + ('light', 'turn_on', {'entity_id': ['light.bla', 'light.test']}, + True), + mock_call.call_args_list[0][0]) + self.assertEqual( + ('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False), + mock_call.call_args_list[1][0]) diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py new file mode 100644 index 00000000000..5e1680ad2a4 --- /dev/null +++ b/tests/components/test_mqtt_eventstream.py @@ -0,0 +1,139 @@ +""" +tests.test_component_mqtt_eventstream +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests MQTT eventstream component. +""" +import json +import unittest +from unittest.mock import ANY, patch + +import homeassistant.components.mqtt_eventstream as eventstream +from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.core import State +from homeassistant.remote import JSONEncoder +import homeassistant.util.dt as dt_util + +from tests.common import ( + get_test_home_assistant, + mock_mqtt_component, + fire_mqtt_message, + mock_state_change_event, + fire_time_changed +) + + +class TestMqttEventStream(unittest.TestCase): + """ Test the MQTT eventstream module. """ + + def setUp(self): # pylint: disable=invalid-name + super(TestMqttEventStream, self).setUp() + self.hass = get_test_home_assistant() + self.mock_mqtt = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def add_eventstream(self, sub_topic=None, pub_topic=None): + """ Add a mqtt_eventstream component to the hass. """ + config = {} + if sub_topic: + config['subscribe_topic'] = sub_topic + if pub_topic: + config['publish_topic'] = pub_topic + return eventstream.setup(self.hass, {eventstream.DOMAIN: config}) + + def test_setup_succeeds(self): + self.assertTrue(self.add_eventstream()) + + def test_setup_with_pub(self): + # Should start off with no listeners for all events + self.assertEqual(self.hass.bus.listeners.get('*'), None) + + self.assertTrue(self.add_eventstream(pub_topic='bar')) + self.hass.pool.block_till_done() + + # Verify that the event handler has been added as a listener + self.assertEqual(self.hass.bus.listeners.get('*'), 1) + + @patch('homeassistant.components.mqtt.subscribe') + def test_subscribe(self, mock_sub): + sub_topic = 'foo' + self.assertTrue(self.add_eventstream(sub_topic=sub_topic)) + self.hass.pool.block_till_done() + + # Verify that the this entity was subscribed to the topic + mock_sub.assert_called_with(self.hass, sub_topic, ANY) + + @patch('homeassistant.components.mqtt.publish') + @patch('homeassistant.core.dt_util.datetime_to_str') + def test_state_changed_event_sends_message(self, mock_datetime, mock_pub): + now = '00:19:19 11-01-2016' + e_id = 'fake.entity' + pub_topic = 'bar' + mock_datetime.return_value = now + + # Add the eventstream component for publishing events + self.assertTrue(self.add_eventstream(pub_topic=pub_topic)) + self.hass.pool.block_till_done() + + # Reset the mock because it will have already gotten calls for the + # mqtt_eventstream state change on initialization, etc. + mock_pub.reset_mock() + + # Set a state of an entity + mock_state_change_event(self.hass, State(e_id, 'on')) + self.hass.pool.block_till_done() + + # The order of the JSON is indeterminate, + # so first just check that publish was called + mock_pub.assert_called_with(self.hass, pub_topic, ANY) + self.assertTrue(mock_pub.called) + + # Get the actual call to publish and make sure it was the one + # we were looking for + msg = mock_pub.call_args[0][2] + event = {} + event['event_type'] = EVENT_STATE_CHANGED + new_state = { + "last_updated": now, + "state": "on", + "entity_id": e_id, + "attributes": {}, + "last_changed": now + } + event['event_data'] = {"new_state": new_state, "entity_id": e_id} + + # Verify that the message received was that expected + self.assertEqual(json.loads(msg), event) + + @patch('homeassistant.components.mqtt.publish') + def test_time_event_does_not_send_message(self, mock_pub): + self.assertTrue(self.add_eventstream(pub_topic='bar')) + self.hass.pool.block_till_done() + + # Reset the mock because it will have already gotten calls for the + # mqtt_eventstream state change on initialization, etc. + mock_pub.reset_mock() + + fire_time_changed(self.hass, dt_util.utcnow()) + self.assertFalse(mock_pub.called) + + def test_receiving_remote_event_fires_hass_event(self): + sub_topic = 'foo' + self.assertTrue(self.add_eventstream(sub_topic=sub_topic)) + self.hass.pool.block_till_done() + + calls = [] + self.hass.bus.listen_once('test_event', lambda _: calls.append(1)) + self.hass.pool.block_till_done() + + payload = json.dumps( + {'event_type': 'test_event', 'event_data': {}}, + cls=JSONEncoder + ) + fire_mqtt_message(self.hass, sub_topic, payload) + self.hass.pool.block_till_done() + + self.assertEqual(1, len(calls)) diff --git a/tests/components/test_scene.py b/tests/components/test_scene.py index 2fc8fe085c2..0f6663354dd 100644 --- a/tests/components/test_scene.py +++ b/tests/components/test_scene.py @@ -32,6 +32,60 @@ class TestScene(unittest.TestCase): 'scene': [[]] })) + def test_config_yaml_alias_anchor(self): + """ + Tests the usage of YAML aliases and anchors. The following test scene + configuration is equivalent to: + + scene: + - name: test + entities: + light_1: &light_1_state + state: 'on' + brightness: 100 + light_2: *light_1_state + + When encountering a YAML alias/anchor, the PyYAML parser will use a + reference to the original dictionary, instead of creating a copy, so + care needs to be taken to not modify the original. + """ + test_light = loader.get_component('light.test') + test_light.init() + + self.assertTrue(light.setup(self.hass, { + light.DOMAIN: {'platform': 'test'} + })) + + light_1, light_2 = test_light.DEVICES[0:2] + + light.turn_off(self.hass, [light_1.entity_id, light_2.entity_id]) + + self.hass.pool.block_till_done() + + entity_state = { + 'state': 'on', + 'brightness': 100, + } + self.assertTrue(scene.setup(self.hass, { + 'scene': [{ + 'name': 'test', + 'entities': { + light_1.entity_id: entity_state, + light_2.entity_id: entity_state, + } + }] + })) + + scene.activate(self.hass, 'scene.test') + self.hass.pool.block_till_done() + + self.assertTrue(light_1.is_on) + self.assertTrue(light_2.is_on) + self.assertEqual(100, + light_1.last_call('turn_on')[1].get('brightness')) + self.assertEqual(100, + light_2.last_call('turn_on')[1].get('brightness')) + def test_activate_scene(self): test_light = loader.get_component('light.test') test_light.init() diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py new file mode 100644 index 00000000000..aa2cab07d0d --- /dev/null +++ b/tests/helpers/test_service.py @@ -0,0 +1,68 @@ +""" +tests.helpers.test_service +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Test service helpers. +""" +import unittest +from unittest.mock import patch + +from homeassistant.const import SERVICE_TURN_ON +from homeassistant.helpers import service + +from tests.common import get_test_home_assistant, mock_service + + +class TestServiceHelpers(unittest.TestCase): + """ + Tests the Home Assistant service helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = get_test_home_assistant() + self.calls = mock_service(self.hass, 'test_domain', 'test_service') + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_split_entity_string(self): + service.call_from_config(self.hass, { + 'service': 'test_domain.test_service', + 'entity_id': 'hello.world, sensor.beer' + }) + self.hass.pool.block_till_done() + self.assertEqual(['hello.world', 'sensor.beer'], + self.calls[-1].data.get('entity_id')) + + def test_not_mutate_input(self): + orig = { + 'service': 'test_domain.test_service', + 'entity_id': 'hello.world, sensor.beer', + 'data': { + 'hello': 1, + }, + } + service.call_from_config(self.hass, orig) + self.hass.pool.block_till_done() + self.assertEqual({ + 'service': 'test_domain.test_service', + 'entity_id': 'hello.world, sensor.beer', + 'data': { + 'hello': 1, + }, + }, orig) + + @patch('homeassistant.helpers.service._LOGGER.error') + def test_fail_silently_if_no_service(self, mock_log): + service.call_from_config(self.hass, None) + self.assertEqual(1, mock_log.call_count) + + service.call_from_config(self.hass, {}) + self.assertEqual(2, mock_log.call_count) + + service.call_from_config(self.hass, { + 'service': 'invalid' + }) + self.assertEqual(3, mock_log.call_count) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py new file mode 100644 index 00000000000..f4e28330f7a --- /dev/null +++ b/tests/helpers/test_state.py @@ -0,0 +1,148 @@ +""" +tests.helpers.test_state +~~~~~~~~~~~~~~~~~~~~~~~~ + +Test state helpers. +""" +from datetime import timedelta +import unittest +from unittest.mock import patch + +import homeassistant.core as ha +import homeassistant.components as core_components +from homeassistant.const import SERVICE_TURN_ON +from homeassistant.util import dt as dt_util +from homeassistant.helpers import state + +from tests.common import get_test_home_assistant, mock_service + + +class TestStateHelpers(unittest.TestCase): + """ + Tests the Home Assistant event helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = get_test_home_assistant() + core_components.setup(self.hass, {}) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_get_changed_since(self): + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + with patch('homeassistant.core.dt_util.utcnow', return_value=point1): + self.hass.states.set('light.test', 'on') + state1 = self.hass.states.get('light.test') + + with patch('homeassistant.core.dt_util.utcnow', return_value=point2): + self.hass.states.set('light.test2', 'on') + state2 = self.hass.states.get('light.test2') + + with patch('homeassistant.core.dt_util.utcnow', return_value=point3): + self.hass.states.set('light.test3', 'on') + state3 = self.hass.states.get('light.test3') + + self.assertEqual( + [state2, state3], + state.get_changed_since([state1, state2, state3], point2)) + + def test_track_states(self): + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: + mock_utcnow.return_value = point2 + + with state.TrackStates(self.hass) as states: + mock_utcnow.return_value = point1 + self.hass.states.set('light.test', 'on') + + mock_utcnow.return_value = point2 + self.hass.states.set('light.test2', 'on') + state2 = self.hass.states.get('light.test2') + + mock_utcnow.return_value = point3 + self.hass.states.set('light.test3', 'on') + state3 = self.hass.states.get('light.test3') + + self.assertEqual( + sorted([state2, state3], key=lambda state: state.entity_id), + sorted(states, key=lambda state: state.entity_id)) + + def test_reproduce_state_with_turn_on(self): + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test', 'off') + + state.reproduce_state(self.hass, ha.State('light.test', 'on')) + + self.hass.pool.block_till_done() + + self.assertTrue(len(calls) > 0) + last_call = calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test'], last_call.data.get('entity_id')) + + def test_reproduce_state_with_complex_service_data(self): + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test', 'off') + + complex_data = ['hello', {'11': '22'}] + + state.reproduce_state(self.hass, ha.State('light.test', 'on', { + 'complex': complex_data + })) + + self.hass.pool.block_till_done() + + self.assertTrue(len(calls) > 0) + last_call = calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(complex_data, last_call.data.get('complex')) + + def test_reproduce_state_with_group(self): + light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('group.test', 'off', { + 'entity_id': ['light.test1', 'light.test2']}) + + state.reproduce_state(self.hass, ha.State('group.test', 'on')) + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(light_calls)) + last_call = light_calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test1', 'light.test2'], + last_call.data.get('entity_id')) + + def test_reproduce_state_group_states_with_same_domain_and_data(self): + light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test1', 'off') + self.hass.states.set('light.test2', 'off') + + state.reproduce_state(self.hass, [ + ha.State('light.test1', 'on', {'brightness': 95}), + ha.State('light.test2', 'on', {'brightness': 95})]) + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(light_calls)) + last_call = light_calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test1', 'light.test2'], + last_call.data.get('entity_id')) + self.assertEqual(95, last_call.data.get('brightness')) diff --git a/tests/test_core.py b/tests/test_core.py index fee46fe2dd4..ca935e2d106 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -321,6 +321,18 @@ class TestStateMachine(unittest.TestCase): self.assertFalse(self.states.is_state('light.Bowl', 'off')) self.assertFalse(self.states.is_state('light.Non_existing', 'on')) + def test_is_state_attr(self): + """ Test is_state_attr method. """ + self.states.set("light.Bowl", "on", {"brightness": 100}) + self.assertTrue( + self.states.is_state_attr('light.Bowl', 'brightness', 100)) + self.assertFalse( + self.states.is_state_attr('light.Bowl', 'friendly_name', 200)) + self.assertFalse( + self.states.is_state_attr('light.Bowl', 'friendly_name', 'Bowl')) + self.assertFalse( + self.states.is_state_attr('light.Non_existing', 'brightness', 100)) + def test_entity_ids(self): """ Test get_entity_ids method. """ ent_ids = self.states.entity_ids() diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 1ecd7d5b894..844826f80d5 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -117,6 +117,14 @@ class TestUtilTemplate(unittest.TestCase): self.hass, '{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}')) + def test_is_state_attr(self): + self.hass.states.set('test.object', 'available', {'mode': 'on'}) + self.assertEqual( + 'yes', + template.render( + self.hass, + '{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}')) + def test_states_function(self): self.hass.states.set('test.object', 'available') self.assertEqual(