diff --git a/.coveragerc b/.coveragerc index e8666da60f4466..51d6d81179f8f5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,6 +20,9 @@ omit = homeassistant/components/ecobee.py homeassistant/components/*/ecobee.py + homeassistant/components/envisalink.py + homeassistant/components/*/envisalink.py + homeassistant/components/insteon_hub.py homeassistant/components/*/insteon_hub.py @@ -81,6 +84,9 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py + homeassistant/components/homematic.py + homeassistant/components/*/homematic.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/binary_sensor/arest.py @@ -111,6 +117,8 @@ omit = homeassistant/components/downloader.py homeassistant/components/feedreader.py homeassistant/components/garage_door/wink.py + homeassistant/components/garage_door/rpi_gpio.py + homeassistant/components/hdmi_cec.py homeassistant/components/ifttt.py homeassistant/components/keyboard.py homeassistant/components/light/blinksticklight.py @@ -120,7 +128,9 @@ omit = homeassistant/components/light/limitlessled.py homeassistant/components/light/osramlightify.py homeassistant/components/lirc.py + homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py @@ -170,6 +180,7 @@ omit = homeassistant/components/sensor/efergy.py homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/fitbit.py + homeassistant/components/sensor/fixer.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/google_travel_time.py @@ -180,6 +191,7 @@ omit = homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/onewire.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/openexchangerates.py homeassistant/components/sensor/plex.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/sabnzbd.py diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 0f6318a063834d..e7483740bc34ba 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -22,6 +22,9 @@ http: # Set to 1 to enable development mode # development: 1 +frontend: +# enable the frontend + light: # platform: hue diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 99382bebe74431..8b3d3ee6f239d3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -3,7 +3,6 @@ import logging import logging.handlers import os -import shutil import sys from collections import defaultdict from threading import RLock @@ -11,22 +10,16 @@ import voluptuous as vol import homeassistant.components as core_components -import homeassistant.components.group as group -import homeassistant.config as config_util +from homeassistant.components import group, persistent_notification +import homeassistant.config as conf_util import homeassistant.core as core import homeassistant.helpers.config_validation as cv import homeassistant.loader as loader -import homeassistant.util.dt as date_util -import homeassistant.util.location as loc_util import homeassistant.util.package as pkg_util -from homeassistant.const import ( - CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED, - TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__) +from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( event_decorators, service, config_per_platform, extract_domain_configs) -from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _SETUP_LOCK = RLock() @@ -208,11 +201,6 @@ def prepare_setup_platform(hass, config, domain, platform_name): return platform -def mount_local_lib_path(config_dir): - """Add local library to Python Path.""" - sys.path.insert(0, os.path.join(config_dir, 'deps')) - - # pylint: disable=too-many-branches, too-many-statements, too-many-arguments def from_config_dict(config, hass=None, config_dir=None, enable_log=True, verbose=False, skip_pip=False, @@ -226,18 +214,17 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, if config_dir is not None: config_dir = os.path.abspath(config_dir) hass.config.config_dir = config_dir - mount_local_lib_path(config_dir) + _mount_local_lib_path(config_dir) core_config = config.get(core.DOMAIN, {}) try: - process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA( - core_config)) - except vol.MultipleInvalid as ex: + conf_util.process_ha_core_config(hass, core_config) + except vol.Invalid as ex: cv.log_exception(_LOGGER, ex, 'homeassistant', core_config) return None - process_ha_config_upgrade(hass) + conf_util.process_ha_config_upgrade(hass) if enable_log: enable_logging(hass, verbose, log_rotate_days) @@ -262,9 +249,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, if not core_components.setup(hass, config): _LOGGER.error('Home Assistant core failed to initialize. ' 'Further initialization aborted.') - return hass + persistent_notification.setup(hass, config) + _LOGGER.info('Home Assistant core initialized') # Give event decorators access to HASS @@ -291,12 +279,12 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True, # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - mount_local_lib_path(config_dir) + _mount_local_lib_path(config_dir) enable_logging(hass, verbose, log_rotate_days) try: - config_dict = config_util.load_yaml_config_file(config_path) + config_dict = conf_util.load_yaml_config_file(config_path) except HomeAssistantError: return None @@ -355,101 +343,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None): 'Unable to setup error log %s (access denied)', err_log_path) -def process_ha_config_upgrade(hass): - """Upgrade config if necessary.""" - version_path = hass.config.path('.HA_VERSION') - - try: - with open(version_path, 'rt') as inp: - conf_version = inp.readline().strip() - except FileNotFoundError: - # Last version to not have this file - conf_version = '0.7.7' - - if conf_version == __version__: - return - - _LOGGER.info('Upgrading config directory from %s to %s', conf_version, - __version__) - - # This was where dependencies were installed before v0.18 - # Probably should keep this around until ~v0.20. - lib_path = hass.config.path('lib') - if os.path.isdir(lib_path): - shutil.rmtree(lib_path) - - lib_path = hass.config.path('deps') - if os.path.isdir(lib_path): - shutil.rmtree(lib_path) - - with open(version_path, 'wt') as outp: - outp.write(__version__) - - -def process_ha_core_config(hass, config): - """Process the [homeassistant] section from the config.""" - hac = hass.config - - def set_time_zone(time_zone_str): - """Helper method to set time zone.""" - if time_zone_str is None: - return - - time_zone = date_util.get_time_zone(time_zone_str) - - if time_zone: - hac.time_zone = time_zone - date_util.set_default_time_zone(time_zone) - else: - _LOGGER.error('Received invalid time zone %s', time_zone_str) - - for key, attr in ((CONF_LATITUDE, 'latitude'), - (CONF_LONGITUDE, 'longitude'), - (CONF_NAME, 'location_name')): - if key in config: - setattr(hac, attr, config[key]) - - if CONF_TIME_ZONE in config: - set_time_zone(config.get(CONF_TIME_ZONE)) - - for entity_id, attrs in config.get(CONF_CUSTOMIZE).items(): - Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) - - if CONF_TEMPERATURE_UNIT in config: - hac.temperature_unit = config[CONF_TEMPERATURE_UNIT] - - # If we miss some of the needed values, auto detect them - if None not in ( - hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone): - return - - _LOGGER.warning('Incomplete core config. Auto detecting location and ' - 'temperature unit') - - info = loc_util.detect_location_info() - - if info is None: - _LOGGER.error('Could not detect location information') - return - - if hac.latitude is None and hac.longitude is None: - hac.latitude = info.latitude - hac.longitude = info.longitude - - if hac.temperature_unit is None: - if info.use_fahrenheit: - hac.temperature_unit = TEMP_FAHRENHEIT - else: - hac.temperature_unit = TEMP_CELSIUS - - if hac.location_name is None: - hac.location_name = info.city - - if hac.time_zone is None: - set_time_zone(info.time_zone) - - def _ensure_loader_prepared(hass): """Ensure Home Assistant loader is prepared.""" if not loader.PREPARED: loader.prepare(hass) + + +def _mount_local_lib_path(config_dir): + """Add local library to Python Path.""" + sys.path.insert(0, os.path.join(config_dir, 'deps')) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index f2696bbbd1ac9e..38780ed9b28a03 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -19,6 +19,8 @@ _LOGGER = logging.getLogger(__name__) +SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' + def is_on(hass, entity_id=None): """Load up the module to call the is_on method. @@ -73,6 +75,11 @@ def toggle(hass, entity_id=None, **service_data): hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) +def reload_core_config(hass): + """Reload the core config.""" + hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) + + def setup(hass, config): """Setup general services related to Home Assistant.""" def handle_turn_service(service): @@ -111,4 +118,21 @@ def handle_turn_service(service): hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) + def handle_reload_config(call): + """Service handler for reloading core config.""" + from homeassistant.exceptions import HomeAssistantError + from homeassistant import config as conf_util + + try: + path = conf_util.find_config_file(hass.config.config_dir) + conf = conf_util.load_yaml_config_file(path) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) + + hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, + handle_reload_config) + return True diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py new file mode 100644 index 00000000000000..ebd54da15587c9 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -0,0 +1,105 @@ +""" +Support for Envisalink-based alarm control panels (Honeywell/DSC). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.envisalink/ +""" +import logging +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.envisalink import (EVL_CONTROLLER, + EnvisalinkDevice, + PARTITION_SCHEMA, + CONF_CODE, + CONF_PARTITIONNAME, + SIGNAL_PARTITION_UPDATE, + SIGNAL_KEYPAD_UPDATE) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN, STATE_ALARM_TRIGGERED) + +DEPENDENCIES = ['envisalink'] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Perform the setup for Envisalink alarm panels.""" + _configured_partitions = discovery_info['partitions'] + _code = discovery_info[CONF_CODE] + for part_num in _configured_partitions: + _device_config_data = PARTITION_SCHEMA( + _configured_partitions[part_num]) + _device = EnvisalinkAlarm( + part_num, + _device_config_data[CONF_PARTITIONNAME], + _code, + EVL_CONTROLLER.alarm_state['partition'][part_num], + EVL_CONTROLLER) + add_devices_callback([_device]) + + return True + + +class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): + """Represents the Envisalink-based alarm panel.""" + + # pylint: disable=too-many-arguments + def __init__(self, partition_number, alarm_name, code, info, controller): + """Initialize the alarm panel.""" + from pydispatch import dispatcher + self._partition_number = partition_number + self._code = code + _LOGGER.debug('Setting up alarm: ' + alarm_name) + EnvisalinkDevice.__init__(self, alarm_name, info, controller) + dispatcher.connect(self._update_callback, + signal=SIGNAL_PARTITION_UPDATE, + sender=dispatcher.Any) + dispatcher.connect(self._update_callback, + signal=SIGNAL_KEYPAD_UPDATE, + sender=dispatcher.Any) + + def _update_callback(self, partition): + """Update HA state, if needed.""" + if partition is None or int(partition) == self._partition_number: + self.update_ha_state() + + @property + def code_format(self): + """The characters if code is defined.""" + return self._code + + @property + def state(self): + """Return the state of the device.""" + if self._info['status']['alarm']: + return STATE_ALARM_TRIGGERED + elif self._info['status']['armed_away']: + return STATE_ALARM_ARMED_AWAY + elif self._info['status']['armed_stay']: + return STATE_ALARM_ARMED_HOME + elif self._info['status']['alpha']: + return STATE_ALARM_DISARMED + else: + return STATE_UNKNOWN + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if self._code: + EVL_CONTROLLER.disarm_partition(str(code), + self._partition_number) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + if self._code: + EVL_CONTROLLER.arm_stay_partition(str(code), + self._partition_number) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + if self._code: + EVL_CONTROLLER.arm_away_partition(str(code), + self._partition_number) + + def alarm_trigger(self, code=None): + """Alarm trigger command. Not possible for us.""" + raise NotImplementedError() diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ad8f21f069bd60..f0073bad838c24 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -6,7 +6,7 @@ """ import json import logging -from time import time +import queue import homeassistant.core as ha import homeassistant.remote as rem @@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView): def get(self, request): """Provide a streaming interface for the event bus.""" - from eventlet.queue import LightQueue, Empty - import eventlet - - cur_hub = eventlet.hubs.get_hub() - request.environ['eventlet.minimum_write_chunk_size'] = 0 - to_write = LightQueue() stop_obj = object() + to_write = queue.Queue() restrict = request.args.get('restrict') if restrict: - restrict = restrict.split(',') + restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] - def thread_forward_events(event): + def forward_events(event): """Forward events to the open request.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -99,28 +94,20 @@ def thread_forward_events(event): else: data = json.dumps(event, cls=rem.JSONEncoder) - cur_hub.schedule_call_global(0, lambda: to_write.put(data)) + to_write.put(data) def stream(): """Stream events to response.""" - self.hass.bus.listen(MATCH_ALL, thread_forward_events) + self.hass.bus.listen(MATCH_ALL, forward_events) _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) - last_msg = time() # Fire off one message right away to have browsers fire open event to_write.put(STREAM_PING_PAYLOAD) while True: try: - # Somehow our queue.get sometimes takes too long to - # be notified of arrival of data. Probably - # because of our spawning on hub in other thread - # hack. Because current goal is to get this out, - # We just timeout every second because it will - # return right away if qsize() > 0. - # So yes, we're basically polling :( - payload = to_write.get(timeout=1) + payload = to_write.get(timeout=STREAM_PING_INTERVAL) if payload is stop_obj: break @@ -129,15 +116,13 @@ def stream(): _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), msg.strip()) yield msg.encode("UTF-8") - last_msg = time() - except Empty: - if time() - last_msg > 50: - to_write.put(STREAM_PING_PAYLOAD) + except queue.Empty: + to_write.put(STREAM_PING_PAYLOAD) except GeneratorExit: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) break - self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events) + _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + self.hass.bus.remove_listener(MATCH_ALL, forward_events) return self.Response(stream(), mimetype='text/event-stream') @@ -204,11 +189,12 @@ def post(self, request, entity_id): return self.json_message('No state specified', HTTP_BAD_REQUEST) attributes = request.json.get('attributes') + force_update = request.json.get('force_update', False) is_new_state = self.hass.states.get(entity_id) is None # Write state - self.hass.states.set(entity_id, new_state, attributes) + self.hass.states.set(entity_id, new_state, attributes, force_update) # Read the state back for our response resp = self.json(self.hass.states.get(entity_id)) diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py new file mode 100644 index 00000000000000..144de83aa53f70 --- /dev/null +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -0,0 +1,71 @@ +""" +Support for Envisalink zone states- represented as binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.envisalink/ +""" +import logging +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.envisalink import (EVL_CONTROLLER, + ZONE_SCHEMA, + CONF_ZONENAME, + CONF_ZONETYPE, + EnvisalinkDevice, + SIGNAL_ZONE_UPDATE) +from homeassistant.const import ATTR_LAST_TRIP_TIME + +DEPENDENCIES = ['envisalink'] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Perform the setup for Envisalink sensor devices.""" + _configured_zones = discovery_info['zones'] + for zone_num in _configured_zones: + _device_config_data = ZONE_SCHEMA(_configured_zones[zone_num]) + _device = EnvisalinkBinarySensor( + zone_num, + _device_config_data[CONF_ZONENAME], + _device_config_data[CONF_ZONETYPE], + EVL_CONTROLLER.alarm_state['zone'][zone_num], + EVL_CONTROLLER) + add_devices_callback([_device]) + + +class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): + """Representation of an envisalink Binary Sensor.""" + + # pylint: disable=too-many-arguments + def __init__(self, zone_number, zone_name, zone_type, info, controller): + """Initialize the binary_sensor.""" + from pydispatch import dispatcher + self._zone_type = zone_type + self._zone_number = zone_number + + _LOGGER.debug('Setting up zone: ' + zone_name) + EnvisalinkDevice.__init__(self, zone_name, info, controller) + dispatcher.connect(self._update_callback, + signal=SIGNAL_ZONE_UPDATE, + sender=dispatcher.Any) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault'] + return attr + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._info['status']['open'] + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return self._zone_type + + def _update_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self.update_ha_state() diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py new file mode 100644 index 00000000000000..8e874079ee60cf --- /dev/null +++ b/homeassistant/components/binary_sensor/homematic.py @@ -0,0 +1,100 @@ +""" +Support for Homematic binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.homematic/ +""" +import logging +from homeassistant.const import STATE_UNKNOWN +from homeassistant.components.binary_sensor import BinarySensorDevice +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + +SENSOR_TYPES_CLASS = { + "Remote": None, + "ShutterContact": "opening", + "Smoke": "smoke", + "SmokeV2": "smoke", + "Motion": "motion", + "MotionV2": "motion", + "RemoteMotion": None +} + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the Homematic binary sensor platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, + discovery_info, + add_callback_devices) + + +class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): + """Representation of a binary Homematic device.""" + + @property + def is_on(self): + """Return true if switch is on.""" + if not self.available: + return False + return bool(self._hm_get_state()) + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + if not self.available: + return None + + # If state is MOTION (RemoteMotion works only) + if self._state == "MOTION": + return "motion" + return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.sensors import HMBinarySensor\ + as pyHMBinarySensor + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # check if the Homematic device correct for this HA device + if not isinstance(self._hmdevice, pyHMBinarySensor): + _LOGGER.critical("This %s can't be use as binary", self._name) + return False + + # if exists user value? + if self._state and self._state not in self._hmdevice.BINARYNODE: + _LOGGER.critical("This %s have no binary with %s", self._name, + self._state) + return False + + # only check and give a warning to the user + if self._state is None and len(self._hmdevice.BINARYNODE) > 1: + _LOGGER.critical("%s have multiple binary params. It use all " + "binary nodes as one. Possible param values: %s", + self._name, str(self._hmdevice.BINARYNODE)) + return False + + return True + + def _init_data_struct(self): + """Generate a data struct (self._data) from the Homematic metadata.""" + super()._init_data_struct() + + # object have 1 binary + if self._state is None and len(self._hmdevice.BINARYNODE) == 1: + for value in self._hmdevice.BINARYNODE: + self._state = value + + # add state to data struct + if self._state: + _LOGGER.debug("%s init datastruct with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index d9c2b7d577a367..9ec85e63503d7e 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -7,10 +7,12 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL +from homeassistant.components.sensor.wink import WinkDevice +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.entity import Entity +from homeassistant.loader import get_component -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { @@ -41,14 +43,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([WinkBinarySensorDevice(sensor)]) -class WinkBinarySensorDevice(BinarySensorDevice, Entity): +class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): """Representation of a Wink sensor.""" def __init__(self, wink): """Initialize the Wink binary sensor.""" - self.wink = wink + super().__init__(wink) + wink = get_component('wink') self._unit_of_measurement = self.wink.UNIT - self._battery = self.wink.battery_level self.capability = self.wink.capability() @property @@ -67,35 +69,3 @@ def is_on(self): def sensor_class(self): """Return the class of this sensor, from SENSOR_CLASSES.""" return SENSOR_TYPES.get(self.capability) - - @property - def unique_id(self): - """Return the ID of this wink sensor.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the sensor if any.""" - return self.wink.name() - - @property - def available(self): - """True if connection == True.""" - return self.wink.available - - def update(self): - """Update state of the sensor.""" - self.wink.update_state() - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 873425289874b8..2f23118a1c3006 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/camera/ """ import logging +import time from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -81,8 +82,6 @@ def camera_image(self): def mjpeg_stream(self, response): """Generate an HTTP MJPEG stream from camera images.""" - import eventlet - def stream(): """Stream images as mjpeg stream.""" try: @@ -99,7 +98,7 @@ def stream(): last_image = img_bytes - eventlet.sleep(0.5) + time.sleep(0.5) except GeneratorExit: pass diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py index cda48d1ddfa147..ee67d097286501 100644 --- a/homeassistant/components/camera/rpi_camera.py +++ b/homeassistant/components/camera/rpi_camera.py @@ -1,5 +1,9 @@ -"""Camera platform that has a Raspberry Pi camera.""" +""" +Camera platform that has a Raspberry Pi camera. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.rpi_camera/ +""" import os import subprocess import logging @@ -43,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RaspberryCamera(Camera): - """Raspberry Pi camera.""" + """Representation of a Raspberry Pi camera.""" def __init__(self, device_info): """Initialize Raspberry Pi camera component.""" diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 148c57a12c3453..f083a96f5b2bd0 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -37,6 +37,7 @@ def setup(hass, config): """Setup a demo environment.""" group = loader.get_component('group') configurator = loader.get_component('configurator') + persistent_notification = loader.get_component('persistent_notification') config.setdefault(ha.DOMAIN, {}) config.setdefault(DOMAIN, {}) @@ -59,6 +60,11 @@ def setup(hass, config): demo_config[component] = {CONF_PLATFORM: 'demo'} bootstrap.setup_component(hass, component, demo_config) + # Setup example persistent notification + persistent_notification.create( + hass, 'This is an example of a persistent notification.', + title='Example Notification') + # Setup room groups lights = sorted(hass.states.entity_ids('light')) switches = sorted(hass.states.entity_ids('switch')) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 282ae46ba8592e..725a49308be0c6 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -83,8 +83,8 @@ def __init__(self, config): """Initialize the scanner.""" self.host = config[CONF_HOST] self.username = str(config[CONF_USERNAME]) - self.password = str(config.get(CONF_PASSWORD)) - self.pub_key = str(config.get('pub_key')) + self.password = str(config.get(CONF_PASSWORD, "")) + self.pub_key = str(config.get('pub_key', "")) self.protocol = config.get('protocol') self.mode = config.get('mode') diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py new file mode 100644 index 00000000000000..23f9acef12f45e --- /dev/null +++ b/homeassistant/components/envisalink.py @@ -0,0 +1,210 @@ +""" +Support for Envisalink devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/envisalink/ +""" +import logging +import time +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity +from homeassistant.components.discovery import load_platform + +REQUIREMENTS = ['pyenvisalink==1.0', 'pydispatcher==2.0.5'] + +_LOGGER = logging.getLogger(__name__) +DOMAIN = 'envisalink' + +EVL_CONTROLLER = None + +CONF_EVL_HOST = 'host' +CONF_EVL_PORT = 'port' +CONF_PANEL_TYPE = 'panel_type' +CONF_EVL_VERSION = 'evl_version' +CONF_CODE = 'code' +CONF_USERNAME = 'user_name' +CONF_PASS = 'password' +CONF_EVL_KEEPALIVE = 'keepalive_interval' +CONF_ZONEDUMP_INTERVAL = 'zonedump_interval' +CONF_ZONES = 'zones' +CONF_PARTITIONS = 'partitions' + +CONF_ZONENAME = 'name' +CONF_ZONETYPE = 'type' +CONF_PARTITIONNAME = 'name' + +DEFAULT_PORT = 4025 +DEFAULT_EVL_VERSION = 3 +DEFAULT_KEEPALIVE = 60 +DEFAULT_ZONEDUMP_INTERVAL = 30 +DEFAULT_ZONETYPE = 'opening' + +SIGNAL_ZONE_UPDATE = 'zones_updated' +SIGNAL_PARTITION_UPDATE = 'partition_updated' +SIGNAL_KEYPAD_UPDATE = 'keypad_updated' + +ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONENAME): cv.string, + vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string}) + +PARTITION_SCHEMA = vol.Schema({ + vol.Required(CONF_PARTITIONNAME): cv.string}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_EVL_HOST): cv.string, + vol.Required(CONF_PANEL_TYPE): + vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASS): cv.string, + vol.Required(CONF_CODE): cv.string, + vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, + vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA}, + vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION): + vol.All(vol.Coerce(int), vol.Range(min=3, max=4)), + vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE): + vol.All(vol.Coerce(int), vol.Range(min=15)), + vol.Optional(CONF_ZONEDUMP_INTERVAL, + default=DEFAULT_ZONEDUMP_INTERVAL): + vol.All(vol.Coerce(int), vol.Range(min=15)), + }), +}, extra=vol.ALLOW_EXTRA) + + +# pylint: disable=unused-argument, too-many-function-args, too-many-locals +# pylint: disable=too-many-return-statements +def setup(hass, base_config): + """Common setup for Envisalink devices.""" + from pyenvisalink import EnvisalinkAlarmPanel + from pydispatch import dispatcher + + global EVL_CONTROLLER + + config = base_config.get(DOMAIN) + + _host = config.get(CONF_EVL_HOST) + _port = config.get(CONF_EVL_PORT) + _code = config.get(CONF_CODE) + _panel_type = config.get(CONF_PANEL_TYPE) + _version = config.get(CONF_EVL_VERSION) + _user = config.get(CONF_USERNAME) + _pass = config.get(CONF_PASS) + _keep_alive = config.get(CONF_EVL_KEEPALIVE) + _zone_dump = config.get(CONF_ZONEDUMP_INTERVAL) + _zones = config.get(CONF_ZONES) + _partitions = config.get(CONF_PARTITIONS) + _connect_status = {} + EVL_CONTROLLER = EnvisalinkAlarmPanel(_host, + _port, + _panel_type, + _version, + _user, + _pass, + _zone_dump, + _keep_alive) + + def login_fail_callback(data): + """Callback for when the evl rejects our login.""" + _LOGGER.error("The envisalink rejected your credentials.") + _connect_status['fail'] = 1 + + def connection_fail_callback(data): + """Network failure callback.""" + _LOGGER.error("Could not establish a connection with the envisalink.") + _connect_status['fail'] = 1 + + def connection_success_callback(data): + """Callback for a successful connection.""" + _LOGGER.info("Established a connection with the envisalink.") + _connect_status['success'] = 1 + + def zones_updated_callback(data): + """Handle zone timer updates.""" + _LOGGER.info("Envisalink sent a zone update event. Updating zones...") + dispatcher.send(signal=SIGNAL_ZONE_UPDATE, + sender=None, + zone=data) + + def alarm_data_updated_callback(data): + """Handle non-alarm based info updates.""" + _LOGGER.info("Envisalink sent new alarm info. Updating alarms...") + dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE, + sender=None, + partition=data) + + def partition_updated_callback(data): + """Handle partition changes thrown by evl (including alarms).""" + _LOGGER.info("The envisalink sent a partition update event.") + dispatcher.send(signal=SIGNAL_PARTITION_UPDATE, + sender=None, + partition=data) + + def stop_envisalink(event): + """Shutdown envisalink connection and thread on exit.""" + _LOGGER.info("Shutting down envisalink.") + EVL_CONTROLLER.stop() + + def start_envisalink(event): + """Startup process for the envisalink.""" + EVL_CONTROLLER.start() + for _ in range(10): + if 'success' in _connect_status: + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) + return True + elif 'fail' in _connect_status: + return False + else: + time.sleep(1) + + _LOGGER.error("Timeout occurred while establishing evl connection.") + return False + + EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback + EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback + EVL_CONTROLLER.callback_partition_state_change = partition_updated_callback + EVL_CONTROLLER.callback_keypad_update = alarm_data_updated_callback + EVL_CONTROLLER.callback_login_failure = login_fail_callback + EVL_CONTROLLER.callback_login_timeout = connection_fail_callback + EVL_CONTROLLER.callback_login_success = connection_success_callback + + _result = start_envisalink(None) + if not _result: + return False + + # Load sub-components for envisalink + if _partitions: + load_platform(hass, 'alarm_control_panel', 'envisalink', + {'partitions': _partitions, + 'code': _code}, config) + load_platform(hass, 'sensor', 'envisalink', + {'partitions': _partitions, + 'code': _code}, config) + if _zones: + load_platform(hass, 'binary_sensor', 'envisalink', + {'zones': _zones}, config) + + return True + + +class EnvisalinkDevice(Entity): + """Representation of an envisalink devicetity.""" + + def __init__(self, name, info, controller): + """Initialize the device.""" + self._controller = controller + self._info = info + self._name = name + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 50c61ad3af7c2b..ac957e0661f6ed 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,3 +1,3 @@ """DO NOT MODIFY. Auto-generated by build_frontend script.""" -CORE = "7962327e4a29e51d4a6f4ee6cca9acc3" -UI = "570e1b8744a58024fc4e256f5e024424" +CORE = "db0bb387f4d3bcace002d62b94baa348" +UI = "5b306b7e7d36799b7b67f592cbe94703" diff --git a/homeassistant/components/frontend/www_static/core.js b/homeassistant/components/frontend/www_static/core.js index 8bb155ea288130..4bc3619c443152 100644 --- a/homeassistant/components/frontend/www_static/core.js +++ b/homeassistant/components/frontend/www_static/core.js @@ -1,4 +1,4 @@ !function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,e,n){Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:n})},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=25)}({17:function(t,e,n){"use strict";(function(t){function n(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 r(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)}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){return e={exports:{}},t(e,e.exports),e.exports}function u(t,e){var n=e.authToken,r=e.host;return Ne({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function a(){return Ue.getInitialState()}function s(t,e){var n=e.errorMessage;return t.withMutations(function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)})}function c(t,e){var n=e.authToken,r=e.host;return Pe({authToken:n,host:r})}function f(){return xe.getInitialState()}function h(t,e){var n=e.rememberAuth;return n}function l(t){return t.withMutations(function(t){t.set("isStreaming",!0).set("useStreaming",!0).set("hasError",!1)})}function p(t){return t.withMutations(function(t){t.set("isStreaming",!1).set("useStreaming",!1).set("hasError",!1)})}function _(t){return t.withMutations(function(t){t.set("isStreaming",!1).set("hasError",!0)})}function d(){return Be.getInitialState()}function v(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?t.set(o,un({})):t,a=Array.isArray(r)?r:[r],s=n.fromJSON||un;return u.withMutations(function(t){return a.forEach(function(e){var n=s(e);t.setIn([o,n.id],n)})})}function y(t,e){var n=e.model,r=e.params;return t.removeIn([n.entity,r.id])}function S(t){var e={};return e.incrementData=function(e,n){var r=arguments.length<=2||void 0===arguments[2]?{}:arguments[2];g(e,t,r,n)},e.replaceData=function(e,n){var r=arguments.length<=2||void 0===arguments[2]?{}:arguments[2];g(e,t,cn({},r,{replace:!0}),n)},e.removeData=function(e,n){I(e,t,{id:n})},t.fetch&&(e.fetch=function(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.dispatch(rn.API_FETCH_START,{model:t,params:n,method:"fetch"}),t.fetch(e,n).then(g.bind(null,e,t,n),m.bind(null,e,t,n))}),e.fetchAll=function(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.dispatch(rn.API_FETCH_START,{model:t,params:n,method:"fetchAll"}),t.fetchAll(e,n).then(g.bind(null,e,t,cn({},n,{replace:!0})),m.bind(null,e,t,n))},t.save&&(e.save=function(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.dispatch(rn.API_SAVE_START,{params:n}),t.save(e,n).then(E.bind(null,e,t,n),b.bind(null,e,t,n))}),t["delete"]&&(e["delete"]=function(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.dispatch(rn.API_DELETE_START,{params:n}),t["delete"](e,n).then(I.bind(null,e,t,n),w.bind(null,e,t,n))}),e}function g(t,e,n,r){return t.dispatch(rn.API_FETCH_SUCCESS,{model:e,params:n,result:r}),r}function m(t,e,n,r){return t.dispatch(rn.API_FETCH_FAIL,{model:e,params:n,reason:r}),Promise.reject(r)}function E(t,e,n,r){return t.dispatch(rn.API_SAVE_SUCCESS,{model:e,params:n,result:r}),r}function b(t,e,n,r){return t.dispatch(rn.API_SAVE_FAIL,{model:e,params:n,reason:r}),Promise.reject(r)}function I(t,e,n,r){return t.dispatch(rn.API_DELETE_SUCCESS,{model:e,params:n,result:r}),r}function w(t,e,n,r){return t.dispatch(rn.API_DELETE_FAIL,{model:e,params:n,reason:r}),Promise.reject(r)}function O(t){t.registerStores({restApiCache:an})}function T(t){return[["restApiCache",t.entity],function(t){return!!t}]}function A(t){return[["restApiCache",t.entity],function(t){return t||fn({})}]}function D(t){return function(e){return["restApiCache",t.entity,e]}}function C(t){return new Date(t)}function z(t,e,n){var r=arguments.length<=3||void 0===arguments[3]?null:arguments[3],i=t.evaluate(si.authInfo),o=i.host+"/api/"+n;return new Promise(function(t,n){var u=new XMLHttpRequest;u.open(e,o,!0),u.setRequestHeader("X-HA-access",i.authToken),u.onload=function(){var e=void 0;try{e="application/json"===u.getResponseHeader("content-type")?JSON.parse(u.responseText):u.responseText}catch(r){e=u.responseText}u.status>199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?u.send(JSON.stringify(r)):u.send()})}function R(t,e){var n=e.message;return t.set(t.size,n)}function M(){return jn.getInitialState()}function j(t,e){t.dispatch(zn.NOTIFICATION_CREATED,{message:e})}function k(t){t.registerStores({notifications:jn})}function L(t,e){if("lock"===t)return!0;if("garage_door"===t)return!0;var n=e.get(t);return!!n&&n.services.has("turn_on")}function N(t,e){return t?"group"===t.domain?"on"===t.state||"off"===t.state:L(t.domain,e):!1}function U(t,e){return[ur(t),function(t){return!!t&&t.services.has(e)}]}function H(t){return[Dn.byId(t),or,N]}function P(t,e){var n=e.component;return t.push(n)}function x(t,e){var n=e.components;return Sr(n)}function V(){return gr.getInitialState()}function q(t,e){var n=e.latitude,r=e.longitude,i=e.location_name,o=e.temperature_unit,u=e.time_zone,a=e.version;return Er({latitude:n,longitude:r,location_name:i,temperature_unit:o,time_zone:u,serverVersion:a})}function F(){return br.getInitialState()}function G(t,e){t.dispatch(vr.SERVER_CONFIG_LOADED,e)}function K(t){dn(t,"GET","config").then(function(e){return G(t,e)})}function Y(t,e){t.dispatch(vr.COMPONENT_LOADED,{component:e})}function B(t){return[["serverComponent"],function(e){return e.contains(t)}]}function J(t){t.registerStores({serverComponent:gr,serverConfig:br})}function W(t){return t.evaluate(_r)}function X(t){W(t)&&(t.hassId in Mr||(Mr[t.hassId]=Qe(Z.bind(null,t),Rr)),Mr[t.hassId]())}function Q(t){var e=Mr[t.hassId];e&&e.cancel()}function Z(t){return t.dispatch(Ze.API_FETCH_ALL_START,{}),dn(t,"GET","bootstrap").then(function(e){t.batch(function(){An.replaceData(t,e.states),cr.replaceData(t,e.services),Xn.replaceData(t,e.events),Dr.configLoaded(t,e.config),t.dispatch(Ze.API_FETCH_ALL_SUCCESS,{})}),X(t)},function(e){return t.dispatch(Ze.API_FETCH_ALL_FAIL,{message:e}),X(t),Promise.reject(e)})}function $(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=e.skipInitialSync,r=void 0===n?!1:n;t.dispatch(Ze.SYNC_SCHEDULED),r?X(t):Z(t)}function tt(t){t.dispatch(Ze.SYNC_SCHEDULE_CANCELLED),Q(t)}function et(t){t.registerStores({isFetchingData:tn,isSyncScheduled:nn})}function nt(t,e){switch(e.event_type){case"state_changed":e.data.new_state?An.incrementData(t,e.data.new_state):An.removeData(t,e.data.entity_id);break;case"component_loaded":Dr.componentLoaded(t,e.data.component);break;case"service_registered":cr.serviceRegistered(t,e.data.domain,e.data.service)}}function rt(t){var e=Hr[t.hassId];e&&(e.scheduleHealthCheck.cancel(),e.source.close(),Hr[t.hassId]=!1)}function it(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=e.syncOnInitialConnect,r=void 0===n?!0:n;rt(t);var i=Qe(it.bind(null,t),Ur),o=t.evaluate(si.authToken),u=new EventSource("/api/stream?api_password="+o+"&restrict="+Pr),a=r;Hr[t.hassId]={source:u,scheduleHealthCheck:i},u.addEventListener("open",function(){i(),t.batch(function(){t.dispatch(Fe.STREAM_START),kr.stop(t),a?kr.fetchAll(t):a=!0})},!1),u.addEventListener("message",function(e){i(),"ping"!==e.data&&nt(t,JSON.parse(e.data))},!1),u.addEventListener("error",function(){i(),u.readyState!==EventSource.CLOSED&&t.dispatch(Fe.STREAM_ERROR)},!1)}function ot(t){rt(t),t.batch(function(){t.dispatch(Fe.STREAM_STOP),kr.start(t)})}function ut(t){t.registerStores({streamStatus:Be})}function at(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2],r=n.useStreaming,i=void 0===r?t.evaluate(Br.isSupported):r,o=n.rememberAuth,u=void 0===o?!1:o,a=n.host,s=void 0===a?"":a;t.dispatch(ke.VALIDATING_AUTH_TOKEN,{authToken:e,host:s}),kr.fetchAll(t).then(function(){t.dispatch(ke.VALID_AUTH_TOKEN,{authToken:e,host:s,rememberAuth:u}),i?Yr.start(t,{syncOnInitialConnect:!1}):kr.start(t,{skipInitialSync:!0})},function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],n=e.message,r=void 0===n?Wr:n;t.dispatch(ke.INVALID_AUTH_TOKEN,{errorMessage:r})})}function st(t){t.dispatch(ke.LOG_OUT,{})}function ct(t){t.registerStores({authAttempt:Ue,authCurrent:xe,rememberAuth:qe})}function ft(t,e){var n=e.pane;return n}function ht(){return li.getInitialState()}function lt(t,e){var n=e.show;return!!n}function pt(){return _i.getInitialState()}function _t(t,e){t.dispatch(fi.SHOW_SIDEBAR,{show:e})}function dt(t,e){t.dispatch(fi.NAVIGATE,{pane:e})}function vt(t){return[vi,function(e){return e===t}]}function yt(t,e){var n=e.entityId;return n}function St(){return Ei.getInitialState()}function gt(t,e){t.dispatch(gi.SELECT_ENTITY,{entityId:e})}function mt(t){t.dispatch(gi.SELECT_ENTITY,{entityId:null})}function Et(t){return!t||(new Date).getTime()-t>6e4}function bt(t){return t.getUTCFullYear()+"-"+(t.getUTCMonth()+1)+"-"+t.getUTCDate()}function It(t,e){var n=e.date;return bt(n)}function wt(){return Oi.getInitialState()}function Ot(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,Ai({})):t.withMutations(function(t){r.forEach(function(e){return t.setIn([n,e[0].entity_id],Ai(e.map(mn.fromJSON)))})})}function Tt(){return Di.getInitialState()}function At(t,e){var n=e.stateHistory;return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,Mi(e.map(mn.fromJSON)))})})}function Dt(){return ji.getInitialState()}function Ct(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(Ni,r)})}function zt(){return Ui.getInitialState()}function Rt(t,e){t.dispatch(Ii.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function Mt(t){var e=arguments.length<=1||void 0===arguments[1]?null:arguments[1];t.dispatch(Ii.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),dn(t,"GET",n).then(function(e){return t.dispatch(Ii.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})},function(){return t.dispatch(Ii.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})})}function jt(t,e){return t.dispatch(Ii.ENTITY_HISTORY_FETCH_START,{date:e}),dn(t,"GET","history/period/"+e).then(function(n){return t.dispatch(Ii.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})},function(){return t.dispatch(Ii.ENTITY_HISTORY_FETCH_ERROR,{})})}function kt(t){var e=t.evaluate(xi);return jt(t,e)}function Lt(t){t.registerStores({currentEntityHistoryDate:Oi,entityHistory:Di,isLoadingEntityHistory:zi,recentEntityHistory:ji,recentEntityHistoryUpdated:Ui})}function Nt(t){t.registerStores({moreInfoEntityId:Ei})}function Ut(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;onu}function ce(t){t.registerStores({currentLogbookDate:qo,isLoadingLogbookEntries:Go,logbookEntries:Xo,logbookEntriesUpdated:$o})}function fe(t,e){return dn(t,"POST","template",{template:e})}function he(t){return t.set("isListening",!0)}function le(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations(function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)})}function pe(t,e){var n=e.finalTranscript;return t.withMutations(function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)})}function _e(){return gu.getInitialState()}function de(){return gu.getInitialState()}function ve(){return gu.getInitialState()}function ye(t){return mu[t.hassId]}function Se(t){var e=ye(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(vu.VOICE_TRANSMITTING,{finalTranscript:n}),cr.callService(t,"conversation","process",{text:n}).then(function(){t.dispatch(vu.VOICE_DONE)},function(){t.dispatch(vu.VOICE_ERROR)})}}function ge(t){var e=ye(t);e&&(e.recognition.stop(),mu[t.hassId]=!1)}function me(t){Se(t),ge(t)}function Ee(t){var e=me.bind(null,t);e();var n=new webkitSpeechRecognition;mu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(vu.VOICE_START)},n.onerror=function(){return t.dispatch(vu.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=ye(t);if(n){for(var r="",i="",o=e.resultIndex;oi;i++)r[i]=t[i+e];return r}function o(t){return void 0===t.size&&(t.size=t.__iterate(a)),t.size}function u(t,e){if("number"!=typeof e){var n=+e;if(""+n!==e)return NaN;e=n}return 0>e?o(t)+e:e}function a(){return!0}function s(t,e,n){return(0===t||void 0!==n&&-n>=t)&&(void 0===e||void 0!==n&&e>=n)}function c(t,e){return h(t,e,0)}function f(t,e){return h(t,e,e)}function h(t,e,n){return void 0===t?n:0>t?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function l(t){return v(t)?t:C(t)}function p(t){return y(t)?t:z(t)}function _(t){return S(t)?t:R(t)}function d(t){return v(t)&&!g(t)?t:M(t)}function v(t){return!(!t||!t[vn])}function y(t){return!(!t||!t[yn])}function S(t){return!(!t||!t[Sn])}function g(t){return y(t)||S(t)}function m(t){return!(!t||!t[gn])}function E(t){this.next=t}function b(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function I(){return{value:void 0,done:!0}}function w(t){return!!A(t)}function O(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(In&&t[In]||t[wn]);return"function"==typeof e?e:void 0}function D(t){return t&&"number"==typeof t.length}function C(t){return null===t||void 0===t?H():v(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?H().toKeyedSeq():v(t)?y(t)?t.toSeq():t.fromEntrySeq():P(t)}function R(t){return null===t||void 0===t?H():v(t)?y(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?H():v(t)?y(t)?t.entrySeq():t:x(t)).toSetSeq()}function j(t){this._array=t,this.size=t.length}function k(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function L(t){this._iterable=t,this.size=t.length||t.size}function N(t){this._iterator=t,this._iteratorCache=[]}function U(t){return!(!t||!t[Tn])}function H(){return An||(An=new j([]))}function P(t){var e=Array.isArray(t)?new j(t).fromEntrySeq():O(t)?new N(t).fromEntrySeq():w(t)?new L(t).fromEntrySeq():"object"===("undefined"==typeof t?"undefined":De(t))?new k(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=q(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=q(t)||"object"===("undefined"==typeof t?"undefined":De(t))&&new k(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return D(t)?new j(t):O(t)?new N(t):w(t)?new L(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;o>=u;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new E(function(){var t=i[n?o-u:u];return u++>o?I():b(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(){throw TypeError("Abstract")}function Y(){}function B(){}function J(){}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){return e?Q(e,t,"",{"":t}):Z(t)}function Q(t,e,n,r){return Array.isArray(e)?t.call(r,n,R(e).map(function(n,r){return Q(t,n,r,e)})):$(e)?t.call(r,n,z(e).map(function(n,r){return Q(t,n,r,e)})):e}function Z(t){return Array.isArray(t)?R(t).map(Z).toList():$(t)?z(t).map(Z).toMap():t}function $(t){return t&&(t.constructor===Object||void 0===t.constructor)}function tt(t){return t>>>1&1073741824|3221225471&t}function et(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e="undefined"==typeof t?"undefined":De(t);if("number"===e){var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return tt(n)}return"string"===e?t.length>Ln?nt(t):rt(t):"function"==typeof t.hashCode?t.hashCode():it(t)}function nt(t){var e=Hn[t];return void 0===e&&(e=rt(t),Un===Nn&&(Un=0,Hn={}),Un++,Hn[t]=e),e}function rt(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ut(t,e){if(!t)throw new Error(e)}function at(t){ut(t!==1/0,"Cannot perform this action with an infinite size.")}function st(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ct(t){this._iter=t,this.size=t.size}function ft(t){this._iter=t,this.size=t.size}function ht(t){this._iter=t,this.size=t.size}function lt(t){var e=jt(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=kt,e.__iterateUncached=function(e,n){var r=this;return t.__iterate(function(t,n){return e(n,t,r)!==!1},n)},e.__iteratorUncached=function(e,n){if(e===bn){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===En?mn:En,n)},e}function pt(t,e,n){var r=jt(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,pn);return o===pn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate(function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1},i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(bn,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return b(r,a,e.call(n,u[1],a,t),i)})},r}function _t(t,e){var n=jt(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=lt(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=kt,n.__iterate=function(e,n){var r=this;return t.__iterate(function(t,n){return e(t,n,r)},!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function dt(t,e,n,r){var i=jt(t);return r&&(i.has=function(r){var i=t.get(r,pn);return i!==pn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,pn);return o!==pn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate(function(t,o,s){return e.call(n,t,o,s)?(a++,i(t,r?o:a-1,u)):void 0},o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(bn,o),a=0;return new E(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return b(i,r?c:a++,f,o)}})},i}function vt(t,e,n){var r=Ut().asMutable();return t.__iterate(function(i,o){r.update(e.call(n,i,o,t),0,function(t){return t+1})}),r.asImmutable()}function yt(t,e,n){var r=y(t),i=(m(t)?Ie():Ut()).asMutable();t.__iterate(function(o,u){i.update(e.call(n,o,u,t),function(t){return t=t||[],t.push(r?[u,o]:o),t})});var o=Mt(t);return i.map(function(e){return Ct(t,o(e))})}function St(t,e,n,r){var i=t.size;if(void 0!==e&&(e=0|e),void 0!==n&&(n=0|n),s(e,n,i))return t;var o=c(e,i),a=f(n,i);if(o!==o||a!==a)return St(t.toSeq().cacheResult(),e,n,r);var h,l=a-o;l===l&&(h=0>l?0:l);var p=jt(t);return p.size=0===h?h:t.size&&h||void 0,!r&&U(t)&&h>=0&&(p.get=function(e,n){return e=u(this,e),e>=0&&h>e?t.get(e+o,n):n}),p.__iterateUncached=function(e,n){var i=this;if(0===h)return 0;if(n)return this.cacheResult().__iterate(e,n);var u=0,a=!0,s=0;return t.__iterate(function(t,n){return a&&(a=u++h)return I();var t=i.next();return r||e===En?t:e===mn?b(e,a-1,void 0,t):b(e,a-1,t.value[1],t)})},p}function gt(t,e,n){var r=jt(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate(function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)}),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(bn,i),a=!0;return new E(function(){if(!a)return I();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===bn?t:b(r,s,c,t):(a=!1,I())})},r}function mt(t,e,n,r){var i=jt(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate(function(t,o,c){return a&&(a=e.call(n,t,o,c))?void 0:(s++,i(t,r?o:s-1,u))}),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(bn,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===En?t:i===mn?b(i,c++,void 0,t):b(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===bn?t:b(i,o,f,t)})},i}function Et(t,e){var n=y(t),r=[t].concat(e).map(function(t){return v(t)?n&&(t=p(t)):t=n?P(t):x(Array.isArray(t)?t:[t]),t}).filter(function(t){return 0!==t.size});if(0===r.length)return t;if(1===r.length){var i=r[0];if(i===t||n&&y(i)||S(t)&&S(i))return i}var o=new j(r);return n?o=o.toKeyedSeq():S(t)||(o=o.toSetSeq()),o=o.flatten(!0),o.size=r.reduce(function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}},0),o}function bt(t,e,n){var r=jt(t);return r.__iterateUncached=function(r,i){function o(t,s){var c=this;t.__iterate(function(t,i){return(!e||e>s)&&v(t)?o(t,s+1):r(t,n?i:u++,c)===!1&&(a=!0),!a},i)}var u=0,a=!1;return o(t,0),u},r.__iteratorUncached=function(r,i){var o=t.__iterator(r,i),u=[],a=0;return new E(function(){for(;o;){var t=o.next();if(t.done===!1){var s=t.value;if(r===bn&&(s=s[1]),e&&!(u.length0}function Dt(t,e,n){var r=jt(t); return r.size=new j(n).map(function(t){return t.size}).min(),r.__iterate=function(t,e){for(var n,r=this.__iterator(En,e),i=0;!(n=r.next()).done&&t(n.value,i++,this)!==!1;);return i},r.__iteratorUncached=function(t,r){var i=n.map(function(t){return t=l(t),T(r?t.reverse():t)}),o=0,u=!1;return new E(function(){var n;return u||(n=i.map(function(t){return t.next()}),u=n.some(function(t){return t.done})),u?I():b(t,o++,e.apply(null,n.map(function(t){return t.value})))})},r}function Ct(t,e){return U(t)?e:t.constructor(e)}function zt(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Rt(t){return at(t.size),o(t)}function Mt(t){return y(t)?p:S(t)?_:d}function jt(t){return Object.create((y(t)?z:S(t)?R:M).prototype)}function kt(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):C.prototype.cacheResult.call(this)}function Lt(t,e){return t>e?1:e>t?-1:0}function Nt(t){var e=T(t);if(!e){if(!D(t))throw new TypeError("Expected iterable or array-like: "+t);e=T(l(t))}return e}function Ut(t){return null===t||void 0===t?Jt():Ht(t)&&!m(t)?t:Jt().withMutations(function(e){var n=p(t);at(n.size),n.forEach(function(t,n){return e.set(n,t)})})}function Ht(t){return!(!t||!t[Pn])}function Pt(t,e){this.ownerID=t,this.entries=e}function xt(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function Vt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function qt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function Ft(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function Gt(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&Yt(t._root)}function Kt(t,e){return b(t,e[0],e[1])}function Yt(t,e){return{node:t,index:0,__prev:e}}function Bt(t,e,n,r){var i=Object.create(xn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Jt(){return Vn||(Vn=Bt(0))}function Wt(t,n,r){var i,o;if(t._root){var u=e(_n),a=e(dn);if(i=Xt(t._root,t.__ownerID,0,void 0,n,r,u,a),!a.value)return t;o=t.size+(u.value?r===pn?-1:1:0)}else{if(r===pn)return t;o=1,i=new Pt(t.__ownerID,[[n,r]])}return t.__ownerID?(t.size=o,t._root=i,t.__hash=void 0,t.__altered=!0,t):i?Bt(o,i):Jt()}function Xt(t,e,r,i,o,u,a,s){return t?t.update(e,r,i,o,u,a,s):u===pn?t:(n(s),n(a),new Ft(e,i,[o,u]))}function Qt(t){return t.constructor===Ft||t.constructor===qt}function Zt(t,e,n,r,i){if(t.keyHash===r)return new qt(e,r,[t.entry,i]);var o,u=(0===n?t.keyHash:t.keyHash>>>n)&ln,a=(0===n?r:r>>>n)&ln,s=u===a?[Zt(t,e,n+fn,r,i)]:(o=new Ft(e,r,i),a>u?[t,o]:[o,t]);return new xt(e,1<a;a++,s<<=1){var f=e[a];void 0!==f&&a!==r&&(i|=s,u[o++]=f)}return new xt(t,i,u)}function ee(t,e,n,r,i){for(var o=0,u=new Array(hn),a=0;0!==n;a++,n>>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new Vt(t,o+1,u)}function ne(t,e,n){for(var r=[],i=0;i>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function ae(t,e,n,r){var o=r?t:i(t);return o[e]=n,o}function se(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;i>a;a++)a===e?(o[a]=n,u=-1):o[a]=t[a+u];return o}function ce(t,e,n){var r=t.length-1;if(n&&e===r)return t.pop(),t;for(var i=new Array(r),o=0,u=0;r>u;u++)u===e&&(o=1),i[u]=t[u+o];return i}function fe(t){var e=de();if(null===t||void 0===t)return e;if(he(t))return t;var n=_(t),r=n.size;return 0===r?e:(at(r),r>0&&hn>r?_e(0,r,fn,null,new le(n.toArray())):e.withMutations(function(t){t.setSize(r),n.forEach(function(e,n){return t.set(n,e)})}))}function he(t){return!(!t||!t[Kn])}function le(t,e){this.array=t,this.ownerID=e}function pe(t,e){function n(t,e,n){return 0===e?r(t,n):i(t,e,n)}function r(t,n){var r=n===a?s&&s.array:t&&t.array,i=n>o?0:o-n,c=u-n;return c>hn&&(c=hn),function(){if(i===c)return Jn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>hn&&(f=hn),function(){for(;;){if(a){var t=a();if(t!==Jn)return t;a=null}if(c===f)return Jn;var o=e?--f:c++;a=n(s&&s[o],r-fn,i+(o<=t.size||0>n)return t.withMutations(function(t){0>n?me(t,n).set(0,r):me(t,0,n+1).set(n,r)});n+=t._origin;var i=t._tail,o=t._root,a=e(dn);return n>=be(t._capacity)?i=ye(i,t.__ownerID,0,n,r,a):o=ye(o,t.__ownerID,t._level,n,r,a),a.value?t.__ownerID?(t._root=o,t._tail=i,t.__hash=void 0,t.__altered=!0,t):_e(t._origin,t._capacity,t._level,o,i):t}function ye(t,e,r,i,o,u){var a=i>>>r&ln,s=t&&a0){var f=t&&t.array[a],h=ye(f,e,r-fn,i,o,u);return h===f?t:(c=Se(t,e),c.array[a]=h,c)}return s&&t.array[a]===o?t:(n(u),c=Se(t,e),void 0===o&&a===c.array.length-1?c.array.pop():c.array[a]=o,c)}function Se(t,e){return e&&t&&e===t.ownerID?t:new le(t?t.array.slice():[],e)}function ge(t,e){if(e>=be(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&ln],r-=fn;return n}}function me(t,e,n){void 0!==e&&(e=0|e),void 0!==n&&(n=0|n);var i=t.__ownerID||new r,o=t._origin,u=t._capacity,a=o+e,s=void 0===n?u:0>n?u+n:o+n;if(a===o&&s===u)return t;if(a>=s)return t.clear();for(var c=t._level,f=t._root,h=0;0>a+h;)f=new le(f&&f.array.length?[void 0,f]:[],i),c+=fn,h+=1<=1<p?ge(t,s-1):p>l?new le([],i):_;if(_&&p>l&&u>a&&_.array.length){f=Se(f,i);for(var v=f,y=c;y>fn;y-=fn){var S=l>>>y&ln;v=v.array[S]=Se(v.array[S],i)}v.array[l>>>fn&ln]=_}if(u>s&&(d=d&&d.removeAfter(i,0,s)),a>=p)a-=p,s-=p,c=fn,f=null,d=d&&d.removeBefore(i,0,a);else if(a>o||l>p){for(h=0;f;){var g=a>>>c&ln;if(g!==p>>>c&ln)break;g&&(h+=(1<o&&(f=f.removeBefore(i,c,a-h)),f&&l>p&&(f=f.removeAfter(i,c,p-h)),h&&(a-=h,s-=h)}return t.__ownerID?(t.size=s-a,t._origin=a,t._capacity=s,t._level=c,t._root=f,t._tail=d,t.__hash=void 0,t.__altered=!0,t):_e(a,s,c,f,d)}function Ee(t,e,n){for(var r=[],i=0,o=0;oi&&(i=a.size),v(u)||(a=a.map(function(t){return X(t)})),r.push(a)}return i>t.size&&(t=t.setSize(i)),ie(t,e,r)}function be(t){return hn>t?0:t-1>>>fn<=hn&&u.size>=2*o.size?(i=u.filter(function(t,e){return void 0!==t&&a!==e}),r=i.toKeyedSeq().map(function(t){return t[0]}).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):Oe(r,i)}function Ce(t){return null===t||void 0===t?Me():ze(t)?t:Me().unshiftAll(t)}function ze(t){return!(!t||!t[Xn])}function Re(t,e,n,r){var i=Object.create(Qn);return i.size=t,i._head=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Me(){return Zn||(Zn=Re(0))}function je(t){return null===t||void 0===t?Ue():ke(t)&&!m(t)?t:Ue().withMutations(function(e){var n=d(t);at(n.size),n.forEach(function(t){return e.add(t)})})}function ke(t){return!(!t||!t[$n])}function Le(t,e){return t.__ownerID?(t.size=e.size,t._map=e,t):e===t._map?t:0===e.size?t.__empty():t.__make(e)}function Ne(t,e){var n=Object.create(tr);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function Ue(){return er||(er=Ne(Jt()))}function He(t){return null===t||void 0===t?Ve():Pe(t)?t:Ve().withMutations(function(e){var n=d(t);at(n.size),n.forEach(function(t){return e.add(t)})})}function Pe(t){return ke(t)&&m(t)}function xe(t,e){var n=Object.create(nr);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function Ve(){return rr||(rr=xe(Te()))}function qe(t,e){var n,r=function(o){if(o instanceof r)return o;if(!(this instanceof r))return new r(o);if(!n){n=!0;var u=Object.keys(t);Ke(i,u),i.size=u.length,i._name=e,i._keys=u,i._defaultValues=t}this._map=Ut(o)},i=r.prototype=Object.create(ir);return i.constructor=r,r}function Fe(t,e,n){var r=Object.create(Object.getPrototypeOf(t));return r._map=e,r.__ownerID=n,r}function Ge(t){return t._name||t.constructor.name||"Record"}function Ke(t,e){try{e.forEach(Ye.bind(void 0,t))}catch(n){}}function Ye(t,e){Object.defineProperty(t,e,{get:function(){return this.get(e)},set:function(t){ut(this.__ownerID,"Cannot set on an immutable record."),this.set(e,t)}})}function Be(t,e){if(t===e)return!0;if(!v(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||y(t)!==y(e)||S(t)!==S(e)||m(t)!==m(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!g(t);if(m(t)){var r=t.entries();return e.every(function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))})&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var o=t;t=e,e=o}var u=!0,a=e.__iterate(function(e,r){return(n?t.has(e):i?W(e,t.get(r,pn)):W(t.get(r,pn),e))?void 0:(u=!1,!1)});return u&&t.size===a}function Je(t,e,n){if(!(this instanceof Je))return new Je(t,e,n);if(ut(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),t>e&&(n=-n),this._start=t,this._end=e,this._step=n,this.size=Math.max(0,Math.ceil((e-t)/n-1)+1),0===this.size){if(or)return or;or=this}}function We(t,e){if(!(this instanceof We))return new We(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(ur)return ur;ur=this}}function Xe(t,e){var n=function(n){t.prototype[n]=e[n]};return Object.keys(e).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(e).forEach(n),t}function Qe(t,e){return e}function Ze(t,e){return[e,t]}function $e(t){return function(){return!t.apply(this,arguments)}}function tn(t){return function(){return-t.apply(this,arguments)}}function en(t){return"string"==typeof t?JSON.stringify(t):t}function nn(){return i(arguments)}function rn(t,e){return e>t?1:t>e?-1:0}function on(t){if(t.size===1/0)return 0;var e=m(t),n=y(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+an(et(t),et(e))|0}:function(t,e){r=r+an(et(t),et(e))|0}:e?function(t){r=31*r+et(t)|0}:function(t){r=r+et(t)|0});return un(i,r)}function un(t,e){return e=Cn(e,3432918353),e=Cn(e<<15|e>>>-15,461845907),e=Cn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Cn(e^e>>>16,2246822507),e=Cn(e^e>>>13,3266489909),e=tt(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sn=Array.prototype.slice,cn="delete",fn=5,hn=1<=i;i++)if(t(n[e?r-i:i],i,this)===!1)return i+1;return i},j.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?I():b(t,i,n[e?r-i++:i++])})},t(k,z),k.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},k.prototype.has=function(t){return this._object.hasOwnProperty(t)},k.prototype.__iterate=function(t,e){for(var n=this._object,r=this._keys,i=r.length-1,o=0;i>=o;o++){var u=r[e?i-o:o];if(t(n[u],u,this)===!1)return o+1}return o},k.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var u=r[e?i-o:o];return o++>i?I():b(t,u,n[u])})},k.prototype[gn]=!0,t(L,R),L.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);var n=this._iterable,r=T(n),i=0;if(O(r))for(var o;!(o=r.next()).done&&t(o.value,i++,this)!==!1;);return i},L.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!O(r))return new E(I);var i=0;return new E(function(){var e=r.next();return e.done?e:b(t,i++,e.value)})},t(N,R),N.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);for(var n=this._iterator,r=this._iteratorCache,i=0;i=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return b(t,i,r[i++])})};var An;t(K,l),t(Y,K),t(B,K),t(J,K),K.Keyed=Y,K.Indexed=B,K.Set=J;var Dn,Cn="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(t,e){t=0|t,e=0|e;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},zn=Object.isExtensible,Rn=function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}}(),Mn="function"==typeof WeakMap;Mn&&(Dn=new WeakMap);var jn=0,kn="__immutablehash__";"function"==typeof Symbol&&(kn=Symbol(kn));var Ln=16,Nn=255,Un=0,Hn={};t(st,z),st.prototype.get=function(t,e){return this._iter.get(t,e)},st.prototype.has=function(t){return this._iter.has(t)},st.prototype.valueSeq=function(){return this._iter.valueSeq()},st.prototype.reverse=function(){var t=this,e=_t(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},st.prototype.map=function(t,e){var n=this,r=pt(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},st.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Rt(this):0,function(i){return t(i,e?--n:n++,r)}),e)},st.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(En,e),r=e?Rt(this):0;return new E(function(){var i=n.next();return i.done?i:b(t,e?--r:r++,i.value,i)})},st.prototype[gn]=!0,t(ct,R),ct.prototype.includes=function(t){return this._iter.includes(t)},ct.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate(function(e){return t(e,r++,n)},e)},ct.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e),r=0;return new E(function(){var e=n.next();return e.done?e:b(t,r++,e.value,e)})},t(ft,M),ft.prototype.has=function(t){return this._iter.includes(t)},ft.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate(function(e){return t(e,e,n)},e)},ft.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){var e=n.next();return e.done?e:b(t,e.value,e.value,e)})},t(ht,z),ht.prototype.entrySeq=function(){return this._iter.toSeq()},ht.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate(function(e){if(e){zt(e);var r=v(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}},e)},ht.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){zt(r);var i=v(r);return b(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ct.prototype.cacheResult=st.prototype.cacheResult=ft.prototype.cacheResult=ht.prototype.cacheResult=kt,t(Ut,Y),Ut.prototype.toString=function(){return this.__toString("Map {","}")},Ut.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},Ut.prototype.set=function(t,e){return Wt(this,t,e)},Ut.prototype.setIn=function(t,e){return this.updateIn(t,pn,function(){return e})},Ut.prototype.remove=function(t){return Wt(this,t,pn)},Ut.prototype.deleteIn=function(t){return this.updateIn(t,function(){return pn})},Ut.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},Ut.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=oe(this,Nt(t),e,n);return r===pn?void 0:r},Ut.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Jt()},Ut.prototype.merge=function(){return ne(this,void 0,arguments)},Ut.prototype.mergeWith=function(t){var e=sn.call(arguments,1);return ne(this,t,e)},Ut.prototype.mergeIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,Jt(),function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]})},Ut.prototype.mergeDeep=function(){return ne(this,re(void 0),arguments)},Ut.prototype.mergeDeepWith=function(t){var e=sn.call(arguments,1);return ne(this,re(t),e)},Ut.prototype.mergeDeepIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,Jt(),function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]})},Ut.prototype.sort=function(t){return Ie(Ot(this,t))},Ut.prototype.sortBy=function(t,e){return Ie(Ot(this,e,t))},Ut.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},Ut.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new r)},Ut.prototype.asImmutable=function(){return this.__ensureOwner()},Ut.prototype.wasAltered=function(){return this.__altered},Ut.prototype.__iterator=function(t,e){return new Gt(this,t,e)},Ut.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate(function(e){return r++,t(e[1],e[0],n)},e),r},Ut.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Bt(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Ut.isMap=Ht;var Pn="@@__IMMUTABLE_MAP__@@",xn=Ut.prototype;xn[Pn]=!0,xn[cn]=xn.remove,xn.removeIn=xn.deleteIn,Pt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;u>o;o++)if(W(n,i[o][0]))return i[o][1];return r},Pt.prototype.update=function(t,e,r,o,u,a,s){for(var c=u===pn,f=this.entries,h=0,l=f.length;l>h&&!W(o,f[h][0]);h++);var p=l>h;if(p?f[h][1]===u:c)return this;if(n(s),(c||!p)&&n(a),!c||1!==f.length){if(!p&&!c&&f.length>=qn)return $t(t,f,o,u);var _=t&&t===this.ownerID,d=_?f:i(f);return p?c?h===l-1?d.pop():d[h]=d.pop():d[h]=[o,u]:d.push([o,u]),_?(this.entries=d,this):new Pt(t,d)}},xt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=1<<((0===t?e:e>>>t)&ln),o=this.bitmap;return 0===(o&i)?r:this.nodes[ue(o&i-1)].get(t+fn,e,n,r)},xt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=et(r));var a=(0===e?n:n>>>e)&ln,s=1<=Fn)return ee(t,l,c,a,_);if(f&&!_&&2===l.length&&Qt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&Qt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?ae(l,h,_,d):ce(l,h,d):se(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new xt(t,v,y)},Vt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=(0===t?e:e>>>t)&ln,o=this.nodes[i];return o?o.get(t+fn,e,n,r):r},Vt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=et(r));var a=(0===e?n:n>>>e)&ln,s=i===pn,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Xt(f,t,e+fn,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,Gn>l))return te(t,c,l,a)}else l++;var p=t&&t===this.ownerID,_=ae(c,a,h,p);return p?(this.count=l,this.nodes=_,this):new Vt(t,l,_)},qt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;u>o;o++)if(W(n,i[o][0]))return i[o][1];return r},qt.prototype.update=function(t,e,r,o,u,a,s){void 0===r&&(r=et(o));var c=u===pn;if(r!==this.keyHash)return c?this:(n(s),n(a),Zt(this,t,e,r,[o,u]));for(var f=this.entries,h=0,l=f.length;l>h&&!W(o,f[h][0]);h++);var p=l>h;if(p?f[h][1]===u:c)return this;if(n(s),(c||!p)&&n(a),c&&2===l)return new Ft(t,this.keyHash,f[1^h]);var _=t&&t===this.ownerID,d=_?f:i(f);return p?c?h===l-1?d.pop():d[h]=d.pop():d[h]=[o,u]:d.push([o,u]),_?(this.entries=d,this):new qt(t,this.keyHash,d)},Ft.prototype.get=function(t,e,n,r){return W(n,this.entry[0])?this.entry[1]:r},Ft.prototype.update=function(t,e,r,i,o,u,a){var s=o===pn,c=W(i,this.entry[0]);return(c?o===this.entry[1]:s)?this:(n(a),s?void n(u):c?t&&t===this.ownerID?(this.entry[1]=o,this):new Ft(t,this.keyHash,[i,o]):(n(u),Zt(this,t,e,et(i),[i,o])))},Pt.prototype.iterate=qt.prototype.iterate=function(t,e){for(var n=this.entries,r=0,i=n.length-1;i>=r;r++)if(t(n[e?i-r:r])===!1)return!1},xt.prototype.iterate=Vt.prototype.iterate=function(t,e){for(var n=this.nodes,r=0,i=n.length-1;i>=r;r++){var o=n[e?i-r:r];if(o&&o.iterate(t,e)===!1)return!1}},Ft.prototype.iterate=function(t,e){return t(this.entry)},t(Gt,E),Gt.prototype.next=function(){for(var t=this._type,e=this._stack;e;){var n,r=e.node,i=e.index++;if(r.entry){if(0===i)return Kt(t,r.entry)}else if(r.entries){if(n=r.entries.length-1,n>=i)return Kt(t,r.entries[this._reverse?n-i:i])}else if(n=r.nodes.length-1,n>=i){var o=r.nodes[this._reverse?n-i:i];if(o){if(o.entry)return Kt(t,o.entry);e=this._stack=Yt(o,e)}continue}e=this._stack=this._stack.__prev}return I()};var Vn,qn=hn/4,Fn=hn/2,Gn=hn/4;t(fe,B),fe.of=function(){return this(arguments)},fe.prototype.toString=function(){return this.__toString("List [","]")},fe.prototype.get=function(t,e){if(t=u(this,t),t>=0&&t>>e&ln;if(r>=this.array.length)return new le([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-fn,n),i===u&&o)return this}if(o&&!i)return this;var a=Se(this,t);if(!o)for(var s=0;r>s;s++)a.array[s]=void 0;return i&&(a.array[r]=i),a},le.prototype.removeAfter=function(t,e,n){if(n===(e?1<>>e&ln;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-fn,n),i===o&&r===this.array.length-1)return this}var u=Se(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Bn,Jn={};t(Ie,Ut),Ie.of=function(){return this(arguments)},Ie.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Ie.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Ie.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):Te()},Ie.prototype.set=function(t,e){return Ae(this,t,e)},Ie.prototype.remove=function(t){return Ae(this,t,pn)},Ie.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Ie.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate(function(e){return e&&t(e[1],e[0],n)},e)},Ie.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Ie.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?Oe(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Ie.isOrderedMap=we,Ie.prototype[gn]=!0,Ie.prototype[cn]=Ie.prototype.remove;var Wn;t(Ce,B),Ce.of=function(){return this(arguments)},Ce.prototype.toString=function(){return this.__toString("Stack [","]")},Ce.prototype.get=function(t,e){var n=this._head;for(t=u(this,t);n&&t--;)n=n.next;return n?n.value:e},Ce.prototype.peek=function(){return this._head&&this._head.value},Ce.prototype.push=function(){if(0===arguments.length)return this;for(var t=this.size+arguments.length,e=this._head,n=arguments.length-1;n>=0;n--)e={value:arguments[n],next:e};return this.__ownerID?(this.size=t,this._head=e,this.__hash=void 0,this.__altered=!0,this):Re(t,e)},Ce.prototype.pushAll=function(t){if(t=_(t),0===t.size)return this;at(t.size);var e=this.size,n=this._head;return t.reverse().forEach(function(t){e++,n={value:t,next:n}}),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Re(e,n)},Ce.prototype.pop=function(){return this.slice(1)},Ce.prototype.unshift=function(){return this.push.apply(this,arguments)},Ce.prototype.unshiftAll=function(t){return this.pushAll(t)},Ce.prototype.shift=function(){return this.pop.apply(this,arguments)},Ce.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Me()},Ce.prototype.slice=function(t,e){if(s(t,e,this.size))return this;var n=c(t,this.size),r=f(e,this.size);if(r!==this.size)return B.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Re(i,o)},Ce.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Re(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Ce.prototype.__iterate=function(t,e){if(e)return this.reverse().__iterate(t);for(var n=0,r=this._head;r&&t(r.value,n++,this)!==!1;)r=r.next;return n},Ce.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,b(t,n++,e)}return I()})},Ce.isStack=ze;var Xn="@@__IMMUTABLE_STACK__@@",Qn=Ce.prototype;Qn[Xn]=!0,Qn.withMutations=xn.withMutations,Qn.asMutable=xn.asMutable,Qn.asImmutable=xn.asImmutable,Qn.wasAltered=xn.wasAltered;var Zn;t(je,J),je.of=function(){return this(arguments)},je.fromKeys=function(t){return this(p(t).keySeq())},je.prototype.toString=function(){return this.__toString("Set {","}")},je.prototype.has=function(t){return this._map.has(t)},je.prototype.add=function(t){return Le(this,this._map.set(t,!0))},je.prototype.remove=function(t){return Le(this,this._map.remove(t))},je.prototype.clear=function(){return Le(this,this._map.clear())},je.prototype.union=function(){var t=sn.call(arguments,0);return t=t.filter(function(t){return 0!==t.size}),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations(function(e){for(var n=0;n1?" by "+this._step:"")+" ]"},Je.prototype.get=function(t,e){return this.has(t)?this._start+u(this,t)*this._step:e},Je.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&e=e?new Je(0,0):new Je(this.get(t,this._end),this.get(e,this._end),this._step))},Je.prototype.indexOf=function(t){var e=t-this._start;if(e%this._step===0){var n=e/this._step;if(n>=0&&n=o;o++){if(t(i,o,this)===!1)return o+1;i+=e?-r:r}return o},Je.prototype.__iterator=function(t,e){var n=this.size-1,r=this._step,i=e?this._start+n*r:this._start,o=0;return new E(function(){var u=i;return i+=e?-r:r,o>n?I():b(t,o++,u)})},Je.prototype.equals=function(t){return t instanceof Je?this._start===t._start&&this._end===t._end&&this._step===t._step:Be(this,t)};var or;t(We,R),We.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},We.prototype.get=function(t,e){return this.has(t)?this._value:e},We.prototype.includes=function(t){return W(this._value,t)},We.prototype.slice=function(t,e){var n=this.size;return s(t,e,n)?this:new We(this._value,f(e,n)-c(t,n))},We.prototype.reverse=function(){return this},We.prototype.indexOf=function(t){return W(this._value,t)?0:-1},We.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},We.prototype.__iterate=function(t,e){for(var n=0;nt?this.count():this.size);var r=this.slice(0,t);return Ct(this,1===n?r:r.concat(i(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.toKeyedSeq().findLastKey(t,e);return void 0===n?-1:n},first:function(){return this.get(0)},flatten:function(t){return Ct(this,bt(this,t,!1))},get:function(t,e){return t=u(this,t),0>t||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find(function(e,n){return n===t},void 0,e)},has:function(t){return t=u(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!==("undefined"==typeof Int8Array?"undefined":De(Int8Array))?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e="undefined"==typeof t?"undefined":De(t);return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments.length;if(!t||2>e)return t||{};for(var n=1;e>n;n++)for(var r=arguments[n],i=Object.keys(r),o=i.length,u=0;o>u;u++){var a=i[u];t[a]=r[a]}return t},e.clone=function(t){return e.isObject(t)?e.isArray(t)?t.slice():e.extend({},t):t},e.each=function(t,e,n){var i,o,u=t?t.length:0,a=-1;if(n&&(o=e,e=function(t,e,r){return o.call(n,t,e,r)}),r(u))for(;++ar;r++)n[r]=arguments[r];return new(i.apply(t,[null].concat(n)))};return e.__proto__=t,e.prototype=t.prototype,e}},function(t,e,n){function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return c["default"].Iterable.isIterable(t)}function o(t){return i(t)||!(0,f.isObject)(t)}function u(t){return i(t)?t.toJS():t}function a(t){return i(t)?t:c["default"].fromJS(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.isImmutable=i,e.isImmutableValue=o,e.toJS=u,e.toImmutable=a;var s=n(3),c=r(s),f=n(4)},function(t,e,n){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,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function u(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var a=function(){function t(t,e){for(var n=0;n0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c["default"].Set().withMutations(function(n){n.union(t.observerState.get("any")),e.forEach(function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)})});n.forEach(function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c["default"].is(a,s)||i.call(null,s)}});var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t}();e["default"]=(0,y.toFactory)(g),t.exports=e["default"]},function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,function(e,r){n[r]=t.evaluate(e)}),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e["default"]=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),function(n,i){var o=t.observe(n,function(t){e.setState(r({},i,t))});e.__unwatchFns.push(o)})},componentWillUnmount:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}},t.exports=e["default"]},function(t,e,n){function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return new M({result:t,reactorState:e})}function o(t,e){return t.withMutations(function(t){(0,R.each)(e,function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,D.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",function(t){return t.set(n,e)}).update("state",function(t){return t.set(n,r)}).update("dirtyStores",function(t){return t.add(n)}).update("storeStates",function(t){return I(t,[n])})}),b(t)})}function u(t,e){return t.withMutations(function(t){(0,R.each)(e,function(e,n){t.update("stores",function(t){return t.set(n,e)})})})}function a(t,e,n){if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var r=t.get("state"),i=t.get("dirtyStores"),o=r.withMutations(function(r){A["default"].dispatchStart(t,e,n),t.get("stores").forEach(function(o,u){var a=r.get(u),s=void 0;try{s=o.handle(a,e,n)}catch(c){throw A["default"].dispatchError(t,c.message),c}if(void 0===s&&f(t,"throwOnUndefinedStoreReturnValue")){var h="Store handler must return a value, did you forget a return statement";throw A["default"].dispatchError(t,h),new Error(h)}r.set(u,s),a!==s&&(i=i.add(u))}),A["default"].dispatchEnd(t,r,i)}),u=t.set("state",o).set("dirtyStores",i).update("storeStates",function(t){return I(t,i)});return b(u)}function s(t,e){var n=[],r=(0,D.toImmutable)({}).withMutations(function(r){(0,R.each)(e,function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}})}),i=O["default"].Set(n);return t.update("state",function(t){return t.merge(r)}).update("dirtyStores",function(t){return t.union(i)}).update("storeStates",function(t){return I(t,n)})}function c(t,e,n){var r=e;(0,z.isKeyPath)(e)&&(e=(0,C.fromKeyPath)(e));var i=t.get("nextId"),o=(0,C.getStoreDeps)(e),u=O["default"].Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",function(t){return t.add(i)}):t.withMutations(function(t){o.forEach(function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,O["default"].Set()),t.updateIn(["stores",e],function(t){return t.add(i)})})}),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter(function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return i?(0,z.isKeyPath)(e)&&(0,z.isKeyPath)(r)?(0,z.isEqual)(e,r):e===r:!1});return t.withMutations(function(t){r.forEach(function(e){return l(t,e)})})}function l(t,e){return t.withMutations(function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",function(t){return t.remove(n)}):r.forEach(function(e){t.updateIn(["stores",e],function(t){return t?t.remove(n):t})}),t.removeIn(["observersMap",n])})}function p(t){var e=t.get("state");return t.withMutations(function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach(function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,D.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)}),t.update("storeStates",function(t){return I(t,r)}),v(t)})}function _(t,e){var n=t.get("state");if((0,z.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,C.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");if(g(t,e))return i(E(t,e),t);var r=(0,C.getDeps)(e).map(function(e){return _(t,e).result}),o=(0,C.getComputeFn)(e).apply(null,r);return i(o,m(t,e,o))}function d(t){var e={};return t.get("stores").forEach(function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)}),e}function v(t){return t.set("dirtyStores",O["default"].Set())}function y(t){return t}function S(t,e){var n=y(e);return t.getIn(["cache",n])}function g(t,e){var n=S(t,e);if(!n)return!1;var r=n.get("storeStates");return 0===r.size?!1:r.every(function(e,n){return t.getIn(["storeStates",n])===e})}function m(t,e,n){var r=y(e),i=t.get("dispatchId"),o=(0,C.getStoreDeps)(e),u=(0,D.toImmutable)({}).withMutations(function(e){o.forEach(function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)})});return t.setIn(["cache",r],O["default"].Map({value:n,storeStates:u,dispatchId:i}))}function E(t,e){var n=y(e);return t.getIn(["cache",n,"value"])}function b(t){return t.update("dispatchId",function(t){return t+1})}function I(t,e){return t.withMutations(function(t){e.forEach(function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)})})}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var w=n(3),O=r(w),T=n(9),A=r(T),D=n(5),C=n(10),z=n(11),R=n(4),M=O["default"].Record({result:null,reactorState:null})},function(t,e,n){var r=n(8);e.dispatchStart=function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},e.dispatchError=function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},e.dispatchEnd=function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}},function(t,e,n){function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h["default"].Set());var n=h["default"].Set().withMutations(function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach(function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}})});return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map(function(t){return t.first()}).filter(function(t){return!!t});return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e["default"]={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e["default"]},function(t,e,n){function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a["default"].List(t),r=a["default"].List(e);return a["default"].is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=i;var o=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=o;var u=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,r.Map)(),storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:i});e.ReactorState=u;var a=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=a}])})}),Re=ze&&"object"===("undefined"==typeof ze?"undefined":De(ze))&&"default"in ze?ze["default"]:ze,Me=o(function(t){var e=function(t){var e,n={};if(!(t instanceof Object)||Array.isArray(t))throw new Error("keyMirror(...): Argument must be an object.");for(e in t)t.hasOwnProperty(e)&&(n[e]=e);return n};t.exports=e}),je=Me&&"object"===("undefined"==typeof Me?"undefined":De(Me))&&"default"in Me?Me["default"]:Me,ke=je({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),Le=Re.Store,Ne=Re.toImmutable,Ue=new Le({getInitialState:function(){return Ne({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(ke.VALIDATING_AUTH_TOKEN,u),this.on(ke.VALID_AUTH_TOKEN,a),this.on(ke.INVALID_AUTH_TOKEN,s)}}),He=Re.Store,Pe=Re.toImmutable,xe=new He({getInitialState:function(){return Pe({authToken:null,host:""})},initialize:function(){this.on(ke.VALID_AUTH_TOKEN,c),this.on(ke.LOG_OUT,f)}}),Ve=Re.Store,qe=new Ve({getInitialState:function(){return!0},initialize:function(){this.on(ke.VALID_AUTH_TOKEN,h)}}),Fe=je({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Ge="object"===("undefined"==typeof window?"undefined":De(window))&&"EventSource"in window,Ke=Re.Store,Ye=Re.toImmutable,Be=new Ke({getInitialState:function(){return Ye({isSupported:Ge,isStreaming:!1,useStreaming:!0,hasError:!1})},initialize:function(){this.on(Fe.STREAM_START,l),this.on(Fe.STREAM_STOP,p),this.on(Fe.STREAM_ERROR,_),this.on(Fe.LOG_OUT,d)}}),Je=o(function(t){function e(){return(new Date).getTime()}t.exports=Date.now||e}),We=Je&&"object"===("undefined"==typeof Je?"undefined":De(Je))&&"default"in Je?Je["default"]:Je,Xe=o(function(t){var e=We;t.exports=function(t,n,r){function i(){var f=e()-s;n>f&&f>0?o=setTimeout(i,n-f):(o=null,r||(c=t.apply(a,u),o||(a=u=null)))}var o,u,a,s,c;return null==n&&(n=100),function(){a=this,u=arguments,s=e();var f=r&&!o;return o||(o=setTimeout(i,n)),f&&(c=t.apply(a,u),a=u=null),c}}}),Qe=Xe&&"object"===("undefined"==typeof Xe?"undefined":De(Xe))&&"default"in Xe?Xe["default"]:Xe,Ze=je({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),$e=Re.Store,tn=new $e({getInitialState:function(){return!0},initialize:function(){this.on(Ze.API_FETCH_ALL_START,function(){return!0}),this.on(Ze.API_FETCH_ALL_SUCCESS,function(){return!1}),this.on(Ze.API_FETCH_ALL_FAIL,function(){return!1}),this.on(Ze.LOG_OUT,function(){return!1})}}),en=Re.Store,nn=new en({getInitialState:function(){return!1},initialize:function(){this.on(Ze.SYNC_SCHEDULED,function(){return!0}),this.on(Ze.SYNC_SCHEDULE_CANCELLED,function(){return!1}),this.on(Ze.LOG_OUT,function(){return!1})}}),rn=je({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}),on=Re.Store,un=Re.toImmutable,an=new on({getInitialState:function(){return un({})},initialize:function(){var t=this;this.on(rn.API_FETCH_SUCCESS,v),this.on(rn.API_SAVE_SUCCESS,v),this.on(rn.API_DELETE_SUCCESS,y),this.on(rn.LOG_OUT,function(){return t.getInitialState()})}}),sn=o(function(t){function e(t){if(null===t||void 0===t)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(t)}function n(){try{if(!Object.assign)return!1;var t=new String("abc");if(t[5]="de","5"===Object.getOwnPropertyNames(t)[0])return!1;for(var e={},n=0;10>n;n++)e["_"+String.fromCharCode(n)]=n;var r=Object.getOwnPropertyNames(e).map(function(t){return e[t]});if("0123456789"!==r.join(""))return!1;var i={};return"abcdefghijklmnopqrst".split("").forEach(function(t){i[t]=t}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},i)).join("")}catch(o){return!1}}var r=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;t.exports=n()?Object.assign:function(t,n){for(var o,u,a=e(t),s=1;so&&(e=t[o]);o++)e._distributeContent();this.shadyRoot._dirtyRoots=[]},_finishDistribute:function(){if(this._useContent){if(this.shadyRoot._distributionClean=!0,h.hasInsertionPoint(this.shadyRoot))this._composeTree(),d(this.shadyRoot);else if(this.shadyRoot._hasDistributed){var e=this._composeNode(this);this._updateChildNodes(this,e)}else u.Composed.clearChildNodes(this),this.appendChild(this.shadyRoot);this.shadyRoot._hasDistributed||a(this),this.shadyRoot._hasDistributed=!0}},elementMatches:function(e,t){return t=t||this,h.matchesSelector.call(t,e)},_resetDistribution:function(){for(var e=u.Logical.getChildNodes(this),o=0;os&&(i=n[s]);s++)this._distributeInsertionPoint(i,t),o(i,this)},_distributeInsertionPoint:function(t,o){for(var i,n=!1,s=0,r=o.length;r>s;s++)i=o[s],i&&this._matchesContentSelect(i,t)&&(e(i,t),o[s]=void 0,n=!0);if(!n)for(var d=u.Logical.getChildNodes(t),a=0;ai&&(e=o[i]);i++)t=u.Logical.getParentNode(e),t._useContent||t===this||t===this.shadyRoot||this._updateChildNodes(t,this._composeNode(t))},_composeNode:function(e){for(var t=[],o=u.Logical.getChildNodes(e.shadyRoot||e),s=0;s0?~setTimeout(e,t):(this._twiddle.textContent=this._twiddleContent++,this._callbacks.push(e),this._currVal++)},cancel:function(e){if(0>e)clearTimeout(~e);else{var t=e-this._lastVal;if(t>=0){if(!this._callbacks[t])throw"invalid async handle: "+e;this._callbacks[t]=null}}},_atEndOfMicrotask:function(){for(var e=this._callbacks.length,t=0;e>t;t++){var o=this._callbacks[t];if(o)try{o()}catch(i){throw t++,this._callbacks.splice(0,t),this._lastVal+=t,this._twiddle.textContent=this._twiddleContent++,i}}this._callbacks.splice(0,e),this._lastVal+=e}},new window.MutationObserver(function(){Polymer.Async._atEndOfMicrotask()}).observe(Polymer.Async._twiddle,{characterData:!0}),Polymer.Debounce=function(){function e(e,t,i){return e?e.stop():e=new o(this),e.go(t,i),e}var t=Polymer.Async,o=function(e){this.context=e;var t=this;this.boundComplete=function(){t.complete()}};return o.prototype={go:function(e,o){var i;this.finish=function(){t.cancel(i)},i=t.run(this.boundComplete,o),this.callback=e},stop:function(){this.finish&&(this.finish(),this.finish=null)},complete:function(){this.finish&&(this.stop(),this.callback.call(this.context))}},e}(),Polymer.Base._addFeature({_setupDebouncers:function(){this._debouncers={}},debounce:function(e,t,o){return this._debouncers[e]=Polymer.Debounce.call(this,this._debouncers[e],t,o)},isDebouncerActive:function(e){var t=this._debouncers[e];return!(!t||!t.finish)},flushDebouncer:function(e){var t=this._debouncers[e];t&&t.complete()},cancelDebouncer:function(e){var t=this._debouncers[e];t&&t.stop()}}),Polymer.DomModule=document.createElement("dom-module"),Polymer.Base._addFeature({_registerFeatures:function(){this._prepIs(),this._prepBehaviors(),this._prepConstructor(),this._prepTemplate(),this._prepShady(),this._prepPropertyInfo()},_prepBehavior:function(e){this._addHostAttributes(e.hostAttributes)},_initFeatures:function(){this._registerHost(),this._template&&(this._poolContent(),this._beginHosting(),this._stampTemplate(),this._endHosting()),this._marshalHostAttributes(),this._setupDebouncers(),this._marshalBehaviors(),this._tryReady()},_marshalBehavior:function(e){}})- \ No newline at end of file +return this._week.doy}function Kt(e){var t=this.localeData().week(this);return null==e?t:this.add(7*(e-t),"d")}function en(e){var t=Oe(this,1,4).week;return null==e?t:this.add(7*(e-t),"d")}function tn(e,t){return"string"!=typeof e?e:isNaN(e)?(e=t.weekdaysParse(e),"number"==typeof e?e:null):parseInt(e,10)}function nn(e,t){return i(this._weekdays)?this._weekdays[e.day()]:this._weekdays[this._weekdays.isFormat.test(t)?"format":"standalone"][e.day()]}function rn(e){return this._weekdaysShort[e.day()]}function an(e){return this._weekdaysMin[e.day()]}function sn(e,t,n){var i,r,a,s=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;7>i;++i)a=u([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(a,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(a,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(a,"").toLocaleLowerCase();return n?"dddd"===t?(r=mi.call(this._weekdaysParse,s),-1!==r?r:null):"ddd"===t?(r=mi.call(this._shortWeekdaysParse,s),-1!==r?r:null):(r=mi.call(this._minWeekdaysParse,s),-1!==r?r:null):"dddd"===t?(r=mi.call(this._weekdaysParse,s),-1!==r?r:(r=mi.call(this._shortWeekdaysParse,s),-1!==r?r:(r=mi.call(this._minWeekdaysParse,s),-1!==r?r:null))):"ddd"===t?(r=mi.call(this._shortWeekdaysParse,s),-1!==r?r:(r=mi.call(this._weekdaysParse,s),-1!==r?r:(r=mi.call(this._minWeekdaysParse,s),-1!==r?r:null))):(r=mi.call(this._minWeekdaysParse,s),-1!==r?r:(r=mi.call(this._weekdaysParse,s),-1!==r?r:(r=mi.call(this._shortWeekdaysParse,s),-1!==r?r:null)))}function on(e,t,n){var i,r,a;if(this._weekdaysParseExact)return sn.call(this,e,t,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;7>i;i++){if(r=u([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(r,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(r,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(r,"").replace(".",".?")+"$","i")),this._weekdaysParse[i]||(a="^"+this.weekdays(r,"")+"|^"+this.weekdaysShort(r,"")+"|^"+this.weekdaysMin(r,""),this._weekdaysParse[i]=new RegExp(a.replace(".",""),"i")),n&&"dddd"===t&&this._fullWeekdaysParse[i].test(e))return i;if(n&&"ddd"===t&&this._shortWeekdaysParse[i].test(e))return i;if(n&&"dd"===t&&this._minWeekdaysParse[i].test(e))return i;if(!n&&this._weekdaysParse[i].test(e))return i}}function un(e){if(!this.isValid())return null!=e?this:NaN;var t=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=e?(e=tn(e,this.localeData()),this.add(e-t,"d")):t}function cn(e){if(!this.isValid())return null!=e?this:NaN;var t=(this.day()+7-this.localeData()._week.dow)%7;return null==e?t:this.add(e-t,"d")}function ln(e){return this.isValid()?null==e?this.day()||7:this.day(this.day()%7?e:e-7):null!=e?this:NaN}function dn(e){return this._weekdaysParseExact?(s(this,"_weekdaysRegex")||mn.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex}function hn(e){return this._weekdaysParseExact?(s(this,"_weekdaysRegex")||mn.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex}function fn(e){return this._weekdaysParseExact?(s(this,"_weekdaysRegex")||mn.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex}function mn(){function e(e,t){return t.length-e.length}var t,n,i,r,a,s=[],o=[],c=[],l=[];for(t=0;7>t;t++)n=u([2e3,1]).day(t),i=this.weekdaysMin(n,""),r=this.weekdaysShort(n,""),a=this.weekdays(n,""),s.push(i),o.push(r),c.push(a),l.push(i),l.push(r),l.push(a);for(s.sort(e),o.sort(e),c.sort(e),l.sort(e),t=0;7>t;t++)o[t]=X(o[t]),c[t]=X(c[t]),l[t]=X(l[t]);this._weekdaysRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+c.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+s.join("|")+")","i")}function pn(e){var t=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?t:this.add(e-t,"d")}function yn(){return this.hours()%12||12}function gn(){return this.hours()||24}function vn(e,t){R(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function _n(e,t){return t._meridiemParse}function bn(e){return"p"===(e+"").toLowerCase().charAt(0)}function wn(e,t,n){return e>11?n?"pm":"PM":n?"am":"AM"}function Sn(e,t){t[zi]=v(1e3*("0."+e))}function On(){return this._isUTC?"UTC":""}function kn(){return this._isUTC?"Coordinated Universal Time":""}function Dn(e){return He(1e3*e)}function Cn(){return He.apply(null,arguments).parseZone()}function Mn(e,t,n){var i=this._calendar[e];return O(i)?i.call(t,n):i}function jn(e){var t=this._longDateFormat[e],n=this._longDateFormat[e.toUpperCase()];return t||!n?t:(this._longDateFormat[e]=n.replace(/MMMM|MM|DD|dddd/g,function(e){return e.slice(1)}),this._longDateFormat[e])}function Tn(){return this._invalidDate}function xn(e){return this._ordinal.replace("%d",e)}function Yn(e){return e}function Pn(e,t,n,i){var r=this._relativeTime[n];return O(r)?r(e,t,n,i):r.replace(/%d/i,e)}function In(e,t){var n=this._relativeTime[e>0?"future":"past"];return O(n)?n(t):n.replace(/%s/i,t)}function An(e,t,n,i){var r=A(),a=u().set(i,t);return r[n](a,e)}function Nn(e,t,n){if("number"==typeof e&&(t=e,e=void 0),e=e||"",null!=t)return An(e,t,n,"month");var i,r=[];for(i=0;12>i;i++)r[i]=An(e,i,n,"month");return r}function Bn(e,t,n,i){"boolean"==typeof e?("number"==typeof t&&(n=t,t=void 0),t=t||""):(t=e,n=t,e=!1,"number"==typeof t&&(n=t,t=void 0),t=t||"");var r=A(),a=e?r._week.dow:0;if(null!=n)return An(t,(n+a)%7,i,"day");var s,o=[];for(s=0;7>s;s++)o[s]=An(t,(s+a)%7,i,"day");return o}function Hn(e,t){return Nn(e,t,"months")}function Ln(e,t){return Nn(e,t,"monthsShort")}function Gn(e,t,n){return Bn(e,t,n,"weekdays")}function En(e,t,n){return Bn(e,t,n,"weekdaysShort")}function Wn(e,t,n){return Bn(e,t,n,"weekdaysMin")}function Vn(){var e=this._data;return this._milliseconds=Gr(this._milliseconds),this._days=Gr(this._days),this._months=Gr(this._months),e.milliseconds=Gr(e.milliseconds),e.seconds=Gr(e.seconds),e.minutes=Gr(e.minutes),e.hours=Gr(e.hours),e.months=Gr(e.months),e.years=Gr(e.years),this}function Fn(e,t,n,i){var r=rt(t,n);return e._milliseconds+=i*r._milliseconds,e._days+=i*r._days,e._months+=i*r._months,e._bubble()}function Rn(e,t){return Fn(this,e,t,1)}function Un(e,t){return Fn(this,e,t,-1)}function zn(e){return 0>e?Math.floor(e):Math.ceil(e)}function $n(){var e,t,n,i,r,a=this._milliseconds,s=this._days,o=this._months,u=this._data;return a>=0&&s>=0&&o>=0||0>=a&&0>=s&&0>=o||(a+=864e5*zn(Jn(o)+s),s=0,o=0),u.milliseconds=a%1e3,e=g(a/1e3),u.seconds=e%60,t=g(e/60),u.minutes=t%60,n=g(t/60),u.hours=n%24,s+=g(n/24),r=g(Zn(s)),o+=r,s-=zn(Jn(r)),i=g(o/12),o%=12,u.days=s,u.months=o,u.years=i,this}function Zn(e){return 4800*e/146097}function Jn(e){return 146097*e/4800}function qn(e){var t,n,i=this._milliseconds;if(e=H(e),"month"===e||"year"===e)return t=this._days+i/864e5,n=this._months+Zn(t),"month"===e?n:n/12;switch(t=this._days+Math.round(Jn(this._months)),e){case"week":return t/7+i/6048e5;case"day":return t+i/864e5;case"hour":return 24*t+i/36e5;case"minute":return 1440*t+i/6e4;case"second":return 86400*t+i/1e3;case"millisecond":return Math.floor(864e5*t)+i;default:throw new Error("Unknown unit "+e)}}function Qn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*v(this._months/12)}function Xn(e){return function(){return this.as(e)}}function Kn(e){return e=H(e),this[e+"s"]()}function ei(e){return function(){return this._data[e]}}function ti(){return g(this.days()/7)}function ni(e,t,n,i,r){return r.relativeTime(t||1,!!n,e,i)}function ii(e,t,n){var i=rt(e).abs(),r=ta(i.as("s")),a=ta(i.as("m")),s=ta(i.as("h")),o=ta(i.as("d")),u=ta(i.as("M")),c=ta(i.as("y")),l=r=a&&["m"]||a=s&&["h"]||s=o&&["d"]||o=u&&["M"]||u=c&&["y"]||["yy",c];return l[2]=t,l[3]=+e>0,l[4]=n,ni.apply(null,l)}function ri(e,t){return void 0===na[e]?!1:void 0===t?na[e]:(na[e]=t,!0)}function ai(e){var t=this.localeData(),n=ii(this,!e,t);return e&&(n=t.pastFuture(+this,n)),t.postformat(n)}function si(){var e,t,n,i=ia(this._milliseconds)/1e3,r=ia(this._days),a=ia(this._months);e=g(i/60),t=g(e/60),i%=60,e%=60,n=g(a/12),a%=12;var s=n,o=a,u=r,c=t,l=e,d=i,h=this.asSeconds();return h?(0>h?"-":"")+"P"+(s?s+"Y":"")+(o?o+"M":"")+(u?u+"D":"")+(c||l||d?"T":"")+(c?c+"H":"")+(l?l+"M":"")+(d?d+"S":""):"P0D"}var oi,ui;ui=Array.prototype.some?Array.prototype.some:function(e){for(var t=Object(this),n=t.length>>>0,i=0;n>i;i++)if(i in t&&e.call(this,t[i],i,t))return!0;return!1};var ci=t.momentProperties=[],li=!1,di={};t.suppressDeprecationWarnings=!1,t.deprecationHandler=null;var hi;hi=Object.keys?Object.keys:function(e){var t,n=[];for(t in e)s(e,t)&&n.push(t);return n};var fi,mi,pi={},yi={},gi=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,vi=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,_i={},bi={},wi=/\d/,Si=/\d\d/,Oi=/\d{3}/,ki=/\d{4}/,Di=/[+-]?\d{6}/,Ci=/\d\d?/,Mi=/\d\d\d\d?/,ji=/\d\d\d\d\d\d?/,Ti=/\d{1,3}/,xi=/\d{1,4}/,Yi=/[+-]?\d{1,6}/,Pi=/\d+/,Ii=/[+-]?\d+/,Ai=/Z|[+-]\d\d:?\d\d/gi,Ni=/Z|[+-]\d\d(?::?\d\d)?/gi,Bi=/[+-]?\d+(\.\d{1,3})?/,Hi=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Li={},Gi={},Ei=0,Wi=1,Vi=2,Fi=3,Ri=4,Ui=5,zi=6,$i=7,Zi=8;mi=Array.prototype.indexOf?Array.prototype.indexOf:function(e){var t;for(t=0;t=e?""+e:"+"+e}),R(0,["YY",2],0,function(){return this.year()%100}),R(0,["YYYY",4],0,"year"),R(0,["YYYYY",5],0,"year"),R(0,["YYYYYY",6,!0],0,"year"),B("year","y"),J("Y",Ii),J("YY",Ci,Si),J("YYYY",xi,ki),J("YYYYY",Yi,Di),J("YYYYYY",Yi,Di),K(["YYYYY","YYYYYY"],Ei),K("YYYY",function(e,n){n[Ei]=2===e.length?t.parseTwoDigitYear(e):v(e)}),K("YY",function(e,n){n[Ei]=t.parseTwoDigitYear(e)}),K("Y",function(e,t){t[Ei]=parseInt(e,10)}),t.parseTwoDigitYear=function(e){return v(e)+(v(e)>68?1900:2e3)};var sr=G("FullYear",!0);t.ISO_8601=function(){};var or=w("moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var e=He.apply(null,arguments);return this.isValid()&&e.isValid()?this>e?this:e:h()}),ur=w("moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var e=He.apply(null,arguments);return this.isValid()&&e.isValid()?e>this?this:e:h()}),cr=function(){return Date.now?Date.now():+new Date};Fe("Z",":"),Fe("ZZ",""),J("Z",Ni),J("ZZ",Ni),K(["Z","ZZ"],function(e,t,n){n._useUTC=!0,n._tzm=Re(Ni,e)});var lr=/([\+\-]|\d\d)/gi;t.updateOffset=function(){};var dr=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/,hr=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;rt.fn=We.prototype;var fr=ct(1,"add"),mr=ct(-1,"subtract");t.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",t.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var pr=w("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(e){return void 0===e?this.localeData():this.locale(e)});R(0,["gg",2],0,function(){return this.weekYear()%100}),R(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Vt("gggg","weekYear"),Vt("ggggg","weekYear"),Vt("GGGG","isoWeekYear"),Vt("GGGGG","isoWeekYear"),B("weekYear","gg"),B("isoWeekYear","GG"),J("G",Ii),J("g",Ii),J("GG",Ci,Si),J("gg",Ci,Si),J("GGGG",xi,ki),J("gggg",xi,ki),J("GGGGG",Yi,Di),J("ggggg",Yi,Di),ee(["gggg","ggggg","GGGG","GGGGG"],function(e,t,n,i){t[i.substr(0,2)]=v(e)}),ee(["gg","GG"],function(e,n,i,r){n[r]=t.parseTwoDigitYear(e)}),R("Q",0,"Qo","quarter"),B("quarter","Q"),J("Q",wi),K("Q",function(e,t){t[Wi]=3*(v(e)-1)}),R("w",["ww",2],"wo","week"),R("W",["WW",2],"Wo","isoWeek"),B("week","w"),B("isoWeek","W"),J("w",Ci),J("ww",Ci,Si),J("W",Ci),J("WW",Ci,Si),ee(["w","ww","W","WW"],function(e,t,n,i){t[i.substr(0,1)]=v(e)});var yr={dow:0,doy:6};R("D",["DD",2],"Do","date"),B("date","D"),J("D",Ci),J("DD",Ci,Si),J("Do",function(e,t){return e?t._ordinalParse:t._ordinalParseLenient}),K(["D","DD"],Vi),K("Do",function(e,t){t[Vi]=v(e.match(Ci)[0],10)});var gr=G("Date",!0);R("d",0,"do","day"),R("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),R("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),R("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),R("e",0,0,"weekday"),R("E",0,0,"isoWeekday"),B("day","d"),B("weekday","e"),B("isoWeekday","E"),J("d",Ci),J("e",Ci),J("E",Ci),J("dd",function(e,t){return t.weekdaysMinRegex(e)}),J("ddd",function(e,t){return t.weekdaysShortRegex(e)}),J("dddd",function(e,t){return t.weekdaysRegex(e)}),ee(["dd","ddd","dddd"],function(e,t,n,i){var r=n._locale.weekdaysParse(e,i,n._strict);null!=r?t.d=r:l(n).invalidWeekday=e}),ee(["d","e","E"],function(e,t,n,i){t[i]=v(e)});var vr="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),_r="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),br="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),wr=Hi,Sr=Hi,Or=Hi;R("DDD",["DDDD",3],"DDDo","dayOfYear"),B("dayOfYear","DDD"),J("DDD",Ti),J("DDDD",Oi),K(["DDD","DDDD"],function(e,t,n){n._dayOfYear=v(e)}),R("H",["HH",2],0,"hour"),R("h",["hh",2],0,yn),R("k",["kk",2],0,gn),R("hmm",0,0,function(){return""+yn.apply(this)+F(this.minutes(),2)}),R("hmmss",0,0,function(){return""+yn.apply(this)+F(this.minutes(),2)+F(this.seconds(),2)}),R("Hmm",0,0,function(){return""+this.hours()+F(this.minutes(),2)}),R("Hmmss",0,0,function(){return""+this.hours()+F(this.minutes(),2)+F(this.seconds(),2)}),vn("a",!0),vn("A",!1),B("hour","h"),J("a",_n),J("A",_n),J("H",Ci),J("h",Ci),J("HH",Ci,Si),J("hh",Ci,Si),J("hmm",Mi),J("hmmss",ji),J("Hmm",Mi),J("Hmmss",ji),K(["H","HH"],Fi),K(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),K(["h","hh"],function(e,t,n){t[Fi]=v(e),l(n).bigHour=!0}),K("hmm",function(e,t,n){var i=e.length-2;t[Fi]=v(e.substr(0,i)),t[Ri]=v(e.substr(i)),l(n).bigHour=!0}),K("hmmss",function(e,t,n){var i=e.length-4,r=e.length-2;t[Fi]=v(e.substr(0,i)),t[Ri]=v(e.substr(i,2)),t[Ui]=v(e.substr(r)),l(n).bigHour=!0}),K("Hmm",function(e,t,n){var i=e.length-2;t[Fi]=v(e.substr(0,i)),t[Ri]=v(e.substr(i))}),K("Hmmss",function(e,t,n){var i=e.length-4,r=e.length-2;t[Fi]=v(e.substr(0,i)),t[Ri]=v(e.substr(i,2)),t[Ui]=v(e.substr(r))});var kr=/[ap]\.?m?\.?/i,Dr=G("Hours",!0);R("m",["mm",2],0,"minute"),B("minute","m"),J("m",Ci),J("mm",Ci,Si),K(["m","mm"],Ri);var Cr=G("Minutes",!1);R("s",["ss",2],0,"second"),B("second","s"),J("s",Ci),J("ss",Ci,Si),K(["s","ss"],Ui);var Mr=G("Seconds",!1);R("S",0,0,function(){return~~(this.millisecond()/100)}),R(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),R(0,["SSS",3],0,"millisecond"),R(0,["SSSS",4],0,function(){return 10*this.millisecond()}),R(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),R(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),R(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),R(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),R(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),B("millisecond","ms"),J("S",Ti,wi),J("SS",Ti,Si),J("SSS",Ti,Oi);var jr;for(jr="SSSS";jr.length<=9;jr+="S")J(jr,Pi);for(jr="S";jr.length<=9;jr+="S")K(jr,Sn);var Tr=G("Milliseconds",!1);R("z",0,0,"zoneAbbr"),R("zz",0,0,"zoneName");var xr=p.prototype;xr.add=fr,xr.calendar=dt,xr.clone=ht,xr.diff=_t,xr.endOf=Yt,xr.format=Ot,xr.from=kt,xr.fromNow=Dt,xr.to=Ct,xr.toNow=Mt,xr.get=V,xr.invalidAt=Et,xr.isAfter=ft,xr.isBefore=mt,xr.isBetween=pt,xr.isSame=yt,xr.isSameOrAfter=gt,xr.isSameOrBefore=vt,xr.isValid=Lt,xr.lang=pr,xr.locale=jt,xr.localeData=Tt,xr.max=ur,xr.min=or,xr.parsingFlags=Gt,xr.set=V,xr.startOf=xt,xr.subtract=mr,xr.toArray=Nt,xr.toObject=Bt,xr.toDate=At,xr.toISOString=St,xr.toJSON=Ht,xr.toString=wt,xr.unix=It,xr.valueOf=Pt,xr.creationData=Wt,xr.year=sr,xr.isLeapYear=be,xr.weekYear=Ft,xr.isoWeekYear=Rt,xr.quarter=xr.quarters=Jt,xr.month=ue,xr.daysInMonth=ce,xr.week=xr.weeks=Kt,xr.isoWeek=xr.isoWeeks=en,xr.weeksInYear=zt,xr.isoWeeksInYear=Ut,xr.date=gr,xr.day=xr.days=un,xr.weekday=cn,xr.isoWeekday=ln,xr.dayOfYear=pn,xr.hour=xr.hours=Dr,xr.minute=xr.minutes=Cr,xr.second=xr.seconds=Mr,xr.millisecond=xr.milliseconds=Tr,xr.utcOffset=$e,xr.utc=Je,xr.local=qe,xr.parseZone=Qe,xr.hasAlignedHourOffset=Xe,xr.isDST=Ke,xr.isDSTShifted=et,xr.isLocal=tt,xr.isUtcOffset=nt,xr.isUtc=it,xr.isUTC=it,xr.zoneAbbr=On,xr.zoneName=kn,xr.dates=w("dates accessor is deprecated. Use date instead.",gr),xr.months=w("months accessor is deprecated. Use month instead",ue),xr.years=w("years accessor is deprecated. Use year instead",sr),xr.zone=w("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Ze);var Yr=xr,Pr={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Ir={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"},Ar="Invalid date",Nr="%d",Br=/\d{1,2}/,Hr={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"},Lr=M.prototype;Lr._calendar=Pr,Lr.calendar=Mn,Lr._longDateFormat=Ir,Lr.longDateFormat=jn,Lr._invalidDate=Ar,Lr.invalidDate=Tn,Lr._ordinal=Nr,Lr.ordinal=xn,Lr._ordinalParse=Br,Lr.preparse=Yn,Lr.postformat=Yn,Lr._relativeTime=Hr,Lr.relativeTime=Pn,Lr.pastFuture=In,Lr.set=D,Lr.months=ie,Lr._months=qi,Lr.monthsShort=re,Lr._monthsShort=Qi,Lr.monthsParse=se,Lr._monthsRegex=Ki,Lr.monthsRegex=de,Lr._monthsShortRegex=Xi,Lr.monthsShortRegex=le,Lr.week=qt,Lr._week=yr,Lr.firstDayOfYear=Xt,Lr.firstDayOfWeek=Qt,Lr.weekdays=nn,Lr._weekdays=vr,Lr.weekdaysMin=an,Lr._weekdaysMin=br,Lr.weekdaysShort=rn,Lr._weekdaysShort=_r,Lr.weekdaysParse=on,Lr._weekdaysRegex=wr,Lr.weekdaysRegex=dn,Lr._weekdaysShortRegex=Sr,Lr.weekdaysShortRegex=hn,Lr._weekdaysMinRegex=Or,Lr.weekdaysMinRegex=fn,Lr.isPM=bn,Lr._meridiemParse=kr,Lr.meridiem=wn,Y("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10,n=1===v(e%100/10)?"th":1===t?"st":2===t?"nd":3===t?"rd":"th";return e+n}}),t.lang=w("moment.lang is deprecated. Use moment.locale instead.",Y),t.langData=w("moment.langData is deprecated. Use moment.localeData instead.",A);var Gr=Math.abs,Er=Xn("ms"),Wr=Xn("s"),Vr=Xn("m"),Fr=Xn("h"),Rr=Xn("d"),Ur=Xn("w"),zr=Xn("M"),$r=Xn("y"),Zr=ei("milliseconds"),Jr=ei("seconds"),qr=ei("minutes"),Qr=ei("hours"),Xr=ei("days"),Kr=ei("months"),ea=ei("years"),ta=Math.round,na={s:45,m:45,h:22,d:26,M:11},ia=Math.abs,ra=We.prototype;ra.abs=Vn,ra.add=Rn,ra.subtract=Un,ra.as=qn,ra.asMilliseconds=Er,ra.asSeconds=Wr,ra.asMinutes=Vr,ra.asHours=Fr,ra.asDays=Rr,ra.asWeeks=Ur,ra.asMonths=zr,ra.asYears=$r,ra.valueOf=Qn,ra._bubble=$n,ra.get=Kn,ra.milliseconds=Zr,ra.seconds=Jr,ra.minutes=qr,ra.hours=Qr,ra.days=Xr,ra.weeks=ti,ra.months=Kr,ra.years=ea,ra.humanize=ai,ra.toISOString=si,ra.toString=si,ra.toJSON=si,ra.locale=jt,ra.localeData=Tt,ra.toIsoString=w("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",si),ra.lang=pr,R("X",0,0,"unix"),R("x",0,0,"valueOf"),J("x",Ii),J("X",Bi),K("X",function(e,t,n){n._d=new Date(1e3*parseFloat(e,10))}),K("x",function(e,t,n){n._d=new Date(v(e))}),t.version="2.13.0",n(He),t.fn=Yr,t.min=Ge,t.max=Ee,t.now=cr,t.utc=u,t.unix=Dn,t.months=Hn,t.isDate=r,t.locale=Y,t.invalid=h,t.duration=rt,t.isMoment=y,t.weekdays=Gn,t.parseZone=Cn,t.localeData=A,t.isDuration=Ve,t.monthsShort=Ln,t.weekdaysMin=Wn,t.defineLocale=P,t.updateLocale=I,t.locales=N,t.weekdaysShort=En,t.normalizeUnits=H,t.relativeTimeThreshold=ri,t.prototype=Yr;var aa=t;return aa})}).call(t,n(73)(e))},,,,,,,function(e,t,n){"use strict";var i=n(0);n(35),new i.a({is:"ha-badges-card",properties:{hass:{type:Object},states:{type:Array}}})},function(e,t,n){"use strict";var i=n(0),r=1e4;new i.a({is:"ha-camera-card",properties:{hass:{type:Object},stateObj:{type:Object,observer:"updateCameraFeedSrc"},cameraFeedSrc:{type:String},imageLoaded:{type:Boolean,value:!0},elevation:{type:Number,value:1,reflectToAttribute:!0}},listeners:{tap:"cardTapped"},attached:function(){var e=this;this.timer=setInterval(function(){return e.updateCameraFeedSrc(e.stateObj)},r)},detached:function(){clearInterval(this.timer)},cardTapped:function(){var e=this;this.async(function(){return e.hass.moreInfoActions.selectEntity(e.stateObj.entityId)},1)},updateCameraFeedSrc:function(e){var t=e.attributes,n=(new Date).getTime();this.cameraFeedSrc=t.entity_picture+"&time="+n},imageLoadSuccess:function(){this.imageLoaded=!0},imageLoadFail:function(){this.imageLoaded=!1}})},function(e,t,n){"use strict";var i=n(0),r=n(3);n(27),n(29),n(30),new i.a({is:"ha-card-chooser",properties:{cardData:{type:Object,observer:"cardDataChanged"}},cardDataChanged:function(e){e&&n.i(r.a)(this,"HA-"+e.cardType.toUpperCase()+"-CARD",e)}})},function(e,t,n){"use strict";var i=n(0),r=n(9);n(33),n(4),new i.a({is:"ha-entities-card",properties:{hass:{type:Object},states:{type:Array},groupEntity:{type:Object}},computeTitle:function(e,t){return t?t.entityDisplay:e[0].domain.replace(/_/g," ")},computeTitleClass:function(e){var t="header horizontal layout center ";return e&&(t+="header-more-info"),t},entityTapped:function(e){var t=this;if(!e.target.classList.contains("paper-toggle-button")&&!e.target.classList.contains("paper-icon-button")&&(e.model||this.groupEntity)){e.stopPropagation();var n=void 0;n=e.model?e.model.item.entityId:this.groupEntity.entityId,this.async(function(){return t.hass.moreInfoActions.selectEntity(n)},1)}},showGroupToggle:function(e,t){var i=this;return!e||!t||"on"!==e.state&&"off"!==e.state?!1:t.reduce(function(e,t){return e+n.i(r.a)(i.hass,t.entityId)},0)>1}})},function(e,t,n){"use strict";var i=n(70),r=i&&i.__esModule?function(){return i["default"]}:function(){return i};n.d(r,"a",r);var a=n(0);new a.a({is:"ha-media_player-card",properties:{hass:{type:Object},stateObj:{type:Object},playerObj:{type:Object,computed:"computePlayerObj(stateObj)",observer:"playerObjChanged"},playbackControlIcon:{type:String,computed:"computePlaybackControlIcon(playerObj)"},elevation:{type:Number,value:1,reflectToAttribute:!0}},playerObjChanged:function(e){e.isOff||e.isIdle||(this.$.cover.style.backgroundImage=e.stateObj.attributes.entity_picture?"url("+e.stateObj.attributes.entity_picture+")":"")},computeBannerClasses:function(e){return r()({banner:!0,"is-off":e.isOff||e.isIdle,"no-cover":!e.stateObj.attributes.entity_picture})},computeHidePowerOnButton:function(e){return!e.isOff||!e.supportsTurnOn},computePlayerObj:function(e){return e.domainModel(this.hass)},computePlaybackControlIcon:function(e){return e.isPlaying?e.supportsPause?"mdi:pause":"mdi:stop":e.isPaused||e.isOff?"mdi:play":""},computeShowControls:function(e){return!e.isOff},handleNext:function(e){e.stopPropagation(),this.playerObj.nextTrack()},handleOpenMoreInfo:function(e){var t=this;e.stopPropagation(),this.async(function(){return t.hass.moreInfoActions.selectEntity(t.stateObj.entityId)},1)},handlePlaybackControl:function(e){e.stopPropagation(),this.playerObj.mediaPlayPause()},handlePrevious:function(e){e.stopPropagation(),this.playerObj.previousTrack()},handleTogglePower:function(e){e.stopPropagation(),this.playerObj.togglePower()}})},function(e,t,n){"use strict";var i=n(0),r=n(11);new i.a({is:"display-time",properties:{dateObj:{type:Object}},computeTime:function(e){return e?n.i(r.a)(e):""}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"ha-entity-marker",properties:{hass:{type:Object},entityId:{type:String,value:"",reflectToAttribute:!0},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:{tap:"badgeTap"},badgeTap:function(e){var t=this;e.stopPropagation(),this.entityId&&this.async(function(){return window.hass.moreInfoActions.selectEntity(t.entityId)},1)},computeState:function(e){return e&&window.hass.reactor.evaluate(window.hass.entityGetters.byId(e))},computeIcon:function(e){return!e&&"home"},computeImage:function(e){return e&&e.attributes.entity_picture},computeValue:function(e){return e&&e.entityDisplay.split(" ").map(function(e){return e.substr(0,1)}).join("")}})},function(e,t,n){"use strict";var i=n(0),r=n(68);new i.a({is:"ha-entity-toggle",properties:{hass:{type:Object},stateObj:{type:Object},toggleChecked:{type:Boolean,value:!1},isOn:{type:Boolean,computed:"computeIsOn(stateObj)",observer:"isOnChanged"}},listeners:{tap:"onTap"},onTap:function(e){e.stopPropagation()},ready:function(){this.forceStateChange()},toggleChanged:function(e){var t=e.target.checked;t&&!this.isOn?this.callService(!0):!t&&this.isOn&&this.callService(!1)},isOnChanged:function(e){this.toggleChecked=e},forceStateChange:function(){this.toggleChecked===this.isOn&&(this.toggleChecked=!this.toggleChecked),this.toggleChecked=this.isOn},turnOn:function(){this.callService(!0)},turnOff:function(){this.callService(!1)},computeIsOn:function(e){return e&&-1===r.a.indexOf(e.state)},callService:function(e){var t=this,n=void 0,i=void 0;"lock"===this.stateObj.domain?(n="lock",i=e?"lock":"unlock"):"garage_door"===this.stateObj.domain?(n="garage_door",i=e?"open":"close"):(n="homeassistant",i=e?"turn_on":"turn_off");var r=this.hass.serviceActions.callService(n,i,{entity_id:this.stateObj.entityId});this.stateObj.attributes.assumed_state||r.then(function(){return t.forceStateChange()})}})},function(e,t,n){"use strict";var i=n(0),r=n(12);new i.a({is:"ha-state-icon",properties:{stateObj:{type:Object}},computeIcon:function(e){return n.i(r.a)(e)}})},function(e,t,n){"use strict";var i=n(0),r=n(5),a=n(12);new i.a({is:"ha-state-label-badge",properties:{hass:{type:Object},state:{type:Object,observer:"stateChanged"}},listeners:{tap:"badgeTap"},badgeTap:function(e){var t=this;e.stopPropagation(),this.async(function(){return t.hass.moreInfoActions.selectEntity(t.state.entityId)},1)},computeClasses:function(e){switch(e.domain){case"binary_sensor":case"updater":return"blue";default:return""}},computeValue:function(e){switch(e.domain){case"binary_sensor":case"device_tracker":case"updater":case"sun":case"alarm_control_panel":return null;case"sensor":default:return"unknown"===e.state?"-":e.state}},computeIcon:function(e){if("unavailable"===e.state)return null;switch(e.domain){case"alarm_control_panel":return"pending"===e.state?"mdi:clock-fast":"armed_away"===e.state?"mdi:nature":"armed_home"===e.state?"mdi:home-variant":n.i(r.a)(e.domain,e.state);case"binary_sensor":case"device_tracker":case"updater":return n.i(a.a)(e);case"sun":return"above_horizon"===e.state?n.i(r.a)(e.domain):"mdi:brightness-3";default:return null}},computeImage:function(e){return e.attributes.entity_picture||null},computeLabel:function(e){if("unavailable"===e.state)return"unavai";switch(e.domain){case"device_tracker":return"not_home"===e.state?"Away":e.state;case"alarm_control_panel":return"pending"===e.state?"pend":"armed_away"===e.state||"armed_home"===e.state?"armed":"disarm";default:return e.attributes.unit_of_measurement||null}},computeDescription:function(e){return e.entityDisplay},stateChanged:function(){this.updateStyles()}})},function(e,t,n){"use strict";var i=n(0);n(34),new i.a({is:"state-badge",properties:{stateObj:{type:Object,observer:"updateIconColor"}},updateIconColor:function(e){return e.attributes.entity_picture?(this.style.backgroundImage="url("+e.attributes.entity_picture+")",void(this.$.icon.style.display="none")):(this.style.backgroundImage="",this.$.icon.style.display="inline",void("light"===e.domain&&"on"===e.state&&e.attributes.rgb_color&&e.attributes.rgb_color.reduce(function(e,t){return e+t},0)<730?this.$.icon.style.color="rgb("+e.attributes.rgb_color.join(",")+")":this.$.icon.style.color=null))}})},function(e,t,n){"use strict";function i(e){return e in o?o[e]:30}function r(e){return"group"===e.domain?e.attributes.order:e.entityDisplay.toLowerCase()}var a=n(0),s=(n(26),n(28),{camera:4,media_player:3,persistent_notification:0}),o={configurator:-20,persistent_notification:-15,group:-10,a:-1,updater:0,sun:1,device_tracker:2,alarm_control_panel:3,sensor:5,binary_sensor:6};new a.a({is:"ha-cards",properties:{hass:{type:Object},showIntroduction:{type:Boolean,value:!1},columns:{type:Number,value:2},states:{type:Object},cards:{type:Object}},observers:["updateCards(columns, states, showIntroduction)"],updateCards:function(e,t,n){var i=this;this.debounce("updateCards",function(){i.cards=i.computeCards(e,t,n)},0)},computeCards:function(e,t,n){function a(e){return e.filter(function(e){return!(e.entityId in d)})}function o(e){for(var t=0,n=t;n1;var u=o(a);r.length>0&&h.columns[u].push({hass:c,cardType:"entities",states:r,groupEntity:n}),i.forEach(function(e){h.columns[u].push({hass:c,cardType:e.domain,stateObj:e})})}}for(var c=this.hass,l=t.groupBy(function(e){return e.domain}),d={},h={ +demo:!1,badges:[],columns:[]},f=[],m=0;e>m;m++)h.columns.push([]),f.push(0);n&&h.columns[o(5)].push({hass:c,cardType:"introduction",showHideInstruction:t.size>0&&!c.demo});var p=this.hass.util.expandGroup;return l.keySeq().sortBy(function(e){return i(e)}).forEach(function(e){if("a"===e)return void(h.demo=!0);var n=i(e);n>=0&&10>n?h.badges.push.apply(h.badges,a(l.get(e)).sortBy(r).toArray()):"group"===e?l.get(e).sortBy(r).forEach(function(e){var n=p(e,t);n.forEach(function(e){d[e.entityId]=!0}),u(e.entityId,n.toArray(),e)}):u(e,a(l.get(e)).sortBy(r).toArray())}),h.columns=h.columns.filter(function(e){return e.length>0}),h}})},function(e,t,n){"use strict";var i=n(0);n(7),n(31),new i.a({is:"logbook-entry",properties:{hass:{type:Object}},entityClicked:function(e){e.preventDefault(),this.hass.moreInfoActions.selectEntity(this.entryObj.entityId)}})},function(e,t,n){"use strict";var i=n(0);n(7),new i.a({is:"services-list",behaviors:[window.hassBehavior],properties:{hass:{type:Object},serviceDomains:{type:Array,bindNuclear:function(e){return e.serviceGetters.entityMap}}},computeDomains:function(e){return e.valueSeq().map(function(e){return e.domain}).sort().toJS()},computeServices:function(e,t){return e.get(t).get("services").keySeq().toArray()},serviceClicked:function(e){e.preventDefault(),this.fire("service-selected",{domain:e.model.domain,service:e.model.service})}})},function(e,t,n){"use strict";function i(e,t){for(var n=[],i=e;t>i;i++)n.push(i);return n}function r(e){var t=parseFloat(e);return!isNaN(t)&&isFinite(t)?t:null}var a=n(0);new a.a({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 e=this.unit,t=this.data;if(0!==t.length){var n={legend:{position:"top"},interpolateNulls:!0,titlePosition:"none",vAxes:{0:{title:e}},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 a=new Date(Math.min.apply(null,t.map(function(e){return e[0].lastChangedAsDate}))),s=new Date(a);s.setDate(s.getDate()+1),s>new Date&&(s=new Date);var o=t.map(function(e){function t(e,t){c&&t&&u.push([e[0]].concat(c.slice(1).map(function(e,n){return t[n]?e:null}))),u.push(e),c=e}var n=e[e.length-1],i=n.domain,a=n.entityDisplay,o=new window.google.visualization.DataTable;o.addColumn({type:"datetime",id:"Time"});var u=[],c=void 0;if("thermostat"===i){var l=e.reduce(function(e,t){return e||t.attributes.target_temp_high!==t.attributes.target_temp_low},!1);o.addColumn("number",a+" current temperature");var d=void 0;l?!function(){o.addColumn("number",a+" target temperature high"),o.addColumn("number",a+" target temperature low");var e=[!1,!0,!0];d=function(n){var i=r(n.attributes.current_temperature),a=r(n.attributes.target_temp_high),s=r(n.attributes.target_temp_low);t([n.lastUpdatedAsDate,i,a,s],e)}}():!function(){o.addColumn("number",a+" target temperature");var e=[!1,!0];d=function(n){var i=r(n.attributes.current_temperature),a=r(n.attributes.temperature);t([n.lastUpdatedAsDate,i,a],e)}}(),e.forEach(d)}else!function(){o.addColumn("number",a);var n="sensor"!==i&&[!0];e.forEach(function(e){var i=r(e.state);t([e.lastChangedAsDate,i],n)})}();return t([s].concat(c.slice(1)),!1),o.addRows(u),o}),u=void 0;u=1===o.length?o[0]:o.slice(1).reduce(function(e,t){return window.google.visualization.data.join(e,t,"full",[[0,0]],i(1,e.getNumberOfColumns()),i(1,t.getNumberOfColumns()))},o[0]),this.chartEngine.draw(u,n)}}}})},function(e,t,n){"use strict";var i=n(0);new i.a({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 e(e,t,n,i){var r=t.replace(/_/g," ");a.addRow([e,r,n,i])}if(this.isAttached){for(var t=i.a.dom(this),n=this.data;t.node.lastChild;)t.node.removeChild(t.node.lastChild);if(n&&0!==n.length){var r=new window.google.visualization.Timeline(this),a=new window.google.visualization.DataTable;a.addColumn({type:"string",id:"Entity"}),a.addColumn({type:"string",id:"State"}),a.addColumn({type:"date",id:"Start"}),a.addColumn({type:"date",id:"End"});var s=new Date(n.reduce(function(e,t){return Math.min(e,t[0].lastChangedAsDate)},new Date)),o=new Date(s);o.setDate(o.getDate()+1),o>new Date&&(o=new Date);var u=0;n.forEach(function(t){if(0!==t.length){var n=t[0].entityDisplay,i=void 0,r=null,a=null;t.forEach(function(t){null!==r&&t.state!==r?(i=t.lastChangedAsDate,e(n,r,a,i),r=t.state,a=i):null===r&&(r=t.state,a=t.lastChangedAsDate)}),e(n,r,a,o),u++}}),r.draw(a,{height:55+42*u,timeline:{showRowLabels:n.length>1},hAxis:{format:"H:mm"}})}}}})},function(e,t,n){"use strict";var i=n(0);n(36),new i.a({is:"state-info",properties:{detailed:{type:Boolean,value:!1},stateObj:{type:Object}}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"ha-voice-command-dialog",behaviors:[window.hassBehavior],properties:{hass:{type:Object},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},finalTranscript:{type:String,bindNuclear:function(e){return e.voiceGetters.finalTranscript}},interimTranscript:{type:String,bindNuclear:function(e){return e.voiceGetters.extraInterimTranscript}},isTransmitting:{type:Boolean,bindNuclear:function(e){return e.voiceGetters.isTransmitting}},isListening:{type:Boolean,bindNuclear:function(e){return e.voiceGetters.isListening}},showListenInterface:{type:Boolean,computed:"computeShowListenInterface(isListening, isTransmitting)",observer:"showListenInterfaceChanged"}},computeShowListenInterface:function(e,t){return e||t},dialogOpenChanged:function(e){!e&&this.isListening&&this.hass.voiceActions.stop()},showListenInterfaceChanged:function(e){!e&&this.dialogOpen?this.dialogOpen=!1:e&&(this.dialogOpen=!0)}})},function(e,t,n){"use strict";var i=n(0),r=(n(4),n(8),n(58),["camera","configurator","scene"]);new i.a({is:"more-info-dialog",behaviors:[window.hassBehavior],properties:{stateObj:{type:Object,bindNuclear:function(e){return e.moreInfoGetters.currentEntity},observer:"stateObjChanged"},stateHistory:{type:Object,bindNuclear:function(e){return[e.moreInfoGetters.currentEntityHistory,function(e){return e?[e]:!1}]}},isLoadingHistoryData:{type:Boolean,computed:"computeIsLoadingHistoryData(delayedDialogOpen, isLoadingEntityHistoryData)"},isLoadingEntityHistoryData:{type:Boolean,bindNuclear:function(e){return e.entityHistoryGetters.isLoadingEntityHistory}},hasHistoryComponent:{type:Boolean,bindNuclear:function(e){return e.configGetters.isComponentLoaded("history")},observer:"fetchHistoryData"},shouldFetchHistory:{type:Boolean,bindNuclear:function(e){return e.moreInfoGetters.isCurrentEntityHistoryStale},observer:"fetchHistoryData"},showHistoryComponent:{type:Boolean,value:!1,computed:"computeShowHistoryComponent(hasHistoryComponent, stateObj)"},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},delayedDialogOpen:{type:Boolean,value:!1}},ready:function(){this.$.scrollable.dialogElement=this.$.dialog},computeIsLoadingHistoryData:function(e,t){return!e||t},computeShowHistoryComponent:function(e,t){return this.hasHistoryComponent&&t&&-1===r.indexOf(t.domain)},fetchHistoryData:function(){this.stateObj&&this.hasHistoryComponent&&this.shouldFetchHistory&&this.hass.entityHistoryActions.fetchRecent(this.stateObj.entityId)},stateObjChanged:function(e){var t=this;return e?void this.async(function(){t.fetchHistoryData(),t.dialogOpen=!0},10):void(this.dialogOpen=!1)},dialogOpenChanged:function(e){var t=this;e?this.async(function(){t.delayedDialogOpen=!0},10):!e&&this.stateObj&&(this.async(function(){return t.hass.moreInfoActions.deselectEntity()},10),this.delayedDialogOpen=!1)}})},function(e,t,n){"use strict";var i=n(19),r=i&&i.__esModule?function(){return i["default"]}:function(){return i};n.d(r,"a",r),n(18),window.moment=r.a},function(e,t,n){"use strict";var i=n(0);n(1),n(37),new i.a({is:"partial-cards",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},isFetching:{type:Boolean,bindNuclear:function(e){return e.syncGetters.isFetching}},isStreaming:{type:Boolean,bindNuclear:function(e){return e.streamGetters.isStreamingEvents}},canListen:{type:Boolean,bindNuclear:function(e){return[e.voiceGetters.isVoiceSupported,e.configGetters.isComponentLoaded("conversation"),function(e,t){return e&&t}]}},introductionLoaded:{type:Boolean,bindNuclear:function(e){return e.configGetters.isComponentLoaded("introduction")}},locationName:{type:String,bindNuclear:function(e){return e.configGetters.locationName}},showMenu:{type:Boolean,value:!1,observer:"windowChange"},currentView:{type:String,bindNuclear:function(e){return[e.viewGetters.currentView,function(e){return e||""}]}},hasViews:{type:Boolean,bindNuclear:function(e){return[e.viewGetters.views,function(e){return e.size>0}]}},states:{type:Object,bindNuclear:function(e){return e.viewGetters.currentViewEntities}},columns:{type:Number,value:1}},created:function(){var e=this;this.windowChange=this.windowChange.bind(this);for(var t=[],n=0;5>n;n++)t.push(300+300*n);this.mqls=t.map(function(t){var n=window.matchMedia("(min-width: "+t+"px)");return n.addListener(e.windowChange),n})},detached:function(){var e=this;this.mqls.forEach(function(t){return t.removeListener(e.windowChange)})},windowChange:function(){var e=this.mqls.reduce(function(e,t){return e+t.matches},0);this.columns=Math.max(1,e-(!this.narrow&&this.showMenu))},scrollToTop:function(){this.$.panel.scrollToTop(!0)},handleRefresh:function(){this.hass.syncActions.fetchAll()},handleListenClick:function(){this.hass.voiceActions.listen()},contentScroll:function(){var e=this;this.debouncedContentScroll||(this.debouncedContentScroll=this.async(function(){e.checkRaised(),e.debouncedContentScroll=!1},100))},checkRaised:function(){this.toggleClass("raised",this.$.panel.scroller.scrollTop>(this.hasViews?56:0),this.$.panel)},headerScrollAdjust:function(e){this.hasViews&&this.translate3d("0","-"+e.detail.y+"px","0",this.$.menu)},computeHeaderHeight:function(e,t){return e?104:t?56:64},computeCondensedHeaderHeight:function(e,t){return e?48:t?56:64},computeMenuButtonClass:function(e,t){return!e&&t?"menu-icon invisible":"menu-icon"},computeRefreshButtonClass:function(e){return e?"ha-spin":""},computeTitle:function(e,t){return e?"Home Assistant":t},computeShowIntroduction:function(e,t,n){return""===e&&(t||0===n.size)},computeHasViews:function(e){return e.length>0},toggleMenu:function(){this.fire("open-menu")}})},function(e,t,n){"use strict";var i=n(0);n(1),n(39),new i.a({is:"partial-dev-call-service",properties:{hass:{type:Object},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(hass, domain, service)"}},computeDescription:function(e,t,n){return e.reactor.evaluate([e.serviceGetters.entityMap,function(e){return e.has(t)&&e.get(t).get("services").has(n)?JSON.stringify(e.get(t).get("services").get(n).toJS(),null,2):"No description available"}])},serviceSelected:function(e){this.domain=e.detail.domain,this.service=e.detail.service},callService:function(){var e=void 0;try{e=this.serviceData?JSON.parse(this.serviceData):{}}catch(t){return void alert("Error parsing JSON: "+t)}this.hass.serviceActions.callService(this.domain,this.service,e)},computeFormClasses:function(e){return"content fit "+(e?"":"layout horizontal")}})},function(e,t,n){"use strict";var i=n(0);n(1),new i.a({is:"partial-dev-fire-event",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},eventType:{type:String,value:""},eventData:{type:String,value:""}},eventSelected:function(e){this.eventType=e.detail.eventType},fireEvent:function(){var e=void 0;try{e=this.eventData?JSON.parse(this.eventData):{}}catch(t){return void alert("Error parsing JSON: "+t)}this.hass.eventActions.fireEvent(this.eventType,e)},computeFormClasses:function(e){return"content fit "+(e?"":"layout horizontal")}})},function(e,t,n){"use strict";var i=n(0);n(1),new i.a({is:"partial-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(e){return e.configGetters.serverVersion}},polymerVersion:{type:String,value:i.a.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){var t=this;e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(e){t.errorLog=e||"No errors have been reported."})}})},function(e,t,n){"use strict";var i=n(0);n(1),new i.a({is:"partial-dev-set-state",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},entityId:{type:String,value:""},state:{type:String,value:""},stateAttributes:{type:String,value:""}},setStateData:function(e){var t=e?JSON.stringify(e,null," "):"";this.$.inputData.value=t,this.$.inputDataWrapper.update(this.$.inputData)},entitySelected:function(e){var t=this.hass.reactor.evaluate(this.hass.entityGetters.byId(e.detail.entityId));this.entityId=t.entityId,this.state=t.state,this.stateAttributes=JSON.stringify(t.attributes,null," ")},handleSetState:function(){var e=void 0;try{e=this.stateAttributes?JSON.parse(this.stateAttributes):{}}catch(t){return void alert("Error parsing JSON: "+t)}this.hass.entityActions.save({entityId:this.entityId,state:this.state,attributes:e})},computeFormClasses:function(e){return"content fit "+(e?"":"layout horizontal")}})},function(e,t,n){"use strict";var i=n(0);n(1),new i.a({is:"partial-dev-template",behaviors:[window.hassBehavior],properties:{hass:{type:Object},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(e){return"content fit "+(e?"":"layout horizontal")},computeRenderedClasses:function(e){return e?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate,500)},renderTemplate:function(){var e=this;this.rendering=!0,this.hass.templateActions.render(this.template).then(function(t){e.processed=t,e.rendering=!1},function(t){e.processed=t.message,e.error=!0,e.rendering=!1})}})},function(e,t,n){"use strict";var i=n(0);n(1),n(8),new i.a({is:"partial-history",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1},isDataLoaded:{type:Boolean,bindNuclear:function(e){return e.entityHistoryGetters.hasDataForCurrentDate},observer:"isDataLoadedChanged"},stateHistory:{type:Object,bindNuclear:function(e){return e.entityHistoryGetters.entityHistoryForCurrentDate}},isLoadingData:{type:Boolean,bindNuclear:function(e){return e.entityHistoryGetters.isLoadingEntityHistory}},selectedDate:{type:String,value:null,bindNuclear:function(e){return e.entityHistoryGetters.currentDate}}},isDataLoadedChanged:function(e){var t=this;e||this.async(function(){return t.hass.entityHistoryActions.fetchSelectedDate()},1)},handleRefreshClick:function(){this.hass.entityHistoryActions.fetchSelectedDate()},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:this.hass.entityHistoryActions.changeCurrentDate})},detached:function(){this.datePicker.destroy()},computeContentClasses:function(e){return"flex content "+(e?"narrow":"wide")}})},function(e,t,n){"use strict";var i=n(0);n(1),n(38),new i.a({is:"partial-logbook",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},selectedDate:{type:String,bindNuclear:function(e){return e.logbookGetters.currentDate}},isLoading:{type:Boolean,bindNuclear:function(e){return e.logbookGetters.isLoadingEntries}},isStale:{type:Boolean,bindNuclear:function(e){return e.logbookGetters.isCurrentStale},observer:"isStaleChanged"},entries:{type:Array,bindNuclear:function(e){return[e.logbookGetters.currentEntries,function(e){return e.reverse().toArray()}]}},datePicker:{type:Object}},isStaleChanged:function(e){var t=this;e&&this.async(function(){return t.hass.logbookActions.fetchDate(t.selectedDate)},1)},handleRefresh:function(){this.hass.logbookActions.fetchDate(this.selectedDate)},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:this.hass.logbookActions.changeCurrentDate})},detached:function(){this.datePicker.destroy()}})},function(e,t,n){"use strict";var i=n(0);n(32),window.L.Icon.Default.imagePath="/static/images/leaflet",new i.a({is:"partial-map",behaviors:[window.hassBehavior],properties:{hass:{type:Object},locationGPS:{type:Number,bindNuclear:function(e){return e.configGetters.locationGPS}},locationName:{type:String,bindNuclear:function(e){return e.configGetters.locationName}},locationEntities:{type:Array,bindNuclear:function(e){return[e.entityGetters.visibleEntityMap,function(e){return e.valueSeq().filter(function(e){return e.attributes.latitude&&"home"!==e.state}).toArray()}]}},zoneEntities:{type:Array,bindNuclear:function(e){return[e.entityGetters.entityMap,function(e){return e.valueSeq().filter(function(e){return"zone"===e.domain&&!e.attributes.passive}).toArray()}]}},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1}},attached:function(){var e=this;(window.L.Browser.mobileWebkit||window.L.Browser.webkit)&&this.async(function(){var t=e.$.map,n=t.style.display;t.style.display="none",e.async(function(){t.style.display=n},1)},1)},computeMenuButtonClass:function(e,t){return!e&&t?"menu-icon invisible":"menu-icon"},toggleMenu:function(){this.fire("open-menu")}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"notification-manager",behaviors:[window.hassBehavior],properties:{hass:{type:Object},neg:{type:Boolean,value:!1},text:{type:String,bindNuclear:function(e){return e.notificationGetters.lastNotificationMessage},observer:"showNotification"}},showNotification:function(e){e&&this.$.toast.show()}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"more-info-alarm_control_panel",properties:{hass:{type:Object},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(e,t){var n=new RegExp(t);return null===t?!0:n.test(e)},stateObjChanged:function(e){var t=this;e&&(this.codeFormat=e.attributes.code_format,this.codeInputVisible=null!==this.codeFormat,this.codeInputEnabled="armed_home"===e.state||"armed_away"===e.state||"disarmed"===e.state||"pending"===e.state||"triggered"===e.state,this.disarmButtonVisible="armed_home"===e.state||"armed_away"===e.state||"pending"===e.state||"triggered"===e.state,this.armHomeButtonVisible="disarmed"===e.state,this.armAwayButtonVisible="disarmed"===e.state),this.async(function(){return t.fire("iron-resize")},500)},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})},callService:function(e,t){var n=t||{};n.entity_id=this.stateObj.entityId,this.hass.serviceActions.callService("alarm_control_panel",e,n)}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"more-info-configurator",behaviors:[window.hassBehavior],properties:{stateObj:{type:Object},action:{type:String,value:"display"},isStreaming:{type:Boolean,bindNuclear:function(e){return e.streamGetters.isStreamingEvents}},isConfigurable:{type:Boolean,computed:"computeIsConfigurable(stateObj)"},isConfiguring:{type:Boolean,value:!1},submitCaption:{type:String,computed:"computeSubmitCaption(stateObj)"},fieldInput:{type:Object,value:{}}},computeIsConfigurable:function(e){return"configure"===e.state},computeSubmitCaption:function(e){return e.attributes.submit_caption||"Set configuration"},fieldChanged:function(e){var t=e.target;this.fieldInput[t.id]=t.value},submitClicked:function(){var e=this;this.isConfiguring=!0;var t={configure_id:this.stateObj.attributes.configure_id,fields:this.fieldInput};this.hass.serviceActions.callService("configurator","configure",t).then(function(){e.isConfiguring=!1,e.isStreaming||e.hass.syncActions.fetchAll()},function(){e.isConfiguring=!1})}})},function(e,t,n){"use strict";var i=n(0),r=n(3),a=n(13);n(59),n(65),n(57),n(66),n(64),n(61),n(63),n(67),n(56),n(62),n(60),new i.a({is:"more-info-content",properties:{hass:{type:Object},stateObj:{type:Object,observer:"stateObjChanged"}},stateObjChanged:function(e){e&&n.i(r.a)(this,"MORE-INFO-"+n.i(a.a)(e).toUpperCase(),{hass:this.hass,stateObj:e})}})},function(e,t,n){"use strict";var i=n(0),r=n(3),a=n(13);n(4),new i.a({is:"more-info-group",behaviors:[window.hassBehavior],properties:{hass:{type:Object},stateObj:{type:Object},states:{type:Array,bindNuclear:function(e){return[e.moreInfoGetters.currentEntity,e.entityGetters.entityMap,function(e,t){return e?e.attributes.entity_id.map(t.get.bind(t)):[]}]}}},observers:["statesChanged(stateObj, states)"],statesChanged:function(e,t){var s=!1;if(t&&t.length>0){var o=t[0];s=o.set("entityId",e.entityId).set("attributes",Object.assign({},o.attributes));for(var u=0;ue||e>=this.stateObj.attributes.source_list.length)){var t=this.stateObj.attributes.source_list[e];t!==this.stateObj.attributes.source&&this.callService("select_source",{source:t})}},handleVolumeTap:function(){this.supportsVolumeMute&&this.callService("volume_mute",{is_volume_muted:!this.isMuted})},handleVolumeUp:function(){var e=this.$.volumeUp;this.handleVolumeWorker("volume_up",e,!0)},handleVolumeDown:function(){var e=this.$.volumeDown;this.handleVolumeWorker("volume_down",e,!0)},handleVolumeWorker:function(e,t,n){var i=this;(n||void 0!==t&&t.pointerDown)&&(this.callService(e),this.async(function(){return i.handleVolumeWorker(e,t,!1)},500))},volumeSliderChanged:function(e){var t=parseFloat(e.target.value),n=t>0?t/100:0;this.callService("volume_set",{volume_level:n})},callService:function(e,t){var n=t||{};n.entity_id=this.stateObj.entityId,this.hass.serviceActions.callService("media_player",e,n)}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"more-info-script",properties:{stateObj:{type:Object}}})},function(e,t,n){"use strict";var i=n(0),r=n(11);new i.a({is:"more-info-sun",properties:{stateObj:{type:Object},risingDate:{type:Object,computed:"computeRising(stateObj)"},settingDate:{type:Object, +computed:"computeSetting(stateObj)"}},computeRising:function(e){return new Date(e.attributes.next_rising)},computeSetting:function(e){return new Date(e.attributes.next_setting)},computeOrder:function(e,t){return e>t?["set","ris"]:["ris","set"]},itemCaption:function(e){return"ris"===e?"Rising ":"Setting "},itemDate:function(e){return"ris"===e?this.risingDate:this.settingDate},itemValue:function(e){return n.i(r.a)(this.itemDate(e))}})},function(e,t,n){"use strict";var i=n(0),r=n(2),a=["away_mode"];new i.a({is:"more-info-thermostat",properties:{hass:{type:Object},stateObj:{type:Object,observer:"stateObjChanged"},tempMin:{type:Number},tempMax:{type:Number},targetTemperatureSliderValue:{type:Number},awayToggleChecked:{type:Boolean}},stateObjChanged:function(e){this.targetTemperatureSliderValue=e.attributes.temperature,this.awayToggleChecked="on"===e.attributes.away_mode,this.tempMin=e.attributes.min_temp,this.tempMax=e.attributes.max_temp},computeClassNames:function(e){return n.i(r.a)(e,a)},targetTemperatureSliderChanged:function(e){this.hass.serviceActions.callService("thermostat","set_temperature",{entity_id:this.stateObj.entityId,temperature:e.target.value})},toggleChanged:function(e){var t=e.target.checked;t&&"off"===this.stateObj.attributes.away_mode?this.service_set_away(!0):t||"on"!==this.stateObj.attributes.away_mode||this.service_set_away(!1)},service_set_away:function(e){var t=this;this.hass.serviceActions.callService("thermostat","set_away_mode",{away_mode:e,entity_id:this.stateObj.entityId}).then(function(){return t.stateObjChanged(t.stateObj)})}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"more-info-updater",properties:{}})},function(e,t,n){"use strict";t.a=["off","closed","unlocked"]},function(e,t,n){"use strict";function i(e,t){return"unavailable"===t.state?"display":-1!==a.indexOf(t.domain)?t.domain:n.i(r.a)(e,t.entityId)?"toggle":"display"}var r=n(9);t.a=i;var a=["configurator","hvac","input_select","input_slider","media_player","rollershutter","scene","script","thermostat","weblink"]},function(e,t,n){var i,r;!function(){"use strict";function n(){for(var e=[],t=0;t \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 58501c46c30810..fa2fdc437c5eb4 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 168706fdb19221..1e1a3a1c845713 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 168706fdb192219d8074d6709c0ce686180d1c8a +Subproject commit 1e1a3a1c845713508d21d7c1cb87a7ecee6222aa diff --git a/homeassistant/components/frontend/www_static/images/smart-tv.png b/homeassistant/components/frontend/www_static/images/smart-tv.png new file mode 100644 index 00000000000000..5ecda68b402903 Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/smart-tv.png differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 084c6e755958a5..2756f2fde3c23e 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -29,7 +29,7 @@ /* eslint-disable quotes, comma-spacing */ -var PrecacheConfig = [["/","595e12c9755af231fd80191e4cc74d2e"],["/devEvent","595e12c9755af231fd80191e4cc74d2e"],["/devInfo","595e12c9755af231fd80191e4cc74d2e"],["/devService","595e12c9755af231fd80191e4cc74d2e"],["/devState","595e12c9755af231fd80191e4cc74d2e"],["/devTemplate","595e12c9755af231fd80191e4cc74d2e"],["/history","595e12c9755af231fd80191e4cc74d2e"],["/logbook","595e12c9755af231fd80191e4cc74d2e"],["/map","595e12c9755af231fd80191e4cc74d2e"],["/states","595e12c9755af231fd80191e4cc74d2e"],["/static/core-7962327e4a29e51d4a6f4ee6cca9acc3.js","9c07ffb3f81cfb74f8a051b80cc8f9f0"],["/static/frontend-570e1b8744a58024fc4e256f5e024424.html","595e12c9755af231fd80191e4cc74d2e"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]]; +var PrecacheConfig = [["/","70eeeca780a5f23c7632c2876dd1795a"],["/devEvent","70eeeca780a5f23c7632c2876dd1795a"],["/devInfo","70eeeca780a5f23c7632c2876dd1795a"],["/devService","70eeeca780a5f23c7632c2876dd1795a"],["/devState","70eeeca780a5f23c7632c2876dd1795a"],["/devTemplate","70eeeca780a5f23c7632c2876dd1795a"],["/history","70eeeca780a5f23c7632c2876dd1795a"],["/logbook","70eeeca780a5f23c7632c2876dd1795a"],["/map","70eeeca780a5f23c7632c2876dd1795a"],["/states","70eeeca780a5f23c7632c2876dd1795a"],["/static/core-db0bb387f4d3bcace002d62b94baa348.js","f938163a392465dc87af3a0094376621"],["/static/frontend-5b306b7e7d36799b7b67f592cbe94703.html","70eeeca780a5f23c7632c2876dd1795a"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]]; /* eslint-enable quotes, comma-spacing */ var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-'; diff --git a/homeassistant/components/frontend/www_static/service_worker.js.gz b/homeassistant/components/frontend/www_static/service_worker.js.gz index 21b37fba8678f8..97a7a860214599 100644 Binary files a/homeassistant/components/frontend/www_static/service_worker.js.gz and b/homeassistant/components/frontend/www_static/service_worker.js.gz differ diff --git a/homeassistant/components/garage_door/rpi_gpio.py b/homeassistant/components/garage_door/rpi_gpio.py new file mode 100644 index 00000000000000..6a50ffb408d7f5 --- /dev/null +++ b/homeassistant/components/garage_door/rpi_gpio.py @@ -0,0 +1,96 @@ +""" +Support for building a Raspberry Pi garage controller in HA. + +Instructions for building the controller can be found here +https://github.com/andrewshilliday/garage-door-controller + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/garage_door.rpi_gpio/ +""" + +import logging +from time import sleep +import voluptuous as vol +from homeassistant.components.garage_door import GarageDoorDevice +import homeassistant.components.rpi_gpio as rpi_gpio +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['rpi_gpio'] + +_LOGGER = logging.getLogger(__name__) + +_DOORS_SCHEMA = vol.All( + cv.ensure_list, + [ + vol.Schema({ + 'name': str, + 'relay_pin': int, + 'state_pin': int, + }) + ] +) +PLATFORM_SCHEMA = vol.Schema({ + 'platform': str, + vol.Required('doors'): _DOORS_SCHEMA, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the garage door platform.""" + doors = [] + doors_conf = config.get('doors') + + for door in doors_conf: + doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'], + door['state_pin'])) + add_devices(doors) + + +class RPiGPIOGarageDoor(GarageDoorDevice): + """Representation of a Raspberry garage door.""" + + def __init__(self, name, relay_pin, state_pin): + """Initialize the garage door.""" + self._name = name + self._state = False + self._relay_pin = relay_pin + self._state_pin = state_pin + rpi_gpio.setup_output(self._relay_pin) + rpi_gpio.setup_input(self._state_pin, 'DOWN') + rpi_gpio.write_output(self._relay_pin, True) + + @property + def unique_id(self): + """Return the ID of this garage door.""" + return "{}.{}".format(self.__class__, self._name) + + @property + def name(self): + """Return the name of the garage door if any.""" + return self._name + + def update(self): + """Update the state of the garage door.""" + self._state = rpi_gpio.read_input(self._state_pin) is True + + @property + def is_closed(self): + """Return true if door is closed.""" + return self._state + + def _trigger(self): + """Trigger the door.""" + rpi_gpio.write_output(self._relay_pin, False) + sleep(0.2) + rpi_gpio.write_output(self._relay_pin, True) + + def close_door(self): + """Close the door.""" + if not self.is_closed: + self._trigger() + + def open_door(self): + """Open the door.""" + if self.is_closed: + self._trigger() diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py index 18ec6f2ba565e4..73692290f50292 100644 --- a/homeassistant/components/garage_door/wink.py +++ b/homeassistant/components/garage_door/wink.py @@ -7,9 +7,10 @@ import logging from homeassistant.components.garage_door import GarageDoorDevice -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL +from homeassistant.components.wink import WinkDevice +from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pywink.get_garage_doors()) -class WinkGarageDoorDevice(GarageDoorDevice): +class WinkGarageDoorDevice(WinkDevice, GarageDoorDevice): """Representation of a Wink garage door.""" def __init__(self, wink): """Initialize the garage door.""" - self.wink = wink - self._battery = self.wink.battery_level - - @property - def unique_id(self): - """Return the ID of this wink garage door.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the garage door if any.""" - return self.wink.name() - - def update(self): - """Update the state of the garage door.""" - self.wink.update_state() + WinkDevice.__init__(self, wink) @property def is_closed(self): """Return true if door is closed.""" return self.wink.state() == 0 - @property - def available(self): - """True if connection == True.""" - return self.wink.available - def close_door(self): """Close the door.""" self.wink.set_state(0) @@ -70,16 +51,3 @@ def close_door(self): def open_door(self): """Open the door.""" self.wink.set_state(1) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 diff --git a/homeassistant/components/garage_door/zwave.py b/homeassistant/components/garage_door/zwave.py new file mode 100644 index 00000000000000..b527fc0052c941 --- /dev/null +++ b/homeassistant/components/garage_door/zwave.py @@ -0,0 +1,70 @@ +""" +Support for Zwave garage door components. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/garagedoor.zwave/ +""" +# Because we do not compile openzwave on CI +# pylint: disable=import-error +import logging +from homeassistant.components.garage_door import DOMAIN +from homeassistant.components.zwave import ZWaveDeviceEntity +from homeassistant.components import zwave +from homeassistant.components.garage_door import GarageDoorDevice + +COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37 + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return Z-Wave garage door device.""" + if discovery_info is None or zwave.NETWORK is None: + return + + node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] + value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] + + if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY: + return + if value.type != zwave.TYPE_BOOL: + return + if value.genre != zwave.GENRE_USER: + return + + value.set_change_verified(False) + add_devices([ZwaveGarageDoor(value)]) + + +class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice): + """Representation of an Zwave garage door device.""" + + def __init__(self, value): + """Initialize the zwave garage door.""" + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + ZWaveDeviceEntity.__init__(self, value, DOMAIN) + self._node = value.node + self._state = value.data + dispatcher.connect( + self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + + def value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.value_id == value.value_id: + self._state = value.data + self.update_ha_state(True) + _LOGGER.debug("Value changed on network %s", value) + + @property + def is_closed(self): + """Return the current position of Zwave garage door.""" + return not self._state + + def close_door(self): + """Close the garage door.""" + self._value.node.set_switch(self._value.value_id, False) + + def open_door(self): + """Open the garage door.""" + self._value.node.set_switch(self._value.value_id, True) diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py new file mode 100644 index 00000000000000..89cbe789c581e0 --- /dev/null +++ b/homeassistant/components/hdmi_cec.py @@ -0,0 +1,122 @@ +""" +CEC component. + +Requires libcec + Python bindings. +""" + +import logging +import voluptuous as vol +from homeassistant.const import EVENT_HOMEASSISTANT_START +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) +_CEC = None +DOMAIN = 'hdmi_cec' +SERVICE_SELECT_DEVICE = 'select_device' +SERVICE_POWER_ON = 'power_on' +SERVICE_STANDBY = 'standby' +CONF_DEVICES = 'devices' +ATTR_DEVICE = 'device' +MAX_DEPTH = 4 + + +# pylint: disable=unnecessary-lambda +DEVICE_SCHEMA = vol.Schema({ + vol.All(cv.positive_int): vol.Any(lambda devices: DEVICE_SCHEMA(devices), + cv.string) +}) + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICES): DEVICE_SCHEMA + }) +}, extra=vol.ALLOW_EXTRA) + + +def parse_mapping(mapping, parents=None): + """Parse configuration device mapping.""" + if parents is None: + parents = [] + for addr, val in mapping.items(): + cur = parents + [str(addr)] + if isinstance(val, dict): + yield from parse_mapping(val, cur) + elif isinstance(val, str): + yield (val, cur) + + +def pad_physical_address(addr): + """Right-pad a physical address.""" + return addr + ['0'] * (MAX_DEPTH - len(addr)) + + +def setup(hass, config): + """Setup CEC capability.""" + global _CEC + + # cec is only available if libcec is properly installed + # and the Python bindings are accessible. + try: + import cec + except ImportError: + _LOGGER.error("libcec must be installed") + return False + + # Parse configuration into a dict of device name + # to physical address represented as a list of + # four elements. + flat = {} + for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})): + flat[pair[0]] = pad_physical_address(pair[1]) + + # Configure libcec. + cfg = cec.libcec_configuration() + cfg.strDeviceName = 'HASS' + cfg.bActivateSource = 0 + cfg.bMonitorOnly = 1 + cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT + + # Set up CEC adapter. + _CEC = cec.ICECAdapter.Create(cfg) + + def _power_on(call): + """Power on all devices.""" + _CEC.PowerOnDevices() + + def _standby(call): + """Standby all devices.""" + _CEC.StandbyDevices() + + def _select_device(call): + """Select the active device.""" + path = flat.get(call.data[ATTR_DEVICE]) + if not path: + _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) + cmds = [] + for i in range(1, MAX_DEPTH - 1): + addr = pad_physical_address(path[:i]) + cmds.append('1f:82:{}{}:{}{}'.format(*addr)) + cmds.append('1f:86:{}{}:{}{}'.format(*addr)) + for cmd in cmds: + _CEC.Transmit(_CEC.CommandFromString(cmd)) + _LOGGER.info("Selected %s", call.data[ATTR_DEVICE]) + + def _start_cec(event): + """Open CEC adapter.""" + adapters = _CEC.DetectAdapters() + if len(adapters) == 0: + _LOGGER.error("No CEC adapter found") + return + + if _CEC.Open(adapters[0].strComName): + hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) + hass.services.register(DOMAIN, SERVICE_STANDBY, _standby) + hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, + _select_device) + else: + _LOGGER.error("Failed to open adapter") + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec) + return True diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py new file mode 100644 index 00000000000000..f2d71bb409a8cd --- /dev/null +++ b/homeassistant/components/homematic.py @@ -0,0 +1,534 @@ +""" +Support for Homematic devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematic/ +""" +import time +import logging +from functools import partial +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity + +DOMAIN = 'homematic' +REQUIREMENTS = ['pyhomematic==0.1.8'] + +HOMEMATIC = None +HOMEMATIC_LINK_DELAY = 0.5 + +DISCOVER_SWITCHES = "homematic.switch" +DISCOVER_LIGHTS = "homematic.light" +DISCOVER_SENSORS = "homematic.sensor" +DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" +DISCOVER_ROLLERSHUTTER = "homematic.rollershutter" +DISCOVER_THERMOSTATS = "homematic.thermostat" + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_PARAM = "param" +ATTR_CHANNEL = "channel" +ATTR_NAME = "name" +ATTR_ADDRESS = "address" + +EVENT_KEYPRESS = "homematic.keypress" + +HM_DEVICE_TYPES = { + DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"], + DISCOVER_LIGHTS: ["Dimmer"], + DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2", + "RemoteMotion", "ThermostatWall", "AreaThermostat", + "RotaryHandleSensor", "WaterSensor"], + DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], + DISCOVER_BINARY_SENSORS: ["ShutterContact", "Smoke", "SmokeV2", + "Motion", "MotionV2", "RemoteMotion"], + DISCOVER_ROLLERSHUTTER: ["Blind"] +} + +HM_IGNORE_DISCOVERY_NODE = [ + "ACTUAL_TEMPERATURE" +] + +HM_ATTRIBUTE_SUPPORT = { + "LOWBAT": ["Battery", {0: "High", 1: "Low"}], + "ERROR": ["Sabotage", {0: "No", 1: "Yes"}], + "RSSI_DEVICE": ["RSSI", {}], + "VALVE_STATE": ["Valve", {}], + "BATTERY_STATE": ["Battery", {}], + "CONTROL_MODE": ["Mode", {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost"}], + "POWER": ["Power", {}], + "CURRENT": ["Current", {}], + "VOLTAGE": ["Voltage", {}] +} + +HM_PRESS_EVENTS = [ + "PRESS_SHORT", + "PRESS_LONG", + "PRESS_CONT", + "PRESS_LONG_RELEASE" +] + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup(hass, config): + """Setup the Homematic component.""" + global HOMEMATIC, HOMEMATIC_LINK_DELAY + + from pyhomematic import HMConnection + + local_ip = config[DOMAIN].get("local_ip", None) + local_port = config[DOMAIN].get("local_port", 8943) + remote_ip = config[DOMAIN].get("remote_ip", None) + remote_port = config[DOMAIN].get("remote_port", 2001) + resolvenames = config[DOMAIN].get("resolvenames", False) + username = config[DOMAIN].get("username", "Admin") + password = config[DOMAIN].get("password", "") + HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5) + + if remote_ip is None or local_ip is None: + _LOGGER.error("Missing remote CCU/Homegear or local address") + return False + + # Create server thread + bound_system_callback = partial(system_callback_handler, hass, config) + HOMEMATIC = HMConnection(local=local_ip, + localport=local_port, + remote=remote_ip, + remoteport=remote_port, + systemcallback=bound_system_callback, + resolvenames=resolvenames, + rpcusername=username, + rpcpassword=password, + interface_id="homeassistant") + + # Start server thread, connect to peer, initialize to receive events + HOMEMATIC.start() + + # Stops server when Homeassistant is shutting down + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop) + hass.config.components.append(DOMAIN) + + return True + + +# pylint: disable=too-many-branches +def system_callback_handler(hass, config, src, *args): + """Callback handler.""" + if src == 'newDevices': + _LOGGER.debug("newDevices with: %s", str(args)) + # pylint: disable=unused-variable + (interface_id, dev_descriptions) = args + key_dict = {} + # Get list of all keys of the devices (ignoring channels) + for dev in dev_descriptions: + key_dict[dev['ADDRESS'].split(':')[0]] = True + + # Register EVENTS + # Search all device with a EVENTNODE that include data + bound_event_callback = partial(_hm_event_handler, hass) + for dev in key_dict: + if dev not in HOMEMATIC.devices: + continue + + hmdevice = HOMEMATIC.devices.get(dev) + # have events? + if len(hmdevice.EVENTNODE) > 0: + _LOGGER.debug("Register Events from %s", dev) + hmdevice.setEventCallback(callback=bound_event_callback, + bequeath=True) + + # If configuration allows autodetection of devices, + # all devices not configured are added. + if key_dict: + for component_name, discovery_type in ( + ('switch', DISCOVER_SWITCHES), + ('light', DISCOVER_LIGHTS), + ('rollershutter', DISCOVER_ROLLERSHUTTER), + ('binary_sensor', DISCOVER_BINARY_SENSORS), + ('sensor', DISCOVER_SENSORS), + ('thermostat', DISCOVER_THERMOSTATS)): + # Get all devices of a specific type + found_devices = _get_devices(discovery_type, key_dict) + + # When devices of this type are found + # they are setup in HA and an event is fired + if found_devices: + # Fire discovery event + discovery.load_platform(hass, component_name, DOMAIN, { + ATTR_DISCOVER_DEVICES: found_devices + }, config) + + +def _get_devices(device_type, keys): + """Get the Homematic devices.""" + # run + device_arr = [] + for key in keys: + device = HOMEMATIC.devices[key] + class_name = device.__class__.__name__ + metadata = {} + + # is class supported by discovery type + if class_name not in HM_DEVICE_TYPES[device_type]: + continue + + # Load metadata if needed to generate a param list + if device_type == DISCOVER_SENSORS: + metadata.update(device.SENSORNODE) + elif device_type == DISCOVER_BINARY_SENSORS: + metadata.update(device.BINARYNODE) + + params = _create_params_list(device, metadata, device_type) + if params: + # Generate options for 1...n elements with 1...n params + for channel in range(1, device.ELEMENT + 1): + _LOGGER.debug("Handling %s:%i", key, channel) + if channel in params: + for param in params[channel]: + name = _create_ha_name(name=device.NAME, + channel=channel, + param=param) + device_dict = dict(platform="homematic", + address=key, + name=name, + channel=channel) + if param is not None: + device_dict[ATTR_PARAM] = param + + # Add new device + device_arr.append(device_dict) + else: + _LOGGER.debug("Channel %i not in params", channel) + else: + _LOGGER.debug("Got no params for %s", key) + _LOGGER.debug("%s autodiscovery: %s", + device_type, str(device_arr)) + return device_arr + + +def _create_params_list(hmdevice, metadata, device_type): + """Create a list from HMDevice with all possible parameters in config.""" + params = {} + merge = False + + # use merge? + if device_type == DISCOVER_SENSORS: + merge = True + elif device_type == DISCOVER_BINARY_SENSORS: + merge = True + + # Search in sensor and binary metadata per elements + for channel in range(1, hmdevice.ELEMENT + 1): + param_chan = [] + for node, meta_chan in metadata.items(): + try: + # Is this attribute ignored? + if node in HM_IGNORE_DISCOVERY_NODE: + continue + if meta_chan == 'c' or meta_chan is None: + # Only channel linked data + param_chan.append(node) + elif channel == 1: + # First channel can have other data channel + param_chan.append(node) + except (TypeError, ValueError): + _LOGGER.error("Exception generating %s (%s)", + hmdevice.ADDRESS, str(metadata)) + + # default parameter is merge is off + if len(param_chan) == 0 and not merge: + param_chan.append(None) + + # Add to channel + if len(param_chan) > 0: + params.update({channel: param_chan}) + + _LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS, + str(params)) + return params + + +def _create_ha_name(name, channel, param): + """Generate a unique object name.""" + # HMDevice is a simple device + if channel == 1 and param is None: + return name + + # Has multiple elements/channels + if channel > 1 and param is None: + return "{} {}".format(name, channel) + + # With multiple param first elements + if channel == 1 and param is not None: + return "{} {}".format(name, param) + + # Multiple param on object with multiple elements + if channel > 1 and param is not None: + return "{} {} {}".format(name, channel, param) + + +def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, + add_callback_devices): + """Helper to setup Homematic devices with discovery info.""" + for config in discovery_info[ATTR_DISCOVER_DEVICES]: + _LOGGER.debug("Add device %s from config: %s", + str(hmdevicetype), str(config)) + + # create object and add to HA + new_device = hmdevicetype(config) + add_callback_devices([new_device]) + + # link to HM + new_device.link_homematic() + + return True + + +def _hm_event_handler(hass, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + try: + channel = int(device.split(":")[1]) + address = device.split(":")[0] + hmdevice = HOMEMATIC.devices.get(address) + except (TypeError, ValueError): + _LOGGER.error("Event handling channel convert error!") + return + + # is not a event? + if attribute not in hmdevice.EVENTNODE: + return + + _LOGGER.debug("Event %s for %s channel %i", attribute, + hmdevice.NAME, channel) + + # a keypress event + if attribute in HM_PRESS_EVENTS: + hass.bus.fire(EVENT_KEYPRESS, { + ATTR_NAME: hmdevice.NAME, + ATTR_PARAM: attribute, + ATTR_CHANNEL: channel + }) + return + + _LOGGER.warning("Event is unknown and not forwarded to HA") + + +class HMDevice(Entity): + """The Homematic device base object.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, config): + """Initialize a generic Homematic device.""" + self._name = config.get(ATTR_NAME, None) + self._address = config.get(ATTR_ADDRESS, None) + self._channel = config.get(ATTR_CHANNEL, 1) + self._state = config.get(ATTR_PARAM, None) + self._data = {} + self._hmdevice = None + self._connected = False + self._available = False + + # Set param to uppercase + if self._state: + self._state = self._state.upper() + + # Generate name + if not self._name: + self._name = _create_ha_name(name=self._address, + channel=self._channel, + param=self._state) + + @property + def should_poll(self): + """Return false. Homematic states are pushed by the XML RPC Server.""" + return False + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def assumed_state(self): + """Return true if unable to access real state of the device.""" + return not self._available + + @property + def available(self): + """Return true if device is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = {} + + # no data available to create + if not self.available: + return attr + + # Generate an attributes list + for node, data in HM_ATTRIBUTE_SUPPORT.items(): + # Is an attributes and exists for this object + if node in self._data: + value = data[1].get(self._data[node], self._data[node]) + attr[data[0]] = value + + # static attributes + attr["ID"] = self._hmdevice.ADDRESS + + return attr + + def link_homematic(self): + """Connect to Homematic.""" + # device is already linked + if self._connected: + return True + + # Does a HMDevice from pyhomematic exist? + if self._address in HOMEMATIC.devices: + # Init + self._hmdevice = HOMEMATIC.devices[self._address] + self._connected = True + + # Check if Homematic class is okay for HA class + _LOGGER.info("Start linking %s to %s", self._address, self._name) + if self._check_hm_to_ha_object(): + try: + # Init datapoints of this object + self._init_data_struct() + if HOMEMATIC_LINK_DELAY: + # We delay / pause loading of data to avoid overloading + # of CCU / Homegear when doing auto detection + time.sleep(HOMEMATIC_LINK_DELAY) + self._load_init_data_from_hm() + _LOGGER.debug("%s datastruct: %s", + self._name, str(self._data)) + + # Link events from pyhomatic + self._subscribe_homematic_events() + self._available = not self._hmdevice.UNREACH + # pylint: disable=broad-except + except Exception as err: + self._connected = False + _LOGGER.error("Exception while linking %s: %s", + self._address, str(err)) + else: + _LOGGER.critical("Delink %s object from HM", self._name) + self._connected = False + + # Update HA + _LOGGER.debug("%s linking done, send update_ha_state", self._name) + self.update_ha_state() + else: + _LOGGER.debug("%s not found in HOMEMATIC.devices", self._address) + + def _hm_event_callback(self, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + _LOGGER.debug("%s received event '%s' value: %s", self._name, + attribute, value) + have_change = False + + # Is data needed for this instance? + if attribute in self._data: + # Did data change? + if self._data[attribute] != value: + self._data[attribute] = value + have_change = True + + # If available it has changed + if attribute is "UNREACH": + self._available = bool(value) + have_change = True + + # If it has changed data point, update HA + if have_change: + _LOGGER.debug("%s update_ha_state after '%s'", self._name, + attribute) + self.update_ha_state() + + def _subscribe_homematic_events(self): + """Subscribe all required events to handle job.""" + channels_to_sub = {} + + # Push data to channels_to_sub from hmdevice metadata + for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE, + self._hmdevice.ACTIONNODE): + for node, channel in metadata.items(): + # Data is needed for this instance + if node in self._data: + # chan is current channel + if channel == 'c' or channel is None: + channel = self._channel + # Prepare for subscription + try: + if int(channel) > 0: + channels_to_sub.update({int(channel): True}) + except (ValueError, TypeError): + _LOGGER("Invalid channel in metadata from %s", + self._name) + + # Set callbacks + for channel in channels_to_sub: + _LOGGER.debug("Subscribe channel %s from %s", + str(channel), self._name) + self._hmdevice.setEventCallback(callback=self._hm_event_callback, + bequeath=False, + channel=channel) + + def _load_init_data_from_hm(self): + """Load first value from pyhomematic.""" + if not self._connected: + return False + + # Read data from pyhomematic + for metadata, funct in ( + (self._hmdevice.ATTRIBUTENODE, + self._hmdevice.getAttributeData), + (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), + (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), + (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)): + for node in metadata: + if node in self._data: + self._data[node] = funct(name=node, channel=self._channel) + + return True + + def _hm_set_state(self, value): + """Set data to main datapoint.""" + if self._state in self._data: + self._data[self._state] = value + + def _hm_get_state(self): + """Get data from main datapoint.""" + if self._state in self._data: + return self._data[self._state] + return None + + def _check_hm_to_ha_object(self): + """Check if it is possible to use the Homematic object as this HA type. + + NEEDS overwrite by inherit! + """ + if not self._connected or self._hmdevice is None: + _LOGGER.error("HA object is not linked to homematic.") + return False + + # Check if button option is correctly set for this object + if self._channel > self._hmdevice.ELEMENT: + _LOGGER.critical("Button option is not correct for this object!") + return False + + return True + + def _init_data_struct(self): + """Generate a data dict (self._data) from the Homematic metadata. + + NEEDS overwrite by inherit! + """ + # Add all attributes to data dict + for data_note in self._hmdevice.ATTRIBUTENODE: + self._data.update({data_note: STATE_UNKNOWN}) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 3ccf92daea2d81..11aa18cad5c5aa 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -1,25 +1,31 @@ -"""This module provides WSGI application to serve the Home Assistant API.""" +""" +This module provides WSGI application to serve the Home Assistant API. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/http/ +""" import hmac import json import logging import mimetypes import threading import re +import ssl import voluptuous as vol -import homeassistant.core as ha import homeassistant.remote as rem from homeassistant import util from homeassistant.const import ( SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, - HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS) + HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS, + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) from homeassistant.helpers.entity import split_entity_id import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv DOMAIN = "http" -REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",) +REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10") CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" @@ -31,6 +37,25 @@ DATA_API_PASSWORD = 'api_password' +# TLS configuation follows the best-practice guidelines +# specified here: https://wiki.mozilla.org/Security/Server_Side_TLS +# Intermediate guidelines are followed. +SSL_VERSION = ssl.PROTOCOL_SSLv23 +SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_COMPRESSION +CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ + "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ + "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ + "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \ + "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \ + "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \ + "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \ + "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \ + "DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \ + "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \ + "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ + "AES256-SHA:DES-CBC3-SHA:!DSS" + _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) _LOGGER = logging.getLogger(__name__) @@ -93,11 +118,17 @@ def setup(hass, config): cors_origins=cors_origins ) - hass.bus.listen_once( - ha.EVENT_HOMEASSISTANT_START, - lambda event: - threading.Thread(target=server.start, daemon=True, - name='WSGI-server').start()) + def start_wsgi_server(event): + """Start the WSGI server.""" + server.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server) + + def stop_wsgi_server(event): + """Stop the WSGI server.""" + server.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server) hass.wsgi = server hass.config.api = rem.API(server_host if server_host != '0.0.0.0' @@ -216,6 +247,7 @@ def __init__(self, hass, development, api_password, ssl_certificate, self.server_port = server_port self.cors_origins = cors_origins self.event_forwarder = None + self.server = None def register_view(self, view): """Register a view with the WSGI server. @@ -283,14 +315,34 @@ def register_wsgi_app(self, url_root, app): def start(self): """Start the wsgi server.""" - from eventlet import wsgi - import eventlet + from cherrypy import wsgiserver + from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter + + # pylint: disable=too-few-public-methods,super-init-not-called + class ContextSSLAdapter(BuiltinSSLAdapter): + """SSL Adapter that takes in an SSL context.""" + + def __init__(self, context): + self.context = context + + # pylint: disable=no-member + self.server = wsgiserver.CherryPyWSGIServer( + (self.server_host, self.server_port), self, + server_name='Home Assistant') - sock = eventlet.listen((self.server_host, self.server_port)) if self.ssl_certificate: - sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, - keyfile=self.ssl_key, server_side=True) - wsgi.server(sock, self, log=_LOGGER) + context = ssl.SSLContext(SSL_VERSION) + context.options |= SSL_OPTS + context.set_ciphers(CIPHERS) + context.load_cert_chain(self.ssl_certificate, self.ssl_key) + self.server.ssl_adapter = ContextSSLAdapter(context) + + threading.Thread(target=self.server.start, daemon=True, + name='WSGI-server').start() + + def stop(self): + """Stop the wsgi server.""" + self.server.stop() def dispatch_request(self, request): """Handle incoming request.""" @@ -337,6 +389,10 @@ def __call__(self, environ, start_response): """Handle a request for base app + extra apps.""" from werkzeug.wsgi import DispatcherMiddleware + if not self.hass.is_running: + from werkzeug.exceptions import BadRequest + return BadRequest()(environ, start_response) + app = DispatcherMiddleware(self.base_app, self.extra_apps) # Strip out any cachebusting MD5 fingerprints fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', '')) @@ -395,7 +451,12 @@ def handle_request(self, request, **values): self.hass.wsgi.api_password): authenticated = True - if self.requires_auth and not authenticated: + if authenticated: + _LOGGER.info('Successful login/request from %s', + request.remote_addr) + elif self.requires_auth and not authenticated: + _LOGGER.warning('Login attempt or request with an invalid' + 'password from %s', request.remote_addr) raise Unauthorized() request.authenticated = authenticated @@ -437,7 +498,7 @@ def file(self, request, fil, mimetype=None): mimetype = mimetypes.guess_type(fil)[0] try: - fil = open(fil) + fil = open(fil, mode='br') except IOError: raise NotFound() diff --git a/homeassistant/components/hvac/__init__.py b/homeassistant/components/hvac/__init__.py index 85d10671a17cac..560f3d13fd6038 100644 --- a/homeassistant/components/hvac/__init__.py +++ b/homeassistant/components/hvac/__init__.py @@ -425,39 +425,39 @@ def swing_list(self): def set_temperature(self, temperature): """Set new target temperature.""" - pass + raise NotImplementedError() def set_humidity(self, humidity): """Set new target humidity.""" - pass + raise NotImplementedError() def set_fan_mode(self, fan): """Set new target fan mode.""" - pass + raise NotImplementedError() def set_operation_mode(self, operation_mode): """Set new target operation mode.""" - pass + raise NotImplementedError() def set_swing_mode(self, swing_mode): """Set new target swing operation.""" - pass + raise NotImplementedError() def turn_away_mode_on(self): """Turn away mode on.""" - pass + raise NotImplementedError() def turn_away_mode_off(self): """Turn away mode off.""" - pass + raise NotImplementedError() def turn_aux_heat_on(self): """Turn auxillary heater on.""" - pass + raise NotImplementedError() def turn_aux_heat_off(self): """Turn auxillary heater off.""" - pass + raise NotImplementedError() @property def min_temp(self): diff --git a/homeassistant/components/hvac/zwave.py b/homeassistant/components/hvac/zwave.py index e1b1614a60f0ea..2a9c0726f92f1a 100755 --- a/homeassistant/components/hvac/zwave.py +++ b/homeassistant/components/hvac/zwave.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): discovery_info, zwave.NETWORK) -# pylint: disable=too-many-arguments +# pylint: disable=too-many-arguments, abstract-method class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): """Represents a HeatControl hvac.""" @@ -98,7 +98,7 @@ def __init__(self, value): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.node == value.node: + if self._value.value_id == value.value_id: self.update_properties() self.update_ha_state(True) _LOGGER.debug("Value changed on network %s", value) @@ -211,6 +211,7 @@ def set_temperature(self, temperature): value.data = int(round(temperature, 0)) else: value.data = int(temperature) + break def set_fan_mode(self, fan): """Set new target fan mode.""" @@ -218,6 +219,7 @@ def set_fan_mode(self, fan): class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values(): if value.command_class == 68 and value.index == 0: value.data = bytes(fan, 'utf-8') + break def set_operation_mode(self, operation_mode): """Set new target operation mode.""" @@ -225,6 +227,7 @@ def set_operation_mode(self, operation_mode): class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): if value.command_class == 64 and value.index == 0: value.data = bytes(operation_mode, 'utf-8') + break def set_swing_mode(self, swing_mode): """Set new target swing mode.""" @@ -233,3 +236,4 @@ def set_swing_mode(self, swing_mode): class_id=COMMAND_CLASS_CONFIGURATION).values(): if value.command_class == 112 and value.index == 33: value.data = int(swing_mode) + break diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index e9ae7de81bcd74..311d3fe83df668 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -23,7 +23,7 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = False -REQUIREMENTS = ['influxdb==2.12.0'] +REQUIREMENTS = ['influxdb==3.0.0'] CONF_HOST = 'host' CONF_PORT = 'port' diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 27c68819909e09..2b0af395d02f4e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -248,7 +248,8 @@ def handle_light_service(service): class Light(ToggleEntity): """Representation of a light.""" - # pylint: disable=no-self-use + # pylint: disable=no-self-use, abstract-method + @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/light/enocean.py b/homeassistant/components/light/enocean.py index adb10a20fda611..2c9db86e662022 100644 --- a/homeassistant/components/light/enocean.py +++ b/homeassistant/components/light/enocean.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.enocean/ """ - import logging import math @@ -86,7 +85,7 @@ def turn_off(self, **kwargs): self._on_state = False def value_changed(self, val): - """Update the internal state of this device in HA.""" + """Update the internal state of this device.""" self._brightness = math.floor(val / 100.0 * 256.0) self._on_state = bool(val != 0) self.update_ha_state() diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py new file mode 100644 index 00000000000000..b7e0328a574207 --- /dev/null +++ b/homeassistant/components/light/homematic.py @@ -0,0 +1,102 @@ +""" +Support for Homematic lighs. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.homematic/ +""" +import logging +from homeassistant.components.light import (ATTR_BRIGHTNESS, Light) +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the Homematic light platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMLight, + discovery_info, + add_callback_devices) + + +class HMLight(homematic.HMDevice, Light): + """Representation of a Homematic light.""" + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + if not self.available: + return None + # Is dimmer? + if self._state is "LEVEL": + return int(self._hm_get_state() * 255) + else: + return None + + @property + def is_on(self): + """Return true if light is on.""" + try: + return self._hm_get_state() > 0 + except TypeError: + return False + + def turn_on(self, **kwargs): + """Turn the light on.""" + if not self.available: + return + + if ATTR_BRIGHTNESS in kwargs and self._state is "LEVEL": + percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 + self._hmdevice.set_level(percent_bright, self._channel) + else: + self._hmdevice.on(self._channel) + + def turn_off(self, **kwargs): + """Turn the light off.""" + if self.available: + self._hmdevice.off(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the Homematic object as this HA type.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the Homematic device is correct for this HA device + if isinstance(self._hmdevice, Switch): + return True + if isinstance(self._hmdevice, Dimmer): + return True + + _LOGGER.critical("This %s can't be use as light", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from the Homematic metadata.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + super()._init_data_struct() + + # Use STATE + if isinstance(self._hmdevice, Switch): + self._state = "STATE" + + # Use LEVEL + if isinstance(self._hmdevice, Dimmer): + self._state = "LEVEL" + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init datadict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init light %s.", self._name) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 5242746dc4256a..010088af824a04 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.limitlessled/ """ +# pylint: disable=abstract-method import logging from homeassistant.components.light import ( diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 04c0942e1f6a6f..d8d288afd0e6dd 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mysensors/ """ +# pylint: disable=abstract-method import logging from homeassistant.components import mysensors diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 33c759b21d5013..243d11116da443 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -1,19 +1,9 @@ """ Support for Osram Lightify. -Uses: https://github.com/aneumeier/python-lightify for the Osram light -interface. - -In order to use the platform just add the following to the configuration.yaml: - -light: - platform: osramlightify - host: - -Todo: -Add support for Non RGBW lights. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.osramlightify/ """ - import logging import socket from datetime import timedelta @@ -40,7 +30,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Find and return lights.""" + """Setup Osram Lightify lights.""" import lightify host = config.get(CONF_HOST) if host: @@ -85,7 +75,7 @@ def update_lights(): class OsramLightifyLight(Light): - """Defines an Osram Lightify Light.""" + """Representation of an Osram Lightify Light.""" def __init__(self, light_id, light, update_lights): """Initialize the light.""" diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 2438cdaab9abd4..5fdec96f5d4fbb 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -8,12 +8,13 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ Light, ATTR_RGB_COLOR +from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import color as color_util from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -35,26 +36,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): WinkLight(light) for light in pywink.get_bulbs()) -class WinkLight(Light): +class WinkLight(WinkDevice, Light): """Representation of a Wink light.""" def __init__(self, wink): - """ - Initialize the light. - - :type wink: pywink.devices.standard.bulb.WinkBulb - """ - self.wink = wink - - @property - def unique_id(self): - """Return the ID of this Wink light.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the light if any.""" - return self.wink.name() + """Initialize the Wink device.""" + WinkDevice.__init__(self, wink) @property def is_on(self): @@ -66,11 +53,6 @@ def brightness(self): """Return the brightness of the light.""" return int(self.wink.brightness() * 255) - @property - def available(self): - """True if connection == True.""" - return self.wink.available - @property def xy_color(self): """Current bulb color in CIE 1931 (XY) color space.""" @@ -112,7 +94,3 @@ def turn_on(self, **kwargs): def turn_off(self): """Turn the switch off.""" self.wink.set_state(False) - - def update(self): - """Update state of the light.""" - self.wink.update_state(require_desired_state_fulfilled=True) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index c91a2ddd489cbe..7c9cb72db26e56 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -4,13 +4,31 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.zwave/ """ +import logging + # Because we do not compile openzwave on CI # pylint: disable=import-error from threading import Timer - -from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN, Light +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ + ATTR_RGB_COLOR, DOMAIN, Light from homeassistant.components import zwave from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \ + color_temperature_mired_to_kelvin, color_temperature_to_rgb + +_LOGGER = logging.getLogger(__name__) + +COLOR_CHANNEL_WARM_WHITE = 0x01 +COLOR_CHANNEL_COLD_WHITE = 0x02 +COLOR_CHANNEL_RED = 0x04 +COLOR_CHANNEL_GREEN = 0x08 +COLOR_CHANNEL_BLUE = 0x10 + +# Generate midpoint color temperatures for bulbs that have limited +# support for white light colors +TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN +TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN +TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN def setup_platform(hass, config, add_devices, discovery_info=None): @@ -29,7 +47,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return value.set_change_verified(False) - add_devices([ZwaveDimmer(value)]) + + if node.has_command_class(zwave.COMMAND_CLASS_COLOR): + try: + add_devices([ZwaveColorLight(value)]) + except ValueError as exception: + _LOGGER.warning( + "Error initializing as color bulb: %s " + "Initializing as standard dimmer.", exception) + add_devices([ZwaveDimmer(value)]) + else: + add_devices([ZwaveDimmer(value)]) def brightness_state(value): @@ -50,8 +78,9 @@ def __init__(self, value): from pydispatch import dispatcher zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) - - self._brightness, self._state = brightness_state(value) + self._brightness = None + self._state = None + self.update_properties() # Used for value change event handling self._refreshing = False @@ -60,6 +89,11 @@ def __init__(self, value): dispatcher.connect( self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + def update_properties(self): + """Update internal properties based on zwave values.""" + # Brightness + self._brightness, self._state = brightness_state(self._value) + def _value_changed(self, value): """Called when a value has changed on the network.""" if self._value.value_id != value.value_id: @@ -67,7 +101,7 @@ def _value_changed(self, value): if self._refreshing: self._refreshing = False - self._brightness, self._state = brightness_state(value) + self.update_properties() else: def _refresh_value(): """Used timer callback for delayed value refresh.""" @@ -108,3 +142,168 @@ def turn_off(self, **kwargs): """Turn the device off.""" if self._value.node.set_dimmer(self._value.value_id, 0): self._state = STATE_OFF + + +def ct_to_rgb(temp): + """Convert color temperature (mireds) to RGB.""" + colorlist = list( + color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp))) + return [int(val) for val in colorlist] + + +class ZwaveColorLight(ZwaveDimmer): + """Representation of a Z-Wave color changing light.""" + + def __init__(self, value): + """Initialize the light.""" + self._value_color = None + self._value_color_channels = None + self._color_channels = None + self._rgb = None + self._ct = None + + # Here we attempt to find a zwave color value with the same instance + # id as the dimmer value. Currently zwave nodes that change colors + # only include one dimmer and one color command, but this will + # hopefully provide some forward compatibility for new devices that + # have multiple color changing elements. + for value_color in value.node.get_rgbbulbs().values(): + if value.instance == value_color.instance: + self._value_color = value_color + + if self._value_color is None: + raise ValueError("No matching color command found.") + + for value_color_channels in value.node.get_values( + class_id=zwave.COMMAND_CLASS_COLOR, genre='System', + type="Int").values(): + self._value_color_channels = value_color_channels + + if self._value_color_channels is None: + raise ValueError("Color Channels not found.") + + super().__init__(value) + + def update_properties(self): + """Update internal properties based on zwave values.""" + super().update_properties() + + # Color Channels + self._color_channels = self._value_color_channels.data + + # Color Data String + data = self._value_color.data + + # RGB is always present in the openzwave color data string. + self._rgb = [ + int(data[1:3], 16), + int(data[3:5], 16), + int(data[5:7], 16)] + + # Parse remaining color channels. Openzwave appends white channels + # that are present. + index = 7 + + # Warm white + if self._color_channels & COLOR_CHANNEL_WARM_WHITE: + warm_white = int(data[index:index+2], 16) + index += 2 + else: + warm_white = 0 + + # Cold white + if self._color_channels & COLOR_CHANNEL_COLD_WHITE: + cold_white = int(data[index:index+2], 16) + index += 2 + else: + cold_white = 0 + + # Color temperature. With two white channels, only two color + # temperatures are supported for the bulb. The channel values + # indicate brightness for warm/cold color temperature. + if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and + self._color_channels & COLOR_CHANNEL_COLD_WHITE): + if warm_white > 0: + self._ct = TEMP_WARM_HASS + self._rgb = ct_to_rgb(self._ct) + elif cold_white > 0: + self._ct = TEMP_COLD_HASS + self._rgb = ct_to_rgb(self._ct) + else: + # RGB color is being used. Just report midpoint. + self._ct = TEMP_MID_HASS + + # If only warm white is reported 0-255 is color temperature. + elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: + self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * ( + warm_white / 255) + self._rgb = ct_to_rgb(self._ct) + + # If only cold white is reported 0-255 is negative color temperature. + elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: + self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * ( + (255 - cold_white) / 255) + self._rgb = ct_to_rgb(self._ct) + + # If no rgb channels supported, report None. + if not (self._color_channels & COLOR_CHANNEL_RED or + self._color_channels & COLOR_CHANNEL_GREEN or + self._color_channels & COLOR_CHANNEL_BLUE): + self._rgb = None + + @property + def rgb_color(self): + """Return the rgb color.""" + return self._rgb + + @property + def color_temp(self): + """Return the color temperature.""" + return self._ct + + def turn_on(self, **kwargs): + """Turn the device on.""" + rgbw = None + + if ATTR_COLOR_TEMP in kwargs: + # With two white channels, only two color temperatures are + # supported for the bulb. + if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and + self._color_channels & COLOR_CHANNEL_COLD_WHITE): + if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS: + self._ct = TEMP_WARM_HASS + rgbw = b'#000000FF00' + else: + self._ct = TEMP_COLD_HASS + rgbw = b'#00000000FF' + + # If only warm white is reported 0-255 is color temperature + elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: + rgbw = b'#000000' + temp = ( + (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) / + (HASS_COLOR_MAX - HASS_COLOR_MIN) * 255) + rgbw += format(int(temp)).encode('utf-8') + + # If only cold white is reported 0-255 is negative color temp + elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: + rgbw = b'#000000' + temp = ( + 255 - (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) / + (HASS_COLOR_MAX - HASS_COLOR_MIN) * 255) + rgbw += format(int(temp)).encode('utf-8') + + elif ATTR_RGB_COLOR in kwargs: + self._rgb = kwargs[ATTR_RGB_COLOR] + + rgbw = b'#' + for colorval in self._rgb: + rgbw += format(colorval, '02x').encode('utf-8') + rgbw += b'0000' + + if rgbw is None: + _LOGGER.warning("rgbw string was not generated for turn_on") + else: + self._value_color.node.set_rgbw(self._value_color.value_id, rgbw) + + super().turn_on(**kwargs) diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py new file mode 100644 index 00000000000000..f10b8857499775 --- /dev/null +++ b/homeassistant/components/lock/vera.py @@ -0,0 +1,65 @@ +""" +Support for Vera locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.vera/ +""" +import logging + +from homeassistant.components.lock import LockDevice +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED) +from homeassistant.components.vera import ( + VeraDevice, VERA_DEVICES, VERA_CONTROLLER) + +DEPENDENCIES = ['vera'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Find and return Vera locks.""" + add_devices_callback( + VeraLock(device, VERA_CONTROLLER) for + device in VERA_DEVICES['lock']) + + +class VeraLock(VeraDevice, LockDevice): + """Representation of a Vera lock.""" + + def __init__(self, vera_device, controller): + """Initialize the Vera device.""" + self._state = None + VeraDevice.__init__(self, vera_device, controller) + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {} + if self.vera_device.has_battery: + attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' + + attr['Vera Device Id'] = self.vera_device.vera_device_id + return attr + + def lock(self, **kwargs): + """Lock the device.""" + self.vera_device.lock() + self._state = STATE_LOCKED + self.update_ha_state() + + def unlock(self, **kwargs): + """Unlock the device.""" + self.vera_device.unlock() + self._state = STATE_UNLOCKED + self.update_ha_state() + + @property + def is_locked(self): + """Return true if device is on.""" + return self._state == STATE_LOCKED + + def update(self): + """Called by the Vera device callback to update state.""" + self._state = (STATE_LOCKED if self.vera_device.is_locked(True) + else STATE_UNLOCKED) diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 2572796df35a79..7551302499a482 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -7,9 +7,10 @@ import logging from homeassistant.components.lock import LockDevice -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL +from homeassistant.components.wink import WinkDevice +from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -30,38 +31,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) -class WinkLockDevice(LockDevice): +class WinkLockDevice(WinkDevice, LockDevice): """Representation of a Wink lock.""" def __init__(self, wink): """Initialize the lock.""" - self.wink = wink - self._battery = self.wink.battery_level - - @property - def unique_id(self): - """Return the id of this wink lock.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the lock if any.""" - return self.wink.name() - - def update(self): - """Update the state of the lock.""" - self.wink.update_state() + WinkDevice.__init__(self, wink) @property def is_locked(self): """Return true if device is locked.""" return self.wink.state() - @property - def available(self): - """True if connection == True.""" - return self.wink.available - def lock(self, **kwargs): """Lock the device.""" self.wink.set_state(True) @@ -69,16 +50,3 @@ def lock(self, **kwargs): def unlock(self, **kwargs): """Unlock the device.""" self.wink.set_state(False) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py new file mode 100644 index 00000000000000..ef5f7516827f8d --- /dev/null +++ b/homeassistant/components/media_player/braviatv.py @@ -0,0 +1,370 @@ +""" +Support for interface with a Sony Bravia TV. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.braviatv/ +""" +import logging +import os +import json +import re +from homeassistant.loader import get_component +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) + +REQUIREMENTS = [ + 'https://github.com/aparraga/braviarc/archive/0.3.2.zip' + '#braviarc==0.3.2'] + +BRAVIA_CONFIG_FILE = 'bravia.conf' +CLIENTID_PREFIX = 'HomeAssistant' +NICKNAME = 'Home Assistant' + +# Map ip to request id for configuring +_CONFIGURING = {} + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + + +def _get_mac_address(ip_address): + """Get the MAC address of the device.""" + from subprocess import Popen, PIPE + + pid = Popen(["arp", "-n", ip_address], stdout=PIPE) + pid_component = pid.communicate()[0] + mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'), + pid_component).groups()[0] + return mac + + +def _config_from_file(filename, config=None): + """Create the configuration from a file.""" + if config: + # We're writing configuration + bravia_config = _config_from_file(filename) + if bravia_config is None: + bravia_config = {} + new_config = bravia_config.copy() + new_config.update(config) + try: + with open(filename, 'w') as fdesc: + fdesc.write(json.dumps(new_config)) + except IOError as error: + _LOGGER.error('Saving config file failed: %s', error) + return False + return True + else: + # We're reading config + if os.path.isfile(filename): + try: + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) + except ValueError as error: + return {} + except IOError as error: + _LOGGER.error('Reading config file failed: %s', error) + # This won't work yet + return False + else: + return {} + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the Sony Bravia TV platform.""" + host = config.get(CONF_HOST) + + if host is None: + return # if no host configured, do not continue + + pin = None + bravia_config = _config_from_file(hass.config.path(BRAVIA_CONFIG_FILE)) + while len(bravia_config): + # Setup a configured TV + host_ip, host_config = bravia_config.popitem() + if host_ip == host: + pin = host_config['pin'] + mac = host_config['mac'] + name = config.get(CONF_NAME) + add_devices_callback([BraviaTVDevice(host, mac, name, pin)]) + return + + setup_bravia(config, pin, hass, add_devices_callback) + + +# pylint: disable=too-many-branches +def setup_bravia(config, pin, hass, add_devices_callback): + """Setup a Sony Bravia TV based on host parameter.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + if name is None: + name = "Sony Bravia TV" + + if pin is None: + request_configuration(config, hass, add_devices_callback) + return + else: + mac = _get_mac_address(host) + if mac is not None: + mac = mac.decode('utf8') + # If we came here and configuring this host, mark as done + if host in _CONFIGURING: + request_id = _CONFIGURING.pop(host) + configurator = get_component('configurator') + configurator.request_done(request_id) + _LOGGER.info('Discovery configuration done!') + + # Save config + if not _config_from_file( + hass.config.path(BRAVIA_CONFIG_FILE), + {host: {'pin': pin, 'host': host, 'mac': mac}}): + _LOGGER.error('failed to save config file') + + add_devices_callback([BraviaTVDevice(host, mac, name, pin)]) + + +def request_configuration(config, hass, add_devices_callback): + """Request configuration steps from the user.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + if name is None: + name = "Sony Bravia" + + configurator = get_component('configurator') + + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.notify_errors( + _CONFIGURING[host], "Failed to register, please try again.") + return + + def bravia_configuration_callback(data): + """Callback after user enter PIN.""" + from braviarc import braviarc + + pin = data.get('pin') + braviarc = braviarc.BraviaRC(host) + braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + if braviarc.is_connected(): + setup_bravia(config, pin, hass, add_devices_callback) + else: + request_configuration(config, hass, add_devices_callback) + + _CONFIGURING[host] = configurator.request_config( + hass, name, bravia_configuration_callback, + description='Enter the Pin shown on your Sony Bravia TV.' + + 'If no Pin is shown, enter 0000 to let TV show you a Pin.', + description_image="/static/images/smart-tv.png", + submit_caption="Confirm", + fields=[{'id': 'pin', 'name': 'Enter the pin', 'type': ''}] + ) + + +# pylint: disable=abstract-method, too-many-public-methods, +# pylint: disable=too-many-instance-attributes, too-many-arguments +class BraviaTVDevice(MediaPlayerDevice): + """Representation of a Sony Bravia TV.""" + + def __init__(self, host, mac, name, pin): + """Initialize the Sony Bravia device.""" + from braviarc import braviarc + + self._pin = pin + self._braviarc = braviarc.BraviaRC(host, mac) + self._name = name + self._state = STATE_OFF + self._muted = False + self._program_name = None + self._channel_name = None + self._channel_number = None + self._source = None + self._source_list = [] + self._original_content_list = [] + self._content_mapping = {} + self._duration = None + self._content_uri = None + self._id = None + self._playing = False + self._start_date_time = None + self._program_media_type = None + self._min_volume = None + self._max_volume = None + self._volume = None + + self._braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + if self._braviarc.is_connected(): + self.update() + else: + self._state = STATE_OFF + + def update(self): + """Update TV info.""" + if not self._braviarc.is_connected(): + self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME) + if not self._braviarc.is_connected(): + return + + # Retrieve the latest data. + try: + if self._state == STATE_ON: + # refresh volume info: + self._refresh_volume() + self._refresh_channels() + + playing_info = self._braviarc.get_playing_info() + if playing_info is None or len(playing_info) == 0: + self._state = STATE_OFF + else: + self._state = STATE_ON + self._program_name = playing_info.get('programTitle') + self._channel_name = playing_info.get('title') + self._program_media_type = playing_info.get( + 'programMediaType') + self._channel_number = playing_info.get('dispNum') + self._source = playing_info.get('source') + self._content_uri = playing_info.get('uri') + self._duration = playing_info.get('durationSec') + self._start_date_time = playing_info.get('startDateTime') + + except Exception as exception_instance: # pylint: disable=broad-except + _LOGGER.error(exception_instance) + self._state = STATE_OFF + + def _refresh_volume(self): + """Refresh volume information.""" + volume_info = self._braviarc.get_volume_info() + if volume_info is not None: + self._volume = volume_info.get('volume') + self._min_volume = volume_info.get('minVolume') + self._max_volume = volume_info.get('maxVolume') + self._muted = volume_info.get('mute') + + def _refresh_channels(self): + if len(self._source_list) == 0: + self._content_mapping = self._braviarc. \ + load_source_list() + self._source_list = [] + for key in self._content_mapping: + self._source_list.append(key) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def source(self): + """Return the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + if self._volume is not None: + return self._volume / 100 + else: + return None + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_BRAVIA + + @property + def media_title(self): + """Title of current playing media.""" + return_value = None + if self._channel_name is not None: + return_value = self._channel_name + if self._program_name is not None: + return_value = return_value + ': ' + self._program_name + return return_value + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self._channel_name + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._duration + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._braviarc.set_volume_level(volume) + + def turn_on(self): + """Turn the media player on.""" + self._braviarc.turn_on() + + def turn_off(self): + """Turn off media player.""" + self._braviarc.turn_off() + + def volume_up(self): + """Volume up the media player.""" + self._braviarc.volume_up() + + def volume_down(self): + """Volume down media player.""" + self._braviarc.volume_down() + + def mute_volume(self, mute): + """Send mute command.""" + self._braviarc.mute_volume(mute) + + def select_source(self, source): + """Set the input source.""" + if source in self._content_mapping: + uri = self._content_mapping[source] + self._braviarc.play_content(uri) + + def media_play_pause(self): + """Simulate play pause media player.""" + if self._playing: + self.media_pause() + else: + self.media_play() + + def media_play(self): + """Send play command.""" + self._playing = True + self._braviarc.media_play() + + def media_pause(self): + """Send media pause command to media player.""" + self._playing = False + self._braviarc.media_pause() + + def media_next_track(self): + """Send next track command.""" + self._braviarc.media_next_track() + + def media_previous_track(self): + """Send the previous track command.""" + self._braviarc.media_previous_track() diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py new file mode 100644 index 00000000000000..4726a1fa6a9bd8 --- /dev/null +++ b/homeassistant/components/media_player/cmus.py @@ -0,0 +1,213 @@ +""" +Support for interacting with and controlling the cmus music player. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.cmus/ +""" +import logging + +from homeassistant.components.media_player import ( + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK, + MediaPlayerDevice) +from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING, + CONF_HOST, CONF_NAME, CONF_PASSWORD, + CONF_PORT) + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pycmus==0.1.0'] + +SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ + SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_PLAY_MEDIA | SUPPORT_SEEK + + +def setup_platform(hass, config, add_devices, discover_info=None): + """Setup the CMUS platform.""" + from pycmus import exceptions + + host = config.get(CONF_HOST, None) + password = config.get(CONF_PASSWORD, None) + port = config.get(CONF_PORT, None) + name = config.get(CONF_NAME, None) + if host and not password: + _LOGGER.error("A password must be set if using a remote cmus server") + return False + try: + cmus_remote = CmusDevice(host, password, port, name) + except exceptions.InvalidPassword: + _LOGGER.error("The provided password was rejected by cmus") + return False + add_devices([cmus_remote]) + + +class CmusDevice(MediaPlayerDevice): + """Representation of a running CMUS.""" + + # pylint: disable=no-member, too-many-public-methods, abstract-method + def __init__(self, server, password, port, name): + """Initialize the CMUS device.""" + from pycmus import remote + + if server: + port = port or 3000 + self.cmus = remote.PyCmus(server=server, password=password, + port=port) + auto_name = "cmus-%s" % server + else: + self.cmus = remote.PyCmus() + auto_name = "cmus-local" + self._name = name or auto_name + self.status = {} + self.update() + + def update(self): + """Get the latest data and update the state.""" + status = self.cmus.get_status_dict() + if not status: + _LOGGER.warning("Recieved no status from cmus") + else: + self.status = status + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the media state.""" + if 'status' not in self.status: + self.update() + if self.status['status'] == 'playing': + return STATE_PLAYING + elif self.status['status'] == 'paused': + return STATE_PAUSED + else: + return STATE_OFF + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self.status.get('file') + + @property + def content_type(self): + """Content type of the current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self.status.get('duration') + + @property + def media_title(self): + """Title of current playing media.""" + return self.status['tag'].get('title') + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self.status['tag'].get('artist') + + @property + def media_track(self): + """Track number of current playing media, music track only.""" + return self.status['tag'].get('tracknumber') + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + return self.status['tag'].get('album') + + @property + def media_album_artist(self): + """Album artist of current playing media, music track only.""" + return self.status['tag'].get('albumartist') + + @property + def volume_level(self): + """Return the volume level.""" + left = self.status['set'].get('vol_left')[0] + right = self.status['set'].get('vol_right')[0] + if left != right: + volume = float(left + right) / 2 + else: + volume = left + return int(volume)/100 + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_CMUS + + def turn_off(self): + """Service to send the CMUS the command to stop playing.""" + self.cmus.player_stop() + + def turn_on(self): + """Service to send the CMUS the command to start playing.""" + self.cmus.player_play() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self.cmus.set_volume(int(volume * 100)) + + def volume_up(self): + """Function to send CMUS the command for volume up.""" + left = self.status['set'].get('vol_left') + right = self.status['set'].get('vol_right') + if left != right: + current_volume = float(left + right) / 2 + else: + current_volume = left + + if current_volume <= 100: + self.cmus.set_volume(int(current_volume) + 5) + + def volume_down(self): + """Function to send CMUS the command for volume down.""" + left = self.status['set'].get('vol_left') + right = self.status['set'].get('vol_right') + if left != right: + current_volume = float(left + right) / 2 + else: + current_volume = left + + if current_volume <= 100: + self.cmus.set_volume(int(current_volume) - 5) + + def play_media(self, media_type, media_id, **kwargs): + """Send the play command.""" + if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]: + self.cmus.player_play_file(media_id) + else: + _LOGGER.error( + "Invalid media type %s. Only %s and %s are supported", + media_type, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST) + + def media_pause(self): + """Send the pause command.""" + self.cmus.player_pause() + + def media_next_track(self): + """Send next track command.""" + self.cmus.player_next() + + def media_previous_track(self): + """Send next track command.""" + self.cmus.player_prev() + + def media_seek(self, position): + """Send seek command.""" + self.cmus.seek(position) + + def media_play(self): + """Send the play command.""" + self.cmus.player_play() + + def media_stop(self): + """Send the stop command.""" + self.cmus.stop() diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index 8259d043cf3231..eb6e15379d8259 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -15,7 +15,7 @@ STATE_PLAYING, STATE_PAUSED, STATE_OFF) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['websocket-client==0.35.0'] +REQUIREMENTS = ['websocket-client==0.37.0'] SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 432ff73c3671aa..3af270a05b0124 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -15,7 +15,7 @@ STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['jsonrpc-requests==0.2'] +REQUIREMENTS = ['jsonrpc-requests==0.3'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ @@ -301,4 +301,4 @@ def media_seek(self, position): def play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - self._server.Player.Open({media_type: media_id}, {}) + self._server.Player.Open({"item": {"file": str(media_id)}}) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index cd16dc4a620ddf..6ff1ae1510fc9d 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.roku/ """ - import logging from homeassistant.components.media_player import ( @@ -77,7 +76,8 @@ def update(self): self.current_app = self.roku.current_app else: self.current_app = None - except requests.exceptions.ConnectionError: + except (requests.exceptions.ConnectionError, + requests.exceptions.ReadTimeout): self.current_app = None def get_source_list(self): diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index d1ef92ee4d5658..9ab831bdbb44ae 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -154,12 +154,20 @@ sonos_group_players: description: Name(s) of entites that will coordinate the grouping. Platform dependent. example: 'media_player.living_room_sonos' +sonos_unjoin: + description: Unjoin the player from a group. + + fields: + entity_id: + description: Name(s) of entites that will be unjoined from their group. Platform dependent. + example: 'media_player.living_room_sonos' + sonos_snapshot: description: Take a snapshot of the media player. fields: entity_id: - description: Name(s) of entites that will coordinate the grouping. Platform dependent. + description: Name(s) of entites that will be snapshot. Platform dependent. example: 'media_player.living_room_sonos' sonos_restore: @@ -167,5 +175,5 @@ sonos_restore: fields: entity_id: - description: Name(s) of entites that will coordinate the grouping. Platform dependent. - example: 'media_player.living_room_sonos' \ No newline at end of file + description: Name(s) of entites that will be restored. Platform dependent. + example: 'media_player.living_room_sonos' diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 44cdd414da41d1..998490fb9b9be6 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.snapcast/ """ - import logging import socket diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 9239f1edae8ed7..7d0cd12175a5e8 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -34,11 +34,12 @@ SUPPORT_SEEK SERVICE_GROUP_PLAYERS = 'sonos_group_players' +SERVICE_UNJOIN = 'sonos_unjoin' SERVICE_SNAPSHOT = 'sonos_snapshot' SERVICE_RESTORE = 'sonos_restore' -# pylint: disable=unused-argument +# pylint: disable=unused-argument, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Sonos platform.""" import soco @@ -72,8 +73,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) _LOGGER.info('Added %s Sonos speakers', len(players)) - def group_players_service(service): - """Group media players, use player as coordinator.""" + def _apply_service(service, service_func, *service_func_args): + """Internal func for applying a service.""" entity_id = service.data.get('entity_id') if entity_id: @@ -83,36 +84,24 @@ def group_players_service(service): _devices = devices for device in _devices: - device.group_players() + service_func(device, *service_func_args) device.update_ha_state(True) - def snapshot(service): - """Take a snapshot.""" - entity_id = service.data.get('entity_id') + def group_players_service(service): + """Group media players, use player as coordinator.""" + _apply_service(service, SonosDevice.group_players) - if entity_id: - _devices = [device for device in devices - if device.entity_id == entity_id] - else: - _devices = devices + def unjoin_service(service): + """Unjoin the player from a group.""" + _apply_service(service, SonosDevice.unjoin) - for device in _devices: - device.snapshot(service) - device.update_ha_state(True) + def snapshot_service(service): + """Take a snapshot.""" + _apply_service(service, SonosDevice.snapshot) - def restore(service): + def restore_service(service): """Restore a snapshot.""" - entity_id = service.data.get('entity_id') - - if entity_id: - _devices = [device for device in devices - if device.entity_id == entity_id] - else: - _devices = devices - - for device in _devices: - device.restore(service) - device.update_ha_state(True) + _apply_service(service, SonosDevice.restore) descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) @@ -121,12 +110,16 @@ def restore(service): group_players_service, descriptions.get(SERVICE_GROUP_PLAYERS)) + hass.services.register(DOMAIN, SERVICE_UNJOIN, + unjoin_service, + descriptions.get(SERVICE_UNJOIN)) + hass.services.register(DOMAIN, SERVICE_SNAPSHOT, - snapshot, + snapshot_service, descriptions.get(SERVICE_SNAPSHOT)) hass.services.register(DOMAIN, SERVICE_RESTORE, - restore, + restore_service, descriptions.get(SERVICE_RESTORE)) return True @@ -356,12 +349,17 @@ def group_players(self): self._player.partymode() @only_if_coordinator - def snapshot(self, service): + def unjoin(self): + """Unjoin the player from a group.""" + self._player.unjoin() + + @only_if_coordinator + def snapshot(self): """Snapshot the player.""" self.soco_snapshot.snapshot() @only_if_coordinator - def restore(self, service): + def restore(self): """Restore snapshot for the player.""" self.soco_snapshot.restore(True) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index f5fa8cc486c735..8bfdeebf85d5b2 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ - import logging # pylint: disable=import-error from copy import copy diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e9087d9c578731..6db231f6bd7fcb 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -29,7 +29,7 @@ SERVICE_PUBLISH = 'publish' EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' -REQUIREMENTS = ['paho-mqtt==1.1'] +REQUIREMENTS = ['paho-mqtt==1.2'] CONF_EMBEDDED = 'embedded' CONF_BROKER = 'broker' diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index b202f38fa7ce01..a8bc3cf51796d3 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -7,7 +7,7 @@ import logging from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) + ATTR_TITLE, ATTR_TARGET, ATTR_DATA, DOMAIN, BaseNotificationService) from homeassistant.const import CONF_API_KEY from homeassistant.helpers import validate_config @@ -51,7 +51,18 @@ def send_message(self, message="", **kwargs): """Send a message to a user.""" from pushover import RequestError + # Make a copy and use empty dict if necessary + data = dict(kwargs.get(ATTR_DATA) or {}) + + data['title'] = kwargs.get(ATTR_TITLE) + + target = kwargs.get(ATTR_TARGET) + if target is not None: + data['device'] = target + try: - self.pushover.send_message(message, title=kwargs.get(ATTR_TITLE)) + self.pushover.send_message(message, **data) + except ValueError as val_err: + _LOGGER.error(str(val_err)) except RequestError: _LOGGER.exception("Could not send pushover notification") diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 49b6f8acc93a07..5257c965cd6707 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.helpers import validate_config -REQUIREMENTS = ['slacker==0.9.16'] +REQUIREMENTS = ['slacker==0.9.17'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py new file mode 100644 index 00000000000000..66a634616faaff --- /dev/null +++ b/homeassistant/components/persistent_notification.py @@ -0,0 +1,80 @@ +""" +A component which is collecting configuration errors. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/persistent_notification/ +""" +import logging + +import voluptuous as vol + +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template, config_validation as cv +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.util import slugify + +DOMAIN = 'persistent_notification' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +SERVICE_CREATE = 'create' +ATTR_TITLE = 'title' +ATTR_MESSAGE = 'message' +ATTR_NOTIFICATION_ID = 'notification_id' + +SCHEMA_SERVICE_CREATE = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.template, + vol.Optional(ATTR_TITLE): cv.template, + vol.Optional(ATTR_NOTIFICATION_ID): cv.string, +}) + + +DEFAULT_OBJECT_ID = 'notification' +_LOGGER = logging.getLogger(__name__) + + +def create(hass, message, title=None, notification_id=None): + """Turn all or specified light off.""" + data = { + key: value for key, value in [ + (ATTR_TITLE, title), + (ATTR_MESSAGE, message), + (ATTR_NOTIFICATION_ID, notification_id), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_CREATE, data) + + +def setup(hass, config): + """Setup the persistent notification component.""" + def create_service(call): + """Handle a create notification service call.""" + title = call.data.get(ATTR_TITLE) + message = call.data.get(ATTR_MESSAGE) + notification_id = call.data.get(ATTR_NOTIFICATION_ID) + + if notification_id is not None: + entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) + else: + entity_id = generate_entity_id(ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, + hass=hass) + attr = {} + if title is not None: + try: + title = template.render(hass, title) + except TemplateError as ex: + _LOGGER.error('Error rendering title %s: %s', title, ex) + + attr[ATTR_TITLE] = title + + try: + message = template.render(hass, message) + except TemplateError as ex: + _LOGGER.error('Error rendering message %s: %s', message, ex) + + hass.states.set(entity_id, message, attr) + + hass.services.register(DOMAIN, SERVICE_CREATE, create_service, {}, + SCHEMA_SERVICE_CREATE) + + return True diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py new file mode 100644 index 00000000000000..9bdad7ee68c643 --- /dev/null +++ b/homeassistant/components/rollershutter/homematic.py @@ -0,0 +1,101 @@ +""" +The homematic rollershutter platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rollershutter.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. +""" + +import logging +from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) +from homeassistant.components.rollershutter import RollershutterDevice,\ + ATTR_CURRENT_POSITION +import homeassistant.components.homematic as homematic + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMRollershutter, + discovery_info, + add_callback_devices) + + +class HMRollershutter(homematic.HMDevice, RollershutterDevice): + """Represents a Homematic Rollershutter in Home Assistant.""" + + @property + def current_position(self): + """ + Return current position of rollershutter. + + None is unknown, 0 is closed, 100 is fully open. + """ + if self.available: + return int((1 - self._hm_get_state()) * 100) + return None + + def position(self, **kwargs): + """Move to a defined position: 0 (closed) and 100 (open).""" + if self.available: + if ATTR_CURRENT_POSITION in kwargs: + position = float(kwargs[ATTR_CURRENT_POSITION]) + position = min(100, max(0, position)) + level = (100 - position) / 100.0 + self._hmdevice.set_level(level, self._channel) + + @property + def state(self): + """Return the state of the rollershutter.""" + current = self.current_position + if current is None: + return STATE_UNKNOWN + + return STATE_CLOSED if current == 100 else STATE_OPEN + + def move_up(self, **kwargs): + """Move the rollershutter up.""" + if self.available: + self._hmdevice.move_up(self._channel) + + def move_down(self, **kwargs): + """Move the rollershutter down.""" + if self.available: + self._hmdevice.move_down(self._channel) + + def stop(self, **kwargs): + """Stop the device if in motion.""" + if self.available: + self._hmdevice.stop(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.actors import Blind + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if isinstance(self._hmdevice, Blind): + return True + + _LOGGER.critical("This %s can't be use as rollershutter!", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + super()._init_data_struct() + + # Add state to data dict + self._state = "LEVEL" + self._data.update({self._state: STATE_UNKNOWN}) diff --git a/homeassistant/components/rollershutter/wink.py b/homeassistant/components/rollershutter/wink.py index e01b2573ac64be..8a31148da01b06 100644 --- a/homeassistant/components/rollershutter/wink.py +++ b/homeassistant/components/rollershutter/wink.py @@ -7,9 +7,10 @@ import logging from homeassistant.components.rollershutter import RollershutterDevice +from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pywink.get_shades()) -class WinkRollershutterDevice(RollershutterDevice): +class WinkRollershutterDevice(WinkDevice, RollershutterDevice): """Representation of a Wink rollershutter (shades).""" def __init__(self, wink): """Initialize the rollershutter.""" - self.wink = wink - self._battery = None + WinkDevice.__init__(self, wink) @property def should_poll(self): """Wink Shades don't track their position.""" return False - @property - def unique_id(self): - """Return the ID of this wink rollershutter.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the rollershutter if any.""" - return self.wink.name() - - def update(self): - """Update the state of the rollershutter.""" - return self.wink.update_state() - - @property - def available(self): - """True if connection == True.""" - return self.wink.available - def move_down(self): """Close the shade.""" self.wink.set_state(0) diff --git a/homeassistant/components/rollershutter/zwave.py b/homeassistant/components/rollershutter/zwave.py new file mode 100644 index 00000000000000..81b891d7bf1921 --- /dev/null +++ b/homeassistant/components/rollershutter/zwave.py @@ -0,0 +1,76 @@ +""" +Support for Zwave roller shutter components. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/rollershutter.zwave/ +""" +# Because we do not compile openzwave on CI +# pylint: disable=import-error +import logging +from homeassistant.components.rollershutter import DOMAIN +from homeassistant.components.zwave import ZWaveDeviceEntity +from homeassistant.components import zwave +from homeassistant.components.rollershutter import RollershutterDevice + +COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38 +COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37 + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return Z-Wave roller shutters.""" + if discovery_info is None or zwave.NETWORK is None: + return + + node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] + value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] + + if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL: + return + if value.index != 0: + return + + value.set_change_verified(False) + add_devices([ZwaveRollershutter(value)]) + + +class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice): + """Representation of an Zwave roller shutter.""" + + def __init__(self, value): + """Initialize the zwave rollershutter.""" + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + ZWaveDeviceEntity.__init__(self, value, DOMAIN) + self._node = value.node + dispatcher.connect( + self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + + def value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.value_id == value.value_id: + self.update_ha_state(True) + _LOGGER.debug("Value changed on network %s", value) + + @property + def current_position(self): + """Return the current position of Zwave roller shutter.""" + return self._value.data + + def move_up(self, **kwargs): + """Move the roller shutter up.""" + self._node.set_dimmer(self._value.value_id, 100) + + def move_down(self, **kwargs): + """Move the roller shutter down.""" + self._node.set_dimmer(self._value.value_id, 0) + + def stop(self, **kwargs): + """Stop the roller shutter.""" + for value in self._node.get_values( + class_id=COMMAND_CLASS_SWITCH_BINARY).values(): + # Rollershutter will toggle between UP (True), DOWN (False). + # It also stops the shutter if the same value is sent while moving. + value.data = value.data + break diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index a38ee76b3bb886..99b96b971c9853 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -1,38 +1,43 @@ """ -Support for information about the German trans system. +Support for information about the German train system. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.deutsche_bahn/ """ import logging -from datetime import timedelta, datetime +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import (CONF_PLATFORM) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['schiene==0.17'] + +CONF_START = 'from' +CONF_DESTINATION = 'to' ICON = 'mdi:train' +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'deutsche_bahn', + vol.Required(CONF_START): cv.string, + vol.Required(CONF_DESTINATION): cv.string, +}) + # Return cached results if last scan was less then this time ago. MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Deutsche Bahn Sensor.""" - start = config.get('from') - goal = config.get('to') - - if start is None: - _LOGGER.error('Missing required variable: "from"') - return False + start = config.get(CONF_START) + destination = config.get(CONF_DESTINATION) - if goal is None: - _LOGGER.error('Missing required variable: "to"') - return False - - dev = [] - dev.append(DeutscheBahnSensor(start, goal)) - add_devices_callback(dev) + add_devices([DeutscheBahnSensor(start, destination)]) # pylint: disable=too-few-public-methods @@ -63,16 +68,17 @@ def state(self): @property def state_attributes(self): """Return the state attributes.""" - return self.data.connections[0] + connections = self.data.connections[0] + connections['next'] = self.data.connections[1]['departure'] + connections['next_on'] = self.data.connections[2]['departure'] + return connections def update(self): """Get the latest delay from bahn.de and updates the state.""" self.data.update() self._state = self.data.connections[0].get('departure', 'Unknown') if self.data.connections[0]['delay'] != 0: - self._state += " + {}".format( - self.data.connections[0]['delay'] - ) + self._state += " + {}".format(self.data.connections[0]['delay']) # pylint: disable=too-few-public-methods @@ -90,18 +96,15 @@ def __init__(self, start, goal): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the connection data.""" - self.connections = self.schiene.connections(self.start, - self.goal, - datetime.now()) + self.connections = self.schiene.connections(self.start, self.goal) + for con in self.connections: - # Details info is not useful. - # Having a more consistent interface simplifies - # usage of Template sensors later on + # Detail info is not useful. Having a more consistent interface + # simplifies usage of template sensors. if 'details' in con: con.pop('details') - delay = con.get('delay', - {'delay_departure': 0, - 'delay_arrival': 0}) - # IMHO only delay_departure is usefull + delay = con.get('delay', {'delay_departure': 0, + 'delay_arrival': 0}) + # IMHO only delay_departure is useful con['delay'] = delay['delay_departure'] con['ontime'] = con.get('ontime', False) diff --git a/homeassistant/components/sensor/envisalink.py b/homeassistant/components/sensor/envisalink.py new file mode 100644 index 00000000000000..cd71673b99f47e --- /dev/null +++ b/homeassistant/components/sensor/envisalink.py @@ -0,0 +1,68 @@ +""" +Support for Envisalink sensors (shows panel info). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.envisalink/ +""" +import logging +from homeassistant.components.envisalink import (EVL_CONTROLLER, + PARTITION_SCHEMA, + CONF_PARTITIONNAME, + EnvisalinkDevice, + SIGNAL_KEYPAD_UPDATE) + +DEPENDENCIES = ['envisalink'] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Perform the setup for Envisalink sensor devices.""" + _configured_partitions = discovery_info['partitions'] + for part_num in _configured_partitions: + _device_config_data = PARTITION_SCHEMA( + _configured_partitions[part_num]) + _device = EnvisalinkSensor( + _device_config_data[CONF_PARTITIONNAME], + part_num, + EVL_CONTROLLER.alarm_state['partition'][part_num], + EVL_CONTROLLER) + add_devices_callback([_device]) + + +class EnvisalinkSensor(EnvisalinkDevice): + """Representation of an envisalink keypad.""" + + def __init__(self, partition_name, partition_number, info, controller): + """Initialize the sensor.""" + from pydispatch import dispatcher + self._icon = 'mdi:alarm' + self._partition_number = partition_number + _LOGGER.debug('Setting up sensor for partition: ' + partition_name) + EnvisalinkDevice.__init__(self, + partition_name + ' Keypad', + info, + controller) + + dispatcher.connect(self._update_callback, + signal=SIGNAL_KEYPAD_UPDATE, + sender=dispatcher.Any) + + @property + def icon(self): + """Return the icon if any.""" + return self._icon + + @property + def state(self): + """Return the overall state.""" + return self._info['status']['alpha'] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._info['status'] + + def _update_callback(self, partition): + """Update the partition state in HA, if needed.""" + if partition is None or int(partition) == self._partition_number: + self.update_ha_state() diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py new file mode 100644 index 00000000000000..05f6003039e774 --- /dev/null +++ b/homeassistant/components/sensor/fixer.py @@ -0,0 +1,125 @@ +""" +Currency exchange rate support that comes from fixer.io. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.fixer/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import (CONF_PLATFORM, CONF_NAME) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['fixerio==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Exchange rate" +ICON = 'mdi:currency' + +CONF_BASE = 'base' +CONF_TARGET = 'target' + +STATE_ATTR_BASE = 'Base currency' +STATE_ATTR_TARGET = 'Target currency' +STATE_ATTR_EXCHANGE_RATE = 'Exchange rate' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'fixer', + vol.Optional(CONF_BASE): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_TARGET): cv.string, +}) + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_UPDATES = timedelta(days=1) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Fixer.io sensor.""" + from fixerio import (Fixerio, exceptions) + + name = config.get(CONF_NAME, DEFAULT_NAME) + base = config.get(CONF_BASE, 'USD') + target = config.get(CONF_TARGET) + + try: + Fixerio(base=base, symbols=[target], secure=True).latest() + except exceptions.FixerioException: + _LOGGER.error('One of the given currencies is not supported') + return False + + data = ExchangeData(base, target) + add_devices([ExchangeRateSensor(data, name, target)]) + + +# pylint: disable=too-few-public-methods +class ExchangeRateSensor(Entity): + """Representation of a Exchange sensor.""" + + def __init__(self, data, name, target): + """Initialize the sensor.""" + self.data = data + self._target = target + self._name = name + self._state = None + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._target + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.data.rate is not None: + return { + STATE_ATTR_BASE: self.data.rate['base'], + STATE_ATTR_TARGET: self._target, + STATE_ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target] + } + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the latest data and updates the states.""" + self.data.update() + self._state = round(self.data.rate['rates'][self._target], 3) + + +class ExchangeData(object): + """Get the latest data and update the states.""" + + def __init__(self, base_currency, target_currency): + """Initialize the data object.""" + from fixerio import Fixerio + + self.rate = None + self.base_currency = base_currency + self.target_currency = target_currency + self.exchange = Fixerio(base=self.base_currency, + symbols=[self.target_currency], + secure=True) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from Fixer.io.""" + self.rate = self.exchange.latest() diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py new file mode 100644 index 00000000000000..efd22b6cd6902a --- /dev/null +++ b/homeassistant/components/sensor/homematic.py @@ -0,0 +1,115 @@ +""" +The homematic sensor platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. +""" + +import logging +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + +HM_STATE_HA_CAST = { + "RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"}, + "WaterSensor": {0: "dry", 1: "wet", 2: "water"} +} + +HM_UNIT_HA_CAST = { + "HUMIDITY": "%", + "TEMPERATURE": "°C", + "BRIGHTNESS": "#", + "POWER": "W", + "CURRENT": "mA", + "VOLTAGE": "V", + "ENERGY_COUNTER": "Wh" +} + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMSensor, + discovery_info, + add_callback_devices) + + +class HMSensor(homematic.HMDevice): + """Represents various Homematic sensors in Home Assistant.""" + + @property + def state(self): + """Return the state of the sensor.""" + if not self.available: + return STATE_UNKNOWN + + # Does a cast exist for this class? + name = self._hmdevice.__class__.__name__ + if name in HM_STATE_HA_CAST: + return HM_STATE_HA_CAST[name].get(self._hm_get_state(), None) + + # No cast, return original value + return self._hm_get_state() + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + if not self.available: + return None + + return HM_UNIT_HA_CAST.get(self._state, None) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.sensors import HMSensor as pyHMSensor + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if not isinstance(self._hmdevice, pyHMSensor): + _LOGGER.critical("This %s can't be use as sensor!", self._name) + return False + + # Does user defined value exist? + if self._state and self._state not in self._hmdevice.SENSORNODE: + # pylint: disable=logging-too-many-args + _LOGGER.critical("This %s have no sensor with %s! Values are", + self._name, self._state, + str(self._hmdevice.SENSORNODE.keys())) + return False + + # No param is set and more than 1 sensor nodes are present + if self._state is None and len(self._hmdevice.SENSORNODE) > 1: + _LOGGER.critical("This %s has multiple sensor nodes. " + + "Please us param. Values are: %s", self._name, + str(self._hmdevice.SENSORNODE.keys())) + return False + + _LOGGER.debug("%s is okay for linking", self._name) + return True + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + super()._init_data_struct() + + if self._state is None and len(self._hmdevice.SENSORNODE) == 1: + for value in self._hmdevice.SENSORNODE: + self._state = value + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init datadict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init sensor %s.", self._name) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 22caab1d1fbce0..05498d41496f86 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -11,19 +11,29 @@ from homeassistant.util import Throttle from homeassistant.loader import get_component + DEPENDENCIES = ["netatmo"] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - 'co2': ['CO2', 'ppm', 'mdi:cloud'], - 'pressure': ['Pressure', 'mbar', 'mdi:gauge'], - 'noise': ['Noise', 'dB', 'mdi:volume-high'], - 'humidity': ['Humidity', '%', 'mdi:water-percent'], - 'rain': ['Rain', 'mm', 'mdi:weather-rainy'], - 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], - 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], + 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], + 'co2': ['CO2', 'ppm', 'mdi:cloud'], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge'], + 'noise': ['Noise', 'dB', 'mdi:volume-high'], + 'humidity': ['Humidity', '%', 'mdi:water-percent'], + 'rain': ['Rain', 'mm', 'mdi:weather-rainy'], + 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], + 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], + 'battery_vp': ['Battery', '', 'mdi:battery'], + 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'], + 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'], + 'WindAngle': ['Angle', '', 'mdi:compass'], + 'WindStrength': ['Strength', 'km/h', 'mdi:weather-windy'], + 'GustAngle': ['Gust Angle', '', 'mdi:compass'], + 'GustStrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'], + 'rf_status': ['Radio', '', 'mdi:signal'], + 'wifi_status': ['Wifi', '', 'mdi:wifi'] } CONF_STATION = 'station' @@ -97,6 +107,8 @@ def unit_of_measurement(self): return self._unit_of_measurement # pylint: disable=too-many-branches + # Fix for pylint too many statements error + # pylint: disable=too-many-statements def update(self): """Get the latest data from NetAtmo API and updates the states.""" self.netatmo_data.update() @@ -118,6 +130,79 @@ def update(self): self._state = data['CO2'] elif self.type == 'pressure': self._state = round(data['Pressure'], 1) + elif self.type == 'battery_vp': + if data['battery_vp'] >= 5500: + self._state = "Full" + elif data['battery_vp'] >= 5100: + self._state = "High" + elif data['battery_vp'] >= 4600: + self._state = "Medium" + elif data['battery_vp'] >= 4100: + self._state = "Low" + elif data['battery_vp'] < 4100: + self._state = "Very Low" + elif self.type == 'min_temp': + self._state = data['min_temp'] + elif self.type == 'max_temp': + self._state = data['max_temp'] + elif self.type == 'WindAngle': + if data['WindAngle'] >= 330: + self._state = "North (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 300: + self._state = "North-West (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 240: + self._state = "West (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 210: + self._state = "South-West (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 150: + self._state = "South (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 120: + self._state = "South-East (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 60: + self._state = "East (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 30: + self._state = "North-East (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 0: + self._state = "North (%d\xb0)" % data['WindAngle'] + elif self.type == 'WindStrength': + self._state = data['WindStrength'] + elif self.type == 'GustAngle': + if data['GustAngle'] >= 330: + self._state = "North (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 300: + self._state = "North-West (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 240: + self._state = "West (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 210: + self._state = "South-West (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 150: + self._state = "South (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 120: + self._state = "South-East (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 60: + self._state = "East (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 30: + self._state = "North-East (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 0: + self._state = "North (%d\xb0)" % data['GustAngle'] + elif self.type == 'GustStrength': + self._state = data['GustStrength'] + elif self.type == 'rf_status': + if data['rf_status'] >= 90: + self._state = "Low" + elif data['rf_status'] >= 76: + self._state = "Medium" + elif data['rf_status'] >= 60: + self._state = "High" + elif data['rf_status'] <= 59: + self._state = "Full" + elif self.type == 'wifi_status': + if data['wifi_status'] >= 86: + self._state = "Bad" + elif data['wifi_status'] >= 71: + self._state = "Middle" + elif data['wifi_status'] <= 70: + self._state = "Good" class NetAtmoData(object): diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 6941fc952a6d5d..a2a3f0811f2d0e 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -12,21 +12,24 @@ from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -BASE_DIR = '/sys/bus/w1/devices/' -DEVICE_FOLDERS = glob(os.path.join(BASE_DIR, '28*')) -SENSOR_IDS = [] -DEVICE_FILES = [] -for device_folder in DEVICE_FOLDERS: - SENSOR_IDS.append(os.path.split(device_folder)[1]) - DEVICE_FILES.append(os.path.join(device_folder, 'w1_slave')) - _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the one wire Sensors.""" - if DEVICE_FILES == []: + base_dir = config.get('mount_dir', '/sys/bus/w1/devices/') + device_folders = glob(os.path.join(base_dir, '[10,22,28,3B,42]*')) + sensor_ids = [] + device_files = [] + for device_folder in device_folders: + sensor_ids.append(os.path.split(device_folder)[1]) + if base_dir.startswith('/sys/bus/w1/devices'): + device_files.append(os.path.join(device_folder, 'w1_slave')) + else: + device_files.append(os.path.join(device_folder, 'temperature')) + + if device_files == []: _LOGGER.error('No onewire sensor found.') _LOGGER.error('Check if dtoverlay=w1-gpio,gpiopin=4.') _LOGGER.error('is in your /boot/config.txt and') @@ -34,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return devs = [] - names = SENSOR_IDS + names = sensor_ids for key in config.keys(): if key == "names": @@ -47,9 +50,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # map names to ids. elif isinstance(config['names'], dict): names = [] - for sensor_id in SENSOR_IDS: + for sensor_id in sensor_ids: names.append(config['names'].get(sensor_id, sensor_id)) - for device_file, name in zip(DEVICE_FILES, names): + for device_file, name in zip(device_files, names): devs.append(OneWire(name, device_file)) add_devices(devs) @@ -88,14 +91,27 @@ def unit_of_measurement(self): def update(self): """Get the latest data from the device.""" - lines = self._read_temp_raw() - while lines[0].strip()[-3:] != 'YES': - time.sleep(0.2) + temp = -99 + if self._device_file.startswith('/sys/bus/w1/devices'): lines = self._read_temp_raw() - equals_pos = lines[1].find('t=') - if equals_pos != -1: - temp_string = lines[1][equals_pos+2:] - temp = round(float(temp_string) / 1000.0, 1) - if temp < -55 or temp > 125: - return - self._state = temp + while lines[0].strip()[-3:] != 'YES': + time.sleep(0.2) + lines = self._read_temp_raw() + equals_pos = lines[1].find('t=') + if equals_pos != -1: + temp_string = lines[1][equals_pos+2:] + temp = round(float(temp_string) / 1000.0, 1) + else: + ds_device_file = open(self._device_file, 'r') + temp_read = ds_device_file.readlines() + ds_device_file.close() + if len(temp_read) == 1: + try: + temp = round(float(temp_read[0]), 1) + except ValueError: + _LOGGER.warning('Invalid temperature value read from ' + + self._device_file) + + if temp < -55 or temp > 125: + return + self._state = temp diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py new file mode 100644 index 00000000000000..920dfc46a90864 --- /dev/null +++ b/homeassistant/components/sensor/openexchangerates.py @@ -0,0 +1,105 @@ +""" +Support for openexchangerates.org exchange rates service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.openexchangerates/ +""" +from datetime import timedelta +import logging +import requests +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.const import CONF_API_KEY + +_RESOURCE = 'https://openexchangerates.org/api/latest.json' +_LOGGER = logging.getLogger(__name__) +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=100) +CONF_BASE = 'base' +CONF_QUOTE = 'quote' +CONF_NAME = 'name' +DEFAULT_NAME = 'Exchange Rate Sensor' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Openexchangerates sensor.""" + payload = config.get('payload', None) + rest = OpenexchangeratesData( + _RESOURCE, + config.get(CONF_API_KEY), + config.get(CONF_BASE, 'USD'), + config.get(CONF_QUOTE), + payload + ) + response = requests.get(_RESOURCE, params={'base': config.get(CONF_BASE, + 'USD'), + 'app_id': + config.get(CONF_API_KEY)}, + timeout=10) + if response.status_code != 200: + _LOGGER.error("Check your OpenExchangeRates API") + return False + rest.update() + add_devices([OpenexchangeratesSensor(rest, config.get(CONF_NAME, + DEFAULT_NAME), + config.get(CONF_QUOTE))]) + + +class OpenexchangeratesSensor(Entity): + """Representation of an Openexchangerates sensor.""" + + def __init__(self, rest, name, quote): + """Initialize the sensor.""" + self.rest = rest + self._name = name + self._quote = quote + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return other attributes of the sensor.""" + return self.rest.data + + def update(self): + """Update current conditions.""" + self.rest.update() + value = self.rest.data + self._state = round(value[str(self._quote)], 4) + + +# pylint: disable=too-few-public-methods +class OpenexchangeratesData(object): + """Get data from Openexchangerates.org.""" + + # pylint: disable=too-many-arguments + def __init__(self, resource, api_key, base, quote, data): + """Initialize the data object.""" + self._resource = resource + self._api_key = api_key + self._base = base + self._quote = quote + self.data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from openexchangerates.org.""" + try: + result = requests.get(self._resource, params={'base': self._base, + 'app_id': + self._api_key}, + timeout=10) + self.data = result.json()['rates'] + except requests.exceptions.HTTPError: + _LOGGER.error("Check Openexchangerates API Key") + self.data = None + return False diff --git a/homeassistant/components/sensor/swiss_hydrological_data.py b/homeassistant/components/sensor/swiss_hydrological_data.py index 6bfda3f55f512f..2589bd449552af 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['beautifulsoup4==4.4.1', 'lxml==3.6.0'] +REQUIREMENTS = ['beautifulsoup4==4.4.1'] _LOGGER = logging.getLogger(__name__) _RESOURCE = 'http://www.hydrodaten.admin.ch/en/' @@ -47,7 +47,7 @@ 'temperature_max']) # Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -148,7 +148,7 @@ def update(self): try: tables = BeautifulSoup(response.content, - 'lxml').findChildren('table') + 'html.parser').findChildren('table') rows = tables[0].findChildren(['th', 'tr']) details = [] diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 20af7e71a59a90..c9767428aaadd6 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -1,5 +1,5 @@ """ -Support for monitoring the local system.. +Support for monitoring the local system. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.systemmonitor/ @@ -10,7 +10,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['psutil==4.2.0'] +REQUIREMENTS = ['psutil==4.3.0'] SENSOR_TYPES = { 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'], 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'], diff --git a/homeassistant/components/sensor/thinkingcleaner.py b/homeassistant/components/sensor/thinkingcleaner.py index 1ba8593650ea2d..f956ec5037f6db 100644 --- a/homeassistant/components/sensor/thinkingcleaner.py +++ b/homeassistant/components/sensor/thinkingcleaner.py @@ -1,4 +1,9 @@ -"""Support for ThinkingCleaner.""" +""" +Support for ThinkingCleaner. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.thinkingcleaner/ +""" import logging from datetime import timedelta diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 3fb914d6cd9e61..ac885152a2edcb 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -7,11 +7,12 @@ import logging from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED, - STATE_OPEN, TEMP_CELSIUS, - ATTR_BATTERY_LEVEL) + STATE_OPEN, TEMP_CELSIUS) from homeassistant.helpers.entity import Entity +from homeassistant.components.wink import WinkDevice +from homeassistant.loader import get_component -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] SENSOR_TYPES = ['temperature', 'humidity'] @@ -38,14 +39,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays()) -class WinkSensorDevice(Entity): +class WinkSensorDevice(WinkDevice, Entity): """Representation of a Wink sensor.""" def __init__(self, wink): - """Initialize the sensor.""" - self.wink = wink + """Initialize the Wink device.""" + super().__init__(wink) + wink = get_component('wink') self.capability = self.wink.capability() - self._battery = self.wink.battery_level if self.wink.UNIT == "°": self._unit_of_measurement = TEMP_CELSIUS else: @@ -55,9 +56,9 @@ def __init__(self, wink): def state(self): """Return the state.""" if self.capability == "humidity": - return self.wink.humidity_percentage() + return round(self.wink.humidity_percentage()) elif self.capability == "temperature": - return self.wink.temperature_float() + return round(self.wink.temperature_float(), 1) else: return STATE_OPEN if self.is_open else STATE_CLOSED @@ -66,80 +67,20 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @property - def unique_id(self): - """Return the ID of this wink sensor.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the sensor if any.""" - return self.wink.name() - - @property - def available(self): - """True if connection == True.""" - return self.wink.available - - def update(self): - """Update state of the sensor.""" - self.wink.update_state() - @property def is_open(self): """Return true if door is open.""" return self.wink.state() - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 - -class WinkEggMinder(Entity): +class WinkEggMinder(WinkDevice, Entity): """Representation of a Wink Egg Minder.""" def __init__(self, wink): """Initialize the sensor.""" - self.wink = wink - self._battery = self.wink.battery_level + WinkDevice.__init__(self, wink) @property def state(self): """Return the state.""" return self.wink.state() - - @property - def unique_id(self): - """Return the id of this wink Egg Minder.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the Egg Minder if any.""" - return self.wink.name() - - def update(self): - """Update state of the Egg Minder.""" - self.wink.update_state() - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 725043c4da8380..ddfbc68d974abd 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -15,7 +15,6 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util -from homeassistant.util import location _LOGGER = logging.getLogger(__name__) @@ -54,16 +53,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yr.no sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - elevation = config.get(CONF_ELEVATION) + elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - if elevation is None: - elevation = location.elevation(latitude, - longitude) - coordinates = dict(lat=latitude, lon=longitude, msl=elevation) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index dec518db6ea8b6..17ffad41f93f77 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -6,6 +6,7 @@ """ import logging import subprocess +import shlex import voluptuous as vol @@ -23,8 +24,6 @@ }), }, extra=vol.ALLOW_EXTRA) -SHELL_COMMAND_SCHEMA = vol.Schema({}) - def setup(hass, config): """Setup the shell_command component.""" @@ -44,8 +43,7 @@ def service_handler(call): _LOGGER.exception('Error running command: %s', cmd) for name in conf.keys(): - hass.services.register(DOMAIN, name, service_handler, - schema=SHELL_COMMAND_SCHEMA) + hass.services.register(DOMAIN, name, service_handler) return True @@ -64,6 +62,6 @@ def _parse_command(hass, cmd, variables): shell = True else: # template used. Must break into list and use shell=False for security - cmd = [prog] + rendered_args.split() + cmd = [prog] + shlex.split(rendered_args) shell = False return cmd, shell diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 791fec791f8950..4b2cd10b781789 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -12,7 +12,6 @@ from homeassistant.helpers.event import ( track_point_in_utc_time, track_utc_time_change) from homeassistant.util import dt as dt_util -from homeassistant.util import location as location_util from homeassistant.const import CONF_ELEVATION REQUIREMENTS = ['astral==1.2'] @@ -108,7 +107,7 @@ def setup(hass, config): elevation = platform_config.get(CONF_ELEVATION) if elevation is None: - elevation = location_util.elevation(latitude, longitude) + elevation = hass.config.elevation or 0 from astral import Location diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 1f92b458d53e9b..60b9c9fdcd8a49 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -108,7 +108,7 @@ def handle_switch_service(service): class SwitchDevice(ToggleEntity): """Representation of a switch.""" - # pylint: disable=no-self-use + # pylint: disable=no-self-use, abstract-method @property def current_power_mwh(self): """Return the current power usage in mWh.""" diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py index e6e1eb76b75641..7a1f3498f18ea9 100644 --- a/homeassistant/components/switch/acer_projector.py +++ b/homeassistant/components/switch/acer_projector.py @@ -4,7 +4,6 @@ This component allows to control almost all projectors from acer using their RS232 serial communication protocol. """ - import logging import re @@ -61,7 +60,8 @@ def __init__(self, serial_port, name='Projector', write_timeout=write_timeout, **kwargs) self._serial_port = serial_port self._name = name - self._state = STATE_UNKNOWN + self._state = False + self._available = False self._attributes = { LAMP_HOURS: STATE_UNKNOWN, INPUT_SOURCE: STATE_UNKNOWN, @@ -100,14 +100,19 @@ def _write_read_format(self, msg): return match.group(1) return STATE_UNKNOWN + @property + def available(self): + """Return if projector is available.""" + return self._available + @property def name(self): """Return name of the projector.""" return self._name @property - def state(self): - """Return the current state of the projector.""" + def is_on(self): + """Return if the projector is turned on.""" return self._state @property @@ -120,11 +125,13 @@ def update(self): msg = CMD_DICT[LAMP] awns = self._write_read_format(msg) if awns == 'Lamp 1': - self._state = STATE_ON + self._state = True + self._available = True elif awns == 'Lamp 0': - self._state = STATE_OFF + self._state = False + self._available = True else: - self._state = STATE_UNKNOWN + self._available = False for key in self._attributes.keys(): msg = CMD_DICT.get(key, None) diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index 1a166a9c2dc308..08138db9e70bfb 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.arest/ """ +# pylint: disable=abstract-method import logging import requests diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py new file mode 100644 index 00000000000000..e9f103b95fa88c --- /dev/null +++ b/homeassistant/components/switch/homematic.py @@ -0,0 +1,102 @@ +""" +Support for Homematic switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.homematic/ +""" +import logging +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the Homematic switch platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMSwitch, + discovery_info, + add_callback_devices) + + +class HMSwitch(homematic.HMDevice, SwitchDevice): + """Representation of a Homematic switch.""" + + @property + def is_on(self): + """Return True if switch is on.""" + try: + return self._hm_get_state() > 0 + except TypeError: + return False + + @property + def current_power_mwh(self): + """Return the current power usage in mWh.""" + if "ENERGY_COUNTER" in self._data: + try: + return self._data["ENERGY_COUNTER"] / 1000 + except ZeroDivisionError: + return 0 + + return None + + def turn_on(self, **kwargs): + """Turn the switch on.""" + if self.available: + self._hmdevice.on(self._channel) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + if self.available: + self._hmdevice.off(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the Homematic object as this HA type.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the Homematic device is correct for this HA device + if isinstance(self._hmdevice, Switch): + return True + if isinstance(self._hmdevice, Dimmer): + return True + + _LOGGER.critical("This %s can't be use as switch", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from the Homematic metadata.""" + from pyhomematic.devicetypes.actors import Dimmer,\ + Switch, SwitchPowermeter + + super()._init_data_struct() + + # Use STATE + if isinstance(self._hmdevice, Switch): + self._state = "STATE" + + # Use LEVEL + if isinstance(self._hmdevice, Dimmer): + self._state = "LEVEL" + + # Need sensor values for SwitchPowermeter + if isinstance(self._hmdevice, SwitchPowermeter): + for node in self._hmdevice.SENSORNODE: + self._data.update({node: STATE_UNKNOWN}) + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init data dict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init light %s.", self._name) diff --git a/homeassistant/components/switch/thinkingcleaner.py b/homeassistant/components/switch/thinkingcleaner.py index 3bc4484db3880e..46adc5a70524fd 100644 --- a/homeassistant/components/switch/thinkingcleaner.py +++ b/homeassistant/components/switch/thinkingcleaner.py @@ -1,4 +1,9 @@ -"""Support for ThinkingCleaner.""" +""" +Support for ThinkingCleaner. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.thinkingcleaner/ +""" import time import logging from datetime import timedelta diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index e77453fa6eb9ee..779f4759442596 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -52,7 +52,7 @@ def should_poll(self): @property def is_on(self): - """True if switch is on.""" + """Return true if switch is on.""" return self._state @property @@ -65,6 +65,10 @@ def turn_on(self): self._wol.send_magic_packet(self._mac_address) self.update_ha_state() + def turn_off(self): + """Do nothing.""" + pass + def update(self): """Check if device is on and update the state.""" if platform.system().lower() == "windows": diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index a5b67f5ddcf0bd..64c19e34bc9c42 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -6,10 +6,11 @@ """ import logging -from homeassistant.components.wink import WinkToggleDevice +from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.entity import ToggleEntity -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -31,3 +32,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkToggleDevice(switch) for switch in pywink.get_powerstrip_outlets()) add_devices(WinkToggleDevice(switch) for switch in pywink.get_sirens()) + + +class WinkToggleDevice(WinkDevice, ToggleEntity): + """Represents a Wink toggle (switch) device.""" + + def __init__(self, wink): + """Initialize the Wink device.""" + WinkDevice.__init__(self, wink) + + @property + def is_on(self): + """Return true if device is on.""" + return self.wink.state() + + def turn_on(self, **kwargs): + """Turn the device on.""" + self.wink.set_state(True) + + def turn_off(self): + """Turn the device off.""" + self.wink.set_state(False) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 4c57a23ff9cb27..8d811c3a5ccf22 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -273,29 +273,29 @@ def is_fan_on(self): """Return true if the fan is on.""" return None - def set_temperate(self, temperature): + def set_temperature(self, temperature): """Set new target temperature.""" - pass + raise NotImplementedError() def set_hvac_mode(self, hvac_mode): """Set hvac mode.""" - pass + raise NotImplementedError() def turn_away_mode_on(self): """Turn away mode on.""" - pass + raise NotImplementedError() def turn_away_mode_off(self): """Turn away mode off.""" - pass + raise NotImplementedError() def turn_fan_on(self): """Turn fan on.""" - pass + raise NotImplementedError() def turn_fan_off(self): """Turn fan off.""" - pass + raise NotImplementedError() @property def min_temp(self): diff --git a/homeassistant/components/thermostat/demo.py b/homeassistant/components/thermostat/demo.py index 5d47276c7bc72b..7718299ef6a35c 100644 --- a/homeassistant/components/thermostat/demo.py +++ b/homeassistant/components/thermostat/demo.py @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -# pylint: disable=too-many-arguments +# pylint: disable=too-many-arguments, abstract-method class DemoThermostat(ThermostatDevice): """Representation of a demo thermostat.""" diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index f07ef47269d1cc..577a33c87f4997 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -68,7 +68,7 @@ def fan_min_on_time_set_service(service): schema=SET_FAN_MIN_ON_TIME_SCHEMA) -# pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods, abstract-method class Thermostat(ThermostatDevice): """A thermostat class for Ecobee.""" diff --git a/homeassistant/components/thermostat/eq3btsmart.py b/homeassistant/components/thermostat/eq3btsmart.py index 34c164f2c0dba2..17f166a297e78b 100644 --- a/homeassistant/components/thermostat/eq3btsmart.py +++ b/homeassistant/components/thermostat/eq3btsmart.py @@ -1,16 +1,16 @@ """ Support for eq3 Bluetooth Smart thermostats. -Uses bluepy_devices library. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/thermostat.eq3btsmart/ """ - import logging from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import TEMP_CELCIUS from homeassistant.helpers.temperature import convert -REQUIREMENTS = ['bluepy_devices>=0.2.0'] +REQUIREMENTS = ['bluepy_devices==0.2.0'] CONF_MAC = 'mac' CONF_DEVICES = 'devices' @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return True -# pylint: disable=too-many-instance-attributes, import-error +# pylint: disable=too-many-instance-attributes, import-error, abstract-method class EQ3BTSmartThermostat(ThermostatDevice): """Representation of a EQ3 Bluetooth Smart thermostat.""" diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 64f95c2e517246..3d5190bcc2f1ed 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -41,7 +41,6 @@ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the heat control thermostat.""" name = config.get(CONF_NAME) @@ -55,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): min_temp, max_temp, target_temp)]) -# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-instance-attributes, abstract-method class HeatControl(ThermostatDevice): """Representation of a HeatControl device.""" diff --git a/homeassistant/components/thermostat/heatmiser.py b/homeassistant/components/thermostat/heatmiser.py index ec8bbeb19817f6..e7bbfd72f9b4e4 100644 --- a/homeassistant/components/thermostat/heatmiser.py +++ b/homeassistant/components/thermostat/heatmiser.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HeatmiserV3Thermostat(ThermostatDevice): """Representation of a HeatmiserV3 thermostat.""" - # pylint: disable=too-many-instance-attributes + # pylint: disable=too-many-instance-attributes, abstract-method def __init__(self, heatmiser, device, name, serport): """Initialize the thermostat.""" self.heatmiser = heatmiser diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index b4ecc6c166b922..345b8785b4262e 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -1,122 +1,34 @@ """ -Support for Homematic (HM-TC-IT-WM-W-EU, HM-CC-RT-DN) thermostats. +Support for Homematic thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.homematic/ """ import logging -import socket -from xmlrpc.client import ServerProxy -from xmlrpc.client import Error -from collections import namedtuple - +import homeassistant.components.homematic as homematic from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.temperature import convert +from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN -REQUIREMENTS = [] +DEPENDENCIES = ['homematic'] _LOGGER = logging.getLogger(__name__) -CONF_ADDRESS = 'address' -CONF_DEVICES = 'devices' -CONF_ID = 'id' -PROPERTY_SET_TEMPERATURE = 'SET_TEMPERATURE' -PROPERTY_VALVE_STATE = 'VALVE_STATE' -PROPERTY_ACTUAL_TEMPERATURE = 'ACTUAL_TEMPERATURE' -PROPERTY_BATTERY_STATE = 'BATTERY_STATE' -PROPERTY_LOWBAT = 'LOWBAT' -PROPERTY_CONTROL_MODE = 'CONTROL_MODE' -PROPERTY_BURST_MODE = 'BURST_RX' -TYPE_HM_THERMOSTAT = 'HOMEMATIC_THERMOSTAT' -TYPE_HM_WALLTHERMOSTAT = 'HOMEMATIC_WALLTHERMOSTAT' -TYPE_MAX_THERMOSTAT = 'MAX_THERMOSTAT' - -HomematicConfig = namedtuple('HomematicConfig', - ['device_type', - 'platform_type', - 'channel', - 'maint_channel']) - -HM_TYPE_MAPPING = { - 'HM-CC-RT-DN': HomematicConfig('HM-CC-RT-DN', - TYPE_HM_THERMOSTAT, - 4, 4), - 'HM-CC-RT-DN-BoM': HomematicConfig('HM-CC-RT-DN-BoM', - TYPE_HM_THERMOSTAT, - 4, 4), - 'HM-TC-IT-WM-W-EU': HomematicConfig('HM-TC-IT-WM-W-EU', - TYPE_HM_WALLTHERMOSTAT, - 2, 2), - 'BC-RT-TRX-CyG': HomematicConfig('BC-RT-TRX-CyG', - TYPE_MAX_THERMOSTAT, - 1, 0), - 'BC-RT-TRX-CyG-2': HomematicConfig('BC-RT-TRX-CyG-2', - TYPE_MAX_THERMOSTAT, - 1, 0), - 'BC-RT-TRX-CyG-3': HomematicConfig('BC-RT-TRX-CyG-3', - TYPE_MAX_THERMOSTAT, - 1, 0) -} - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Homematic thermostat.""" - devices = [] - try: - address = config[CONF_ADDRESS] - homegear = ServerProxy(address) - - for name, device_cfg in config[CONF_DEVICES].items(): - # get device description to detect the type - device_type = homegear.getDeviceDescription( - device_cfg[CONF_ID] + ':-1')['TYPE'] - - if device_type in HM_TYPE_MAPPING.keys(): - devices.append(HomematicThermostat( - HM_TYPE_MAPPING[device_type], - address, - device_cfg[CONF_ID], - name)) - else: - raise ValueError( - "Device Type '{}' currently not supported".format( - device_type)) - except socket.error: - _LOGGER.exception("Connection error to homematic web service") - return False - add_devices(devices) +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the Homematic thermostat platform.""" + if discovery_info is None: + return - return True + return homematic.setup_hmdevice_discovery_helper(HMThermostat, + discovery_info, + add_callback_devices) -# pylint: disable=too-many-instance-attributes -class HomematicThermostat(ThermostatDevice): +# pylint: disable=abstract-method +class HMThermostat(homematic.HMDevice, ThermostatDevice): """Representation of a Homematic thermostat.""" - def __init__(self, hm_config, address, _id, name): - """Initialize the thermostat.""" - self._hm_config = hm_config - self.address = address - self._id = _id - self._name = name - self._full_device_name = '{}:{}'.format(self._id, - self._hm_config.channel) - self._maint_device_name = '{}:{}'.format(self._id, - self._hm_config.maint_channel) - self._current_temperature = None - self._target_temperature = None - self._valve = None - self._battery = None - self._mode = None - self.update() - - @property - def name(self): - """Return the name of the Homematic device.""" - return self._name - @property def unit_of_measurement(self): """Return the unit of measurement that is used.""" @@ -125,26 +37,22 @@ def unit_of_measurement(self): @property def current_temperature(self): """Return the current temperature.""" - return self._current_temperature + if not self.available: + return None + return self._data["ACTUAL_TEMPERATURE"] @property def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature + """Return the target temperature.""" + if not self.available: + return None + return self._data["SET_TEMPERATURE"] def set_temperature(self, temperature): """Set new target temperature.""" - device = ServerProxy(self.address) - device.setValue(self._full_device_name, - PROPERTY_SET_TEMPERATURE, - temperature) - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return {"valve": self._valve, - "battery": self._battery, - "mode": self._mode} + if not self.available: + return None + self._hmdevice.set_temperature(temperature) @property def min_temp(self): @@ -156,39 +64,27 @@ def max_temp(self): """Return the maximum temperature - 30.5 means on.""" return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) - def update(self): - """Update the data from the thermostat.""" - try: - device = ServerProxy(self.address) - self._current_temperature = device.getValue( - self._full_device_name, - PROPERTY_ACTUAL_TEMPERATURE) - self._target_temperature = device.getValue( - self._full_device_name, - PROPERTY_SET_TEMPERATURE) - self._valve = device.getValue( - self._full_device_name, - PROPERTY_VALVE_STATE) - self._mode = device.getValue( - self._full_device_name, - PROPERTY_CONTROL_MODE) - - if self._hm_config.platform_type in [TYPE_HM_THERMOSTAT, - TYPE_HM_WALLTHERMOSTAT]: - self._battery = device.getValue(self._maint_device_name, - PROPERTY_BATTERY_STATE) - elif self._hm_config.platform_type == TYPE_MAX_THERMOSTAT: - # emulate homematic battery voltage, - # max reports lowbat if voltage < 2.2V - # while homematic battery_state should - # be between 1.5V and 4.6V - lowbat = device.getValue(self._maint_device_name, - PROPERTY_LOWBAT) - if lowbat: - self._battery = 1.5 - else: - self._battery = 4.6 - - except Error: - _LOGGER.exception("Did not receive any temperature data from the " - "homematic API.") + def _check_hm_to_ha_object(self): + """Check if possible to use the Homematic object as this HA type.""" + from pyhomematic.devicetypes.thermostats import HMThermostat\ + as pyHMThermostat + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the Homematic device correct for this HA device + if isinstance(self._hmdevice, pyHMThermostat): + return True + + _LOGGER.critical("This %s can't be use as thermostat", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from the Homematic metadata.""" + super()._init_data_struct() + + # Add state to data dict + self._data.update({"CONTROL_MODE": STATE_UNKNOWN, + "SET_TEMPERATURE": STATE_UNKNOWN, + "ACTUAL_TEMPERATURE": STATE_UNKNOWN}) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 633212e02b5b43..f45b07b9fd613e 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -49,7 +49,6 @@ def _setup_round(username, password, config, add_devices): # config will be used later -# pylint: disable=unused-argument def _setup_us(username, password, config, add_devices): """Setup user.""" import somecomfort @@ -74,7 +73,6 @@ def _setup_us(username, password, config, add_devices): return True -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the honeywel thermostat.""" username = config.get(CONF_USERNAME) @@ -98,7 +96,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """Representation of a Honeywell Round Connected thermostat.""" - # pylint: disable=too-many-instance-attributes + # pylint: disable=too-many-instance-attributes, abstract-method def __init__(self, device, zone_id, master, away_temp): """Initialize the thermostat.""" self.device = device @@ -182,6 +180,7 @@ def update(self): self._is_dhw = False +# pylint: disable=abstract-method class HoneywellUSThermostat(ThermostatDevice): """Representation of a Honeywell US Thermostat.""" diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 881eb821865bca..00a1acf07b4b45 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -26,6 +26,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for structure, device in nest.devices()]) +# pylint: disable=abstract-method class NestThermostat(ThermostatDevice): """Representation of a Nest thermostat.""" diff --git a/homeassistant/components/thermostat/proliphix.py b/homeassistant/components/thermostat/proliphix.py index 4b86f556352add..bf5c61d2be60a2 100644 --- a/homeassistant/components/thermostat/proliphix.py +++ b/homeassistant/components/thermostat/proliphix.py @@ -27,6 +27,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) +# pylint: disable=abstract-method class ProliphixThermostat(ThermostatDevice): """Representation a Proliphix thermostat.""" diff --git a/homeassistant/components/thermostat/radiotherm.py b/homeassistant/components/thermostat/radiotherm.py index a6ae39434e77c1..963ef1a9a7ff2f 100644 --- a/homeassistant/components/thermostat/radiotherm.py +++ b/homeassistant/components/thermostat/radiotherm.py @@ -45,6 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(tstats) +# pylint: disable=abstract-method class RadioThermostat(ThermostatDevice): """Representation of a Radio Thermostat.""" diff --git a/homeassistant/components/thermostat/zwave.py b/homeassistant/components/thermostat/zwave.py index 4eb18664a24c30..ed653874af2925 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -58,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=abstract-method class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice): """Represents a HeatControl thermostat.""" @@ -80,7 +81,7 @@ def __init__(self, value): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.node == value.node: + if self._value.value_id == value.value_id: self.update_properties() self.update_ha_state() @@ -156,3 +157,4 @@ def set_temperature(self, temperature): COMMAND_CLASS_THERMOSTAT_SETPOINT).items(): if int(value.data) != 0 and value.index == self._index: value.data = temperature + break diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index ee55ec858cc5e7..455927ca9992f8 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -13,12 +13,13 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.10'] +REQUIREMENTS = ['pyvera==0.2.12'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'vera' + VERA_CONTROLLER = None CONF_EXCLUDE = 'exclude' @@ -33,6 +34,7 @@ 'Switch': 'switch', 'Armable Sensor': 'switch', 'On/Off Switch': 'switch', + 'Doorlock': 'lock', # 'Window Covering': NOT SUPPORTED YET } @@ -91,7 +93,7 @@ def stop_subscription(event): dev_type = 'light' VERA_DEVICES[dev_type].append(device) - for component in 'binary_sensor', 'sensor', 'light', 'switch': + for component in 'binary_sensor', 'sensor', 'light', 'switch', 'lock': discovery.load_platform(hass, component, DOMAIN, {}, base_config) return True diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 85bc7f46cefc57..4e9fec77ba51d1 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -5,13 +5,17 @@ https://home-assistant.io/components/wink/ """ import logging +import json -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL from homeassistant.helpers import validate_config, discovery -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL +from homeassistant.helpers.entity import Entity DOMAIN = "wink" -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] + +SUBSCRIPTION_HANDLER = None +CHANNELS = [] def setup(hass, config): @@ -22,7 +26,11 @@ def setup(hass, config): return False import pywink + from pubnub import Pubnub pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN]) + global SUBSCRIPTION_HANDLER + SUBSCRIPTION_HANDLER = Pubnub("N/A", pywink.get_subscription_key()) + SUBSCRIPTION_HANDLER.set_heartbeat(120) # Load components for the devices in the Wink that we support for component_name, func_exists in ( @@ -41,13 +49,33 @@ def setup(hass, config): return True -class WinkToggleDevice(ToggleEntity): - """Represents a Wink toggle (switch) device.""" +class WinkDevice(Entity): + """Represents a base Wink device.""" def __init__(self, wink): """Initialize the Wink device.""" + from pubnub import Pubnub self.wink = wink self._battery = self.wink.battery_level + if self.wink.pubnub_channel in CHANNELS: + pubnub = Pubnub("N/A", self.wink.pubnub_key) + pubnub.set_heartbeat(120) + pubnub.subscribe(self.wink.pubnub_channel, + self._pubnub_update, + error=self._pubnub_error) + else: + CHANNELS.append(self.wink.pubnub_channel) + SUBSCRIPTION_HANDLER.subscribe(self.wink.pubnub_channel, + self._pubnub_update, + error=self._pubnub_error) + + def _pubnub_update(self, message, channel): + self.wink.pubnub_update(json.loads(message)) + self.update_ha_state() + + def _pubnub_error(self, message): + logging.getLogger(__name__).error( + "Error on pubnub update for " + self.wink.name()) @property def unique_id(self): @@ -59,28 +87,20 @@ def name(self): """Return the name of the device.""" return self.wink.name() - @property - def is_on(self): - """Return true if device is on.""" - return self.wink.state() - @property def available(self): """True if connection == True.""" return self.wink.available - def turn_on(self, **kwargs): - """Turn the device on.""" - self.wink.set_state(True) - - def turn_off(self): - """Turn the device off.""" - self.wink.set_state(False) - def update(self): """Update state of the device.""" self.wink.update_state() + @property + def should_poll(self): + """Only poll if we are not subscribed to pubnub.""" + return self.wink.pubnub_channel is None + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index b2dd036074c172..f8959f330330b5 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -39,16 +39,41 @@ EVENT_SCENE_ACTIVATED = "zwave.scene_activated" -COMMAND_CLASS_SWITCH_MULTILEVEL = 38 -COMMAND_CLASS_DOOR_LOCK = 98 -COMMAND_CLASS_SWITCH_BINARY = 37 -COMMAND_CLASS_SENSOR_BINARY = 48 +COMMAND_CLASS_WHATEVER = None COMMAND_CLASS_SENSOR_MULTILEVEL = 49 +COMMAND_CLASS_COLOR = 51 COMMAND_CLASS_METER = 50 +COMMAND_CLASS_ALARM = 113 +COMMAND_CLASS_SWITCH_BINARY = 37 +COMMAND_CLASS_SENSOR_BINARY = 48 +COMMAND_CLASS_SWITCH_MULTILEVEL = 38 +COMMAND_CLASS_DOOR_LOCK = 98 +COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 +COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 COMMAND_CLASS_BATTERY = 128 -COMMAND_CLASS_ALARM = 113 # 0x71 -COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43 -COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 # 0x44 + +GENERIC_COMMAND_CLASS_WHATEVER = None +GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH = 17 +GENERIC_COMMAND_CLASS_BINARY_SWITCH = 16 +GENERIC_COMMAND_CLASS_ENTRY_CONTROL = 64 +GENERIC_COMMAND_CLASS_BINARY_SENSOR = 32 +GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR = 33 +GENERIC_COMMAND_CLASS_METER = 49 +GENERIC_COMMAND_CLASS_ALARM_SENSOR = 161 +GENERIC_COMMAND_CLASS_THERMOSTAT = 8 + +SPECIFIC_DEVICE_CLASS_WHATEVER = None +SPECIFIC_DEVICE_CLASS_NOT_USED = 0 +SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH = 1 +SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK = 2 +SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR = 3 +SPECIFIC_DEVICE_CLASS_SECURE_KEYPAD_DOOR_LOCK = 3 +SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE = 4 +SPECIFIC_DEVICE_CLASS_SECURE_DOOR = 5 +SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A = 5 +SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B = 6 +SPECIFIC_DEVICE_CLASS_SECURE_BARRIER_ADD_ON = 7 +SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C = 7 GENRE_WHATEVER = None GENRE_USER = "User" @@ -60,38 +85,71 @@ # List of tuple (DOMAIN, discovered service, supported command classes, -# value type). +# value type, genre type, specific device class). DISCOVERY_COMPONENTS = [ ('sensor', + [GENERIC_COMMAND_CLASS_WHATEVER], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SENSOR_MULTILEVEL, COMMAND_CLASS_METER, COMMAND_CLASS_ALARM], TYPE_WHATEVER, GENRE_USER), ('light', + [GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH], + [SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH, + SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE], [COMMAND_CLASS_SWITCH_MULTILEVEL], TYPE_BYTE, GENRE_USER), ('switch', + [GENERIC_COMMAND_CLASS_BINARY_SWITCH], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SWITCH_BINARY], TYPE_BOOL, GENRE_USER), ('binary_sensor', + [GENERIC_COMMAND_CLASS_BINARY_SENSOR, + GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SENSOR_BINARY], TYPE_BOOL, GENRE_USER), ('thermostat', + [GENERIC_COMMAND_CLASS_THERMOSTAT], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_THERMOSTAT_SETPOINT], TYPE_WHATEVER, GENRE_WHATEVER), ('hvac', + [GENERIC_COMMAND_CLASS_THERMOSTAT], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_THERMOSTAT_FAN_MODE], TYPE_WHATEVER, GENRE_WHATEVER), ('lock', + [GENERIC_COMMAND_CLASS_ENTRY_CONTROL], + [SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK, + SPECIFIC_DEVICE_CLASS_SECURE_KEYPAD_DOOR_LOCK], [COMMAND_CLASS_DOOR_LOCK], TYPE_BOOL, GENRE_USER), + ('rollershutter', + [GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH], + [SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A, + SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B, + SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C, + SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR], + [COMMAND_CLASS_WHATEVER], + TYPE_WHATEVER, + GENRE_USER), + ('garage_door', + [GENERIC_COMMAND_CLASS_ENTRY_CONTROL], + [SPECIFIC_DEVICE_CLASS_SECURE_BARRIER_ADD_ON, + SPECIFIC_DEVICE_CLASS_SECURE_DOOR], + [COMMAND_CLASS_SWITCH_BINARY], + TYPE_BOOL, + GENRE_USER) ] @@ -220,18 +278,49 @@ def log_all(signal, value=None): def value_added(node, value): """Called when a value is added to a node on the network.""" for (component, - command_ids, + generic_device_class, + specific_device_class, + command_class, value_type, value_genre) in DISCOVERY_COMPONENTS: - if value.command_class not in command_ids: + _LOGGER.debug("Component=%s Node_id=%s query start", + component, node.node_id) + if node.generic not in generic_device_class and \ + None not in generic_device_class: + _LOGGER.debug("node.generic %s not None and in \ + generic_device_class %s", + node.generic, generic_device_class) + continue + if node.specific not in specific_device_class and \ + None not in specific_device_class: + _LOGGER.debug("node.specific %s is not None and in \ + specific_device_class %s", node.specific, + specific_device_class) + continue + if value.command_class not in command_class and \ + None not in command_class: + _LOGGER.debug("value.command_class %s is not None \ + and in command_class %s", + value.command_class, command_class) continue - if value_type is not None and value_type != value.type: + if value_type != value.type and value_type is not None: + _LOGGER.debug("value.type %s != value_type %s", + value.type, value_type) continue - if value_genre is not None and value_genre != value.genre: + if value_genre != value.genre and value_genre is not None: + _LOGGER.debug("value.genre %s != value_genre %s", + value.genre, value_genre) continue # Configure node + _LOGGER.debug("Adding Node_id=%s Generic_command_class=%s, \ + Specific_command_class=%s, \ + Command_class=%s, Value type=%s, \ + Genre=%s", node.node_id, + node.generic, node.specific, + value.command_class, value.type, + value.genre) name = "{}.{}".format(component, _object_id(value)) node_config = customize.get(name, {}) diff --git a/homeassistant/config.py b/homeassistant/config.py index b89d358045dac8..55e97f67c7e43f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,31 +1,35 @@ """Module to help with parsing and generating configuration files.""" import logging import os +import shutil from types import MappingProxyType import voluptuous as vol -import homeassistant.util.location as loc_util from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT, - CONF_TIME_ZONE, CONF_CUSTOMIZE) + CONF_TIME_ZONE, CONF_CUSTOMIZE, CONF_ELEVATION, TEMP_FAHRENHEIT, + TEMP_CELSIUS, __version__) from homeassistant.exceptions import HomeAssistantError from homeassistant.util.yaml import load_yaml import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import valid_entity_id +from homeassistant.helpers.entity import valid_entity_id, set_customize +from homeassistant.util import dt as date_util, location as loc_util _LOGGER = logging.getLogger(__name__) YAML_CONFIG_FILE = 'configuration.yaml' +VERSION_FILE = '.HA_VERSION' CONFIG_DIR_NAME = '.homeassistant' DEFAULT_CONFIG = ( # Tuples (attribute, default, auto detect property, description) (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' 'running'), - (CONF_LATITUDE, None, 'latitude', 'Location required to calculate the time' + (CONF_LATITUDE, 0, 'latitude', 'Location required to calculate the time' ' the sun rises and sets'), - (CONF_LONGITUDE, None, 'longitude', None), + (CONF_LONGITUDE, 0, 'longitude', None), + (CONF_ELEVATION, 0, None, 'Impacts weather/sunrise data'), (CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celsius, F for Fahrenheit'), (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), @@ -39,7 +43,7 @@ 'history:': 'Enables support for tracking state changes over time.', 'logbook:': 'View all events in a logbook', 'sun:': 'Track the sun', - 'sensor:\n platform: yr': 'Prediction of weather', + 'sensor:\n platform: yr': 'Weather Prediction', } @@ -61,6 +65,7 @@ def _valid_customize(value): CONF_NAME: vol.Coerce(str), CONF_LATITUDE: cv.latitude, CONF_LONGITUDE: cv.longitude, + CONF_ELEVATION: vol.Coerce(float), CONF_TEMPERATURE_UNIT: cv.temperature_unit, CONF_TIME_ZONE: cv.time_zone, vol.Required(CONF_CUSTOMIZE, @@ -97,6 +102,7 @@ def create_default_config(config_dir, detect_location=True): Return path to new config file if success, None if failed. """ config_path = os.path.join(config_dir, YAML_CONFIG_FILE) + version_path = os.path.join(config_dir, VERSION_FILE) info = {attr: default for attr, default, _, _ in DEFAULT_CONFIG} @@ -111,6 +117,10 @@ def create_default_config(config_dir, detect_location=True): continue info[attr] = getattr(location_info, prop) or default + if location_info.latitude and location_info.longitude: + info[CONF_ELEVATION] = loc_util.elevation(location_info.latitude, + location_info.longitude) + # Writing files with YAML does not create the most human readable results # So we're hard coding a YAML template. try: @@ -130,6 +140,9 @@ def create_default_config(config_dir, detect_location=True): config_file.write("# {}\n".format(description)) config_file.write("{}\n\n".format(component)) + with open(version_path, 'wt') as version_file: + version_file.write(__version__) + return config_path except IOError: @@ -149,9 +162,118 @@ def load_yaml_config_file(config_path): conf_dict = load_yaml(config_path) if not isinstance(conf_dict, dict): - _LOGGER.error( - 'The configuration file %s does not contain a dictionary', + msg = 'The configuration file {} does not contain a dictionary'.format( os.path.basename(config_path)) - raise HomeAssistantError() + _LOGGER.error(msg) + raise HomeAssistantError(msg) return conf_dict + + +def process_ha_config_upgrade(hass): + """Upgrade config if necessary.""" + version_path = hass.config.path(VERSION_FILE) + + try: + with open(version_path, 'rt') as inp: + conf_version = inp.readline().strip() + except FileNotFoundError: + # Last version to not have this file + conf_version = '0.7.7' + + if conf_version == __version__: + return + + _LOGGER.info('Upgrading config directory from %s to %s', conf_version, + __version__) + + lib_path = hass.config.path('deps') + if os.path.isdir(lib_path): + shutil.rmtree(lib_path) + + with open(version_path, 'wt') as outp: + outp.write(__version__) + + +def process_ha_core_config(hass, config): + """Process the [homeassistant] section from the config.""" + # pylint: disable=too-many-branches + config = CORE_CONFIG_SCHEMA(config) + hac = hass.config + + def set_time_zone(time_zone_str): + """Helper method to set time zone.""" + if time_zone_str is None: + return + + time_zone = date_util.get_time_zone(time_zone_str) + + if time_zone: + hac.time_zone = time_zone + date_util.set_default_time_zone(time_zone) + else: + _LOGGER.error('Received invalid time zone %s', time_zone_str) + + for key, attr in ((CONF_LATITUDE, 'latitude'), + (CONF_LONGITUDE, 'longitude'), + (CONF_NAME, 'location_name'), + (CONF_ELEVATION, 'elevation')): + if key in config: + setattr(hac, attr, config[key]) + + if CONF_TIME_ZONE in config: + set_time_zone(config.get(CONF_TIME_ZONE)) + + set_customize(config.get(CONF_CUSTOMIZE) or {}) + + if CONF_TEMPERATURE_UNIT in config: + hac.temperature_unit = config[CONF_TEMPERATURE_UNIT] + + # Shortcut if no auto-detection necessary + if None not in (hac.latitude, hac.longitude, hac.temperature_unit, + hac.time_zone, hac.elevation): + return + + discovered = [] + + # If we miss some of the needed values, auto detect them + if None in (hac.latitude, hac.longitude, hac.temperature_unit, + hac.time_zone): + info = loc_util.detect_location_info() + + if info is None: + _LOGGER.error('Could not detect location information') + return + + if hac.latitude is None and hac.longitude is None: + hac.latitude = info.latitude + hac.longitude = info.longitude + discovered.append(('latitude', hac.latitude)) + discovered.append(('longitude', hac.longitude)) + + if hac.temperature_unit is None: + if info.use_fahrenheit: + hac.temperature_unit = TEMP_FAHRENHEIT + discovered.append(('temperature_unit', 'F')) + else: + hac.temperature_unit = TEMP_CELSIUS + discovered.append(('temperature_unit', 'C')) + + if hac.location_name is None: + hac.location_name = info.city + discovered.append(('name', info.city)) + + if hac.time_zone is None: + set_time_zone(info.time_zone) + discovered.append(('time_zone', info.time_zone)) + + if hac.elevation is None and hac.latitude is not None and \ + hac.longitude is not None: + elevation = loc_util.elevation(hac.latitude, hac.longitude) + hac.elevation = elevation + discovered.append(('elevation', elevation)) + + if discovered: + _LOGGER.warning( + 'Incomplete core config. Auto detected %s', + ', '.join('{}: {}'.format(key, val) for key, val in discovered)) diff --git a/homeassistant/const.py b/homeassistant/const.py index b06525f20415c1..ab9fc35ee4c123 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = "0.22.1" +__version__ = "0.23.0" REQUIRED_PYTHON_VER = (3, 4) PLATFORM_FORMAT = '{}.{}' diff --git a/homeassistant/core.py b/homeassistant/core.py index ffaccdeae43ff8..82ec20c82f9abb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -49,6 +49,19 @@ _LOGGER = logging.getLogger(__name__) +class CoreState(enum.Enum): + """Represent the current state of Home Assistant.""" + + not_running = "NOT_RUNNING" + starting = "STARTING" + running = "RUNNING" + stopping = "STOPPING" + + def __str__(self): + """Return the event.""" + return self.value + + class HomeAssistant(object): """Root object of the Home Assistant home automation.""" @@ -59,14 +72,23 @@ def __init__(self): self.services = ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus) self.config = Config() + self.state = CoreState.not_running + + @property + def is_running(self): + """Return if Home Assistant is running.""" + return self.state == CoreState.running def start(self): """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) + self.state = CoreState.starting create_timer(self) self.bus.fire(EVENT_HOMEASSISTANT_START) + self.pool.block_till_done() + self.state = CoreState.running def block_till_stopped(self): """Register service homeassistant/stop and will block until called.""" @@ -113,8 +135,10 @@ def restart_homeassistant(*args): def stop(self): """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") + self.state = CoreState.stopping self.bus.fire(EVENT_HOMEASSISTANT_STOP) self.pool.stop() + self.state = CoreState.not_running class JobPriority(util.OrderedEnum): @@ -456,7 +480,7 @@ def remove(self, entity_id): return True - def set(self, entity_id, new_state, attributes=None): + def set(self, entity_id, new_state, attributes=None, force_update=False): """Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. @@ -472,7 +496,8 @@ def set(self, entity_id, new_state, attributes=None): old_state = self._states.get(entity_id) is_existing = old_state is not None - same_state = is_existing and old_state.state == new_state + same_state = (is_existing and old_state.state == new_state and + not force_update) same_attr = is_existing and old_state.attributes == attributes if same_state and same_attr: @@ -680,6 +705,7 @@ def __init__(self): """Initialize a new config object.""" self.latitude = None self.longitude = None + self.elevation = None self.temperature_unit = None self.location_name = None self.time_zone = None diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 423a276f11a495..d120a3b2cf6406 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,6 +1,6 @@ """An abstract class for entities.""" +import logging import re -from collections import defaultdict from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, @@ -10,8 +10,10 @@ from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify -# Dict mapping entity_id to a boolean that overwrites the hidden property -_OVERWRITE = defaultdict(dict) +# Entity attributes that we will overwrite +_OVERWRITE = {} + +_LOGGER = logging.getLogger(__name__) # Pattern for validating entity IDs (format: .) ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") @@ -22,7 +24,7 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): name = (name or DEVICE_DEFAULT_NAME).lower() if current_ids is None: if hass is None: - raise RuntimeError("Missing required parameter currentids or hass") + raise ValueError("Missing required parameter currentids or hass") current_ids = hass.states.entity_ids() @@ -30,6 +32,13 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): entity_id_format.format(slugify(name)), current_ids) +def set_customize(customize): + """Overwrite all current customize settings.""" + global _OVERWRITE + + _OVERWRITE = {key.lower(): val for key, val in customize.items()} + + def split_entity_id(entity_id): """Split a state entity_id into domain, object_id.""" return entity_id.split(".", 1) @@ -116,6 +125,15 @@ def assumed_state(self): """Return True if unable to access real state of the entity.""" return False + @property + def force_update(self): + """Return True if state updates should be forced. + + If True, a state change will be triggered anytime the state property is + updated, not just when the value changes. + """ + return False + def update(self): """Retrieve latest state.""" pass @@ -181,7 +199,8 @@ def update_ha_state(self, force_refresh=False): state, attr[ATTR_UNIT_OF_MEASUREMENT]) state = str(state) - return self.hass.states.set(self.entity_id, state, attr) + return self.hass.states.set( + self.entity_id, state, attr, self.force_update) def _attr_setter(self, name, typ, attr, attrs): """Helper method to populate attributes based on properties.""" @@ -207,20 +226,6 @@ def __repr__(self): """Return the representation.""" return "".format(self.name, self.state) - @staticmethod - def overwrite_attribute(entity_id, attrs, vals): - """Overwrite any attribute of an entity. - - This function should receive a list of attributes and a - list of values. Set attribute to None to remove any overwritten - value in place. - """ - for attr, val in zip(attrs, vals): - if val is None: - _OVERWRITE[entity_id.lower()].pop(attr, None) - else: - _OVERWRITE[entity_id.lower()][attr] = val - class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" @@ -234,15 +239,15 @@ def state(self): @property def is_on(self): """Return True if entity is on.""" - return False + raise NotImplementedError() def turn_on(self, **kwargs): """Turn the entity on.""" - pass + raise NotImplementedError() def turn_off(self, **kwargs): """Turn the entity off.""" - pass + raise NotImplementedError() def toggle(self, **kwargs): """Toggle the entity off.""" diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 4bfb01890cf962..6c49decdff2c1b 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -11,6 +11,7 @@ import enum import json import logging +import time import threading import urllib.parse @@ -123,6 +124,7 @@ def __init__(self, remote_api, local_api=None): self.services = ha.ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus, self.remote_api) self.config = ha.Config() + self.state = ha.CoreState.not_running self.config.api = local_api @@ -134,17 +136,20 @@ def start(self): raise HomeAssistantError( 'Unable to setup local API to receive events') + self.state = ha.CoreState.starting ha.create_timer(self) self.bus.fire(ha.EVENT_HOMEASSISTANT_START, origin=ha.EventOrigin.remote) - # Give eventlet time to startup - import eventlet - eventlet.sleep(0.1) + # Ensure local HTTP is started + self.pool.block_till_done() + self.state = ha.CoreState.running + time.sleep(0.05) # Setup that events from remote_api get forwarded to local_api - # Do this after we fire START, otherwise HTTP is not started + # Do this after we are running, otherwise HTTP is not started + # or requests are blocked if not connect_remote_events(self.remote_api, self.config.api): raise HomeAssistantError(( 'Could not setup event forwarding from api {} to ' @@ -153,6 +158,7 @@ def start(self): def stop(self): """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") + self.state = ha.CoreState.stopping self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, origin=ha.EventOrigin.remote) @@ -161,6 +167,7 @@ def stop(self): # Disconnect master event forwarding disconnect_remote_events(self.remote_api, self.config.api) + self.state = ha.CoreState.not_running class EventBus(ha.EventBus): @@ -259,9 +266,9 @@ def remove(self, entity_id): """ return remove_state(self._api, entity_id) - def set(self, entity_id, new_state, attributes=None): + def set(self, entity_id, new_state, attributes=None, force_update=False): """Call set_state on remote API.""" - set_state(self._api, entity_id, new_state, attributes) + set_state(self._api, entity_id, new_state, attributes, force_update) def mirror(self): """Discard current data and mirrors the remote state machine.""" @@ -450,7 +457,7 @@ def remove_state(api, entity_id): return False -def set_state(api, entity_id, new_state, attributes=None): +def set_state(api, entity_id, new_state, attributes=None, force_update=False): """Tell API to update state for entity_id. Return True if success. @@ -458,7 +465,8 @@ def set_state(api, entity_id, new_state, attributes=None): attributes = attributes or {} data = {'state': new_state, - 'attributes': attributes} + 'attributes': attributes, + 'force_update': force_update} try: req = api(METHOD_POST, diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index a596d9bc47635d..a9b980bc871722 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -8,7 +8,8 @@ import requests ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' -DATA_SOURCE = ['https://freegeoip.io/json/', 'http://ip-api.com/json'] +FREEGEO_API = 'https://freegeoip.io/json/' +IP_API = 'http://ip-api.com/json' # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 @@ -32,30 +33,13 @@ def detect_location_info(): """Detect location information.""" - success = None + data = _get_freegeoip() - for source in DATA_SOURCE: - try: - raw_info = requests.get(source, timeout=5).json() - success = source - break - except (requests.RequestException, ValueError): - success = False + if data is None: + data = _get_ip_api() - if success is False: + if data is None: return None - else: - data = {key: raw_info.get(key) for key in LocationInfo._fields} - if success is DATA_SOURCE[1]: - data['ip'] = raw_info.get('query') - data['country_code'] = raw_info.get('countryCode') - data['country_name'] = raw_info.get('country') - data['region_code'] = raw_info.get('region') - data['region_name'] = raw_info.get('regionName') - data['zip_code'] = raw_info.get('zip') - data['time_zone'] = raw_info.get('timezone') - data['latitude'] = raw_info.get('lat') - data['longitude'] = raw_info.get('lon') # From Wikipedia: Fahrenheit is used in the Bahamas, Belize, # the Cayman Islands, Palau, and the United States and associated @@ -73,11 +57,16 @@ def distance(lat1, lon1, lat2, lon2): def elevation(latitude, longitude): """Return elevation for given latitude and longitude.""" - req = requests.get(ELEVATION_URL, - params={'locations': '{},{}'.format(latitude, - longitude), - 'sensor': 'false'}, - timeout=10) + try: + req = requests.get( + ELEVATION_URL, + params={ + 'locations': '{},{}'.format(latitude, longitude), + 'sensor': 'false', + }, + timeout=10) + except requests.RequestException: + return 0 if req.status_code != 200: return 0 @@ -157,3 +146,45 @@ def vincenty(point1, point2, miles=False): s *= MILES_PER_KILOMETER # kilometers to miles return round(s, 6) + + +def _get_freegeoip(): + """Query freegeoip.io for location data.""" + try: + raw_info = requests.get(FREEGEO_API, timeout=5).json() + except (requests.RequestException, ValueError): + return None + + return { + 'ip': raw_info.get('ip'), + 'country_code': raw_info.get('country_code'), + 'country_name': raw_info.get('country_name'), + 'region_code': raw_info.get('region_code'), + 'region_name': raw_info.get('region_name'), + 'city': raw_info.get('city'), + 'zip_code': raw_info.get('zip_code'), + 'time_zone': raw_info.get('time_zone'), + 'latitude': raw_info.get('latitude'), + 'longitude': raw_info.get('longitude'), + } + + +def _get_ip_api(): + """Query ip-api.com for location data.""" + try: + raw_info = requests.get(IP_API, timeout=5).json() + except (requests.RequestException, ValueError): + return None + + return { + 'ip': raw_info.get('query'), + 'country_code': raw_info.get('countryCode'), + 'country_name': raw_info.get('country'), + 'region_code': raw_info.get('region'), + 'region_name': raw_info.get('regionName'), + 'city': raw_info.get('city'), + 'zip_code': raw_info.get('zip'), + 'time_zone': raw_info.get('timezone'), + 'latitude': raw_info.get('lat'), + 'longitude': raw_info.get('lon'), + } diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 58458986063093..0e6ec01f26e55c 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -5,10 +5,15 @@ import glob import yaml +try: + import keyring +except ImportError: + keyring = None from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) +_SECRET_NAMESPACE = 'homeassistant' # pylint: disable=too-many-ancestors @@ -119,10 +124,49 @@ def _env_var_yaml(loader, node): raise HomeAssistantError(node.value) +# pylint: disable=protected-access +def _secret_yaml(loader, node): + """Load secrets and embed it into the configuration YAML.""" + # Create secret cache on loader and load secret.yaml + if not hasattr(loader, '_SECRET_CACHE'): + loader._SECRET_CACHE = {} + + secret_path = os.path.join(os.path.dirname(loader.name), 'secrets.yaml') + if secret_path not in loader._SECRET_CACHE: + if os.path.isfile(secret_path): + loader._SECRET_CACHE[secret_path] = load_yaml(secret_path) + secrets = loader._SECRET_CACHE[secret_path] + if 'logger' in secrets: + logger = str(secrets['logger']).lower() + if logger == 'debug': + _LOGGER.setLevel(logging.DEBUG) + else: + _LOGGER.error("secrets.yaml: 'logger: debug' expected," + " but 'logger: %s' found", logger) + del secrets['logger'] + else: + loader._SECRET_CACHE[secret_path] = None + secrets = loader._SECRET_CACHE[secret_path] + + # Retrieve secret, first from secrets.yaml, then from keyring + if secrets is not None and node.value in secrets: + _LOGGER.debug('Secret %s retrieved from secrets.yaml.', node.value) + return secrets[node.value] + elif keyring: + # do ome keyring stuff + pwd = keyring.get_password(_SECRET_NAMESPACE, node.value) + if pwd: + _LOGGER.debug('Secret %s retrieved from keyring.', node.value) + return pwd + + _LOGGER.error('Secret %s not defined.', node.value) + raise HomeAssistantError(node.value) + yaml.SafeLoader.add_constructor('!include', _include_yaml) yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict) yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml) +yaml.SafeLoader.add_constructor('!secret', _secret_yaml) yaml.SafeLoader.add_constructor('!include_dir_list', _include_dir_list_yaml) yaml.SafeLoader.add_constructor('!include_dir_merge_list', _include_dir_merge_list_yaml) diff --git a/requirements_all.txt b/requirements_all.txt index 368ea27649caec..2dc4da447104b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,6 @@ pytz>=2016.4 pip>=7.0.0 jinja2>=2.8 voluptuous==0.8.9 -eventlet==0.19.0 # homeassistant.components.isy994 PyISY==1.0.6 @@ -23,7 +22,7 @@ SoCo==0.11.1 TwitterAPI==2.4.1 # homeassistant.components.http -Werkzeug==0.11.5 +Werkzeug==0.11.10 # homeassistant.components.apcupsd apcaccess==0.0.4 @@ -41,13 +40,16 @@ blinkstick==1.1.7 blockchain==1.3.3 # homeassistant.components.thermostat.eq3btsmart -# bluepy_devices>=0.2.0 +# bluepy_devices==0.2.0 # homeassistant.components.notify.aws_lambda # homeassistant.components.notify.aws_sns # homeassistant.components.notify.aws_sqs boto3==1.3.1 +# homeassistant.components.http +cherrypy==6.0.2 + # homeassistant.components.notify.xmpp dnspython3==1.12.0 @@ -61,9 +63,6 @@ eliqonline==1.0.12 # homeassistant.components.enocean enocean==0.31 -# homeassistant.components.http -eventlet==0.19.0 - # homeassistant.components.thermostat.honeywell evohomeclient==0.2.5 @@ -73,6 +72,9 @@ feedparser==5.2.1 # homeassistant.components.sensor.fitbit fitbit==0.2.2 +# homeassistant.components.sensor.fixer +fixerio==0.1.1 + # homeassistant.components.notify.free_mobile freesms==0.1.0 @@ -111,6 +113,9 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl # homeassistant.components.alarm_control_panel.alarmdotcom https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 +# homeassistant.components.media_player.braviatv +https://github.com/aparraga/braviarc/archive/0.3.2.zip#braviarc==0.3.2 + # homeassistant.components.media_player.roku https://github.com/bah2830/python-roku/archive/3.1.1.zip#python-roku==3.1.1 @@ -157,13 +162,13 @@ https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753 https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 # homeassistant.components.influxdb -influxdb==2.12.0 +influxdb==3.0.0 # homeassistant.components.insteon_hub insteon_hub==0.4.5 # homeassistant.components.media_player.kodi -jsonrpc-requests==0.2 +jsonrpc-requests==0.3 # homeassistant.components.light.lifx liffylights==0.9.4 @@ -174,9 +179,6 @@ lightify==1.0.3 # homeassistant.components.light.limitlessled limitlessled==1.0.0 -# homeassistant.components.sensor.swiss_hydrological_data -lxml==3.6.0 - # homeassistant.components.notify.message_bird messagebird==1.2.0 @@ -194,7 +196,7 @@ neurio==0.2.10 orvibo==1.1.1 # homeassistant.components.mqtt -paho-mqtt==1.1 +paho-mqtt==1.2 # homeassistant.components.media_player.panasonic_viera panasonic_viera==0.2 @@ -215,7 +217,17 @@ plexapi==1.1.0 proliphix==0.1.0 # homeassistant.components.sensor.systemmonitor -psutil==4.2.0 +psutil==4.3.0 + +# homeassistant.components.wink +# homeassistant.components.binary_sensor.wink +# homeassistant.components.garage_door.wink +# homeassistant.components.light.wink +# homeassistant.components.lock.wink +# homeassistant.components.rollershutter.wink +# homeassistant.components.sensor.wink +# homeassistant.components.switch.wink +pubnub==3.7.8 # homeassistant.components.notify.pushbullet pushbullet.py==0.10.0 @@ -241,12 +253,22 @@ pyasn1==0.1.9 # homeassistant.components.media_player.cast pychromecast==0.7.2 +# homeassistant.components.media_player.cmus +pycmus==0.1.0 + +# homeassistant.components.envisalink # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.envisalink +pyenvisalink==1.0 + # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.homematic +pyhomematic==0.1.8 + # homeassistant.components.device_tracker.icloud pyicloud==0.8.3 @@ -311,13 +333,13 @@ python-twitch==1.2.0 # homeassistant.components.rollershutter.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.7.7 +python-wink==0.7.8 # homeassistant.components.keyboard pyuserinput==0.1.9 # homeassistant.components.vera -pyvera==0.2.10 +pyvera==0.2.12 # homeassistant.components.wemo pywemo==0.4.3 @@ -344,7 +366,7 @@ scsgate==0.1.0 sendgrid>=1.6.0,<1.7.0 # homeassistant.components.notify.slack -slacker==0.9.16 +slacker==0.9.17 # homeassistant.components.notify.xmpp sleekxmpp==1.3.1 @@ -397,7 +419,7 @@ vsure==0.8.1 wakeonlan==0.2.2 # homeassistant.components.media_player.gpmdp -websocket-client==0.35.0 +websocket-client==0.37.0 # homeassistant.components.zigbee xbee-helper==0.0.7 diff --git a/requirements_test.txt b/requirements_test.txt index 5ec8619b37f7a4..649859f2506001 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,10 +1,9 @@ -flake8>=2.5.4 -pylint>=1.5.5 +flake8>=2.6.0 +pylint>=1.5.6 coveralls>=1.1 -pytest>=2.9.1 -pytest-cov>=2.2.0 +pytest>=2.9.2 +pytest-cov>=2.2.1 pytest-timeout>=1.0.0 pytest-capturelog>=0.7 -betamax==0.7.0 pydocstyle>=1.0.0 -httpretty==0.8.14 +requests_mock>=1.0 diff --git a/setup.py b/setup.py index b574e156931458..fbce912c3d6e4c 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ 'pip>=7.0.0', 'jinja2>=2.8', 'voluptuous==0.8.9', - 'eventlet==0.19.0', ] setup( diff --git a/tests/__init__.py b/tests/__init__.py index c1f50d86dfb20f..a931604fdce059 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,27 +1,25 @@ -"""Test the initialization.""" -import betamax +"""Setup some common test helper things.""" +import functools from homeassistant import util from homeassistant.util import location -with betamax.Betamax.configure() as config: - config.cassette_library_dir = 'tests/cassettes' -# Automatically called during different setups. Too often forgotten -# so mocked by default. -location.detect_location_info = lambda: location.LocationInfo( - ip='1.1.1.1', - country_code='US', - country_name='United States', - region_code='CA', - region_name='California', - city='San Diego', - zip_code='92122', - time_zone='America/Los_Angeles', - latitude='2.0', - longitude='1.0', - use_fahrenheit=True, -) +def test_real(func): + """Force a function to require a keyword _test_real to be passed in.""" + @functools.wraps(func) + def guard_func(*args, **kwargs): + real = kwargs.pop('_test_real', None) -location.elevation = lambda latitude, longitude: 0 + if not real: + raise Exception('Forgot to mock or pass "_test_real=True" to %s', + func.__name__) + + return func(*args, **kwargs) + + return guard_func + +# Guard a few functions that would make network connections +location.detect_location_info = test_real(location.detect_location_info) +location.elevation = test_real(location.elevation) util.get_local_ip = lambda: '127.0.0.1' 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 deleted file mode 100644 index c647c4ae017696..00000000000000 --- a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json +++ /dev/null @@ -1 +0,0 @@ -{"http_interactions": [{"request": {"uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:23", "response": {"url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Location": ["http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"], "Age": ["0"], "X-Varnish": ["4249781791"], "Via": ["1.1 varnish"], "Server": ["Varnish"], "Date": ["Thu, 09 Jun 2016 04:02:21 GMT"], "Connection": ["close"], "Accept-Ranges": ["bytes"]}, "body": {"encoding": null, "string": ""}, "status": {"message": "Moved permanently", "code": 301}}}, {"request": {"uri": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:23", "response": {"url": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Content-Length": ["3637"], "X-Backend-Host": ["ravn_loc"], "X-Varnish": ["4249782320 4249780502"], "Via": ["1.1 varnish"], "Last-Modified": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-forecast-models": ["proff,ecdet"], "Accept-Ranges": ["bytes"], "Expires": ["Thu, 09 Jun 2016 05:01:15 GMT"], "Content-Encoding": ["gzip"], "Age": ["1"], "Content-Type": ["text/xml; charset=utf-8"], "Server": ["Apache"], "Vary": ["Accept-Encoding"], "Date": ["Thu, 09 Jun 2016 04:02:22 GMT"], "X-slicenumber": ["83"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "base64_string": "H4sIAAAAAAAAA+1dTY/bthbd91cI3tcWSUmUBpNumjQtkEyKl3noQ3eKrWSE5y/Ycibpry/lsWxZImlfiqQoOEAWSfRhizLPPTw899775ywtnrLNLC1S79tivtzefdvmr0ZPRbG+m0yen5/Hz2S82nyZYN9Hk/+9f/dx+pQt0p/z5bZIl9Ns5LHz75arh3SRbdfpNHs5/m41TYt8tTzeKV3n40VWjJeryeEj2f9M5ofTPq822TTdFhM0Tibb/R1G3nTDTsxmr0bYR9HPPvuTPPrBnY/vMPp79MtPnufds1um+7/t/7GaZXNvyb7Jq9GbX8dv33wY+wiHI6/INot8WbtR/Ijwne+zP3+PvM1umS1nZx/Ejid3JLgjlB1fZt8Kds7594iPl3/erBbnx6LjsWJ1OoLOPnRy/NaT4zPcrzer2W5aeNN5ut2+Gq1X+bJ4zd7M6HhykS8yr3xZxfc1e8pq3K7/FmdHqtuWd65ehZfOi7zYzdjd/ZE3T6t/EDyOKQnYf62WXw7/hxAdY0yD+o323zJbrNkLLnabzMvZuD4+Po683TIvXo2m2Xyb77Yj72s637Fb4HjsjyaN65/z5ex1zh5t/4XKO8xmI2+WfWEf6ZNxNKpeMvfKj+ssm+2v+vx55C3WbCTJmP0KPmXpjg0Y+xK4usHHr+n/va/smtaNnnaLfJYX36vvGYblLV6egT3bNFsWrWvWm2y7rZ55valOf/ozPT4u8v1kTFpXTuer3Sxfssv31z48jLzDh+wv4QzR59WX/am/fXhbO5d35nz1/Gt5+5dbv/vwV+38mI1l8/xFNst3i9ol79+8/uO/7y98ylP+5al2ze9/vP394jPMsuf9T/yx+XN5Lfq1oJi9hfpt7o8QcppP5RxRmTCkvwnDfjnTfJ0XLzd8efbF4vjYh8G7335ffFrN92P0Z7op5t/3I85+zLvFp2zDPtLY2PjOjw0D+NbvSAI7eIxeLku/QS6Le38TifBNJC7BOh0nIFhHIbugQuWrcR2PaVdcD9gb1YLrdByAcD2JOEOkB9YDNiw2YJ37CAqo3vyx6JwvYhpkfL44j+riiHfzY1Oj6o2xOR1xAWcJh03KcJYBM1bB2aAjzlJa0nYdONukgN4lnCXj2BDOEg78GcFZooc9J40QZYWXmJ8vrmOJOAY5MzZA9nxAHTh7Jj2jeih8E6FLqO5zIOsCqhM4qiP2KTVUR9Udfpunu+tQPUElAdejirSfWIbqccThuHpQnXe+CVTnPoISqkc9sCDj88V5VBdHvJsfGxQLxyZ2C2cxDGdRSYTh7Lmr+pwQfey5rfheUCmQIZzFnKWLIZWi/QhusWcJLzE+X5zHEnEMcmVsgOz5gDpQ9kxefn/9vQmMRG/idMQBVGcTtc2rpNpzeYECquOuqB7pYs8xB+Gk7DkxhurW2DPvERxDdSELMj9fnEd1YcS78bFBvmi39eyIAziLA86Olwxng1hlj29Pfbtqz4EunIV5N2hozLoRBRz8MwG0mBdO4UDLWI8x64aAmFiZME6DiTgIuTQ2MPpc0TsofQ56pc/lePO3W8+OuADrUOtGVK6vK1C2x57DpIwmelC9jYlSVOfRbT2oHtoSRVgsbgdvh8RnOQ0yPl9cR3VJxLvtsSlHQOC/9t1yPhMfSJ/joLbJdz3O+h33+EK/hHcdOBtyiKRUew45YKjJ+Iw5+43X4Gz7KjnO0ogjuCvhLOqBl7hi7q1hSQNFzIg38ujjyqgAZeeKAMN4cwVTvbwD/hbr2RE3kBxmwmOMOQYjOTrXQVSQHOnKYQmAjDnhsc1eGXPE2fkd9jainPk4aOi1hiLi+HbDoyLYVj074gC24hDIkqM9yIGxNeyIrTTRpTGHQIeGyfxAHkpexlYeItvJD8TIUCaJlIw44+J1cMntzNgAyXKoSpbD3gCdv6N6dsQFQCccGiUD9NBXyVgpgaAbosdEF1vmSQA9ee54g28C0LnSjUObhnICdNv+XXmsu/mxEW+ouuVtRsAckqieGWhPXk60wSyUOJPhm+C4j6BEm/0eaIkr9t3+oEQcglwZGyBtPoAO1JsR9u3NEO+nOmVtZnwI5s3A0alakk3ynES6Elagm4YhL8NlWKjOfQQl8mzMmyEmQT/su+KId+tjg4S7qsgxa3MEVJ19fEoMBMAs7qpR+Lo8cBSYQZIkHGm4X5iFWjO4j6AAs6RxGwu0xMZ0cRtKxCHIobEB5wUqkeeoX/KMhBusyC1jM9hwR4NaXuD1oB50BPVoHxf0CM/ulBq1ZWzWtpWITfk0pCzo5t27kpB342ODhNusyDFnM+KsXyVAW1Y3O6Dkw7Uw63d3w2mrqcErtynNvkbGuDNVsjVjXpriBV8z0qU890FLXHHwurgOd2VsgOQ5UjNsHKCqxzch2GpFjnmcefNdgulheEoKfLAoPIdUVz1nsMfZnCISckqeXoHqAdzjTPXkehuz4Uk50M26eaWx7oZHRbS/ihzzOFNYGbrYP+kSAGjtSphpokuXcMnizNONL2Mrz0dnSZcgpnb1ZFzEGRuvg2tvZ8YGyJipUhk6Ulk8+sBzoeTvlvWOV5pHguc0UWhp1T1lJQnKLEQ9VBlocDaXDqi2eUhjjq/GDlUmphzOUvozUHtZoNa1qUI665CFhUILdszwQGEpzC8l1YCQ1b3mzwvz1UNBYV1EEK9IUJ8MVGFnjGdqUUCspjBiIcDbmC2mOmUkaojV6pTxjr3cogFbhownWMh2sWM7TUA/bH1HH1CAsnOpslgX08LwVDJTTebUeoe2pEHvMtPSU3gBNx0IdqL8QLdTqFKHH4IOtRl7YFoitosdE/uA1lKKlcS+rovDGOtyljZ9NN4lyDLXrw3zMnHNbKQ0vfeeY2KfLMQPVdBS3gLuqcAVlvBdtwStpsXZk2NW2aSdKIBWV+dkQvWBVvuBpcvDhLOe7JNnIcz5QhcKfetJ2W9ujdgJ8QMVtIhaKxiO5dvi8pAI+S5xTNWKgDnw+/ZYcFkLdZe1iC7cgjm+SWhsfchubad4lLZ9Veth3sZ0MYNbWK0Gf9TeO7SLWwLOSxyTtZr1GLxLuBWcOqtYdISEsZbS0EnSrCLvXYAtzHMj6oEtxOsJeQXf4l12yRHCwV4lXcsQ4ZLG+YHqWpFSjl01IXsDLhHzJc6JW7D6FIxwYQXC1bXo0H6/Xw9wtZ9XKm6Zq4Os5BGGl0HWxrYMifGyID9cYUtlE5Ggtu3h4255QitjYUPMd92StQJo3faaUQtQIi3p2icUa1keJkkMbcDBk8GGVUuH+whu4ZUktg9U1QqUNg+PMNeTxzcQ0t3AMVmLwvJrESUKnYNI5671sZ7OQSXLArbdNLaDaAu2qB6W1dy4tBDjbcwVU1q8yvZhNRlts6xAyHMDx8QsDGvHjkm94Pdf19Osrm2CQ6qJZkVQmsUzC+iq991en14lZkFNWprasZuzw0uj+0DFrAp6gOtC3DZp2eVZIsYbOKZm8cpiyJDLj0+u+OuRSwPR0pOZWS4Q3SkYpUa12FBA6wBqzMw0tIMoi/W9a1qR6hpRSYg/uEvq2PWfNK/TLUPJCYGE8bqlajVDqCdHLRLgUzfCq+svaSj8H2qpv1SCFkyDNwlaXNvVNenkUL6lEbSMLVAkYb5/YStWWyKGKkvEsJ3N83qT//PPPKt5TA0ZfUMh7w0dU7XAjaFOZOtq2Nrn/5xg66iKvcuKwvu0ybeXYQvpaofNqwLXG25x722kjarrJYbEgd7GhLmIW4maXUspC5G0u9nZxC0B5w3d0rfAanygQLf213TCrZDqwy1YQk+MjcnxpNkfzjOlx/OkAIXmz00zv50wP1B5SxG1OIK8TXkrFPLe0DF5C2h/CMKT+wFS0rGryzSKdZV05KUxSW2mPmeEhrWRyH0EJeAytJMoi/O9a1vKwKXUyIH26o4PJbzXLYGLNwtksnxCThuKD4ANxc4KV6CrJzwPqaVpPeayepQ2FOG4pUfdQk3niJ0o37+6pQJbKFHjW0HbtmXDABEJGW/knLAFMsUHca0fgsXs6SjQ13emveDrB66sNeLVksqDmukTFoK7jcliBq5itRxE0htcCXhu5JielcD8WuHJW3q1nIXOU6YVwIrE+sAK1vmQ19xmWGDFewIFsGpWubUT2QeqZRG1JWHStmrZASsRu42c07BglWlIrS8JpCF23DXjEOkqAQjt1Mot7donYPEyFS+IWDwLsdJqsIfgPlQRK1ArTZP0KmJFEp7rlojF20WTAVfs17yl12tYXU3xLxVT9eAWrOIyryzysBpMa9KwjC0KpVF+mBpWNa3gGlYvqdJUyHSpYxpWBLXC1yrSXG+F797/LQp1lQDktXOT45VbTvgeNXdDNnhxcLcxW0z1fFdMlW7Vo7GEVwKeS90SsaCF4fF+RMF45XdeFwZEF15B+RWKHSNY3C9kBbGoKcSSxveBKlnHfB9Y4o7flt0trgupkOpSxwQtXm9E2bpw7+JSELRoV0GL6HJl8Qzhch+8we1ClVY8CunSmpJ3/MbQWQn0QxW0QrVs6Z6qaFEJ2XVNyYIhVhSfOlpcrWR1XxnGsZZ8wx8rw8MVSitDQ0qWNLwPVslSgasjylmGq1jIdGPHlCze/pk0XQfVytFYLAofEU1F4VGTJXg/8OpavIptB3cbs8WUkqVkx6Ltgg6W8EpAcGPHlCxeQwSpw4HWlCxAk8OuCYYB0ZVgmHBm74+dwquSdAzpWNLoPlAdiyr1kq6mo328EjHc2DEBi+enlApYUW2nEMCvujob9KUVwvHqB7/iJnJZie5Dla8iVbxqtQpTxav7yXqzmu2mBbvd5DlLi6dsU76MX7yf/gVzmt+KSPAAAA=="}, "status": {"message": "OK", "code": 200}}}], "recorded_with": "betamax/0.7.0"} \ 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 deleted file mode 100644 index 8226cbbf96e209..00000000000000 --- a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json +++ /dev/null @@ -1 +0,0 @@ -{"http_interactions": [{"request": {"uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:22", "response": {"url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Location": ["http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"], "Age": ["0"], "X-Varnish": ["4249779869"], "Via": ["1.1 varnish"], "Server": ["Varnish"], "Date": ["Thu, 09 Jun 2016 04:02:20 GMT"], "Connection": ["close"], "Accept-Ranges": ["bytes"]}, "body": {"encoding": null, "string": ""}, "status": {"message": "Moved permanently", "code": 301}}}, {"request": {"uri": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:22", "response": {"url": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Content-Length": ["3637"], "X-Backend-Host": ["ravn_loc"], "X-Varnish": ["4249780502"], "Via": ["1.1 varnish"], "Last-Modified": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-forecast-models": ["proff,ecdet"], "Accept-Ranges": ["bytes"], "Expires": ["Thu, 09 Jun 2016 05:01:15 GMT"], "Content-Encoding": ["gzip"], "Age": ["0"], "Content-Type": ["text/xml; charset=utf-8"], "Server": ["Apache"], "Vary": ["Accept-Encoding"], "Date": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-slicenumber": ["83"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "base64_string": "H4sIAAAAAAAAA+1dTY/bthbd91cI3tcWSUmUBpNumjQtkEyKl3noQ3eKrWSE5y/Ycibpry/lsWxZImlfiqQoOEAWSfRhizLPPTw899775ywtnrLNLC1S79tivtzefdvmr0ZPRbG+m0yen5/Hz2S82nyZYN9Hk/+9f/dx+pQt0p/z5bZIl9Ns5LHz75arh3SRbdfpNHs5/m41TYt8tTzeKV3n40VWjJeryeEj2f9M5ofTPq822TTdFhM0Tibb/R1G3nTDTsxmr0bYR9HPPvuTPPrBnY/vMPp79MtPnufds1um+7/t/7GaZXNvyb7Jq9GbX8dv33wY+wiHI6/INot8WbtR/Ijwne+zP3+PvM1umS1nZx/Ejid3JLgjlB1fZt8Kds7594iPl3/erBbnx6LjsWJ1OoLOPnRy/NaT4zPcrzer2W5aeNN5ut2+Gq1X+bJ4zd7M6HhykS8yr3xZxfc1e8pq3K7/FmdHqtuWd65ehZfOi7zYzdjd/ZE3T6t/EDyOKQnYf62WXw7/hxAdY0yD+o323zJbrNkLLnabzMvZuD4+Po683TIvXo2m2Xyb77Yj72s637Fb4HjsjyaN65/z5ex1zh5t/4XKO8xmI2+WfWEf6ZNxNKpeMvfKj+ssm+2v+vx55C3WbCTJmP0KPmXpjg0Y+xK4usHHr+n/va/smtaNnnaLfJYX36vvGYblLV6egT3bNFsWrWvWm2y7rZ55valOf/ozPT4u8v1kTFpXTuer3Sxfssv31z48jLzDh+wv4QzR59WX/am/fXhbO5d35nz1/Gt5+5dbv/vwV+38mI1l8/xFNst3i9ol79+8/uO/7y98ylP+5al2ze9/vP394jPMsuf9T/yx+XN5Lfq1oJi9hfpt7o8QcppP5RxRmTCkvwnDfjnTfJ0XLzd8efbF4vjYh8G7335ffFrN92P0Z7op5t/3I85+zLvFp2zDPtLY2PjOjw0D+NbvSAI7eIxeLku/QS6Le38TifBNJC7BOh0nIFhHIbugQuWrcR2PaVdcD9gb1YLrdByAcD2JOEOkB9YDNiw2YJ37CAqo3vyx6JwvYhpkfL44j+riiHfzY1Oj6o2xOR1xAWcJh03KcJYBM1bB2aAjzlJa0nYdONukgN4lnCXj2BDOEg78GcFZooc9J40QZYWXmJ8vrmOJOAY5MzZA9nxAHTh7Jj2jeih8E6FLqO5zIOsCqhM4qiP2KTVUR9Udfpunu+tQPUElAdejirSfWIbqccThuHpQnXe+CVTnPoISqkc9sCDj88V5VBdHvJsfGxQLxyZ2C2cxDGdRSYTh7Lmr+pwQfey5rfheUCmQIZzFnKWLIZWi/QhusWcJLzE+X5zHEnEMcmVsgOz5gDpQ9kxefn/9vQmMRG/idMQBVGcTtc2rpNpzeYECquOuqB7pYs8xB+Gk7DkxhurW2DPvERxDdSELMj9fnEd1YcS78bFBvmi39eyIAziLA86Olwxng1hlj29Pfbtqz4EunIV5N2hozLoRBRz8MwG0mBdO4UDLWI8x64aAmFiZME6DiTgIuTQ2MPpc0TsofQ56pc/lePO3W8+OuADrUOtGVK6vK1C2x57DpIwmelC9jYlSVOfRbT2oHtoSRVgsbgdvh8RnOQ0yPl9cR3VJxLvtsSlHQOC/9t1yPhMfSJ/joLbJdz3O+h33+EK/hHcdOBtyiKRUew45YKjJ+Iw5+43X4Gz7KjnO0ogjuCvhLOqBl7hi7q1hSQNFzIg38ujjyqgAZeeKAMN4cwVTvbwD/hbr2RE3kBxmwmOMOQYjOTrXQVSQHOnKYQmAjDnhsc1eGXPE2fkd9jainPk4aOi1hiLi+HbDoyLYVj074gC24hDIkqM9yIGxNeyIrTTRpTGHQIeGyfxAHkpexlYeItvJD8TIUCaJlIw44+J1cMntzNgAyXKoSpbD3gCdv6N6dsQFQCccGiUD9NBXyVgpgaAbosdEF1vmSQA9ee54g28C0LnSjUObhnICdNv+XXmsu/mxEW+ouuVtRsAckqieGWhPXk60wSyUOJPhm+C4j6BEm/0eaIkr9t3+oEQcglwZGyBtPoAO1JsR9u3NEO+nOmVtZnwI5s3A0alakk3ynES6Elagm4YhL8NlWKjOfQQl8mzMmyEmQT/su+KId+tjg4S7qsgxa3MEVJ19fEoMBMAs7qpR+Lo8cBSYQZIkHGm4X5iFWjO4j6AAs6RxGwu0xMZ0cRtKxCHIobEB5wUqkeeoX/KMhBusyC1jM9hwR4NaXuD1oB50BPVoHxf0CM/ulBq1ZWzWtpWITfk0pCzo5t27kpB342ODhNusyDFnM+KsXyVAW1Y3O6Dkw7Uw63d3w2mrqcErtynNvkbGuDNVsjVjXpriBV8z0qU890FLXHHwurgOd2VsgOQ5UjNsHKCqxzch2GpFjnmcefNdgulheEoKfLAoPIdUVz1nsMfZnCISckqeXoHqAdzjTPXkehuz4Uk50M26eaWx7oZHRbS/ihzzOFNYGbrYP+kSAGjtSphpokuXcMnizNONL2Mrz0dnSZcgpnb1ZFzEGRuvg2tvZ8YGyJipUhk6Ulk8+sBzoeTvlvWOV5pHguc0UWhp1T1lJQnKLEQ9VBlocDaXDqi2eUhjjq/GDlUmphzOUvozUHtZoNa1qUI665CFhUILdszwQGEpzC8l1YCQ1b3mzwvz1UNBYV1EEK9IUJ8MVGFnjGdqUUCspjBiIcDbmC2mOmUkaojV6pTxjr3cogFbhownWMh2sWM7TUA/bH1HH1CAsnOpslgX08LwVDJTTebUeoe2pEHvMtPSU3gBNx0IdqL8QLdTqFKHH4IOtRl7YFoitosdE/uA1lKKlcS+rovDGOtyljZ9NN4lyDLXrw3zMnHNbKQ0vfeeY2KfLMQPVdBS3gLuqcAVlvBdtwStpsXZk2NW2aSdKIBWV+dkQvWBVvuBpcvDhLOe7JNnIcz5QhcKfetJ2W9ujdgJ8QMVtIhaKxiO5dvi8pAI+S5xTNWKgDnw+/ZYcFkLdZe1iC7cgjm+SWhsfchubad4lLZ9Veth3sZ0MYNbWK0Gf9TeO7SLWwLOSxyTtZr1GLxLuBWcOqtYdISEsZbS0EnSrCLvXYAtzHMj6oEtxOsJeQXf4l12yRHCwV4lXcsQ4ZLG+YHqWpFSjl01IXsDLhHzJc6JW7D6FIxwYQXC1bXo0H6/Xw9wtZ9XKm6Zq4Os5BGGl0HWxrYMifGyID9cYUtlE5Ggtu3h4255QitjYUPMd92StQJo3faaUQtQIi3p2icUa1keJkkMbcDBk8GGVUuH+whu4ZUktg9U1QqUNg+PMNeTxzcQ0t3AMVmLwvJrESUKnYNI5671sZ7OQSXLArbdNLaDaAu2qB6W1dy4tBDjbcwVU1q8yvZhNRlts6xAyHMDx8QsDGvHjkm94Pdf19Osrm2CQ6qJZkVQmsUzC+iq991en14lZkFNWprasZuzw0uj+0DFrAp6gOtC3DZp2eVZIsYbOKZm8cpiyJDLj0+u+OuRSwPR0pOZWS4Q3SkYpUa12FBA6wBqzMw0tIMoi/W9a1qR6hpRSYg/uEvq2PWfNK/TLUPJCYGE8bqlajVDqCdHLRLgUzfCq+svaSj8H2qpv1SCFkyDNwlaXNvVNenkUL6lEbSMLVAkYb5/YStWWyKGKkvEsJ3N83qT//PPPKt5TA0ZfUMh7w0dU7XAjaFOZOtq2Nrn/5xg66iKvcuKwvu0ybeXYQvpaofNqwLXG25x722kjarrJYbEgd7GhLmIW4maXUspC5G0u9nZxC0B5w3d0rfAanygQLf213TCrZDqwy1YQk+MjcnxpNkfzjOlx/OkAIXmz00zv50wP1B5SxG1OIK8TXkrFPLe0DF5C2h/CMKT+wFS0rGryzSKdZV05KUxSW2mPmeEhrWRyH0EJeAytJMoi/O9a1vKwKXUyIH26o4PJbzXLYGLNwtksnxCThuKD4ANxc4KV6CrJzwPqaVpPeayepQ2FOG4pUfdQk3niJ0o37+6pQJbKFHjW0HbtmXDABEJGW/knLAFMsUHca0fgsXs6SjQ13emveDrB66sNeLVksqDmukTFoK7jcliBq5itRxE0htcCXhu5JielcD8WuHJW3q1nIXOU6YVwIrE+sAK1vmQ19xmWGDFewIFsGpWubUT2QeqZRG1JWHStmrZASsRu42c07BglWlIrS8JpCF23DXjEOkqAQjt1Mot7donYPEyFS+IWDwLsdJqsIfgPlQRK1ArTZP0KmJFEp7rlojF20WTAVfs17yl12tYXU3xLxVT9eAWrOIyryzysBpMa9KwjC0KpVF+mBpWNa3gGlYvqdJUyHSpYxpWBLXC1yrSXG+F797/LQp1lQDktXOT45VbTvgeNXdDNnhxcLcxW0z1fFdMlW7Vo7GEVwKeS90SsaCF4fF+RMF45XdeFwZEF15B+RWKHSNY3C9kBbGoKcSSxveBKlnHfB9Y4o7flt0trgupkOpSxwQtXm9E2bpw7+JSELRoV0GL6HJl8Qzhch+8we1ClVY8CunSmpJ3/MbQWQn0QxW0QrVs6Z6qaFEJ2XVNyYIhVhSfOlpcrWR1XxnGsZZ8wx8rw8MVSitDQ0qWNLwPVslSgasjylmGq1jIdGPHlCze/pk0XQfVytFYLAofEU1F4VGTJXg/8OpavIptB3cbs8WUkqVkx6Ltgg6W8EpAcGPHlCxeQwSpw4HWlCxAk8OuCYYB0ZVgmHBm74+dwquSdAzpWNLoPlAdiyr1kq6mo328EjHc2DEBi+enlApYUW2nEMCvujob9KUVwvHqB7/iJnJZie5Dla8iVbxqtQpTxav7yXqzmu2mBbvd5DlLi6dsU76MX7yf/gVzmt+KSPAAAA=="}, "status": {"message": "OK", "code": 200}}}], "recorded_with": "betamax/0.7.0"} \ No newline at end of file diff --git a/tests/common.py b/tests/common.py index 98c61dfc16e890..26d466bc4b8b38 100644 --- a/tests/common.py +++ b/tests/common.py @@ -35,6 +35,7 @@ def get_test_home_assistant(num_threads=None): hass.config.config_dir = get_test_config_dir() hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 + hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone('US/Pacific') hass.config.temperature_unit = TEMP_CELSIUS @@ -105,6 +106,13 @@ def ensure_sun_set(hass): fire_time_changed(hass, sun.next_setting_utc(hass) + timedelta(seconds=10)) +def load_fixture(filename): + """Helper to load a fixture.""" + path = os.path.join(os.path.dirname(__file__), 'fixtures', filename) + with open(path) as fp: + return fp.read() + + def mock_state_change_event(hass, new_state, old_state=None): """Mock state change envent.""" event_data = { diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 210ce2c58faa4d..241e4a65a0f295 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -67,3 +67,68 @@ def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock): self.assertIsNotNone(device_tracker.asuswrt.get_scanner( self.hass, conf_dict)) asuswrt_mock.assert_called_once_with(conf_dict[device_tracker.DOMAIN]) + + def test_ssh_login_with_pub_key(self): + """Test that login is done with pub_key when configured to.""" + ssh = mock.MagicMock() + ssh_mock = mock.patch('pexpect.pxssh.pxssh', return_value=ssh) + ssh_mock.start() + self.addCleanup(ssh_mock.stop) + conf_dict = { + CONF_PLATFORM: 'asuswrt', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + 'pub_key': '/fake_path' + } + update_mock = mock.patch( + 'homeassistant.components.device_tracker.asuswrt.' + 'AsusWrtDeviceScanner.get_asuswrt_data') + update_mock.start() + self.addCleanup(update_mock.stop) + asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) + asuswrt.ssh_connection() + ssh.login.assert_called_once_with('fake_host', 'fake_user', + ssh_key='/fake_path') + + def test_ssh_login_with_password(self): + """Test that login is done with password when configured to.""" + ssh = mock.MagicMock() + ssh_mock = mock.patch('pexpect.pxssh.pxssh', return_value=ssh) + ssh_mock.start() + self.addCleanup(ssh_mock.stop) + conf_dict = { + CONF_PLATFORM: 'asuswrt', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: 'fake_pass' + } + update_mock = mock.patch( + 'homeassistant.components.device_tracker.asuswrt.' + 'AsusWrtDeviceScanner.get_asuswrt_data') + update_mock.start() + self.addCleanup(update_mock.stop) + asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) + asuswrt.ssh_connection() + ssh.login.assert_called_once_with('fake_host', 'fake_user', + 'fake_pass') + + def test_ssh_login_without_password_or_pubkey(self): + """Test that login is not called without password or pub_key.""" + ssh = mock.MagicMock() + ssh_mock = mock.patch('pexpect.pxssh.pxssh', return_value=ssh) + ssh_mock.start() + self.addCleanup(ssh_mock.stop) + conf_dict = { + CONF_PLATFORM: 'asuswrt', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + } + update_mock = mock.patch( + 'homeassistant.components.device_tracker.asuswrt.' + 'AsusWrtDeviceScanner.get_asuswrt_data') + update_mock.start() + self.addCleanup(update_mock.stop) + asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) + result = asuswrt.ssh_connection() + ssh.login.assert_not_called() + self.assertIsNone(result) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 7445b5daf8ce3c..427980be5f19a2 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -1,8 +1,8 @@ """The tests the for Locative device tracker platform.""" +import time import unittest from unittest.mock import patch -import eventlet import requests from homeassistant import bootstrap, const @@ -32,12 +32,9 @@ def setUpModule(): # pylint: disable=invalid-name 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: { @@ -46,7 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name }) hass.start() - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/media_player/test_cmus.py b/tests/components/media_player/test_cmus.py new file mode 100644 index 00000000000000..24322b5bce0b24 --- /dev/null +++ b/tests/components/media_player/test_cmus.py @@ -0,0 +1,31 @@ +"""The tests for the Demo Media player platform.""" +import unittest +from unittest import mock + +from homeassistant.components.media_player import cmus +from homeassistant import const + +from tests.common import get_test_home_assistant + +entity_id = 'media_player.cmus' + + +class TestCmusMediaPlayer(unittest.TestCase): + """Test the media_player module.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch('homeassistant.components.media_player.cmus.CmusDevice') + def test_password_required_with_host(self, cmus_mock): + """Test that a password is required when specifying a remote host.""" + fake_config = { + const.CONF_HOST: 'a_real_hostname', + } + self.assertFalse( + cmus.setup_platform(self.hass, fake_config, mock.MagicMock())) diff --git a/tests/components/test_forecast.py b/tests/components/sensor/test_forecast.py similarity index 68% rename from tests/components/test_forecast.py rename to tests/components/sensor/test_forecast.py index bfda22596c2c5d..55bdec20a3511a 100644 --- a/tests/components/test_forecast.py +++ b/tests/components/sensor/test_forecast.py @@ -1,17 +1,17 @@ """The tests for the forecast.io platform.""" -import json import re -import os import unittest from unittest.mock import MagicMock, patch import forecastio -import httpretty from requests.exceptions import HTTPError +import requests_mock from homeassistant.components.sensor import forecast from homeassistant import core as ha +from tests.common import load_fixture + class TestForecastSetup(unittest.TestCase): """Test the forecast.io platform.""" @@ -48,29 +48,14 @@ def test_setup_bad_api_key(self, mock_get_forecast): response = forecast.setup_platform(self.hass, self.config, MagicMock()) self.assertFalse(response) - @httpretty.activate + @requests_mock.Mocker() @patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast) - def test_setup(self, mock_get_forecast): + def test_setup(self, m, mock_get_forecast): """Test for successfully setting up the forecast.io platform.""" - def load_fixture_from_json(): - cwd = os.path.dirname(__file__) - fixture_path = os.path.join(cwd, '..', 'fixtures', 'forecast.json') - with open(fixture_path) as file: - content = json.load(file) - return json.dumps(content) - - # Mock out any calls to the actual API and - # return the fixture json instead - uri = 'api.forecast.io\/forecast\/(\w+)\/(-?\d+\.?\d*),(-?\d+\.?\d*)' - httpretty.register_uri( - httpretty.GET, - re.compile(uri), - body=load_fixture_from_json(), - ) - # The following will raise an error if the regex for the mock was - # incorrect and we actually try to go out to the internet. - httpretty.HTTPretty.allow_net_connect = False - + uri = ('https://api.forecast.io\/forecast\/(\w+)\/' + '(-?\d+\.?\d*),(-?\d+\.?\d*)') + m.get(re.compile(uri), + text=load_fixture('forecast.json')) forecast.setup_platform(self.hass, self.config, MagicMock()) self.assertTrue(mock_get_forecast.called) self.assertEqual(mock_get_forecast.call_count, 1) diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 43a14578690447..3ea94938f0d929 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -1,39 +1,40 @@ """The tests for the Yr sensor platform.""" from datetime import datetime +from unittest import TestCase from unittest.mock import patch -import pytest +import requests_mock from homeassistant.bootstrap import _setup_component import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, load_fixture -@pytest.mark.usefixtures('betamax_session') -class TestSensorYr: +class TestSensorYr(TestCase): """Test the Yr sensor.""" - def setup_method(self, method): + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.latitude = 32.87336 self.hass.config.longitude = 117.22743 - def teardown_method(self, method): + def tearDown(self): """Stop everything that was started.""" self.hass.stop() - def test_default_setup(self, betamax_session): + @requests_mock.Mocker() + def test_default_setup(self, m): """Test the default setup.""" + m.get('http://api.yr.no/weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) now = datetime(2016, 6, 9, 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 _setup_component(self.hass, 'sensor', { - 'sensor': {'platform': 'yr', - 'elevation': 0}}) + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert _setup_component(self.hass, 'sensor', { + 'sensor': {'platform': 'yr', + 'elevation': 0}}) state = self.hass.states.get('sensor.yr_symbol') @@ -41,23 +42,24 @@ def test_default_setup(self, betamax_session): assert state.state.isnumeric() assert state.attributes.get('unit_of_measurement') is None - def test_custom_setup(self, betamax_session): + @requests_mock.Mocker() + def test_custom_setup(self, m): """Test a custom setup.""" + m.get('http://api.yr.no/weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) now = datetime(2016, 6, 9, 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 _setup_component(self.hass, 'sensor', { - 'sensor': {'platform': 'yr', - 'elevation': 0, - 'monitored_conditions': [ - 'pressure', - 'windDirection', - 'humidity', - 'fog', - 'windSpeed']}}) + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert _setup_component(self.hass, 'sensor', { + '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') diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index e1eb257577cc3c..97d73b8b49d4c0 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -1,9 +1,9 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access,too-many-public-methods -import unittest import json +import time +import unittest -import eventlet import requests from homeassistant import bootstrap, const @@ -86,8 +86,7 @@ def setUpModule(): # pylint: disable=invalid-name }) hass.start() - - eventlet.sleep(0.1) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 66fb97dfd33392..752980e65c8833 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -1,12 +1,12 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access,too-many-public-methods -# from contextlib import closing +from contextlib import closing import json import tempfile +import time import unittest from unittest.mock import patch -import eventlet import requests from homeassistant import bootstrap, const @@ -48,10 +48,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - # To start HTTP - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name @@ -136,6 +133,27 @@ def test_api_state_change_with_bad_data(self): self.assertEqual(400, req.status_code) + # pylint: disable=invalid-name + def test_api_state_change_push(self): + """Test if we can push a change the state of an entity.""" + hass.states.set("test.test", "not_to_be_set") + + events = [] + hass.bus.listen(const.EVENT_STATE_CHANGED, events.append) + + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "not_to_be_set"}), + headers=HA_HEADERS) + hass.bus._pool.block_till_done() + self.assertEqual(0, len(events)) + + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "not_to_be_set", + "force_update": True}), + headers=HA_HEADERS) + hass.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + # pylint: disable=invalid-name def test_api_fire_event_with_no_data(self): """Test if the API allows us to fire an event.""" @@ -225,7 +243,7 @@ def test_api_get_components(self): def test_api_get_error_log(self): """Test the return of the error log.""" - test_content = 'Test String' + test_content = 'Test String°' with tempfile.NamedTemporaryFile() as log: log.write(test_content.encode('utf-8')) log.flush() @@ -366,25 +384,23 @@ def test_api_event_forward(self): headers=HA_HEADERS) self.assertEqual(422, req.status_code) - # TODO disabled because eventlet cannot validate - # a connection to itself, need a second instance - # # Setup a real one - # req = requests.post( - # _url(const.URL_API_EVENT_FORWARD), - # data=json.dumps({ - # 'api_password': API_PASSWORD, - # 'host': '127.0.0.1', - # 'port': SERVER_PORT - # }), - # headers=HA_HEADERS) - # self.assertEqual(200, req.status_code) - - # # Delete it again.. - # req = requests.delete( - # _url(const.URL_API_EVENT_FORWARD), - # data=json.dumps({}), - # headers=HA_HEADERS) - # self.assertEqual(400, req.status_code) + # Setup a real one + req = requests.post( + _url(const.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'api_password': API_PASSWORD, + 'host': '127.0.0.1', + 'port': SERVER_PORT + }), + headers=HA_HEADERS) + self.assertEqual(200, req.status_code) + + # Delete it again.. + req = requests.delete( + _url(const.URL_API_EVENT_FORWARD), + data=json.dumps({}), + headers=HA_HEADERS) + self.assertEqual(400, req.status_code) req = requests.delete( _url(const.URL_API_EVENT_FORWARD), @@ -404,57 +420,58 @@ def test_api_event_forward(self): headers=HA_HEADERS) self.assertEqual(200, req.status_code) - # def test_stream(self): - # """Test the stream.""" - # listen_count = self._listen_count() - # with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, - # stream=True, headers=HA_HEADERS)) as req: - - # self.assertEqual(listen_count + 1, self._listen_count()) - - # hass.bus.fire('test_event') - - # data = self._stream_next_event(req) - - # self.assertEqual('test_event', data['event_type']) - - # def test_stream_with_restricted(self): - # """Test the stream with restrictions.""" - # listen_count = self._listen_count() - # url = _url('{}?restrict=test_event1,test_event3'.format( - # const.URL_API_STREAM)) - # with closing(requests.get(url, stream=True, timeout=3, - # headers=HA_HEADERS)) as req: - # self.assertEqual(listen_count + 1, self._listen_count()) - - # hass.bus.fire('test_event1') - # data = self._stream_next_event(req) - # self.assertEqual('test_event1', data['event_type']) - - # hass.bus.fire('test_event2') - # hass.bus.fire('test_event3') - - # data = self._stream_next_event(req) - # self.assertEqual('test_event3', data['event_type']) - - # def _stream_next_event(self, stream): - # """Read the stream for next event while ignoring ping.""" - # while True: - # data = b'' - # last_new_line = False - # for dat in stream.iter_content(1): - # if dat == b'\n' and last_new_line: - # break - # data += dat - # last_new_line = dat == b'\n' - - # conv = data.decode('utf-8').strip()[6:] - - # if conv != 'ping': - # break - - # return json.loads(conv) - - # def _listen_count(self): - # """Return number of event listeners.""" - # return sum(hass.bus.listeners.values()) + def test_stream(self): + """Test the stream.""" + listen_count = self._listen_count() + with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, + stream=True, headers=HA_HEADERS)) as req: + stream = req.iter_content(1) + self.assertEqual(listen_count + 1, self._listen_count()) + + hass.bus.fire('test_event') + + data = self._stream_next_event(stream) + + self.assertEqual('test_event', data['event_type']) + + def test_stream_with_restricted(self): + """Test the stream with restrictions.""" + listen_count = self._listen_count() + url = _url('{}?restrict=test_event1,test_event3'.format( + const.URL_API_STREAM)) + with closing(requests.get(url, stream=True, timeout=3, + headers=HA_HEADERS)) as req: + stream = req.iter_content(1) + self.assertEqual(listen_count + 1, self._listen_count()) + + hass.bus.fire('test_event1') + data = self._stream_next_event(stream) + self.assertEqual('test_event1', data['event_type']) + + hass.bus.fire('test_event2') + hass.bus.fire('test_event3') + + data = self._stream_next_event(stream) + self.assertEqual('test_event3', data['event_type']) + + def _stream_next_event(self, stream): + """Read the stream for next event while ignoring ping.""" + while True: + data = b'' + last_new_line = False + for dat in stream: + if dat == b'\n' and last_new_line: + break + data += dat + last_new_line = dat == b'\n' + + conv = data.decode('utf-8').strip()[6:] + + if conv != 'ping': + break + + return json.loads(conv) + + def _listen_count(self): + """Return number of event listeners.""" + return sum(hass.bus.listeners.values()) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 61e33931c24a7f..083ebd2eb0c1f5 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -1,9 +1,9 @@ """The tests for Home Assistant frontend.""" # pylint: disable=protected-access,too-many-public-methods import re +import time import unittest -import eventlet import requests import homeassistant.bootstrap as bootstrap @@ -42,10 +42,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'frontend') hass.start() - - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_http.py b/tests/components/test_http.py index f665a9530c8c61..6ab79f3e0ccf6d 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -1,8 +1,8 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access,too-many-public-methods import logging +import time -import eventlet import requests from homeassistant import bootstrap, const @@ -43,8 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name @@ -83,7 +82,7 @@ def test_access_with_password_in_header(self, caplog): logs = caplog.text() - assert const.URL_API in logs + # assert const.URL_API in logs assert API_PASSWORD not in logs def test_access_denied_with_wrong_password_in_url(self): @@ -106,5 +105,5 @@ def test_access_with_password_in_url(self, caplog): logs = caplog.text() - assert const.URL_API in logs + # assert const.URL_API in logs assert API_PASSWORD not in logs diff --git a/tests/components/test_init.py b/tests/components/test_init.py index ff663a95a5378d..7abaf63b407703 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -2,13 +2,18 @@ # pylint: disable=protected-access,too-many-public-methods import unittest from unittest.mock import patch +from tempfile import TemporaryDirectory + +import yaml import homeassistant.core as ha +from homeassistant import config from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) import homeassistant.components as comps +from homeassistant.helpers import entity -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_service class TestComponentsCore(unittest.TestCase): @@ -31,47 +36,40 @@ def test_is_on(self): self.assertTrue(comps.is_on(self.hass, 'light.Bowl')) self.assertFalse(comps.is_on(self.hass, 'light.Ceiling')) self.assertTrue(comps.is_on(self.hass)) + self.assertFalse(comps.is_on(self.hass, 'non_existing.entity')) + + def test_turn_on_without_entities(self): + """Test turn_on method without entities.""" + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + comps.turn_on(self.hass) + self.hass.pool.block_till_done() + self.assertEqual(0, len(calls)) def test_turn_on(self): """Test turn_on method.""" - runs = [] - self.hass.services.register( - 'light', SERVICE_TURN_ON, lambda x: runs.append(1)) - + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) comps.turn_on(self.hass, 'light.Ceiling') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(runs)) + self.assertEqual(1, len(calls)) def test_turn_off(self): """Test turn_off method.""" - runs = [] - self.hass.services.register( - 'light', SERVICE_TURN_OFF, lambda x: runs.append(1)) - + calls = mock_service(self.hass, 'light', SERVICE_TURN_OFF) comps.turn_off(self.hass, 'light.Bowl') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(runs)) + self.assertEqual(1, len(calls)) def test_toggle(self): """Test toggle method.""" - runs = [] - self.hass.services.register( - 'light', SERVICE_TOGGLE, lambda x: runs.append(1)) - + calls = mock_service(self.hass, 'light', SERVICE_TOGGLE) comps.toggle(self.hass, 'light.Bowl') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(runs)) + self.assertEqual(1, len(calls)) @patch('homeassistant.core.ServiceRegistry.call') def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): """Test if turn_on is blocking domain with no service.""" - self.hass.services.register('light', SERVICE_TURN_ON, lambda x: x) + mock_service(self.hass, 'light', SERVICE_TURN_ON) # 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 @@ -89,3 +87,62 @@ def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): self.assertEqual( ('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False), mock_call.call_args_list[1][0]) + + def test_reload_core_conf(self): + """Test reload core conf service.""" + ent = entity.Entity() + ent.entity_id = 'test.entity' + ent.hass = self.hass + ent.update_ha_state() + + state = self.hass.states.get('test.entity') + assert state is not None + assert state.state == 'unknown' + assert state.attributes == {} + + with TemporaryDirectory() as conf_dir: + self.hass.config.config_dir = conf_dir + conf_yaml = self.hass.config.path(config.YAML_CONFIG_FILE) + + with open(conf_yaml, 'a') as fp: + fp.write(yaml.dump({ + ha.DOMAIN: { + 'latitude': 10, + 'longitude': 20, + 'customize': { + 'test.Entity': { + 'hello': 'world' + } + } + } + })) + + comps.reload_core_config(self.hass) + self.hass.pool.block_till_done() + + assert 10 == self.hass.config.latitude + assert 20 == self.hass.config.longitude + + ent.update_ha_state() + + state = self.hass.states.get('test.entity') + assert state is not None + assert state.state == 'unknown' + assert state.attributes.get('hello') == 'world' + + @patch('homeassistant.components._LOGGER.error') + @patch('homeassistant.config.process_ha_core_config') + def test_reload_core_with_wrong_conf(self, mock_process, mock_error): + """Test reload core conf service.""" + with TemporaryDirectory() as conf_dir: + self.hass.config.config_dir = conf_dir + conf_yaml = self.hass.config.path(config.YAML_CONFIG_FILE) + + with open(conf_yaml, 'a') as fp: + fp.write(yaml.dump(['invalid', 'config'])) + + comps.reload_core_config(self.hass) + self.hass.pool.block_till_done() + + assert mock_error.called + assert mock_process.called is False diff --git a/tests/components/test_persistent_notification.py b/tests/components/test_persistent_notification.py new file mode 100644 index 00000000000000..6f6d8b8e1b036b --- /dev/null +++ b/tests/components/test_persistent_notification.py @@ -0,0 +1,65 @@ +"""The tests for the persistent notification component.""" +import homeassistant.components.persistent_notification as pn + +from tests.common import get_test_home_assistant + + +class TestPersistentNotification: + """Test persistent notification component.""" + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + pn.setup(self.hass, {}) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_create(self): + """Test creating notification without title or notification id.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, 'Hello World {{ 1 + 1 }}', + title='{{ 1 + 1 }} beers') + self.hass.pool.block_till_done() + + entity_ids = self.hass.states.entity_ids(pn.DOMAIN) + assert len(entity_ids) == 1 + + state = self.hass.states.get(entity_ids[0]) + assert state.state == 'Hello World 2' + assert state.attributes.get('title') == '2 beers' + + def test_create_notification_id(self): + """Ensure overwrites existing notification with same id.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, 'test', notification_id='Beer 2') + self.hass.pool.block_till_done() + + assert len(self.hass.states.entity_ids()) == 1 + state = self.hass.states.get('persistent_notification.beer_2') + assert state.state == 'test' + + pn.create(self.hass, 'test 2', notification_id='Beer 2') + self.hass.pool.block_till_done() + + # We should have overwritten old one + assert len(self.hass.states.entity_ids()) == 1 + state = self.hass.states.get('persistent_notification.beer_2') + assert state.state == 'test 2' + + def test_create_template_error(self): + """Ensure we output templates if contain error.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, '{{ message + 1 }}', '{{ title + 1 }}') + self.hass.pool.block_till_done() + + entity_ids = self.hass.states.entity_ids(pn.DOMAIN) + assert len(entity_ids) == 1 + + state = self.hass.states.get(entity_ids[0]) + assert state.state == '{{ message + 1 }}' + assert state.attributes.get('title') == '{{ title + 1 }}' diff --git a/tests/fixtures/freegeoip.io.json b/tests/fixtures/freegeoip.io.json new file mode 100644 index 00000000000000..8afdaba070e998 --- /dev/null +++ b/tests/fixtures/freegeoip.io.json @@ -0,0 +1,13 @@ +{ + "ip": "1.2.3.4", + "country_code": "US", + "country_name": "United States", + "region_code": "CA", + "region_name": "California", + "city": "San Diego", + "zip_code": "92122", + "time_zone": "America\/Los_Angeles", + "latitude": 32.8594, + "longitude": -117.2073, + "metro_code": 825 +} diff --git a/tests/fixtures/google_maps_elevation.json b/tests/fixtures/google_maps_elevation.json new file mode 100644 index 00000000000000..95eeb0fe239ad1 --- /dev/null +++ b/tests/fixtures/google_maps_elevation.json @@ -0,0 +1,13 @@ +{ + "results" : [ + { + "elevation" : 101.5, + "location" : { + "lat" : 32.54321, + "lng" : -117.12345 + }, + "resolution" : 4.8 + } + ], + "status" : "OK" +} diff --git a/tests/fixtures/ip-api.com.json b/tests/fixtures/ip-api.com.json new file mode 100644 index 00000000000000..d31d4560589b3b --- /dev/null +++ b/tests/fixtures/ip-api.com.json @@ -0,0 +1,16 @@ +{ + "as": "AS20001 Time Warner Cable Internet LLC", + "city": "San Diego", + "country": "United States", + "countryCode": "US", + "isp": "Time Warner Cable", + "lat": 32.8594, + "lon": -117.2073, + "org": "Time Warner Cable", + "query": "1.2.3.4", + "region": "CA", + "regionName": "California", + "status": "success", + "timezone": "America\/Los_Angeles", + "zip": "92122" +} diff --git a/tests/fixtures/yr.no.json b/tests/fixtures/yr.no.json new file mode 100644 index 00000000000000..b181fdfd85b225 --- /dev/null +++ b/tests/fixtures/yr.no.json @@ -0,0 +1,1184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index a317702b29f0f8..a465c2f2c74b8a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -21,8 +21,7 @@ def setUp(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - entity.Entity.overwrite_attribute(self.entity.entity_id, - [ATTR_HIDDEN], [None]) + entity.set_customize({}) def test_default_hidden_not_in_attributes(self): """Test that the default hidden property is set to False.""" @@ -32,8 +31,7 @@ def test_default_hidden_not_in_attributes(self): def test_overwriting_hidden_property_to_true(self): """Test we can overwrite hidden property to True.""" - entity.Entity.overwrite_attribute(self.entity.entity_id, - [ATTR_HIDDEN], [True]) + entity.set_customize({self.entity.entity_id: {ATTR_HIDDEN: True}}) self.entity.update_ha_state() state = self.hass.states.get(self.entity.entity_id) @@ -43,3 +41,30 @@ def test_split_entity_id(self): """Test split_entity_id.""" self.assertEqual(['domain', 'object_id'], entity.split_entity_id('domain.object_id')) + + def test_generate_entity_id_requires_hass_or_ids(self): + """Ensure we require at least hass or current ids.""" + fmt = 'test.{}' + with self.assertRaises(ValueError): + entity.generate_entity_id(fmt, 'hello world') + + def test_generate_entity_id_given_hass(self): + """Test generating an entity id given hass object.""" + fmt = 'test.{}' + self.assertEqual( + 'test.overwrite_hidden_true_2', + entity.generate_entity_id(fmt, 'overwrite hidden true', + hass=self.hass)) + + def test_generate_entity_id_given_keys(self): + """Test generating an entity id given current ids.""" + fmt = 'test.{}' + self.assertEqual( + 'test.overwrite_hidden_true_2', + entity.generate_entity_id( + fmt, 'overwrite hidden true', + current_ids=['test.overwrite_hidden_true'])) + self.assertEqual( + 'test.overwrite_hidden_true', + entity.generate_entity_id(fmt, 'overwrite hidden true', + current_ids=['test.another_entity'])) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 152818d02e4709..34aaa1b83eda04 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,6 +1,5 @@ """Test the bootstrapping.""" # pylint: disable=too-many-public-methods,protected-access -import os import tempfile from unittest import mock import threading @@ -8,10 +7,7 @@ import voluptuous as vol from homeassistant import bootstrap, loader -from homeassistant.const import (__version__, CONF_LATITUDE, CONF_LONGITUDE, - CONF_NAME, CONF_CUSTOMIZE) import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from tests.common import get_test_home_assistant, MockModule, MockPlatform @@ -24,23 +20,22 @@ class TestBootstrap: def setup_method(self, method): """Setup the test.""" + self.backup_cache = loader._COMPONENT_CACHE + if method == self.test_from_config_file: return self.hass = get_test_home_assistant() - self.backup_cache = loader._COMPONENT_CACHE def teardown_method(self, method): """Clean up.""" dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE - - if method == self.test_from_config_file: - return - self.hass.stop() loader._COMPONENT_CACHE = self.backup_cache - def test_from_config_file(self): + @mock.patch('homeassistant.util.location.detect_location_info', + return_value=None) + def test_from_config_file(self, mock_detect): """Test with configuration file.""" components = ['browser', 'conversation', 'script'] with tempfile.NamedTemporaryFile() as fp: @@ -48,71 +43,10 @@ def test_from_config_file(self): fp.write('{}:\n'.format(comp).encode('utf-8')) fp.flush() - hass = bootstrap.from_config_file(fp.name) - - components.append('group') - - assert sorted(components) == sorted(hass.config.components) - - def test_remove_lib_on_upgrade(self): - """Test removal of library on upgrade.""" - with tempfile.TemporaryDirectory() as config_dir: - version_path = os.path.join(config_dir, '.HA_VERSION') - lib_dir = os.path.join(config_dir, 'deps') - check_file = os.path.join(lib_dir, 'check') - - with open(version_path, 'wt') as outp: - outp.write('0.7.0') - - os.mkdir(lib_dir) - - with open(check_file, 'w'): - pass - - self.hass.config.config_dir = config_dir - - assert os.path.isfile(check_file) - bootstrap.process_ha_config_upgrade(self.hass) - assert not os.path.isfile(check_file) - - def test_not_remove_lib_if_not_upgrade(self): - """Test removal of library with no upgrade.""" - with tempfile.TemporaryDirectory() as config_dir: - version_path = os.path.join(config_dir, '.HA_VERSION') - lib_dir = os.path.join(config_dir, 'deps') - check_file = os.path.join(lib_dir, 'check') - - with open(version_path, 'wt') as outp: - outp.write(__version__) - - os.mkdir(lib_dir) - - with open(check_file, 'w'): - pass - - self.hass.config.config_dir = config_dir - - bootstrap.process_ha_config_upgrade(self.hass) - - assert os.path.isfile(check_file) - - def test_entity_customization(self): - """Test entity customization through configuration.""" - config = {CONF_LATITUDE: 50, - CONF_LONGITUDE: 50, - CONF_NAME: 'Test', - CONF_CUSTOMIZE: {'test.test': {'hidden': True}}} - - bootstrap.process_ha_core_config(self.hass, config) - - entity = Entity() - entity.entity_id = 'test.test' - entity.hass = self.hass - entity.update_ha_state() - - state = self.hass.states.get('test.test') + self.hass = bootstrap.from_config_file(fp.name) - assert state.attributes['hidden'] + components.append('group') + assert sorted(components) == sorted(self.hass.config.components) def test_handle_setup_circular_dependency(self): """Test the setup of circular dependencies.""" @@ -302,8 +236,7 @@ def exception_setup(hass, config): assert not bootstrap._setup_component(self.hass, 'comp', None) assert 'comp' not in self.hass.config.components - @mock.patch('homeassistant.bootstrap.process_ha_core_config') - def test_home_assistant_core_config_validation(self, mock_process): + def test_home_assistant_core_config_validation(self): """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done in test_config.py assert None is bootstrap.from_config_dict({ @@ -311,7 +244,6 @@ def test_home_assistant_core_config_validation(self, mock_process): 'latitude': 'some string' } }) - assert not mock_process.called def test_component_setup_with_validation_and_dependency(self): """Test all config is passed to dependencies.""" diff --git a/tests/test_config.py b/tests/test_config.py index 8a5ec306b3bed7..6be3f585967c2a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,22 +1,28 @@ """Test config utils.""" # pylint: disable=too-many-public-methods,protected-access +import os +import tempfile import unittest import unittest.mock as mock -import os import pytest from voluptuous import MultipleInvalid -from homeassistant.core import DOMAIN, HomeAssistantError +from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, - CONF_TIME_ZONE) + CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, + TEMP_FAHRENHEIT) +from homeassistant.util import location as location_util, dt as dt_util +from homeassistant.helpers.entity import Entity -from tests.common import get_test_config_dir +from tests.common import ( + get_test_config_dir, get_test_home_assistant) CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE def create_file(path): @@ -30,9 +36,14 @@ class TestConfig(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """Clean up.""" + dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE + if os.path.isfile(YAML_PATH): os.remove(YAML_PATH) + if hasattr(self, 'hass'): + self.hass.stop() + def test_create_default_config(self): """Test creation of default config.""" config_util.create_default_config(CONFIG_DIR, False) @@ -108,8 +119,15 @@ def test_load_yaml_config_preserves_key_order(self): [('hello', 0), ('world', 1)], list(config_util.load_yaml_config_file(YAML_PATH).items())) + @mock.patch('homeassistant.util.location.detect_location_info', + return_value=location_util.LocationInfo( + '0.0.0.0', 'US', 'United States', 'CA', 'California', + 'San Diego', '92122', 'America/Los_Angeles', 32.8594, + -117.2073, True)) + @mock.patch('homeassistant.util.location.elevation', return_value=101) @mock.patch('builtins.print') - def test_create_default_config_detect_location(self, mock_print): + def test_create_default_config_detect_location(self, mock_detect, + mock_elev, mock_print): """Test that detect location sets the correct config keys.""" config_util.ensure_config_exists(CONFIG_DIR) @@ -120,15 +138,16 @@ def test_create_default_config_detect_location(self, mock_print): ha_conf = config[DOMAIN] expected_values = { - CONF_LATITUDE: 2.0, - CONF_LONGITUDE: 1.0, + CONF_LATITUDE: 32.8594, + CONF_LONGITUDE: -117.2073, + CONF_ELEVATION: 101, CONF_TEMPERATURE_UNIT: 'F', CONF_NAME: 'Home', CONF_TIME_ZONE: 'America/Los_Angeles' } - self.assertEqual(expected_values, ha_conf) - self.assertTrue(mock_print.called) + assert expected_values == ha_conf + assert mock_print.called @mock.patch('builtins.print') def test_create_default_config_returns_none_if_write_error(self, @@ -166,3 +185,127 @@ def test_core_config_schema(self): }, }, }) + + def test_entity_customization(self): + """Test entity customization through configuration.""" + self.hass = get_test_home_assistant() + + config = {CONF_LATITUDE: 50, + CONF_LONGITUDE: 50, + CONF_NAME: 'Test', + CONF_CUSTOMIZE: {'test.test': {'hidden': True}}} + + config_util.process_ha_core_config(self.hass, config) + + entity = Entity() + entity.entity_id = 'test.test' + entity.hass = self.hass + entity.update_ha_state() + + state = self.hass.states.get('test.test') + + assert state.attributes['hidden'] + + def test_remove_lib_on_upgrade(self): + """Test removal of library on upgrade.""" + with tempfile.TemporaryDirectory() as config_dir: + version_path = os.path.join(config_dir, '.HA_VERSION') + lib_dir = os.path.join(config_dir, 'deps') + check_file = os.path.join(lib_dir, 'check') + + with open(version_path, 'wt') as outp: + outp.write('0.7.0') + + os.mkdir(lib_dir) + + with open(check_file, 'w'): + pass + + self.hass = get_test_home_assistant() + self.hass.config.config_dir = config_dir + + assert os.path.isfile(check_file) + config_util.process_ha_config_upgrade(self.hass) + assert not os.path.isfile(check_file) + + def test_not_remove_lib_if_not_upgrade(self): + """Test removal of library with no upgrade.""" + with tempfile.TemporaryDirectory() as config_dir: + version_path = os.path.join(config_dir, '.HA_VERSION') + lib_dir = os.path.join(config_dir, 'deps') + check_file = os.path.join(lib_dir, 'check') + + with open(version_path, 'wt') as outp: + outp.write(__version__) + + os.mkdir(lib_dir) + + with open(check_file, 'w'): + pass + + self.hass = get_test_home_assistant() + self.hass.config.config_dir = config_dir + + config_util.process_ha_config_upgrade(self.hass) + + assert os.path.isfile(check_file) + + def test_loading_configuration(self): + """Test loading core config onto hass object.""" + config = Config() + hass = mock.Mock(config=config) + + config_util.process_ha_core_config(hass, { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + 'temperature_unit': 'F', + 'time_zone': 'America/New_York', + }) + + assert config.latitude == 60 + assert config.longitude == 50 + assert config.elevation == 25 + assert config.location_name == 'Huis' + assert config.temperature_unit == TEMP_FAHRENHEIT + assert config.time_zone.zone == 'America/New_York' + + @mock.patch('homeassistant.util.location.detect_location_info', + return_value=location_util.LocationInfo( + '0.0.0.0', 'US', 'United States', 'CA', 'California', + 'San Diego', '92122', 'America/Los_Angeles', 32.8594, + -117.2073, True)) + @mock.patch('homeassistant.util.location.elevation', return_value=101) + def test_discovering_configuration(self, mock_detect, mock_elevation): + """Test auto discovery for missing core configs.""" + config = Config() + hass = mock.Mock(config=config) + + config_util.process_ha_core_config(hass, {}) + + assert config.latitude == 32.8594 + assert config.longitude == -117.2073 + assert config.elevation == 101 + assert config.location_name == 'San Diego' + assert config.temperature_unit == TEMP_FAHRENHEIT + assert config.time_zone.zone == 'America/Los_Angeles' + + @mock.patch('homeassistant.util.location.detect_location_info', + return_value=None) + @mock.patch('homeassistant.util.location.elevation', return_value=0) + def test_discovering_configuration_auto_detect_fails(self, mock_detect, + mock_elevation): + """Test config remains unchanged if discovery fails.""" + config = Config() + hass = mock.Mock(config=config) + + config_util.process_ha_core_config(hass, {}) + + blankConfig = Config() + assert config.latitude == blankConfig.latitude + assert config.longitude == blankConfig.longitude + assert config.elevation == blankConfig.elevation + assert config.location_name == blankConfig.location_name + assert config.temperature_unit == blankConfig.temperature_unit + assert config.time_zone == blankConfig.time_zone diff --git a/tests/test_core.py b/tests/test_core.py index 4930bcef6ed1b9..cb698cdc53cd7b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -334,6 +334,20 @@ def test_last_changed_not_updated_on_same_state(self): self.assertEqual(state.last_changed, self.states.get('light.Bowl').last_changed) + def test_force_update(self): + """Test force update option.""" + self.pool.add_worker() + events = [] + self.bus.listen(EVENT_STATE_CHANGED, events.append) + + self.states.set('light.bowl', 'on') + self.bus._pool.block_till_done() + self.assertEqual(0, len(events)) + + self.states.set('light.bowl', 'on', None, True) + self.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + class TestServiceCall(unittest.TestCase): """Test ServiceCall class.""" diff --git a/tests/test_remote.py b/tests/test_remote.py index 58b2f9b359d9d7..f3ec35daee5ab7 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,14 +1,13 @@ """Test Home Assistant remote methods and classes.""" # pylint: disable=protected-access,too-many-public-methods +import time import unittest -import eventlet - import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote import homeassistant.components.http as http -from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.const import HTTP_HEADER_HA_AUTH, EVENT_STATE_CHANGED import homeassistant.util.dt as dt_util from tests.common import get_test_instance_port, get_test_home_assistant @@ -47,10 +46,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) master_api = remote.API("127.0.0.1", API_PASSWORD, MASTER_PORT) @@ -63,10 +59,6 @@ def setUpModule(): # pylint: disable=invalid-name slave.start() - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) - def tearDownModule(): # pylint: disable=invalid-name """Stop the Home Assistant server and slave.""" @@ -155,6 +147,21 @@ def test_set_state(self): self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test')) + def test_set_state_with_push(self): + """TestPython API set_state with push option.""" + events = [] + hass.bus.listen(EVENT_STATE_CHANGED, events.append) + + remote.set_state(master_api, 'test.test', 'set_test_2') + remote.set_state(master_api, 'test.test', 'set_test_2') + hass.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + + remote.set_state( + master_api, 'test.test', 'set_test_2', force_update=True) + hass.bus._pool.block_till_done() + self.assertEqual(2, len(events)) + def test_is_state(self): """Test Python API is_state.""" self.assertTrue( @@ -242,7 +249,6 @@ def test_statemachine_set(self): slave.pool.block_till_done() # Wait till master gives updated state hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertEqual("remote.statemachine test", slave.states.get("remote.test").state) @@ -251,13 +257,11 @@ def test_statemachine_remove_from_master(self): """Remove statemachine from master.""" hass.states.set("remote.master_remove", "remove me!") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertIn('remote.master_remove', slave.states.entity_ids()) hass.states.remove("remote.master_remove") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertNotIn('remote.master_remove', slave.states.entity_ids()) @@ -265,14 +269,12 @@ def test_statemachine_remove_from_slave(self): """Remove statemachine from slave.""" hass.states.set("remote.slave_remove", "remove me!") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertIn('remote.slave_remove', slave.states.entity_ids()) self.assertTrue(slave.states.remove("remote.slave_remove")) slave.pool.block_till_done() hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertNotIn('remote.slave_remove', slave.states.entity_ids()) @@ -291,6 +293,5 @@ def listener(event): slave.pool.block_till_done() # Wait till master gives updated event hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertEqual(1, len(test_value)) diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 7d0052fe62c2a3..1dfb71a87bf5a2 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,9 +1,15 @@ """Test Home Assistant location util methods.""" # pylint: disable=too-many-public-methods -import unittest +from unittest import TestCase +from unittest.mock import patch + +import requests +import requests_mock import homeassistant.util.location as location_util +from tests.common import load_fixture + # Paris COORDINATES_PARIS = (48.864716, 2.349014) # New York @@ -20,26 +26,124 @@ DISTANCE_MILES = 3632.78 -class TestLocationUtil(unittest.TestCase): +class TestLocationUtil(TestCase): """Test util location methods.""" + def test_get_distance_to_same_place(self): + """Test getting the distance.""" + meters = location_util.distance(COORDINATES_PARIS[0], + COORDINATES_PARIS[1], + COORDINATES_PARIS[0], + COORDINATES_PARIS[1]) + + assert meters == 0 + def test_get_distance(self): """Test getting the distance.""" meters = location_util.distance(COORDINATES_PARIS[0], COORDINATES_PARIS[1], COORDINATES_NEW_YORK[0], COORDINATES_NEW_YORK[1]) - self.assertAlmostEqual(meters / 1000, DISTANCE_KM, places=2) + + assert meters/1000 - DISTANCE_KM < 0.01 def test_get_kilometers(self): """Test getting the distance between given coordinates in km.""" kilometers = location_util.vincenty(COORDINATES_PARIS, COORDINATES_NEW_YORK) - self.assertEqual(round(kilometers, 2), DISTANCE_KM) + assert round(kilometers, 2) == DISTANCE_KM def test_get_miles(self): """Test getting the distance between given coordinates in miles.""" miles = location_util.vincenty(COORDINATES_PARIS, COORDINATES_NEW_YORK, miles=True) - self.assertEqual(round(miles, 2), DISTANCE_MILES) + assert round(miles, 2) == DISTANCE_MILES + + @requests_mock.Mocker() + def test_detect_location_info_freegeoip(self, m): + """Test detect location info using freegeoip.""" + m.get(location_util.FREEGEO_API, + text=load_fixture('freegeoip.io.json')) + + info = location_util.detect_location_info(_test_real=True) + + assert info is not None + assert info.ip == '1.2.3.4' + assert info.country_code == 'US' + assert info.country_name == 'United States' + assert info.region_code == 'CA' + assert info.region_name == 'California' + assert info.city == 'San Diego' + assert info.zip_code == '92122' + assert info.time_zone == 'America/Los_Angeles' + assert info.latitude == 32.8594 + assert info.longitude == -117.2073 + assert info.use_fahrenheit + + @requests_mock.Mocker() + @patch('homeassistant.util.location._get_freegeoip', return_value=None) + def test_detect_location_info_ipapi(self, mock_req, mock_freegeoip): + """Test detect location info using freegeoip.""" + mock_req.get(location_util.IP_API, + text=load_fixture('ip-api.com.json')) + + info = location_util.detect_location_info(_test_real=True) + + assert info is not None + assert info.ip == '1.2.3.4' + assert info.country_code == 'US' + assert info.country_name == 'United States' + assert info.region_code == 'CA' + assert info.region_name == 'California' + assert info.city == 'San Diego' + assert info.zip_code == '92122' + assert info.time_zone == 'America/Los_Angeles' + assert info.latitude == 32.8594 + assert info.longitude == -117.2073 + assert info.use_fahrenheit + + @patch('homeassistant.util.location.elevation', return_value=0) + @patch('homeassistant.util.location._get_freegeoip', return_value=None) + @patch('homeassistant.util.location._get_ip_api', return_value=None) + def test_detect_location_info_both_queries_fail(self, mock_ipapi, + mock_freegeoip, + mock_elevation): + """Ensure we return None if both queries fail.""" + info = location_util.detect_location_info(_test_real=True) + assert info is None + + @patch('homeassistant.util.location.requests.get', + side_effect=requests.RequestException) + def test_freegeoip_query_raises(self, mock_get): + """Test freegeoip query when the request to API fails.""" + info = location_util._get_freegeoip() + assert info is None + + @patch('homeassistant.util.location.requests.get', + side_effect=requests.RequestException) + def test_ip_api_query_raises(self, mock_get): + """Test ip api query when the request to API fails.""" + info = location_util._get_ip_api() + assert info is None + + @patch('homeassistant.util.location.requests.get', + side_effect=requests.RequestException) + def test_elevation_query_raises(self, mock_get): + """Test elevation when the request to API fails.""" + elevation = location_util.elevation(10, 10, _test_real=True) + assert elevation == 0 + + @requests_mock.Mocker() + def test_elevation_query_fails(self, mock_req): + """Test elevation when the request to API fails.""" + mock_req.get(location_util.ELEVATION_URL, text='{}', status_code=401) + elevation = location_util.elevation(10, 10, _test_real=True) + assert elevation == 0 + + @requests_mock.Mocker() + def test_elevation_query_nonjson(self, mock_req): + """Test if elevation API returns a non JSON value.""" + mock_req.get(location_util.ELEVATION_URL, text='{ I am not JSON }') + elevation = location_util.elevation(10, 10, _test_real=True) + assert elevation == 0 diff --git a/tests/util/test_package.py b/tests/util/test_package.py index ea9a8f23dfae71..a4e0019695902e 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -49,7 +49,7 @@ def test_install_package_zip(self): self.assertTrue(package.check_package_exists( TEST_NEW_REQ, self.lib_dir)) - bootstrap.mount_local_lib_path(self.tmp_dir.name) + bootstrap._mount_local_lib_path(self.tmp_dir.name) try: import pyhelloworld3 diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 244f932333475a..7bede7edca967d 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -3,8 +3,9 @@ import unittest import os import tempfile - from homeassistant.util import yaml +import homeassistant.config as config_util +from tests.common import get_test_config_dir class TestYaml(unittest.TestCase): @@ -135,3 +136,81 @@ def test_include_dir_merge_named(self): "key2": "two", "key3": "three" } + + +def load_yaml(fname, string): + """Write a string to file and return the parsed yaml.""" + with open(fname, 'w') as file: + file.write(string) + return config_util.load_yaml_config_file(fname) + + +class FakeKeyring(): + """Fake a keyring class.""" + + def __init__(self, secrets_dict): + """Store keyring dictionary.""" + self._secrets = secrets_dict + + # pylint: disable=protected-access + def get_password(self, domain, name): + """Retrieve password.""" + assert domain == yaml._SECRET_NAMESPACE + return self._secrets.get(name) + + +class TestSecrets(unittest.TestCase): + """Test the secrets parameter in the yaml utility.""" + + def setUp(self): # pylint: disable=invalid-name + """Create & load secrets file.""" + config_dir = get_test_config_dir() + self._yaml_path = os.path.join(config_dir, + config_util.YAML_CONFIG_FILE) + self._secret_path = os.path.join(config_dir, 'secrets.yaml') + + load_yaml(self._secret_path, + 'http_pw: pwhttp\n' + 'comp1_un: un1\n' + 'comp1_pw: pw1\n' + 'stale_pw: not_used\n' + 'logger: debug\n') + self._yaml = load_yaml(self._yaml_path, + 'http:\n' + ' api_password: !secret http_pw\n' + 'component:\n' + ' username: !secret comp1_un\n' + ' password: !secret comp1_pw\n' + '') + + def tearDown(self): # pylint: disable=invalid-name + """Clean up secrets.""" + for path in [self._yaml_path, self._secret_path]: + if os.path.isfile(path): + os.remove(path) + + def test_secrets_from_yaml(self): + """Did secrets load ok.""" + expected = {'api_password': 'pwhttp'} + self.assertEqual(expected, self._yaml['http']) + + expected = { + 'username': 'un1', + 'password': 'pw1'} + self.assertEqual(expected, self._yaml['component']) + + def test_secrets_keyring(self): + """Test keyring fallback & get_password.""" + yaml.keyring = None # Ensure its not there + yaml_str = 'http:\n api_password: !secret http_pw_keyring' + with self.assertRaises(yaml.HomeAssistantError): + load_yaml(self._yaml_path, yaml_str) + + yaml.keyring = FakeKeyring({'http_pw_keyring': 'yeah'}) + _yaml = load_yaml(self._yaml_path, yaml_str) + self.assertEqual({'http': {'api_password': 'yeah'}}, _yaml) + + def test_secrets_logger_removed(self): + """Ensure logger: debug was removed.""" + with self.assertRaises(yaml.HomeAssistantError): + load_yaml(self._yaml_path, 'api_password: !secret logger')