diff --git a/.coveragerc b/.coveragerc index ff540ca1f2e..8f12390556c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -93,19 +93,18 @@ omit = homeassistant/components/*/pilight.py homeassistant/components/knx.py - homeassistant/components/switch/knx.py - homeassistant/components/binary_sensor/knx.py - homeassistant/components/thermostat/knx.py + homeassistant/components/*/knx.py + + homeassistant/components/ffmpeg.py + homeassistant/components/*/ffmpeg.py homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/binary_sensor/arest.py - homeassistant/components/binary_sensor/ffmpeg.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py homeassistant/components/camera/bloomsky.py - homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py homeassistant/components/camera/rpi_camera.py @@ -147,6 +146,7 @@ omit = homeassistant/components/ifttt.py homeassistant/components/joaoapps_join.py homeassistant/components/keyboard.py + homeassistant/components/keyboard_remote.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/flux_led.py homeassistant/components/light/hue.py @@ -188,6 +188,7 @@ omit = homeassistant/components/notify/group.py homeassistant/components/notify/instapush.py homeassistant/components/notify/joaoapps_join.py + homeassistant/components/notify/kodi.py homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/message_bird.py homeassistant/components/notify/nma.py @@ -196,6 +197,7 @@ omit = homeassistant/components/notify/pushover.py homeassistant/components/notify/rest.py homeassistant/components/notify/sendgrid.py + homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py @@ -203,9 +205,12 @@ omit = homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py + homeassistant/components/nuimo_controller.py + homeassistant/components/openalpr.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py + homeassistant/components/sensor/bom.py homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/deutsche_bahn.py @@ -213,6 +218,7 @@ omit = homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/efergy.py homeassistant/components/sensor/eliqonline.py + homeassistant/components/sensor/emoncms.py homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fixer.py @@ -224,10 +230,12 @@ omit = homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/imap.py + homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/mhz19.py + homeassistant/components/sensor/miflora.py homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/nzbget.py @@ -255,6 +263,7 @@ omit = homeassistant/components/sensor/uber.py homeassistant/components/sensor/worldclock.py homeassistant/components/sensor/xbox_live.py + homeassistant/components/sensor/yahoo_finance.py homeassistant/components/sensor/yweather.py homeassistant/components/switch/acer_projector.py homeassistant/components/switch/arest.py diff --git a/.gitignore b/.gitignore index 147d68c36d3..43eae33f554 100644 --- a/.gitignore +++ b/.gitignore @@ -99,4 +99,7 @@ virtualization/vagrant/config .vscode # Built docs -docs/build \ No newline at end of file +docs/build + +# Windows Explorer +desktop.ini diff --git a/Dockerfile b/Dockerfile index 22c0c13ddf6..14e70a0412c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.4 +FROM python:3.5 MAINTAINER Paulus Schoutsen VOLUME /config @@ -10,7 +10,7 @@ RUN pip3 install --no-cache-dir colorlog cython # For the nmap tracker, bluetooth tracker, Z-Wave RUN apt-get update && \ - apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev && \ + apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* COPY script/build_python_openzwave script/build_python_openzwave @@ -21,7 +21,7 @@ RUN script/build_python_openzwave && \ COPY requirements_all.txt requirements_all.txt # certifi breaks Debian based installs RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \ - pip3 install mysqlclient psycopg2 + pip3 install mysqlclient psycopg2 uvloop # Copy source COPY . . diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 39a18feb1f2..a7b2027963f 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -16,16 +16,14 @@ from homeassistant.const import ( REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, ) +from homeassistant.util.async import run_callback_threadsafe def validate_python() -> None: """Validate we're running the right Python version.""" - major, minor = sys.version_info[:2] - req_major, req_minor = REQUIRED_PYTHON_VER - - if major < req_major or (major == req_major and minor < req_minor): - print("Home Assistant requires at least Python {}.{}".format( - req_major, req_minor)) + if sys.version_info[:3] < REQUIRED_PYTHON_VER: + print("Home Assistant requires at least Python {}.{}.{}".format( + *REQUIRED_PYTHON_VER)) sys.exit(1) @@ -256,12 +254,14 @@ def setup_and_run_hass(config_dir: str, import webbrowser webbrowser.open(hass.config.api.base_url) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser) + run_callback_threadsafe( + hass.loop, + hass.bus.async_listen_once, + EVENT_HOMEASSISTANT_START, open_browser + ) hass.start() - exit_code = int(hass.block_till_stopped()) - - return exit_code + return hass.exit_code def try_to_restart() -> None: diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 5e291e90717..21c56f55bad 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -118,11 +118,13 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool: # Assumption: if a component does not depend on groups # it communicates with devices - if 'group' not in getattr(component, 'DEPENDENCIES', []): + if 'group' not in getattr(component, 'DEPENDENCIES', []) and \ + hass.pool.worker_count <= 10: hass.pool.add_worker() hass.bus.fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}) + EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} + ) return True @@ -144,7 +146,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, if hasattr(component, 'CONFIG_SCHEMA'): try: config = component.CONFIG_SCHEMA(config) - except vol.MultipleInvalid as ex: + except vol.Invalid as ex: log_exception(ex, domain, config) return None @@ -154,8 +156,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, # Validate component specific platform schema try: p_validated = component.PLATFORM_SCHEMA(p_config) - except vol.MultipleInvalid as ex: - log_exception(ex, domain, p_config) + except vol.Invalid as ex: + log_exception(ex, domain, config) return None # Not all platform components follow same pattern for platforms @@ -175,7 +177,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, if hasattr(platform, 'PLATFORM_SCHEMA'): try: p_validated = platform.PLATFORM_SCHEMA(p_validated) - except vol.MultipleInvalid as ex: + except vol.Invalid as ex: log_exception(ex, '{}.{}'.format(domain, p_name), p_validated) return None @@ -278,23 +280,29 @@ def from_config_dict(config: Dict[str, Any], components = set(key.split(' ')[0] for key in config.keys() if key != core.DOMAIN) - if not core_components.setup(hass, config): - _LOGGER.error('Home Assistant core failed to initialize. ' - 'Further initialization aborted.') - return hass + # Setup in a thread to avoid blocking + def component_setup(): + """Set up a component.""" + 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) + persistent_notification.setup(hass, config) - _LOGGER.info('Home Assistant core initialized') + _LOGGER.info('Home Assistant core initialized') - # Give event decorators access to HASS - event_decorators.HASS = hass - service.HASS = hass + # Give event decorators access to HASS + event_decorators.HASS = hass + service.HASS = hass - # Setup the components - for domain in loader.load_order_components(components): - _setup_component(hass, domain, config) + # Setup the components + for domain in loader.load_order_components(components): + _setup_component(hass, domain, config) + hass.loop.run_until_complete( + hass.loop.run_in_executor(None, component_setup) + ) return hass @@ -390,16 +398,21 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None: def log_exception(ex, domain, config): """Generate log exception for config validation.""" message = 'Invalid config for [{}]: '.format(domain) + if 'extra keys not allowed' in ex.error_message: message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\ .format(ex.path[-1], domain, domain, '->'.join('%s' % m for m in ex.path)) else: - message += humanize_error(config, ex) + message += '{}.'.format(humanize_error(config, ex)) if hasattr(config, '__line__'): - message += " (See {}:{})".format(config.__config_file__, - config.__line__ or '?') + message += " (See {}:{})".format( + config.__config_file__, config.__line__ or '?') + + if domain != 'homeassistant': + message += (' Please check the docs at ' + 'https://home-assistant.io/components/{}/'.format(domain)) _LOGGER.error(message) diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index ebd54da1558..ff1ec2cc7b7 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -10,6 +10,7 @@ from homeassistant.components.envisalink import (EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, + CONF_PANIC, CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE) @@ -26,6 +27,7 @@ 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] + _panic_type = discovery_info[CONF_PANIC] for part_num in _configured_partitions: _device_config_data = PARTITION_SCHEMA( _configured_partitions[part_num]) @@ -33,6 +35,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): part_num, _device_config_data[CONF_PARTITIONNAME], _code, + _panic_type, EVL_CONTROLLER.alarm_state['partition'][part_num], EVL_CONTROLLER) add_devices_callback([_device]) @@ -44,11 +47,13 @@ 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): + def __init__(self, partition_number, alarm_name, + code, panic_type, info, controller): """Initialize the alarm panel.""" from pydispatch import dispatcher self._partition_number = partition_number self._code = code + self._panic_type = panic_type _LOGGER.debug('Setting up alarm: ' + alarm_name) EnvisalinkDevice.__init__(self, alarm_name, info, controller) dispatcher.connect(self._update_callback, @@ -61,7 +66,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): def _update_callback(self, partition): """Update HA state, if needed.""" if partition is None or int(partition) == self._partition_number: - self.update_ha_state() + self.hass.async_add_job(self.update_ha_state) @property def code_format(self): @@ -101,5 +106,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): self._partition_number) def alarm_trigger(self, code=None): - """Alarm trigger command. Not possible for us.""" - raise NotImplementedError() + """Alarm trigger command. Will be used to trigger a panic alarm.""" + if self._code: + EVL_CONTROLLER.panic_alarm(self._panic_type) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index a95eff20e1f..986f1966797 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -28,7 +28,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_DISARM_AFTER_TRIGGER, diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 969c20583ee..e1b860e95c3 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -4,11 +4,14 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ +import copy import enum import logging +import voluptuous as vol + from homeassistant.const import HTTP_BAD_REQUEST -from homeassistant.helpers import template, script +from homeassistant.helpers import template, script, config_validation as cv from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) @@ -20,10 +23,49 @@ CONF_CARD = 'card' CONF_INTENTS = 'intents' CONF_SPEECH = 'speech' +CONF_TYPE = 'type' +CONF_TITLE = 'title' +CONF_CONTENT = 'content' +CONF_TEXT = 'text' + DOMAIN = 'alexa' DEPENDENCIES = ['http'] +class SpeechType(enum.Enum): + """The Alexa speech types.""" + + plaintext = "PlainText" + ssml = "SSML" + + +class CardType(enum.Enum): + """The Alexa card types.""" + + simple = "Simple" + link_account = "LinkAccount" + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: { + CONF_INTENTS: { + cv.string: { + vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_CARD): { + vol.Required(CONF_TYPE): cv.enum(CardType), + vol.Required(CONF_TITLE): cv.template, + vol.Required(CONF_CONTENT): cv.template, + }, + vol.Optional(CONF_SPEECH): { + vol.Required(CONF_TYPE): cv.enum(SpeechType), + vol.Required(CONF_TEXT): cv.template, + } + } + } + } +}) + + def setup(hass, config): """Activate Alexa component.""" hass.wsgi.register_view(AlexaView(hass, @@ -42,6 +84,9 @@ class AlexaView(HomeAssistantView): """Initialize Alexa view.""" super().__init__(hass) + intents = copy.deepcopy(intents) + template.attach(hass, intents) + for name, intent in intents.items(): if CONF_ACTION in intent: intent[CONF_ACTION] = script.Script( @@ -101,29 +146,15 @@ class AlexaView(HomeAssistantView): # pylint: disable=unsubscriptable-object if speech is not None: - response.add_speech(SpeechType[speech['type']], speech['text']) + response.add_speech(speech[CONF_TYPE], speech[CONF_TEXT]) if card is not None: - response.add_card(CardType[card['type']], card['title'], - card['content']) + response.add_card(card[CONF_TYPE], card[CONF_TITLE], + card[CONF_CONTENT]) return self.json(response) -class SpeechType(enum.Enum): - """The Alexa speech types.""" - - plaintext = "PlainText" - ssml = "SSML" - - -class CardType(enum.Enum): - """The Alexa card types.""" - - simple = "Simple" - link_account = "LinkAccount" - - class AlexaResponse(object): """Help generating the response for Alexa.""" @@ -153,8 +184,8 @@ class AlexaResponse(object): self.card = card return - card["title"] = self._render(title), - card["content"] = self._render(content) + card["title"] = title.render(self.variables) + card["content"] = content.render(self.variables) self.card = card def add_speech(self, speech_type, text): @@ -163,9 +194,12 @@ class AlexaResponse(object): key = 'ssml' if speech_type == SpeechType.ssml else 'text' + if isinstance(text, template.Template): + text = text.render(self.variables) + self.speech = { 'type': speech_type.value, - key: self._render(text) + key: text } def add_reprompt(self, speech_type, text): @@ -176,7 +210,7 @@ class AlexaResponse(object): self.reprompt = { 'type': speech_type.value, - key: self._render(text) + key: text.render(self.variables) } def as_dict(self): @@ -201,7 +235,3 @@ class AlexaResponse(object): 'sessionAttributes': self.session_attributes, 'response': response, } - - def _render(self, template_string): - """Render a response, adding data from intent if available.""" - return template.render(self.hass, template_string, self.variables) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index be455995743..5eb28c53a34 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -4,6 +4,7 @@ Rest API for Home Assistant. For more details about the RESTful API, please refer to the documentation at https://home-assistant.io/developers/api/ """ +import asyncio import json import logging import queue @@ -79,6 +80,7 @@ class APIEventStream(HomeAssistantView): if restrict: restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] + @asyncio.coroutine def forward_events(event): """Forward events to the open request.""" if event.event_type == EVENT_TIME_CHANGED: @@ -376,8 +378,8 @@ class APITemplateView(HomeAssistantView): def post(self, request): """Render a template.""" try: - return template.render(self.hass, request.json['template'], - request.json.get('variables')) + tpl = template.Template(request.json['template'], self.hass) + return tpl.render(request.json.get('variables')) except TemplateError as ex: return self.json_message('Error rendering template: {}'.format(ex), HTTP_BAD_REQUEST) diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index 73bd7a51dad..239c80523df 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -13,7 +13,7 @@ from homeassistant.const import ( from homeassistant.const import CONF_PORT import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['PyMata==2.12'] +REQUIREMENTS = ['PyMata==2.13'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 863d94033a8..b65516130d9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -30,6 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' DEPENDENCIES = ['group'] CONF_ALIAS = 'alias' +CONF_HIDE_ENTITY = 'hide_entity' CONF_CONDITION = 'condition' CONF_ACTION = 'action' @@ -41,6 +42,7 @@ CONDITION_TYPE_AND = 'and' CONDITION_TYPE_OR = 'or' DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND +DEFAULT_HIDE_ENTITY = False METHOD_TRIGGER = 'trigger' METHOD_IF_ACTION = 'if_action' @@ -99,6 +101,7 @@ _CONDITION_SCHEMA = vol.Any( PLATFORM_SCHEMA = vol.Schema({ CONF_ALIAS: cv.string, + vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE): vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)), @@ -206,7 +209,8 @@ def setup(hass, config): class AutomationEntity(ToggleEntity): """Entity to show status of entity.""" - def __init__(self, name, attach_triggers, cond_func, action): + # pylint: disable=too-many-arguments, too-many-instance-attributes + def __init__(self, name, attach_triggers, cond_func, action, hidden): """Initialize an automation entity.""" self._name = name self._attach_triggers = attach_triggers @@ -215,6 +219,7 @@ class AutomationEntity(ToggleEntity): self._action = action self._enabled = True self._last_triggered = None + self._hidden = hidden @property def name(self): @@ -233,6 +238,11 @@ class AutomationEntity(ToggleEntity): ATTR_LAST_TRIGGERED: self._last_triggered } + @property + def hidden(self) -> bool: + """Return True if the automation entity should be hidden from UIs.""" + return self._hidden + @property def is_on(self) -> bool: """Return True if entity is on.""" @@ -281,6 +291,8 @@ def _process_config(hass, config, component): name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no) + hidden = config_block[CONF_HIDE_ENTITY] + action = _get_action(hass, config_block.get(CONF_ACTION, {}), name) if CONF_CONDITION in config_block: @@ -295,7 +307,8 @@ def _process_config(hass, config, component): attach_triggers = partial(_process_trigger, hass, config, config_block.get(CONF_TRIGGER, []), name) - entity = AutomationEntity(name, attach_triggers, cond_func, action) + entity = AutomationEntity(name, attach_triggers, cond_func, action, + hidden) component.add_entities((entity,)) success = True diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 795dd94a71f..7b77dd04627 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -4,6 +4,7 @@ Offer event listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#event-trigger """ +import asyncio import logging import voluptuous as vol @@ -28,11 +29,12 @@ def trigger(hass, config, action): event_type = config.get(CONF_EVENT_TYPE) event_data = config.get(CONF_EVENT_DATA) + @asyncio.coroutine def handle_event(event): """Listen for events and calls the action when data matches.""" if not event_data or all(val == event.data.get(key) for key, val in event_data.items()): - action({ + hass.async_add_job(action, { 'trigger': { 'platform': 'event', 'event': event, diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 608063b4708..168ca05b62b 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -31,6 +31,8 @@ def trigger(hass, config, action): below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass # pylint: disable=unused-argument def state_automation_listener(entity, from_s, to_s): diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 0891590a539..1ca0c679424 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -4,12 +4,12 @@ Offer template automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#template-trigger """ +import asyncio import logging import voluptuous as vol -from homeassistant.const import ( - CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL) +from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM from homeassistant.helpers import condition from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv @@ -26,19 +26,21 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ def trigger(hass, config, action): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) + value_template.hass = hass # Local variable to keep track of if the action has already been triggered already_triggered = False + @asyncio.coroutine def state_changed_listener(entity_id, from_s, to_s): """Listen for state changes and calls action.""" nonlocal already_triggered - template_result = condition.template(hass, value_template) + template_result = condition.async_template(hass, value_template) # Check to see if template returns true if template_result and not already_triggered: already_triggered = True - action({ + hass.async_add_job(action, { 'trigger': { 'platform': 'template', 'entity_id': entity_id, @@ -49,4 +51,5 @@ def trigger(hass, config, action): elif not template_result: already_triggered = False - return track_state_change(hass, MATCH_ALL, state_changed_listener) + return track_state_change(hass, value_template.extract_entities(), + state_changed_listener) diff --git a/homeassistant/components/binary_sensor/arest.py b/homeassistant/components/binary_sensor/arest.py index 899dd44a42b..56362611514 100644 --- a/homeassistant/components/binary_sensor/arest.py +++ b/homeassistant/components/binary_sensor/arest.py @@ -9,18 +9,15 @@ from datetime import timedelta import requests -from homeassistant.components.binary_sensor import (BinarySensorDevice, - SENSOR_CLASSES) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, SENSOR_CLASSES) +from homeassistant.const import CONF_RESOURCE, CONF_PIN from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -# Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) -CONF_RESOURCE = 'resource' -CONF_PIN = 'pin' - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the aREST binary sensor.""" diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index f56f9cb7a39..57b3c7c03f7 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor.command_line import CommandSensorData from homeassistant.const import ( CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_SENSOR_CLASS, CONF_COMMAND) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +44,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): payload_on = config.get(CONF_PAYLOAD_ON) sensor_class = config.get(CONF_SENSOR_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) - + if value_template is not None: + value_template.hass = hass data = CommandSensorData(command) add_devices([CommandBinarySensor( @@ -91,8 +91,8 @@ class CommandBinarySensor(BinarySensorDevice): value = self.data.value if self._value_template is not None: - value = template.render_with_possible_json_value( - self._hass, self._value_template, value, False) + value = self._value_template.render_with_possible_json_value( + value, False) if value == self._payload_on: self._state = True elif value == self._payload_off: diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index 6ed257bd809..b0dad12522b 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -68,4 +68,4 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): 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() + self.hass.async_add_job(self.update_ha_state) diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index 9c37ff7744c..ce89ae2e4db 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -10,13 +10,17 @@ from os import path import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import (BinarySensorDevice, - PLATFORM_SCHEMA, DOMAIN) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN) +from homeassistant.components.ffmpeg import ( + get_binary, run_test, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS) from homeassistant.config import load_yaml_config_file from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME, ATTR_ENTITY_ID) -REQUIREMENTS = ["ha-ffmpeg==0.10"] +DEPENDENCIES = ['ffmpeg'] + +_LOGGER = logging.getLogger(__name__) SERVICE_RESTART = 'ffmpeg_restart' @@ -29,10 +33,6 @@ MAP_FFMPEG_BIN = [ ] CONF_TOOL = 'tool' -CONF_INPUT = 'input' -CONF_FFMPEG_BIN = 'ffmpeg_bin' -CONF_EXTRA_ARGUMENTS = 'extra_arguments' -CONF_OUTPUT = 'output' CONF_PEAK = 'peak' CONF_DURATION = 'duration' CONF_RESET = 'reset' @@ -40,11 +40,12 @@ CONF_CHANGES = 'changes' CONF_REPEAT = 'repeat' CONF_REPEAT_TIME = 'repeat_time' +DEFAULT_NAME = 'FFmpeg' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN), vol.Required(CONF_INPUT): cv.string, - vol.Optional(CONF_FFMPEG_BIN, default="ffmpeg"): cv.string, - vol.Optional(CONF_NAME, default="FFmpeg"): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, vol.Optional(CONF_OUTPUT): cv.string, vol.Optional(CONF_PEAK, default=-30): vol.Coerce(int), @@ -65,16 +66,25 @@ SERVICE_RESTART_SCHEMA = vol.Schema({ }) +def restart(hass, entity_id=None): + """Restart a ffmpeg process on entity.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_RESTART, data) + + # list of all ffmpeg sensors DEVICES = [] -_LOGGER = logging.getLogger(__name__) - def setup_platform(hass, config, add_entities, discovery_info=None): """Create the binary sensor.""" from haffmpeg import SensorNoise, SensorMotion + # check source + if not run_test(config.get(CONF_INPUT)): + return + + # generate sensor object if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE: entity = FFmpegNoise(SensorNoise, config) else: @@ -88,7 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # exists service? if hass.services.has_service(DOMAIN, SERVICE_RESTART): - return True + return descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) @@ -105,13 +115,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _devices = DEVICES for device in _devices: - device.reset_ffmpeg() + device.restart_ffmpeg() hass.services.register(DOMAIN, SERVICE_RESTART, _service_handle_restart, descriptions.get(SERVICE_RESTART), schema=SERVICE_RESTART_SCHEMA) - return True class FFmpegBinarySensor(BinarySensorDevice): @@ -122,7 +131,7 @@ class FFmpegBinarySensor(BinarySensorDevice): self._state = False self._config = config self._name = config.get(CONF_NAME) - self._ffmpeg = ffobj(config.get(CONF_FFMPEG_BIN), self._callback) + self._ffmpeg = ffobj(get_binary(), self._callback) self._start_ffmpeg(config) @@ -139,7 +148,7 @@ class FFmpegBinarySensor(BinarySensorDevice): """For STOP event to shutdown ffmpeg.""" self._ffmpeg.close() - def reset_ffmpeg(self): + def restart_ffmpeg(self): """Restart ffmpeg with new config.""" self._ffmpeg.close() self._start_ffmpeg(self._config) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py new file mode 100644 index 00000000000..8f4cb1637b4 --- /dev/null +++ b/homeassistant/components/binary_sensor/isy994.py @@ -0,0 +1,71 @@ +""" +Support for ISY994 binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.isy994/ +""" +import logging +from typing import Callable # noqa + +from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN +import homeassistant.components.isy994 as isy +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.helpers.typing import ConfigType + + +_LOGGER = logging.getLogger(__name__) + +VALUE_TO_STATE = { + False: STATE_OFF, + True: STATE_ON, +} + +UOM = ['2', '78'] +STATES = [STATE_OFF, STATE_ON, 'true', 'false'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config: ConfigType, + add_devices: Callable[[list], None], discovery_info=None): + """Setup the ISY994 binary sensor platform.""" + if isy.ISY is None or not isy.ISY.connected: + _LOGGER.error('A connection has not been made to the ISY controller.') + return False + + devices = [] + + for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM, + states=STATES): + devices.append(ISYBinarySensorDevice(node)) + + for program in isy.PROGRAMS.get(DOMAIN, []): + try: + status = program[isy.KEY_STATUS] + except (KeyError, AssertionError): + pass + else: + devices.append(ISYBinarySensorProgram(program.name, status)) + + add_devices(devices) + + +class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice): + """Representation of an ISY994 binary sensor device.""" + + def __init__(self, node) -> None: + """Initialize the ISY994 binary sensor device.""" + isy.ISYDevice.__init__(self, node) + + @property + def is_on(self) -> bool: + """Get whether the ISY994 binary sensor device is on.""" + return bool(self.value) + + +class ISYBinarySensorProgram(ISYBinarySensorDevice): + """Representation of an ISY994 binary sensor program.""" + + def __init__(self, name, node) -> None: + """Initialize the ISY994 binary sensor program.""" + ISYBinarySensorDevice.__init__(self, node) + self._name = name diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py index e5a36ceb867..304dad9d71b 100644 --- a/homeassistant/components/binary_sensor/knx.py +++ b/homeassistant/components/binary_sensor/knx.py @@ -5,17 +5,14 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.knx/ """ from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.knx import ( - KNXConfig, KNXGroupAddress) +from homeassistant.components.knx import (KNXConfig, KNXGroupAddress) -DEPENDENCIES = ["knx"] +DEPENDENCIES = ['knx'] -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the KNX binary sensor platform.""" - add_entities([ - KNXSwitch(hass, KNXConfig(config)) - ]) + add_devices([KNXSwitch(hass, KNXConfig(config))]) class KNXSwitch(KNXGroupAddress, BinarySensorDevice): diff --git a/homeassistant/components/binary_sensor/modbus.py b/homeassistant/components/binary_sensor/modbus.py new file mode 100644 index 00000000000..d43c348f116 --- /dev/null +++ b/homeassistant/components/binary_sensor/modbus.py @@ -0,0 +1,61 @@ +""" +Support for Modbus Coil sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.modbus/ +""" +import logging +import voluptuous as vol + +import homeassistant.components.modbus as modbus +from homeassistant.const import CONF_NAME +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.helpers import config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['modbus'] + +CONF_COIL = "coil" +CONF_COILS = "coils" +CONF_SLAVE = "slave" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COILS): [{ + vol.Required(CONF_COIL): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_SLAVE): cv.positive_int + }] +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Modbus binary sensors.""" + sensors = [] + for coil in config.get(CONF_COILS): + sensors.append(ModbusCoilSensor( + coil.get(CONF_NAME), + coil.get(CONF_SLAVE), + coil.get(CONF_COIL))) + add_devices(sensors) + + +class ModbusCoilSensor(BinarySensorDevice): + """Modbus coil sensor.""" + + def __init__(self, name, slave, coil): + """Initialize the modbus coil sensor.""" + self._name = name + self._slave = int(slave) if slave else None + self._coil = int(coil) + self._value = None + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._value + + def update(self): + """Update the state of the sensor.""" + result = modbus.HUB.read_coils(self._slave, self._coil, 1) + self._value = result.bits[0] diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index fd767bb1528..e26ee7d08dc 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_SENSOR_CLASS) from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,6 +36,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MQTT binary sensor.""" + value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass add_devices([MqttBinarySensor( hass, config.get(CONF_NAME), @@ -45,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get(CONF_QOS), config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_OFF), - config.get(CONF_VALUE_TEMPLATE) + value_template )]) @@ -68,8 +70,8 @@ class MqttBinarySensor(BinarySensorDevice): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) if payload == self._payload_on: self._state = True self.update_ha_state() diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 71666b91d06..b8be340d43c 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, CONF_SENSOR_CLASS, CONF_VERIFY_SSL) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL) sensor_class = config.get(CONF_SENSOR_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) - + if value_template is not None: + value_template.hass = hass rest = RestData(method, resource, payload, verify_ssl) rest.update() @@ -88,8 +88,8 @@ class RestBinarySensor(BinarySensorDevice): return False if self._value_template is not None: - response = template.render_with_possible_json_value( - self._hass, self._value_template, self.rest.data, False) + response = self._value_template.render_with_possible_json_value( + self.rest.data, False) try: return bool(int(response)) diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index a52b020dfe2..7bc05cd6764 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -6,16 +6,37 @@ https://home-assistant.io/components/binary_sensor.rpi_gpio/ """ import logging -import homeassistant.components.rpi_gpio as rpi_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import DEVICE_DEFAULT_NAME +import voluptuous as vol + +import homeassistant.components.rpi_gpio as rpi_gpio +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_BOUNCETIME = 'bouncetime' +CONF_INVERT_LOGIC = 'invert_logic' +CONF_PORTS = 'ports' +CONF_PULL_MODE = 'pull_mode' -DEFAULT_PULL_MODE = "UP" DEFAULT_BOUNCETIME = 50 DEFAULT_INVERT_LOGIC = False +DEFAULT_PULL_MODE = 'UP' DEPENDENCIES = ['rpi_gpio'] -_LOGGER = logging.getLogger(__name__) + +_SENSORS_SCHEMA = vol.Schema({ + cv.positive_int: cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_PORTS): _SENSORS_SCHEMA, + vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string, +}) # pylint: disable=unused-argument diff --git a/homeassistant/components/binary_sensor/sleepiq.py b/homeassistant/components/binary_sensor/sleepiq.py new file mode 100644 index 00000000000..08d575ebcb8 --- /dev/null +++ b/homeassistant/components/binary_sensor/sleepiq.py @@ -0,0 +1,53 @@ +""" +Support for SleepIQ sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.sleepiq/ +""" +from homeassistant.components import sleepiq +from homeassistant.components.binary_sensor import BinarySensorDevice + +DEPENDENCIES = ['sleepiq'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the SleepIQ sensors.""" + if discovery_info is None: + return + + data = sleepiq.DATA + data.update() + + dev = list() + for bed_id, _ in data.beds.items(): + for side in sleepiq.SIDES: + dev.append(IsInBedBinarySensor(data, bed_id, side)) + add_devices(dev) + + +# pylint: disable=too-many-instance-attributes +class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice): + """Implementation of a SleepIQ presence sensor.""" + + def __init__(self, sleepiq_data, bed_id, side): + """Initialize the sensor.""" + sleepiq.SleepIQSensor.__init__(self, sleepiq_data, bed_id, side) + self.type = sleepiq.IS_IN_BED + self._state = None + self._name = sleepiq.SENSOR_TYPES[self.type] + self.update() + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state is True + + @property + def sensor_class(self): + """Return the class of this sensor.""" + return "occupancy" + + def update(self): + """Get the latest data from SleepIQ and updates the states.""" + sleepiq.SleepIQSensor.update(self) + self._state = self.side.is_in_bed diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index e0b748bbbbe..662a6982a11 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -12,10 +12,9 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA) from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, MATCH_ALL, CONF_VALUE_TEMPLATE, + ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_SENSOR_CLASS, CONF_SENSORS) from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv @@ -25,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_ENTITY_ID, default=MATCH_ALL): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA }) @@ -40,10 +39,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device, device_config in config[CONF_SENSORS].items(): value_template = device_config[CONF_VALUE_TEMPLATE] - entity_ids = device_config[ATTR_ENTITY_ID] + entity_ids = (device_config.get(ATTR_ENTITY_ID) or + value_template.extract_entities()) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) sensor_class = device_config.get(CONF_SENSOR_CLASS) + if value_template is not None: + value_template.hass = hass + sensors.append( BinarySensorTemplate( hass, @@ -107,8 +110,7 @@ class BinarySensorTemplate(BinarySensorDevice): def update(self): """Get the latest data and update the state.""" try: - self._state = template.render( - self.hass, self._template).lower() == 'true' + self._state = self._template.render().lower() == 'true' except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 940f80a757b..e258d6cf443 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -2,7 +2,7 @@ A sensor that monitors trands in other components. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.template/ +https://home-assistant.io/components/sensor.trend/ """ import logging import voluptuous as vol @@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the template sensors.""" + """Setup the trend sensors.""" sensors = [] for device, device_config in config[CONF_SENSORS].items(): @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SensorTrend(BinarySensorDevice): - """Representation of a Template Sensor.""" + """Representation of a trend Sensor.""" # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, hass, device_id, friendly_name, @@ -90,14 +90,14 @@ class SensorTrend(BinarySensorDevice): self.update() - def template_sensor_state_listener(entity, old_state, new_state): + def trend_sensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" self.from_state = old_state self.to_state = new_state self.update_ha_state(True) track_state_change(hass, target_entity, - template_sensor_state_listener) + trend_sensor_state_listener) @property def name(self): diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 9ba717782e9..636a5747ed5 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -1,19 +1,17 @@ """ -Support for Wink sensors. +Support for Wink binary sensors. For more details about this platform, please refer to the documentation at -at https://home-assistant.io/components/sensor.wink/ +at https://home-assistant.io/components/binary_sensor.wink/ """ -import logging import json from homeassistant.components.binary_sensor import BinarySensorDevice 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.13', 'pubnub==3.8.2'] +DEPENDENCIES = ['wink'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { @@ -21,7 +19,8 @@ SENSOR_TYPES = { "brightness": "light", "vibration": "vibration", "loudness": "sound", - "liquid_detected": "moisture" + "liquid_detected": "moisture", + "motion": "motion" } @@ -29,17 +28,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink binary sensor platform.""" import pywink - if discovery_info is None: - token = config.get(CONF_ACCESS_TOKEN) - - if token is None: - logging.getLogger(__name__).error( - "Missing wink access_token. " - "Get one at https://winkbearertoken.appspot.com/") - return - - pywink.set_bearer_token(token) - for sensor in pywink.get_sensors(): if sensor.capability() in SENSOR_TYPES: add_devices([WinkBinarySensorDevice(sensor)]) @@ -77,6 +65,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): return self.wink.brightness_boolean() elif self.capability == "liquid_detected": return self.wink.liquid_boolean() + elif self.capability == "motion": + return self.wink.motion_boolean() else: return self.wink.state() diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py index e610082951b..dd27a0ba954 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky.py @@ -65,7 +65,7 @@ class BloomSky(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def refresh_devices(self): - """Use the API to retreive a list of devices.""" + """Use the API to retrieve a list of devices.""" _LOGGER.debug("Fetching BloomSky update") response = requests.get(self.API_URL, headers={"Authorization": self._api_key}, diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index af21537e3c3..1115bc2d2c1 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -9,30 +9,28 @@ import logging import voluptuous as vol from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.components.ffmpeg import ( + run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME -REQUIREMENTS = ['ha-ffmpeg==0.10'] +DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) -CONF_INPUT = 'input' -CONF_FFMPEG_BIN = 'ffmpeg_bin' -CONF_EXTRA_ARGUMENTS = 'extra_arguments' - -DEFAULT_BINARY = 'ffmpeg' DEFAULT_NAME = 'FFmpeg' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_INPUT): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, - vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) def setup_platform(hass, config, add_devices, discovery_info=None): """Setup a FFmpeg Camera.""" + if not run_test(config.get(CONF_INPUT)): + return add_devices([FFmpegCamera(config)]) @@ -45,12 +43,11 @@ class FFmpegCamera(Camera): self._name = config.get(CONF_NAME) self._input = config.get(CONF_INPUT) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) - self._ffmpeg_bin = config.get(CONF_FFMPEG_BIN) def camera_image(self): """Return a still image response from the camera.""" from haffmpeg import ImageSingle, IMAGE_JPEG - ffmpeg = ImageSingle(self._ffmpeg_bin) + ffmpeg = ImageSingle(get_binary()) return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments) @@ -59,7 +56,7 @@ class FFmpegCamera(Camera): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg import CameraMjpeg - stream = CameraMjpeg(self._ffmpeg_bin) + stream = CameraMjpeg(get_binary()) stream.open_camera(self._input, extra_cmd=self._extra_arguments) return response( stream, diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 85a662065a6..5d7488b8e68 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -15,7 +15,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.exceptions import TemplateError from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -25,7 +25,7 @@ CONF_STILL_IMAGE_URL = 'still_image_url' DEFAULT_NAME = 'Generic Camera' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STILL_IMAGE_URL): vol.Any(cv.url, cv.template), + vol.Required(CONF_STILL_IMAGE_URL): cv.template, vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean, @@ -38,18 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup a generic IP Camera.""" - add_devices([GenericCamera(config)]) + add_devices([GenericCamera(hass, config)]) # pylint: disable=too-many-instance-attributes class GenericCamera(Camera): """A generic implementation of an IP camera.""" - def __init__(self, device_info): + def __init__(self, hass, device_info): """Initialize a generic camera.""" super().__init__() + self.hass = hass self._name = device_info.get(CONF_NAME) self._still_image_url = device_info[CONF_STILL_IMAGE_URL] + self._still_image_url.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] username = device_info.get(CONF_USERNAME) @@ -69,7 +71,7 @@ class GenericCamera(Camera): def camera_image(self): """Return a still image response from the camera.""" try: - url = template.render(self.hass, self._still_image_url) + url = self._still_image_url.render() except TemplateError as err: _LOGGER.error('Error parsing template %s: %s', self._still_image_url, err) diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index b75d1480b09..c29100ecaad 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -8,28 +8,33 @@ import logging import socket import requests +import voluptuous as vol -from homeassistant.components.camera import DOMAIN, Camera -from homeassistant.helpers import validate_config +from homeassistant.const import CONF_PORT +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['uvcclient==0.9.0'] _LOGGER = logging.getLogger(__name__) +CONF_NVR = 'nvr' +CONF_KEY = 'key' + +DEFAULT_PORT = 7080 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NVR): cv.string, + vol.Required(CONF_KEY): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Discover cameras on a Unifi NVR.""" - if not validate_config({DOMAIN: config}, {DOMAIN: ['nvr', 'key']}, - _LOGGER): - return None - - addr = config.get('nvr') - key = config.get('key') - try: - port = int(config.get('port', 7080)) - except ValueError: - _LOGGER.error('Invalid port number provided') - return False + addr = config[CONF_NVR] + key = config[CONF_KEY] + port = config[CONF_PORT] from uvcclient import nvr nvrconn = nvr.UVCRemote(addr, port, key) diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index cb85a153cc8..51346e62269 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): None, None, "Auto", "heat", None, None, None), DemoClimate("Hvac", 21, TEMP_CELSIUS, True, 22, "On High", 67, 54, "Off", "cool", False, None, None), - DemoClimate("Ecobee", 23, TEMP_CELSIUS, None, 23, "Auto Low", + DemoClimate("Ecobee", None, TEMP_CELSIUS, None, 23, "Auto Low", None, None, "Auto", "auto", None, 24, 21) ]) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 21a2c89c31a..10e56490c84 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -14,7 +14,7 @@ from homeassistant.components.climate import ( DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS) from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv @@ -86,10 +86,16 @@ class Thermostat(ClimateDevice): self.hold_temp = hold_temp self._operation_list = ['auto', 'auxHeatOnly', 'cool', 'heat', 'off'] + self.update_without_throttle = False def update(self): """Get the latest state from the thermostat.""" - self.data.update() + if self.update_without_throttle: + self.data.update(no_throttle=True) + self.update_without_throttle = False + else: + self.data.update() + self.thermostat = self.data.ecobee.get_thermostat( self.thermostat_index) @@ -101,22 +107,16 @@ class Thermostat(ClimateDevice): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return TEMP_FAHRENHEIT + if self.thermostat['settings']['useCelsius']: + return TEMP_CELSIUS + else: + return TEMP_FAHRENHEIT @property def current_temperature(self): """Return the current temperature.""" return self.thermostat['runtime']['actualTemperature'] / 10 - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if (self.operation_mode == 'heat' or - self.operation_mode == 'auxHeatOnly'): - return self.target_temperature_low - elif self.operation_mode == 'cool': - return self.target_temperature_high - @property def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" @@ -211,17 +211,15 @@ class Thermostat(ClimateDevice): "away", "indefinite") else: self.data.ecobee.set_climate_hold(self.thermostat_index, "away") + self.update_without_throttle = True def turn_away_mode_off(self): """Turn away off.""" self.data.ecobee.resume_program(self.thermostat_index) + self.update_without_throttle = True def set_temperature(self, **kwargs): """Set new target temperature.""" - if kwargs.get(ATTR_TEMPERATURE) is not None: - temperature = kwargs.get(ATTR_TEMPERATURE) - low_temp = int(temperature) - high_temp = int(temperature) if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \ kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None: high_temp = int(kwargs.get(ATTR_TARGET_TEMP_LOW)) @@ -241,15 +239,18 @@ class Thermostat(ClimateDevice): "high=%s, is=%s", low_temp, isinstance( low_temp, (int, float)), high_temp, isinstance(high_temp, (int, float))) + self.update_without_throttle = True def set_operation_mode(self, operation_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode) + self.update_without_throttle = True def set_fan_min_on_time(self, fan_min_on_time): """Set the minimum fan on time.""" self.data.ecobee.set_fan_min_on_time(self.thermostat_index, fan_min_on_time) + self.update_without_throttle = True # Home and Sleep mode aren't used in UI yet: diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index fd85d7fd46b..97ca7fe012f 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -5,16 +5,19 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.generic_thermostat/ """ import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import switch from homeassistant.components.climate import ( - STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice) + STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE) from homeassistant.helpers import condition from homeassistant.helpers.event import track_state_change +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['switch', 'sensor'] @@ -30,18 +33,16 @@ CONF_TARGET_TEMP = 'target_temp' CONF_AC_MODE = 'ac_mode' CONF_MIN_DUR = 'min_cycle_duration' -_LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = vol.Schema({ - vol.Required("platform"): "generic_thermostat", - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HEATER): cv.entity_id, vol.Required(CONF_SENSOR): cv.entity_id, - vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_AC_MODE): cv.boolean, vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), - vol.Optional(CONF_AC_MODE): vol.Coerce(bool), vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), }) @@ -56,10 +57,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ac_mode = config.get(CONF_AC_MODE) min_cycle_duration = config.get(CONF_MIN_DUR) - add_devices([GenericThermostat(hass, name, heater_entity_id, - sensor_entity_id, min_temp, - max_temp, target_temp, ac_mode, - min_cycle_duration)]) + add_devices([GenericThermostat( + hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, + target_temp, ac_mode, min_cycle_duration)]) # pylint: disable=too-many-instance-attributes, abstract-method @@ -110,7 +110,7 @@ class GenericThermostat(ClimateDevice): return self._cur_temp @property - def operation(self): + def current_operation(self): """Return current operation ie. heat, cool, idle.""" if self.ac_mode: cooling = self._active and self._is_device_active diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index 7e0b4fd6450..c9901c40aea 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -99,6 +99,9 @@ class HMThermostat(homematic.HMDevice, ClimateDevice): return None if temperature is None: return + + if self.current_operation == STATE_AUTO: + return self._hmdevice.actionNodeData('MANU_MODE', temperature) self._hmdevice.set_temperature(temperature) def set_operation_mode(self, operation_mode): diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 001bf8806ac..fb7b2887344 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -7,44 +7,67 @@ https://home-assistant.io/components/climate.honeywell/ import logging import socket -from homeassistant.components.climate import ClimateDevice +import voluptuous as vol + +from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['evohomeclient==0.2.5', - 'somecomfort==0.2.1'] + 'somecomfort==0.3.2'] _LOGGER = logging.getLogger(__name__) -CONF_AWAY_TEMP = "away_temperature" -DEFAULT_AWAY_TEMP = 16 +ATTR_FAN = 'fan' +ATTR_FANMODE = 'fanmode' +ATTR_SYSTEM_MODE = 'system_mode' + +CONF_AWAY_TEMPERATURE = 'away_temperature' +CONF_REGION = 'region' + +DEFAULT_AWAY_TEMPERATURE = 16 +DEFAULT_REGION = 'eu' +REGIONS = ['eu', 'us'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_AWAY_TEMPERATURE, default=DEFAULT_AWAY_TEMPERATURE): + vol.Coerce(float), + vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the HoneywelL thermostat.""" + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + region = config.get(CONF_REGION) + + if region == 'us': + return _setup_us(username, password, config, add_devices) + else: + return _setup_round(username, password, config, add_devices) def _setup_round(username, password, config, add_devices): """Setup rounding function.""" from evohomeclient import EvohomeClient - try: - away_temp = float(config.get(CONF_AWAY_TEMP, DEFAULT_AWAY_TEMP)) - except ValueError: - _LOGGER.error("value entered for item %s should convert to a number", - CONF_AWAY_TEMP) - return False - + away_temp = config.get(CONF_AWAY_TEMPERATURE) evo_api = EvohomeClient(username, password) try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, - zone['id'], - i == 0, - away_temp)]) + add_devices( + [RoundThermostat(evo_api, zone['id'], i == 0, away_temp)] + ) except socket.error: _LOGGER.error( - "Connection error logging into the honeywell evohome web service" - ) + "Connection error logging into the honeywell evohome web service") return False return True @@ -74,26 +97,6 @@ def _setup_us(username, password, config, add_devices): return True -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the honeywel thermostat.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - region = config.get('region', 'eu').lower() - - if username is None or password is None: - _LOGGER.error("Missing required configuration items %s or %s", - CONF_USERNAME, CONF_PASSWORD) - return False - if region not in ('us', 'eu'): - _LOGGER.error('Region `%s` is invalid (use either us or eu)', region) - return False - - if region == 'us': - return _setup_us(username, password, config, add_devices) - else: - return _setup_round(username, password, config, add_devices) - - class RoundThermostat(ClimateDevice): """Representation of a Honeywell Round Connected thermostat.""" @@ -103,7 +106,7 @@ class RoundThermostat(ClimateDevice): self.device = device self._current_temperature = None self._target_temperature = None - self._name = "round connected" + self._name = 'round connected' self._id = zone_id self._master = master self._is_dhw = False @@ -143,7 +146,7 @@ class RoundThermostat(ClimateDevice): @property def current_operation(self: ClimateDevice) -> str: """Get the current operation of the system.""" - return getattr(self.device, 'system_mode', None) + return getattr(self.device, ATTR_SYSTEM_MODE, None) @property def is_away_mode_on(self): @@ -152,7 +155,7 @@ class RoundThermostat(ClimateDevice): def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: """Set the HVAC mode for the thermostat.""" - if hasattr(self.device, 'system_mode'): + if hasattr(self.device, ATTR_SYSTEM_MODE): self.device.system_mode = operation_mode def turn_away_mode_on(self): @@ -186,8 +189,8 @@ class RoundThermostat(ClimateDevice): self._current_temperature = data['temp'] self._target_temperature = data['setpoint'] - if data['thermostat'] == "DOMESTIC_HOT_WATER": - self._name = "Hot Water" + if data['thermostat'] == 'DOMESTIC_HOT_WATER': + self._name = 'Hot Water' self._is_dhw = True else: self._name = data['name'] @@ -236,7 +239,7 @@ class HoneywellUSThermostat(ClimateDevice): @property def current_operation(self: ClimateDevice) -> str: """Return current operation ie. heat, cool, idle.""" - return getattr(self._device, 'system_mode', None) + return getattr(self._device, ATTR_SYSTEM_MODE, None) def set_temperature(self, **kwargs): """Set target temperature.""" @@ -255,9 +258,11 @@ class HoneywellUSThermostat(ClimateDevice): @property def device_state_attributes(self): """Return the device specific state attributes.""" - return {'fan': (self.is_fan_on and 'running' or 'idle'), - 'fanmode': self._device.fan_mode, - 'system_mode': self._device.system_mode} + return { + ATTR_FAN: (self.is_fan_on and 'running' or 'idle'), + ATTR_FANMODE: self._device.fan_mode, + ATTR_SYSTEM_MODE: self._device.system_mode, + } def turn_away_mode_on(self): """Turn away on.""" @@ -269,5 +274,5 @@ class HoneywellUSThermostat(ClimateDevice): def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: """Set the system mode (Cool, Heat, etc).""" - if hasattr(self._device, 'system_mode'): + if hasattr(self._device, ATTR_SYSTEM_MODE): self._device.system_mode = operation_mode diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py index a9d4358a059..5ea932ab8f5 100644 --- a/homeassistant/components/climate/knx.py +++ b/homeassistant/components/climate/knx.py @@ -2,26 +2,37 @@ Support for KNX thermostats. For more details about this platform, please refer to the documentation -https://home-assistant.io/components/knx/ +https://home-assistant.io/components/climate.knx/ """ import logging -from homeassistant.components.climate import ClimateDevice -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +import voluptuous as vol -from homeassistant.components.knx import ( - KNXConfig, KNXMultiAddressDevice) - -DEPENDENCIES = ["knx"] +from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA) +from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice) +from homeassistant.const import (CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +CONF_ADDRESS = 'address' +CONF_SETPOINT_ADDRESS = 'setpoint_address' +CONF_TEMPERATURE_ADDRESS = 'temperature_address' -def setup_platform(hass, config, add_entities, discovery_info=None): +DEFAULT_NAME = 'KNX Thermostat' +DEPENDENCIES = ['knx'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_SETPOINT_ADDRESS): cv.string, + vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): """Create and add an entity based on the configuration.""" - add_entities([ - KNXThermostat(hass, KNXConfig(config)) - ]) + add_devices([KNXThermostat(hass, KNXConfig(config))]) class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): @@ -39,9 +50,8 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): def __init__(self, hass, config): """Initialize the thermostat based on the given configuration.""" - KNXMultiAddressDevice.__init__(self, hass, config, - ["temperature", "setpoint"], - ["mode"]) + KNXMultiAddressDevice.__init__( + self, hass, config, ['temperature', 'setpoint'], ['mode']) self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius self._away = False # not yet supported @@ -62,14 +72,14 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): """Return the current temperature.""" from knxip.conversion import knx2_to_float - return knx2_to_float(self.value("temperature")) + return knx2_to_float(self.value('temperature')) @property def target_temperature(self): """Return the temperature we try to reach.""" from knxip.conversion import knx2_to_float - return knx2_to_float(self.value("setpoint")) + return knx2_to_float(self.value('setpoint')) def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -78,7 +88,7 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice): return from knxip.conversion import float_to_knx2 - self.set_value("setpoint", float_to_knx2(temperature)) + self.set_value('setpoint', float_to_knx2(temperature)) _LOGGER.debug("Set target temperature to %s", temperature) def set_operation_mode(self, operation_mode): diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py new file mode 100755 index 00000000000..9b997954889 --- /dev/null +++ b/homeassistant/components/climate/mysensors.py @@ -0,0 +1,192 @@ +""" +mysensors platform that offers a Climate(MySensors-HVAC) component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/climate.mysensors +""" +import logging + +from homeassistant.components import mysensors +from homeassistant.components.climate import ( + STATE_COOL, STATE_HEAT, STATE_OFF, STATE_AUTO, ClimateDevice, + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE + +_LOGGER = logging.getLogger(__name__) + +DICT_HA_TO_MYS = {STATE_COOL: "CoolOn", STATE_HEAT: "HeatOn", + STATE_AUTO: "AutoChangeOver", STATE_OFF: "Off"} +DICT_MYS_TO_HA = {"CoolOn": STATE_COOL, "HeatOn": STATE_HEAT, + "AutoChangeOver": STATE_AUTO, "Off": STATE_OFF} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the mysensors climate.""" + if discovery_info is None: + return + for gateway in mysensors.GATEWAYS.values(): + if float(gateway.protocol_version) < 1.5: + continue + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + map_sv_types = { + pres.S_HVAC: [set_req.V_HVAC_FLOW_STATE], + } + devices = {} + gateway.platform_callbacks.append(mysensors.pf_callback_factory( + map_sv_types, devices, add_devices, MySensorsHVAC)) + + +# pylint: disable=too-many-arguments, too-many-public-methods +class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): + """Representation of a MySensorsHVAC hvac.""" + + @property + def assumed_state(self): + """Return True if unable to access real state of entity.""" + return self.gateway.optimistic + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return (TEMP_CELSIUS + if self.gateway.metric else TEMP_FAHRENHEIT) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._values.get(self.gateway.const.SetReq.V_TEMP) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SETPOINT_COOL in self._values and \ + set_req.V_HVAC_SETPOINT_HEAT in self._values: + return None + temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL) + if temp is None: + temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) + return temp + + @property + def target_temperature_high(self): + """Return the highbound target temperature we try to reach.""" + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SETPOINT_HEAT in self._values: + return self._values.get(set_req.V_HVAC_SETPOINT_COOL) + + @property + def target_temperature_low(self): + """Return the lowbound target temperature we try to reach.""" + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SETPOINT_COOL in self._values: + return self._values.get(set_req.V_HVAC_SETPOINT_HEAT) + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._values.get(self.gateway.const.SetReq.V_HVAC_FLOW_STATE) + + @property + def operation_list(self): + """List of available operation modes.""" + return [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._values.get(self.gateway.const.SetReq.V_HVAC_SPEED) + + @property + def fan_list(self): + """List of available fan modes.""" + return ["Auto", "Min", "Normal", "Max"] + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + set_req = self.gateway.const.SetReq + temp = kwargs.get(ATTR_TEMPERATURE) + low = kwargs.get(ATTR_TARGET_TEMP_LOW) + high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + heat = self._values.get(set_req.V_HVAC_SETPOINT_HEAT) + cool = self._values.get(set_req.V_HVAC_SETPOINT_COOL) + updates = () + if temp is not None: + if heat is not None: + # Set HEAT Target temperature + value_type = set_req.V_HVAC_SETPOINT_HEAT + elif cool is not None: + # Set COOL Target temperature + value_type = set_req.V_HVAC_SETPOINT_COOL + if heat is not None or cool is not None: + updates = [(value_type, temp)] + elif all(val is not None for val in (low, high, heat, cool)): + updates = [ + (set_req.V_HVAC_SETPOINT_HEAT, low), + (set_req.V_HVAC_SETPOINT_COOL, high)] + for value_type, value in updates: + self.gateway.set_child_value( + self.node_id, self.child_id, value_type, value) + if self.gateway.optimistic: + # optimistically assume that switch has changed state + self._values[value_type] = value + self.update_ha_state() + + def set_fan_mode(self, fan): + """Set new target temperature.""" + set_req = self.gateway.const.SetReq + self.gateway.set_child_value(self.node_id, self.child_id, + set_req.V_HVAC_SPEED, fan) + if self.gateway.optimistic: + # optimistically assume that switch has changed state + self._values[set_req.V_HVAC_SPEED] = fan + self.update_ha_state() + + def set_operation_mode(self, operation_mode): + """Set new target temperature.""" + set_req = self.gateway.const.SetReq + self.gateway.set_child_value(self.node_id, self.child_id, + set_req.V_HVAC_FLOW_STATE, + DICT_HA_TO_MYS[operation_mode]) + if self.gateway.optimistic: + # optimistically assume that switch has changed state + self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode + self.update_ha_state() + + def update(self): + """Update the controller with the latest value from a sensor.""" + set_req = self.gateway.const.SetReq + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + for value_type, value in child.values.items(): + _LOGGER.debug( + '%s: value_type %s, value = %s', self._name, value_type, value) + if value_type == set_req.V_HVAC_FLOW_STATE: + self._values[value_type] = DICT_MYS_TO_HA[value] + else: + self._values[value_type] = value + + def set_humidity(self, humidity): + """Set new target humidity.""" + _LOGGER.error("Service Not Implemented yet") + + def set_swing_mode(self, swing_mode): + """Set new target swing operation.""" + _LOGGER.error("Service Not Implemented yet") + + def turn_away_mode_on(self): + """Turn away mode on.""" + _LOGGER.error("Service Not Implemented yet") + + def turn_away_mode_off(self): + """Turn away mode off.""" + _LOGGER.error("Service Not Implemented yet") + + def turn_aux_heat_on(self): + """Turn auxillary heater on.""" + _LOGGER.error("Service Not Implemented yet") + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + _LOGGER.error("Service Not Implemented yet") diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index f55d1d856eb..91e5f5824e9 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -4,15 +4,18 @@ Support for Nest thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.nest/ """ +import logging import voluptuous as vol - import homeassistant.components.nest as nest from homeassistant.components.climate import ( - STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, + PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW) from homeassistant.const import ( - TEMP_CELSIUS, CONF_SCAN_INTERVAL, ATTR_TEMPERATURE) + TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, TEMP_FAHRENHEIT) +from homeassistant.util.temperature import convert as convert_temperature DEPENDENCIES = ['nest'] +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SCAN_INTERVAL): @@ -22,7 +25,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Nest thermostat.""" - add_devices([NestThermostat(structure, device) + temp_unit = hass.config.units.temperature_unit + add_devices([NestThermostat(structure, device, temp_unit) for structure, device in nest.devices()]) @@ -30,10 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class NestThermostat(ClimateDevice): """Representation of a Nest thermostat.""" - def __init__(self, structure, device): + def __init__(self, structure, device, temp_unit): """Initialize the thermostat.""" + self._unit = temp_unit self.structure = structure self.device = device + self._fan_list = [STATE_ON, STATE_AUTO] @property def name(self): @@ -51,7 +57,10 @@ class NestThermostat(ClimateDevice): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return TEMP_CELSIUS + if self.device.measurment_scale == 'F': + return TEMP_FAHRENHEIT + elif self.device.measurement_scale == 'C': + return TEMP_CELSIUS @property def device_state_attributes(self): @@ -78,35 +87,6 @@ class NestThermostat(ClimateDevice): else: return STATE_IDLE - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self.device.mode == 'range': - low, high = self.target_temperature_low, \ - self.target_temperature_high - if self.operation == STATE_COOL: - temp = high - elif self.operation == STATE_HEAT: - temp = low - else: - # If the outside temp is lower than the current temp, consider - # the 'low' temp to the target, otherwise use the high temp - if (self.device.structure.weather.current.temperature < - self.current_temperature): - temp = low - else: - temp = high - else: - if self.is_away_mode_on: - # away_temperature is a low, high tuple. Only one should be set - # if not in range mode, the other will be None - temp = self.device.away_temperature[0] or \ - self.device.away_temperature[1] - else: - temp = self.device.target - - return temp - @property def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" @@ -134,15 +114,16 @@ class NestThermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: - return - if self.device.mode == 'range': - if self.target_temperature == self.target_temperature_low: - temperature = (temperature, self.target_temperature_high) - elif self.target_temperature == self.target_temperature_high: - temperature = (self.target_temperature_low, temperature) - self.device.target = temperature + if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \ + kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None: + target_temp_high = convert_temperature(kwargs.get( + ATTR_TARGET_TEMP_HIGH), self._unit, TEMP_CELSIUS) + target_temp_low = convert_temperature(kwargs.get( + ATTR_TARGET_TEMP_LOW), self._unit, TEMP_CELSIUS) + + temp = (target_temp_low, target_temp_high) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) + self.device.target = temp def set_operation_mode(self, operation_mode): """Set operation mode.""" @@ -157,17 +138,18 @@ class NestThermostat(ClimateDevice): self.structure.away = False @property - def is_fan_on(self): + def current_fan_mode(self): """Return whether the fan is on.""" - return self.device.fan + return STATE_ON if self.device.fan else STATE_AUTO - def turn_fan_on(self): - """Turn fan on.""" - self.device.fan = True + @property + def fan_list(self): + """List of available fan modes.""" + return self._fan_list - def turn_fan_off(self): - """Turn fan off.""" - self.device.fan = False + def set_fan_mode(self, fan): + """Turn fan on/off.""" + self.device.fan = fan.lower() @property def min_temp(self): diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index fa2230fba55..da5f5918d7c 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -4,13 +4,24 @@ Support for Proliphix NT10e Thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.proliphix/ """ +import voluptuous as vol + from homeassistant.components.climate import ( - STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice) + STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['proliphix==0.3.1'] +ATTR_FAN = 'fan' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Proliphix thermostats.""" @@ -22,9 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pdp = proliphix.PDP(host, username, password) - add_devices([ - ProliphixThermostat(pdp) - ]) + add_devices([ProliphixThermostat(pdp)]) # pylint: disable=abstract-method @@ -56,7 +65,7 @@ class ProliphixThermostat(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" return { - "fan": self._pdp.fan_state + ATTR_FAN: self._pdp.fan_state } @property diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 90611ce20b2..6af0e96045c 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -8,15 +8,28 @@ import datetime import logging from urllib.error import URLError +import voluptuous as vol + from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF, - ClimateDevice) + ClimateDevice, PLATFORM_SCHEMA) from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['radiotherm==1.2'] -HOLD_TEMP = 'hold_temp' + _LOGGER = logging.getLogger(__name__) +ATTR_FAN = 'fan' +ATTR_MODE = 'mode' + +CONF_HOLD_TEMP = 'hold_temp' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Radio Thermostat.""" @@ -29,10 +42,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hosts.append(radiotherm.discover.discover_address()) if hosts is None: - _LOGGER.error("No radiotherm thermostats detected.") + _LOGGER.error("No Radiotherm Thermostats detected") return False - hold_temp = config.get(HOLD_TEMP, False) + hold_temp = config.get(CONF_HOLD_TEMP) tstats = [] for host in hosts: @@ -60,6 +73,7 @@ class RadioThermostat(ClimateDevice): self._name = None self.hold_temp = hold_temp self.update() + self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF] @property def name(self): @@ -75,8 +89,8 @@ class RadioThermostat(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" return { - "fan": self.device.fmode['human'], - "mode": self.device.tmode['human'] + ATTR_FAN: self.device.fmode['human'], + ATTR_MODE: self.device.tmode['human'] } @property @@ -89,6 +103,11 @@ class RadioThermostat(ClimateDevice): """Return the current operation. head, cool idle.""" return self._current_operation + @property + def operation_list(self): + """Return the operation modes list.""" + return self._operation_list + @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -124,8 +143,11 @@ class RadioThermostat(ClimateDevice): def set_time(self): """Set device time.""" now = datetime.datetime.now() - self.device.time = {'day': now.weekday(), - 'hour': now.hour, 'minute': now.minute} + self.device.time = { + 'day': now.weekday(), + 'hour': now.hour, + 'minute': now.minute + } def set_operation_mode(self, operation_mode): """Set operation mode (auto, cool, heat, off).""" diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py new file mode 100644 index 00000000000..26d81e2b510 --- /dev/null +++ b/homeassistant/components/climate/vera.py @@ -0,0 +1,137 @@ +""" +Support for Vera thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.vera/ +""" +import logging + +from homeassistant.util import convert +from homeassistant.components.climate import ClimateDevice +from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE + +from homeassistant.components.vera import ( + VeraDevice, VERA_DEVICES, VERA_CONTROLLER) + +DEPENDENCIES = ['vera'] + +_LOGGER = logging.getLogger(__name__) + +OPERATION_LIST = ["Heat", "Cool", "Auto Changeover", "Off"] +FAN_OPERATION_LIST = ["On", "Auto", "Cycle"] + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Find and return Vera thermostats.""" + add_devices_callback( + VeraThermostat(device, VERA_CONTROLLER) for + device in VERA_DEVICES['climate']) + + +# pylint: disable=abstract-method +class VeraThermostat(VeraDevice, ClimateDevice): + """Representation of a Vera Thermostat.""" + + def __init__(self, vera_device, controller): + """Initialize the Vera device.""" + VeraDevice.__init__(self, vera_device, controller) + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + mode = self.vera_device.get_hvac_mode() + if mode == "HeatOn": + return OPERATION_LIST[0] # heat + elif mode == "CoolOn": + return OPERATION_LIST[1] # cool + elif mode == "AutoChangeOver": + return OPERATION_LIST[2] # auto + elif mode == "Off": + return OPERATION_LIST[3] # off + return "Off" + + @property + def operation_list(self): + """List of available operation modes.""" + return OPERATION_LIST + + @property + def current_fan_mode(self): + """Return the fan setting.""" + mode = self.vera_device.get_fan_mode() + if mode == "ContinuousOn": + return FAN_OPERATION_LIST[0] # on + elif mode == "Auto": + return FAN_OPERATION_LIST[1] # auto + elif mode == "PeriodicOn": + return FAN_OPERATION_LIST[2] # cycle + return "Auto" + + @property + def fan_list(self): + """List of available fan modes.""" + return FAN_OPERATION_LIST + + def set_fan_mode(self, mode): + """Set new target temperature.""" + if mode == FAN_OPERATION_LIST[0]: + self.vera_device.fan_on() + elif mode == FAN_OPERATION_LIST[1]: + self.vera_device.fan_auto() + elif mode == FAN_OPERATION_LIST[2]: + return self.vera_device.fan_cycle() + + @property + def current_power_mwh(self): + """Current power usage in mWh.""" + power = self.vera_device.power + if power: + return convert(power, float, 0.0) * 1000 + + def update(self): + """Called by the vera device callback to update state.""" + self._state = self.vera_device.get_hvac_mode() + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.vera_device.get_current_temperature() + + @property + def operation(self): + """Return current operation ie. heat, cool, idle.""" + return self.vera_device.get_hvac_state() + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.vera_device.get_current_goal_temperature() + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + if kwargs.get(ATTR_TEMPERATURE) is not None: + self.vera_device.set_temperature(kwargs.get(ATTR_TEMPERATURE)) + + def set_operation_mode(self, operation_mode): + """Set HVAC mode (auto, cool, heat, off).""" + if operation_mode == OPERATION_LIST[3]: # off + self.vera_device.turn_off() + elif operation_mode == OPERATION_LIST[2]: # auto + self.vera_device.turn_auto_on() + elif operation_mode == OPERATION_LIST[1]: # cool + self.vera_device.turn_cool_on() + elif operation_mode == OPERATION_LIST[0]: # heat + self.vera_device.turn_heat_on() + + def turn_fan_on(self): + """Turn fan on.""" + self.vera_device.fan_on() + + def turn_fan_off(self): + """Turn fan off.""" + self.vera_device.fan_auto() diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 6c08bf391d8..4eec1f9c652 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -261,6 +261,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._target_temperature = temperature # ZXT-120 responds only to whole int value.data = round(temperature, 0) + self.update_ha_state() break else: _LOGGER.debug("Setting new setpoint for %s, " diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index 89cbd73b6ab..2a9a55d289a 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['fuzzywuzzy==0.11.1'] +REQUIREMENTS = ['fuzzywuzzy==0.12.0'] ATTR_TEXT = 'text' diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 876a8b46cfa..db517aec978 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -25,9 +25,8 @@ from homeassistant.const import ( DOMAIN = 'cover' SCAN_INTERVAL = 15 -GROUP_NAME_ALL_COVERS = 'all_covers' -ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format( - GROUP_NAME_ALL_COVERS) +GROUP_NAME_ALL_COVERS = 'all covers' +ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers') ENTITY_ID_FORMAT = DOMAIN + '.{}' diff --git a/homeassistant/components/cover/command_line.py b/homeassistant/components/cover/command_line.py index 0a1da9d7a20..d70d43463cf 100644 --- a/homeassistant/components/cover/command_line.py +++ b/homeassistant/components/cover/command_line.py @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, CONF_COMMAND_STATE, CONF_COMMAND_STOP, CONF_COVERS, CONF_VALUE_TEMPLATE, CONF_FRIENDLY_NAME) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) @@ -24,7 +23,7 @@ COVER_SCHEMA = vol.Schema({ vol.Optional(CONF_COMMAND_STATE): cv.string, vol.Optional(CONF_COMMAND_STOP, default='true'): cv.string, vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE, default='{{ value }}'): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -38,6 +37,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): covers = [] for device_name, device_config in devices.items(): + value_template = device_config.get(CONF_VALUE_TEMPLATE) + value_template.hass = hass + covers.append( CommandCover( hass, @@ -46,7 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device_config.get(CONF_COMMAND_CLOSE), device_config.get(CONF_COMMAND_STOP), device_config.get(CONF_COMMAND_STATE), - device_config.get(CONF_VALUE_TEMPLATE), + value_template, ) ) @@ -136,8 +138,8 @@ class CommandCover(CoverDevice): if self._command_state: payload = str(self._query_state()) if self._value_template: - payload = template.render_with_possible_json_value( - self._hass, self._value_template, payload) + payload = self._value_template.render_with_possible_json_value( + payload) self._state = int(payload) def open_cover(self, **kwargs): diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py new file mode 100644 index 00000000000..27619de738d --- /dev/null +++ b/homeassistant/components/cover/isy994.py @@ -0,0 +1,109 @@ +""" +Support for ISY994 covers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.isy994/ +""" +import logging +from typing import Callable # noqa + +from homeassistant.components.cover import CoverDevice, DOMAIN +import homeassistant.components.isy994 as isy +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN +from homeassistant.helpers.typing import ConfigType + + +_LOGGER = logging.getLogger(__name__) + +VALUE_TO_STATE = { + 0: STATE_CLOSED, + 101: STATE_UNKNOWN, +} + +UOM = ['97'] +STATES = [STATE_OPEN, STATE_CLOSED, 'closing', 'opening', 'stopped'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config: ConfigType, + add_devices: Callable[[list], None], discovery_info=None): + """Setup the ISY994 cover platform.""" + if isy.ISY is None or not isy.ISY.connected: + _LOGGER.error('A connection has not been made to the ISY controller.') + return False + + devices = [] + + for node in isy.filter_nodes(isy.NODES, units=UOM, + states=STATES): + devices.append(ISYCoverDevice(node)) + + for program in isy.PROGRAMS.get(DOMAIN, []): + try: + status = program[isy.KEY_STATUS] + actions = program[isy.KEY_ACTIONS] + assert actions.dtype == 'program', 'Not a program' + except (KeyError, AssertionError): + pass + else: + devices.append(ISYCoverProgram(program.name, status, actions)) + + add_devices(devices) + + +class ISYCoverDevice(isy.ISYDevice, CoverDevice): + """Representation of an ISY994 cover device.""" + + def __init__(self, node: object): + """Initialize the ISY994 cover device.""" + isy.ISYDevice.__init__(self, node) + + @property + def current_cover_position(self) -> int: + """Get the current cover position.""" + return sorted((0, self.value, 100))[1] + + @property + def is_closed(self) -> bool: + """Get whether the ISY994 cover device is closed.""" + return self.state == STATE_CLOSED + + @property + def state(self) -> str: + """Get the state of the ISY994 cover device.""" + return VALUE_TO_STATE.get(self.value, STATE_OPEN) + + def open_cover(self, **kwargs) -> None: + """Send the open cover command to the ISY994 cover device.""" + if not self._node.on(val=100): + _LOGGER.error('Unable to open the cover') + + def close_cover(self, **kwargs) -> None: + """Send the close cover command to the ISY994 cover device.""" + if not self._node.off(): + _LOGGER.error('Unable to close the cover') + + +class ISYCoverProgram(ISYCoverDevice): + """Representation of an ISY994 cover program.""" + + def __init__(self, name: str, node: object, actions: object) -> None: + """Initialize the ISY994 cover program.""" + ISYCoverDevice.__init__(self, node) + self._name = name + self._actions = actions + + @property + def state(self) -> str: + """Get the state of the ISY994 cover program.""" + return STATE_CLOSED if bool(self.value) else STATE_OPEN + + def open_cover(self, **kwargs) -> None: + """Send the open cover command to the ISY994 cover program.""" + if not self._actions.runThen(): + _LOGGER.error('Unable to open the cover') + + def close_cover(self, **kwargs) -> None: + """Send the close cover command to the ISY994 cover program.""" + if not self._actions.runElse(): + _LOGGER.error('Unable to close the cover') diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index b47bcf124e1..9b61f52b67c 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -15,7 +15,6 @@ from homeassistant.const import ( STATE_CLOSED) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -28,10 +27,10 @@ CONF_PAYLOAD_STOP = 'payload_stop' CONF_STATE_OPEN = 'state_open' CONF_STATE_CLOSED = 'state_closed' -DEFAULT_NAME = "MQTT Cover" -DEFAULT_PAYLOAD_OPEN = "OPEN" -DEFAULT_PAYLOAD_CLOSE = "CLOSE" -DEFAULT_PAYLOAD_STOP = "STOP" +DEFAULT_NAME = 'MQTT Cover' +DEFAULT_PAYLOAD_OPEN = 'OPEN' +DEFAULT_PAYLOAD_CLOSE = 'CLOSE' +DEFAULT_PAYLOAD_STOP = 'STOP' DEFAULT_OPTIMISTIC = False DEFAULT_RETAIN = False @@ -43,27 +42,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Add MQTT Cover.""" - add_devices_callback([MqttCover( +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the MQTT Cover.""" + value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass + add_devices([MqttCover( hass, - config[CONF_NAME], + config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), - config[CONF_COMMAND_TOPIC], - config[CONF_QOS], - config[CONF_RETAIN], - config[CONF_STATE_OPEN], - config[CONF_STATE_CLOSED], - config[CONF_PAYLOAD_OPEN], - config[CONF_PAYLOAD_CLOSE], - config[CONF_PAYLOAD_STOP], - config[CONF_OPTIMISTIC], - config.get(CONF_VALUE_TEMPLATE) + config.get(CONF_COMMAND_TOPIC), + config.get(CONF_QOS), + config.get(CONF_RETAIN), + config.get(CONF_STATE_OPEN), + config.get(CONF_STATE_CLOSED), + config.get(CONF_PAYLOAD_OPEN), + config.get(CONF_PAYLOAD_CLOSE), + config.get(CONF_PAYLOAD_STOP), + config.get(CONF_OPTIMISTIC), + value_template, )]) @@ -93,8 +93,8 @@ class MqttCover(CoverDevice): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) if payload == self._state_open: self._state = False _LOGGER.warning("state=%s", int(self._state)) @@ -111,8 +111,8 @@ class MqttCover(CoverDevice): self.update_ha_state() else: _LOGGER.warning( - "Payload is not True or False or" - " integer(0-100) %s", payload) + "Payload is not True, False, or integer (0-100): %s", + payload) if self._state_topic is None: # Force into optimistic mode. self._optimistic = True @@ -149,7 +149,7 @@ class MqttCover(CoverDevice): self._qos, self._retain) if self._optimistic: # Optimistically assume that cover has changed state. - self._state = 100 + self._state = False self.update_ha_state() def close_cover(self, **kwargs): @@ -158,7 +158,7 @@ class MqttCover(CoverDevice): self._qos, self._retain) if self._optimistic: # Optimistically assume that cover has changed state. - self._state = 0 + self._state = True self.update_ha_state() def stop_cover(self, **kwargs): diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 6cef5dc08e7..00034bd718b 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -7,64 +7,69 @@ https://github.com/andrewshilliday/garage-door-controller For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.rpi_gpio/ """ - import logging from time import sleep + import voluptuous as vol -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME import homeassistant.components.rpi_gpio as rpi_gpio import homeassistant.helpers.config_validation as cv -RELAY_TIME = 'relay_time' -STATE_PULL_MODE = 'state_pull_mode' -DEFAULT_PULL_MODE = 'UP' -DEFAULT_RELAY_TIME = .2 -DEPENDENCIES = ['rpi_gpio'] - _LOGGER = logging.getLogger(__name__) +CONF_COVERS = 'covers' +CONF_RELAY_PIN = 'relay_pin' +CONF_RELAY_TIME = 'relay_time' +CONF_STATE_PIN = 'state_pin' +CONF_STATE_PULL_MODE = 'state_pull_mode' + +DEFAULT_RELAY_TIME = .2 +DEFAULT_STATE_PULL_MODE = 'UP' +DEPENDENCIES = ['rpi_gpio'] + _COVERS_SCHEMA = vol.All( cv.ensure_list, [ vol.Schema({ - 'name': str, - 'relay_pin': int, - 'state_pin': int, + CONF_NAME: cv.string, + CONF_RELAY_PIN: cv.positive_int, + CONF_STATE_PIN: cv.positive_int, }) ] ) -PLATFORM_SCHEMA = vol.Schema({ - 'platform': str, - vol.Required('covers'): _COVERS_SCHEMA, - vol.Optional(STATE_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string, - vol.Optional(RELAY_TIME, default=DEFAULT_RELAY_TIME): vol.Coerce(int), + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): _COVERS_SCHEMA, + vol.Optional(CONF_STATE_PULL_MODE, default=DEFAULT_STATE_PULL_MODE): + cv.string, + vol.Optional(CONF_RELAY_TIME, default=DEFAULT_RELAY_TIME): cv.positive_int, }) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the cover platform.""" - relay_time = config.get(RELAY_TIME) - state_pull_mode = config.get(STATE_PULL_MODE) + """Setup the RPi cover platform.""" + relay_time = config.get(CONF_RELAY_TIME) + state_pull_mode = config.get(CONF_STATE_PULL_MODE) covers = [] - covers_conf = config.get('covers') + covers_conf = config.get(CONF_COVERS) for cover in covers_conf: - covers.append(RPiGPIOCover(cover['name'], cover['relay_pin'], - cover['state_pin'], - state_pull_mode, - relay_time)) + covers.append(RPiGPIOCover( + cover[CONF_NAME], cover[CONF_RELAY_PIN], cover[CONF_STATE_PIN], + state_pull_mode, relay_time)) add_devices(covers) # pylint: disable=abstract-method class RPiGPIOCover(CoverDevice): - """Representation of a Raspberry cover.""" + """Representation of a Raspberry GPIO cover.""" # pylint: disable=too-many-arguments - def __init__(self, name, relay_pin, state_pin, - state_pull_mode, relay_time): + def __init__(self, name, relay_pin, state_pin, state_pull_mode, + relay_time): """Initialize the cover.""" self._name = name self._state = False @@ -79,7 +84,7 @@ class RPiGPIOCover(CoverDevice): @property def unique_id(self): """Return the ID of this cover.""" - return "{}.{}".format(self.__class__, self._name) + return '{}.{}'.format(self.__class__, self._name) @property def name(self): diff --git a/homeassistant/components/cover/scsgate.py b/homeassistant/components/cover/scsgate.py index 18692534e90..c491f3bf548 100644 --- a/homeassistant/components/cover/scsgate.py +++ b/homeassistant/components/cover/scsgate.py @@ -6,37 +6,43 @@ https://home-assistant.io/components/cover.scsgate/ """ import logging +import voluptuous as vol + import homeassistant.components.scsgate as scsgate -from homeassistant.components.cover import CoverDevice -from homeassistant.const import CONF_NAME +from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_DEVICES, CONF_NAME) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['scsgate'] -SCS_ID = 'scs_id' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}), +}) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the SCSGate cover.""" - devices = config.get('devices') + devices = config.get(CONF_DEVICES) covers = [] logger = logging.getLogger(__name__) if devices: for _, entity_info in devices.items(): - if entity_info[SCS_ID] in scsgate.SCSGATE.devices: + if entity_info[scsgate.CONF_SCS_ID] in scsgate.SCSGATE.devices: continue - logger.info("Adding %s scsgate.cover", entity_info[CONF_NAME]) - name = entity_info[CONF_NAME] - scs_id = entity_info[SCS_ID] - cover = SCSGateCover( - name=name, - scs_id=scs_id, - logger=logger) + scs_id = entity_info[scsgate.CONF_SCS_ID] + + logger.info("Adding %s scsgate.cover", name) + + cover = SCSGateCover(name=name, scs_id=scs_id, logger=logger) scsgate.SCSGATE.add_device(cover) covers.append(cover) - add_devices_callback(covers) + add_devices(covers) # pylint: disable=too-many-arguments, too-many-instance-attributes @@ -91,6 +97,5 @@ class SCSGateCover(CoverDevice): def process_event(self, message): """Handle a SCSGate message related with this cover.""" - self._logger.debug( - "Rollershutter %s, got message %s", - self._scs_id, message.toggled) + self._logger.debug("Cover %s, got message %s", + self._scs_id, message.toggled) diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py new file mode 100644 index 00000000000..0a9e8abb243 --- /dev/null +++ b/homeassistant/components/cover/vera.py @@ -0,0 +1,70 @@ +""" +Support for Vera cover - curtains, rollershutters etc. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.vera/ +""" +import logging + +from homeassistant.components.cover import CoverDevice +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 covers.""" + add_devices_callback( + VeraCover(device, VERA_CONTROLLER) for + device in VERA_DEVICES['cover']) + + +# pylint: disable=abstract-method +class VeraCover(VeraDevice, CoverDevice): + """Represents a Vera Cover in Home Assistant.""" + + def __init__(self, vera_device, controller): + """Initialize the Vera device.""" + VeraDevice.__init__(self, vera_device, controller) + + @property + def current_cover_position(self): + """ + Return current position of cover. + + 0 is closed, 100 is fully open. + """ + position = self.vera_device.get_level() + if position <= 5: + return 0 + if position >= 95: + return 100 + return position + + def set_cover_position(self, position, **kwargs): + """Move the cover to a specific position.""" + self.vera_device.set_level(position) + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self.current_cover_position is not None: + if self.current_cover_position > 0: + return False + else: + return True + + def open_cover(self, **kwargs): + """Open the cover.""" + self.vera_device.open() + + def close_cover(self, **kwargs): + """Close the cover.""" + self.vera_device.close() + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self.vera_device.stop() diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index 59676b1f6a7..c57c2180446 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -4,30 +4,17 @@ Support for Wink Covers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.wink/ """ -import logging from homeassistant.components.cover import CoverDevice from homeassistant.components.wink import WinkDevice -from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.13', 'pubnub==3.8.2'] +DEPENDENCIES = ['wink'] def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Wink cover platform.""" import pywink - if discovery_info is None: - token = config.get(CONF_ACCESS_TOKEN) - - if token is None: - logging.getLogger(__name__).error( - "Missing wink access_token. " - "Get one at https://winkbearertoken.appspot.com/") - return - - pywink.set_bearer_token(token) - add_devices(WinkCoverDevice(shade) for shade in pywink.get_shades()) add_devices(WinkCoverDevice(door) for door in @@ -35,17 +22,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class WinkCoverDevice(WinkDevice, CoverDevice): - """Representation of a Wink covers.""" + """Representation of a Wink cover device.""" def __init__(self, wink): """Initialize the cover.""" WinkDevice.__init__(self, wink) - @property - def should_poll(self): - """Wink Shades don't track their position.""" - return False - def close_cover(self): """Close the shade.""" self.wink.set_state(0) diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index d7ebfb834e8..df7b2e14cb5 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -139,7 +139,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def set_cover_position(self, position, **kwargs): """Move the roller shutter to a specific position.""" - self._node.set_dimmer(self._value.value_id, 100 - position) + self._node.set_dimmer(self._value.value_id, position) def stop_cover(self, **kwargs): """Stop the roller shutter.""" diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index 9ee30dd0ce2..5b5b9ce2411 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -1,4 +1,4 @@ -"""Tracking for bluetooth devices.""" +"""Tracking for bluetooth low energy devices.""" import logging from datetime import timedelta diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 1a0ec457304..f3f2c3c94f5 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -6,9 +6,11 @@ https://home-assistant.io/components/device_tracker.locative/ """ import logging -from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME from homeassistant.components.http import HomeAssistantView +# pylint: disable=unused-import +from homeassistant.components.device_tracker import ( # NOQA + DOMAIN, PLATFORM_SCHEMA) _LOGGER = logging.getLogger(__name__) @@ -25,8 +27,8 @@ def setup_scanner(hass, config, see): class LocativeView(HomeAssistantView): """View to handle locative requests.""" - url = "/api/locative" - name = "api:locative" + url = '/api/locative' + name = 'api:locative' def __init__(self, hass, see): """Initialize Locative url endpoints.""" @@ -43,22 +45,22 @@ class LocativeView(HomeAssistantView): data = request.values if 'latitude' not in data or 'longitude' not in data: - return ("Latitude and longitude not specified.", + return ('Latitude and longitude not specified.', HTTP_UNPROCESSABLE_ENTITY) if 'device' not in data: - _LOGGER.error("Device id not specified.") - return ("Device id not specified.", + _LOGGER.error('Device id not specified.') + return ('Device id not specified.', HTTP_UNPROCESSABLE_ENTITY) if 'id' not in data: - _LOGGER.error("Location id not specified.") - return ("Location id not specified.", + _LOGGER.error('Location id not specified.') + return ('Location id not specified.', HTTP_UNPROCESSABLE_ENTITY) if 'trigger' not in data: - _LOGGER.error("Trigger is not specified.") - return ("Trigger is not specified.", + _LOGGER.error('Trigger is not specified.') + return ('Trigger is not specified.', HTTP_UNPROCESSABLE_ENTITY) device = data['device'].replace('-', '') @@ -67,15 +69,15 @@ class LocativeView(HomeAssistantView): if direction == 'enter': self.see(dev_id=device, location_name=location_name) - return "Setting location to {}".format(location_name) + return 'Setting location to {}'.format(location_name) elif direction == 'exit': current_state = self.hass.states.get( - "{}.{}".format(DOMAIN, device)) + '{}.{}'.format(DOMAIN, device)) if current_state is None or current_state.state == location_name: self.see(dev_id=device, location_name=STATE_NOT_HOME) - return "Setting location to not home" + return 'Setting location to not home' else: # Ignore the message if it is telling us to exit a zone that we # aren't currently in. This occurs when a zone is entered @@ -87,10 +89,10 @@ class LocativeView(HomeAssistantView): elif direction == 'test': # In the app, a test message can be sent. Just return something to # the user to let them know that it works. - return "Received test message." + return 'Received test message.' else: - _LOGGER.error("Received unidentified message from Locative: %s", + _LOGGER.error('Received unidentified message from Locative: %s', direction) - return ("Received unidentified message: {}".format(direction), + return ('Received unidentified message: {}'.format(direction), HTTP_UNPROCESSABLE_ENTITY) diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index e23d5f31145..fb217e66c48 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -10,11 +10,13 @@ import subprocess from collections import namedtuple from datetime import timedelta +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from homeassistant.components.device_tracker import DOMAIN +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOSTS -from homeassistant.helpers import validate_config -from homeassistant.util import Throttle, convert +from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) @@ -27,18 +29,21 @@ CONF_EXCLUDE = 'exclude' REQUIREMENTS = ['python-nmap==0.6.1'] +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOSTS): cv.string, + vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int, + vol.Optional(CONF_EXCLUDE, default=[]): + vol.All(cv.ensure_list, vol.Length(min=1)) +}) + def get_scanner(hass, config): """Validate the configuration and return a Nmap scanner.""" - if not validate_config(config, {DOMAIN: [CONF_HOSTS]}, - _LOGGER): - return None - scanner = NmapDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None -Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) +Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update']) def _arp(ip_address): @@ -49,24 +54,26 @@ def _arp(ip_address): match = re.search(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})', str(out)) if match: return match.group(0) - _LOGGER.info("No MAC address found for %s", ip_address) + _LOGGER.info('No MAC address found for %s', ip_address) return None class NmapDeviceScanner(object): """This class scans for devices using nmap.""" + exclude = [] + def __init__(self, config): """Initialize the scanner.""" self.last_results = [] self.hosts = config[CONF_HOSTS] self.exclude = config.get(CONF_EXCLUDE, []) - minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0) + minutes = config[CONF_HOME_INTERVAL] self.home_interval = timedelta(minutes=minutes) self.success_init = self._update_info() - _LOGGER.info("nmap scanner initialized") + _LOGGER.info('nmap scanner initialized') def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -90,21 +97,18 @@ class NmapDeviceScanner(object): Returns boolean if scanning successful. """ - _LOGGER.info("Scanning") + _LOGGER.info('Scanning') from nmap import PortScanner, PortScannerError scanner = PortScanner() - options = "-F --host-timeout 5s " - exclude = "--exclude " + options = '-F --host-timeout 5s ' if self.home_interval: boundary = dt_util.now() - self.home_interval last_results = [device for device in self.last_results if device.last_update > boundary] if last_results: - # Pylint is confused here. - # pylint: disable=no-member exclude_hosts = self.exclude + [device.ip for device in last_results] else: @@ -113,8 +117,7 @@ class NmapDeviceScanner(object): last_results = [] exclude_hosts = self.exclude if exclude_hosts: - exclude = " --exclude {}".format(",".join(exclude_hosts)) - options += exclude + options += ' --exclude {}'.format(','.join(exclude_hosts)) try: result = scanner.scan(hosts=self.hosts, arguments=options) @@ -134,5 +137,5 @@ class NmapDeviceScanner(object): self.last_results = last_results - _LOGGER.info("nmap scan successful") + _LOGGER.info('nmap scan successful') return True diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 4f6e6647f1c..1395f2940fe 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -218,9 +218,18 @@ def setup_scanner(hass, config, see): lat = wayp[WAYPOINT_LAT_KEY] lon = wayp[WAYPOINT_LON_KEY] rad = wayp['rad'] + + # check zone exists + entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name)) + + # Check if state already exists + if hass.states.get(entity_id) is not None: + continue + zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad, - zone_comp.ICON_IMPORT, False, True) - zone_comp.add_zone(hass, pretty_name, zone) + zone_comp.ICON_IMPORT, False) + zone.entity_id = entity_id + zone.update_ha_state() def see_beacons(dev_id, kwargs_param): """Set active beacons to the current location.""" diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py index be3249c8b00..cf8f808f82a 100644 --- a/homeassistant/components/device_tracker/thomson.py +++ b/homeassistant/components/device_tracker/thomson.py @@ -10,9 +10,11 @@ import telnetlib import threading from datetime import timedelta -from homeassistant.components.device_tracker import DOMAIN +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago. @@ -21,23 +23,24 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) _DEVICES_REGEX = re.compile( - r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' + - r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' + - r'(?P([^\s]+))\s+' + - r'(?P([^\s]+))\s+' + - r'(?P([^\s]+))\s+' + - r'(?P([^\s]+))\s+' + + r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' + r'(?P([^\s]+))\s+' + r'(?P([^\s]+))\s+' + r'(?P([^\s]+))\s+' + r'(?P([^\s]+))\s+' r'(?P([^\s]+))') +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string +}) + # pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a THOMSON scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return None - scanner = ThomsonDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None @@ -84,7 +87,7 @@ class ThomsonDeviceScanner(object): return False with self.lock: - _LOGGER.info("Checking ARP") + _LOGGER.info('Checking ARP') data = self.get_thomson_data() if not data: return False @@ -108,11 +111,11 @@ class ThomsonDeviceScanner(object): devices_result = telnet.read_until(b'=>').split(b'\r\n') telnet.write('exit\r\n'.encode('ascii')) except EOFError: - _LOGGER.exception("Unexpected response from router") + _LOGGER.exception('Unexpected response from router') return except ConnectionRefusedError: - _LOGGER.exception("Connection refused by router," + - " is telnet enabled?") + _LOGGER.exception('Connection refused by router,' + ' is telnet enabled?') return devices = {} diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index ad295099bf5..3e691a7149d 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -12,10 +12,11 @@ import threading from datetime import timedelta import requests +import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago @@ -23,26 +24,22 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string +}) + def get_scanner(hass, config): """Validate the configuration and return a TP-Link scanner.""" - if not validate_config(config, - {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, - _LOGGER): - return None + for cls in [Tplink4DeviceScanner, Tplink3DeviceScanner, + Tplink2DeviceScanner, TplinkDeviceScanner]: + scanner = cls(config[DOMAIN]) + if scanner.success_init: + return scanner - scanner = Tplink4DeviceScanner(config[DOMAIN]) - - if not scanner.success_init: - scanner = Tplink3DeviceScanner(config[DOMAIN]) - - if not scanner.success_init: - scanner = Tplink2DeviceScanner(config[DOMAIN]) - - if not scanner.success_init: - scanner = TplinkDeviceScanner(config[DOMAIN]) - - return scanner if scanner.success_init else None + return None class TplinkDeviceScanner(object): diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py old mode 100755 new mode 100644 index d39a1602ec2..3c7f81b2ed9 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -1,542 +1,543 @@ -""" -Support for local control of entities by emulating the Phillips Hue bridge. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/emulated_hue/ -""" -import threading -import socket -import logging -import json -import os -import select - -import voluptuous as vol - -from homeassistant import util, core -from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_ON -) -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS -) -from homeassistant.components.http import ( - HomeAssistantView, HomeAssistantWSGI -) -import homeassistant.helpers.config_validation as cv - -DOMAIN = 'emulated_hue' - -_LOGGER = logging.getLogger(__name__) - -CONF_HOST_IP = 'host_ip' -CONF_LISTEN_PORT = 'listen_port' -CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' -CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' -CONF_EXPOSED_DOMAINS = 'exposed_domains' - -ATTR_EMULATED_HUE = 'emulated_hue' -ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' - -DEFAULT_LISTEN_PORT = 8300 -DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene'] -DEFAULT_EXPOSE_BY_DEFAULT = True -DEFAULT_EXPOSED_DOMAINS = [ - 'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan' -] - -HUE_API_STATE_ON = 'on' -HUE_API_STATE_BRI = 'bri' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_HOST_IP): cv.string, - vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), - vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list, - vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, - vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, yaml_config): - """Activate the emulated_hue component.""" - config = Config(yaml_config) - - server = HomeAssistantWSGI( - hass, - development=False, - server_host=config.host_ip_addr, - server_port=config.listen_port, - api_password=None, - ssl_certificate=None, - ssl_key=None, - cors_origins=[] - ) - - server.register_view(DescriptionXmlView(hass, config)) - server.register_view(HueUsernameView(hass)) - server.register_view(HueLightsView(hass, config)) - - upnp_listener = UPNPResponderThread( - config.host_ip_addr, config.listen_port) - - def start_emulated_hue_bridge(event): - """Start the emulated hue bridge.""" - server.start() - upnp_listener.start() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) - - def stop_emulated_hue_bridge(event): - """Stop the emulated hue bridge.""" - upnp_listener.stop() - server.stop() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) - - return True - - -# pylint: disable=too-few-public-methods -class Config(object): - """Holds configuration variables for the emulated hue bridge.""" - - def __init__(self, yaml_config): - """Initialize the instance.""" - conf = yaml_config.get(DOMAIN, {}) - - # Get the IP address that will be passed to the Echo during discovery - self.host_ip_addr = conf.get(CONF_HOST_IP) - if self.host_ip_addr is None: - self.host_ip_addr = util.get_local_ip() - _LOGGER.warning( - "Listen IP address not specified, auto-detected address is %s", - self.host_ip_addr) - - # Get the port that the Hue bridge will listen on - self.listen_port = conf.get(CONF_LISTEN_PORT) - if not isinstance(self.listen_port, int): - self.listen_port = DEFAULT_LISTEN_PORT - _LOGGER.warning( - "Listen port not specified, defaulting to %s", - self.listen_port) - - # Get domains that cause both "on" and "off" commands to map to "on" - # This is primarily useful for things like scenes or scripts, which - # don't really have a concept of being off - self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS) - if not isinstance(self.off_maps_to_on_domains, list): - self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS - - # Get whether or not entities should be exposed by default, or if only - # explicitly marked ones will be exposed - self.expose_by_default = conf.get( - CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT) - - # Get domains that are exposed by default when expose_by_default is - # True - self.exposed_domains = conf.get( - CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) - - -class DescriptionXmlView(HomeAssistantView): - """Handles requests for the description.xml file.""" - - url = '/description.xml' - name = 'description:xml' - requires_auth = False - - def __init__(self, hass, config): - """Initialize the instance of the view.""" - super().__init__(hass) - self.config = config - - def get(self, request): - """Handle a GET request.""" - xml_template = """ - - -1 -0 - -http://{0}:{1}/ - -urn:schemas-upnp-org:device:Basic:1 -HASS Bridge ({0}) -Royal Philips Electronics -http://www.philips.com -Philips hue Personal Wireless Lighting -Philips hue bridge 2015 -BSB002 -http://www.meethue.com -1234 -uuid:2f402f80-da50-11e1-9b23-001788255acc - - -""" - - resp_text = xml_template.format( - self.config.host_ip_addr, self.config.listen_port) - - return self.Response(resp_text, mimetype='text/xml') - - -class HueUsernameView(HomeAssistantView): - """Handle requests to create a username for the emulated hue bridge.""" - - url = '/api' - name = 'hue:api' - requires_auth = False - - def __init__(self, hass): - """Initialize the instance of the view.""" - super().__init__(hass) - - def post(self, request): - """Handle a POST request.""" - data = request.json - - if 'devicetype' not in data: - return self.Response("devicetype not specified", status=400) - - json_response = [{'success': {'username': '12345678901234567890'}}] - - return self.json(json_response) - - -class HueLightsView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" - - url = '/api//lights' - name = 'api:username:lights' - extra_urls = ['/api//lights/', - '/api//lights//state'] - requires_auth = False - - def __init__(self, hass, config): - """Initialize the instance of the view.""" - super().__init__(hass) - self.config = config - self.cached_states = {} - - def get(self, request, username, entity_id=None): - """Handle a GET request.""" - if entity_id is None: - return self.get_lights_list() - - if not request.base_url.endswith('state'): - return self.get_light_state(entity_id) - - return self.Response("Method not allowed", status=405) - - def put(self, request, username, entity_id=None): - """Handle a PUT request.""" - if not request.base_url.endswith('state'): - return self.Response("Method not allowed", status=405) - - content_type = request.environ.get('CONTENT_TYPE', '') - if content_type == 'application/x-www-form-urlencoded': - # Alexa sends JSON data with a form data content type, for - # whatever reason, and Werkzeug parses form data automatically, - # so we need to do some gymnastics to get the data we need - json_data = None - - for key in request.form: - try: - json_data = json.loads(key) - break - except ValueError: - # Try the next key? - pass - - if json_data is None: - return self.Response("Bad request", status=400) - else: - json_data = request.json - - return self.put_light_state(json_data, entity_id) - - def get_lights_list(self): - """Process a request to get the list of available lights.""" - json_response = {} - - for entity in self.hass.states.all(): - if self.is_entity_exposed(entity): - json_response[entity.entity_id] = entity_to_json(entity) - - return self.json(json_response) - - def get_light_state(self, entity_id): - """Process a request to get the state of an individual light.""" - entity = self.hass.states.get(entity_id) - if entity is None or not self.is_entity_exposed(entity): - return self.Response("Entity not found", status=404) - - cached_state = self.cached_states.get(entity_id, None) - - if cached_state is None: - final_state = entity.state == STATE_ON - final_brightness = entity.attributes.get( - ATTR_BRIGHTNESS, 255 if final_state else 0) - else: - final_state, final_brightness = cached_state - - json_response = entity_to_json(entity, final_state, final_brightness) - - return self.json(json_response) - - def put_light_state(self, request_json, entity_id): - """Process a request to set the state of an individual light.""" - config = self.config - - # Retrieve the entity from the state machine - entity = self.hass.states.get(entity_id) - if entity is None: - return self.Response("Entity not found", status=404) - - if not self.is_entity_exposed(entity): - return self.Response("Entity not found", status=404) - - # Parse the request into requested "on" status and brightness - parsed = parse_hue_api_put_light_body(request_json, entity) - - if parsed is None: - return self.Response("Bad request", status=400) - - result, brightness = parsed - - # Convert the resulting "on" status into the service we need to call - service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF - - # Construct what we need to send to the service - data = {ATTR_ENTITY_ID: entity_id} - - if brightness is not None: - data[ATTR_BRIGHTNESS] = brightness - - if entity.domain.lower() in config.off_maps_to_on_domains: - # Map the off command to on - service = SERVICE_TURN_ON - - # Caching is required because things like scripts and scenes won't - # report as "off" to Alexa if an "off" command is received, because - # they'll map to "on". Thus, instead of reporting its actual - # status, we report what Alexa will want to see, which is the same - # as the actual requested command. - self.cached_states[entity_id] = (result, brightness) - - # Perform the requested action - self.hass.services.call(core.DOMAIN, service, data, blocking=True) - - json_response = \ - [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] - - if brightness is not None: - json_response.append(create_hue_success_response( - entity_id, HUE_API_STATE_BRI, brightness)) - - return self.json(json_response) - - def is_entity_exposed(self, entity): - """Determine if an entity should be exposed on the emulated bridge.""" - config = self.config - - if entity.attributes.get('view') is not None: - # Ignore entities that are views - return False - - domain = entity.domain.lower() - explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None) - - domain_exposed_by_default = \ - config.expose_by_default and domain in config.exposed_domains - - # Expose an entity if the entity's domain is exposed by default and - # the configuration doesn't explicitly exclude it from being - # exposed, or if the entity is explicitly exposed - is_default_exposed = \ - domain_exposed_by_default and explicit_expose is not False - - return is_default_exposed or explicit_expose - - -def parse_hue_api_put_light_body(request_json, entity): - """Parse the body of a request to change the state of a light.""" - if HUE_API_STATE_ON in request_json: - if not isinstance(request_json[HUE_API_STATE_ON], bool): - return None - - if request_json['on']: - # Echo requested device be turned on - brightness = None - report_brightness = False - result = True - else: - # Echo requested device be turned off - brightness = None - report_brightness = False - result = False - - if HUE_API_STATE_BRI in request_json: - # Make sure the entity actually supports brightness - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: - try: - # Clamp brightness from 0 to 255 - brightness = \ - max(0, min(int(request_json[HUE_API_STATE_BRI]), 255)) - except ValueError: - return None - - report_brightness = True - result = (brightness > 0) - - return (result, brightness) if report_brightness else (result, None) - - -def entity_to_json(entity, is_on=None, brightness=None): - """Convert an entity to its Hue bridge JSON representation.""" - if is_on is None: - is_on = entity.state == STATE_ON - - if brightness is None: - brightness = 255 if is_on else 0 - - name = entity.attributes.get( - ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME]) - - return { - 'state': - { - HUE_API_STATE_ON: is_on, - HUE_API_STATE_BRI: brightness, - 'reachable': True - }, - 'type': 'Dimmable light', - 'name': name, - 'modelid': 'HASS123', - 'uniqueid': entity.entity_id, - 'swversion': '123' - } - - -def create_hue_success_response(entity_id, attr, value): - """Create a success response for an attribute set on a light.""" - success_key = '/lights/{}/state/{}'.format(entity_id, attr) - return {'success': {success_key: value}} - - -class UPNPResponderThread(threading.Thread): - """Handle responding to UPNP/SSDP discovery requests.""" - - _interrupted = False - - def __init__(self, host_ip_addr, listen_port): - """Initialize the class.""" - threading.Thread.__init__(self) - - self.host_ip_addr = host_ip_addr - self.listen_port = listen_port - - # Note that the double newline at the end of - # this string is required per the SSDP spec - resp_template = """HTTP/1.1 200 OK -CACHE-CONTROL: max-age=60 -EXT: -LOCATION: http://{0}:{1}/description.xml -SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1 -ST: urn:schemas-upnp-org:device:basic:1 -USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 - -""" - - self.upnp_response = resp_template.format(host_ip_addr, listen_port) \ - .replace("\n", "\r\n") \ - .encode('utf-8') - - # Set up a pipe for signaling to the receiver that it's time to - # shutdown. Essentially, we place the SSDP socket into nonblocking - # mode and use select() to wait for data to arrive on either the SSDP - # socket or the pipe. If data arrives on either one, select() returns - # and tells us which filenos have data ready to read. - # - # When we want to stop the responder, we write data to the pipe, which - # causes the select() to return and indicate that said pipe has data - # ready to be read, which indicates to us that the responder needs to - # be shutdown. - self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe() - - def run(self): - """Run the server.""" - # Listen for UDP port 1900 packets sent to SSDP multicast address - ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - ssdp_socket.setblocking(False) - - # Required for receiving multicast - ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - ssdp_socket.setsockopt( - socket.SOL_IP, - socket.IP_MULTICAST_IF, - socket.inet_aton(self.host_ip_addr)) - - ssdp_socket.setsockopt( - socket.SOL_IP, - socket.IP_ADD_MEMBERSHIP, - socket.inet_aton("239.255.255.250") + - socket.inet_aton(self.host_ip_addr)) - - ssdp_socket.bind(("239.255.255.250", 1900)) - - while True: - if self._interrupted: - clean_socket_close(ssdp_socket) - return - - try: - read, _, _ = select.select( - [self._interrupted_read_pipe, ssdp_socket], [], - [ssdp_socket]) - - if self._interrupted_read_pipe in read: - # Implies self._interrupted is True - clean_socket_close(ssdp_socket) - return - elif ssdp_socket in read: - data, addr = ssdp_socket.recvfrom(1024) - else: - continue - except socket.error as ex: - if self._interrupted: - clean_socket_close(ssdp_socket) - return - - _LOGGER.error("UPNP Responder socket exception occured: %s", - ex.__str__) - - if "M-SEARCH" in data.decode('utf-8'): - # SSDP M-SEARCH method received, respond to it with our info - resp_socket = socket.socket( - socket.AF_INET, socket.SOCK_DGRAM) - - resp_socket.sendto(self.upnp_response, addr) - resp_socket.close() - - def stop(self): - """Stop the server.""" - # Request for server - self._interrupted = True - os.write(self._interrupted_write_pipe, bytes([0])) - self.join() - - -def clean_socket_close(sock): - """Close a socket connection and logs its closure.""" - _LOGGER.info("UPNP responder shutting down.") - - sock.close() +""" +Support for local control of entities by emulating the Phillips Hue bridge. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/emulated_hue/ +""" +import threading +import socket +import logging +import json +import os +import select + +import voluptuous as vol + +from homeassistant import util, core +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + STATE_ON, HTTP_BAD_REQUEST +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS +) +from homeassistant.components.http import ( + HomeAssistantView, HomeAssistantWSGI +) +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'emulated_hue' + +_LOGGER = logging.getLogger(__name__) + +CONF_HOST_IP = 'host_ip' +CONF_LISTEN_PORT = 'listen_port' +CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains' +CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' +CONF_EXPOSED_DOMAINS = 'exposed_domains' + +ATTR_EMULATED_HUE = 'emulated_hue' +ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' + +DEFAULT_LISTEN_PORT = 8300 +DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene'] +DEFAULT_EXPOSE_BY_DEFAULT = True +DEFAULT_EXPOSED_DOMAINS = [ + 'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan' +] + +HUE_API_STATE_ON = 'on' +HUE_API_STATE_BRI = 'bri' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_HOST_IP): cv.string, + vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list, + vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean, + vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, yaml_config): + """Activate the emulated_hue component.""" + config = Config(yaml_config) + + server = HomeAssistantWSGI( + hass, + development=False, + server_host=config.host_ip_addr, + server_port=config.listen_port, + api_password=None, + ssl_certificate=None, + ssl_key=None, + cors_origins=[], + approved_ips=[] + ) + + server.register_view(DescriptionXmlView(hass, config)) + server.register_view(HueUsernameView(hass)) + server.register_view(HueLightsView(hass, config)) + + upnp_listener = UPNPResponderThread( + config.host_ip_addr, config.listen_port) + + def start_emulated_hue_bridge(event): + """Start the emulated hue bridge.""" + server.start() + upnp_listener.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) + + def stop_emulated_hue_bridge(event): + """Stop the emulated hue bridge.""" + upnp_listener.stop() + server.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) + + return True + + +# pylint: disable=too-few-public-methods +class Config(object): + """Holds configuration variables for the emulated hue bridge.""" + + def __init__(self, yaml_config): + """Initialize the instance.""" + conf = yaml_config.get(DOMAIN, {}) + + # Get the IP address that will be passed to the Echo during discovery + self.host_ip_addr = conf.get(CONF_HOST_IP) + if self.host_ip_addr is None: + self.host_ip_addr = util.get_local_ip() + _LOGGER.warning( + "Listen IP address not specified, auto-detected address is %s", + self.host_ip_addr) + + # Get the port that the Hue bridge will listen on + self.listen_port = conf.get(CONF_LISTEN_PORT) + if not isinstance(self.listen_port, int): + self.listen_port = DEFAULT_LISTEN_PORT + _LOGGER.warning( + "Listen port not specified, defaulting to %s", + self.listen_port) + + # Get domains that cause both "on" and "off" commands to map to "on" + # This is primarily useful for things like scenes or scripts, which + # don't really have a concept of being off + self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS) + if not isinstance(self.off_maps_to_on_domains, list): + self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS + + # Get whether or not entities should be exposed by default, or if only + # explicitly marked ones will be exposed + self.expose_by_default = conf.get( + CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT) + + # Get domains that are exposed by default when expose_by_default is + # True + self.exposed_domains = conf.get( + CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) + + +class DescriptionXmlView(HomeAssistantView): + """Handles requests for the description.xml file.""" + + url = '/description.xml' + name = 'description:xml' + requires_auth = False + + def __init__(self, hass, config): + """Initialize the instance of the view.""" + super().__init__(hass) + self.config = config + + def get(self, request): + """Handle a GET request.""" + xml_template = """ + + +1 +0 + +http://{0}:{1}/ + +urn:schemas-upnp-org:device:Basic:1 +HASS Bridge ({0}) +Royal Philips Electronics +http://www.philips.com +Philips hue Personal Wireless Lighting +Philips hue bridge 2015 +BSB002 +http://www.meethue.com +1234 +uuid:2f402f80-da50-11e1-9b23-001788255acc + + +""" + + resp_text = xml_template.format( + self.config.host_ip_addr, self.config.listen_port) + + return self.Response(resp_text, mimetype='text/xml') + + +class HueUsernameView(HomeAssistantView): + """Handle requests to create a username for the emulated hue bridge.""" + + url = '/api' + name = 'hue:api' + extra_urls = ['/api/'] + requires_auth = False + + def __init__(self, hass): + """Initialize the instance of the view.""" + super().__init__(hass) + + def post(self, request): + """Handle a POST request.""" + data = request.json + + if 'devicetype' not in data: + return self.json_message('devicetype not specified', + HTTP_BAD_REQUEST) + + return self.json([{'success': {'username': '12345678901234567890'}}]) + + +class HueLightsView(HomeAssistantView): + """Handle requests for getting and setting info about entities.""" + + url = '/api//lights' + name = 'api:username:lights' + extra_urls = ['/api//lights/', + '/api//lights//state'] + requires_auth = False + + def __init__(self, hass, config): + """Initialize the instance of the view.""" + super().__init__(hass) + self.config = config + self.cached_states = {} + + def get(self, request, username, entity_id=None): + """Handle a GET request.""" + if entity_id is None: + return self.get_lights_list() + + if not request.base_url.endswith('state'): + return self.get_light_state(entity_id) + + return self.Response("Method not allowed", status=405) + + def put(self, request, username, entity_id=None): + """Handle a PUT request.""" + if not request.base_url.endswith('state'): + return self.Response("Method not allowed", status=405) + + content_type = request.environ.get('CONTENT_TYPE', '') + if content_type == 'application/x-www-form-urlencoded': + # Alexa sends JSON data with a form data content type, for + # whatever reason, and Werkzeug parses form data automatically, + # so we need to do some gymnastics to get the data we need + json_data = None + + for key in request.form: + try: + json_data = json.loads(key) + break + except ValueError: + # Try the next key? + pass + + if json_data is None: + return self.Response("Bad request", status=400) + else: + json_data = request.json + + return self.put_light_state(json_data, entity_id) + + def get_lights_list(self): + """Process a request to get the list of available lights.""" + json_response = {} + + for entity in self.hass.states.all(): + if self.is_entity_exposed(entity): + json_response[entity.entity_id] = entity_to_json(entity) + + return self.json(json_response) + + def get_light_state(self, entity_id): + """Process a request to get the state of an individual light.""" + entity = self.hass.states.get(entity_id) + if entity is None or not self.is_entity_exposed(entity): + return self.Response("Entity not found", status=404) + + cached_state = self.cached_states.get(entity_id, None) + + if cached_state is None: + final_state = entity.state == STATE_ON + final_brightness = entity.attributes.get( + ATTR_BRIGHTNESS, 255 if final_state else 0) + else: + final_state, final_brightness = cached_state + + json_response = entity_to_json(entity, final_state, final_brightness) + + return self.json(json_response) + + def put_light_state(self, request_json, entity_id): + """Process a request to set the state of an individual light.""" + config = self.config + + # Retrieve the entity from the state machine + entity = self.hass.states.get(entity_id) + if entity is None: + return self.Response("Entity not found", status=404) + + if not self.is_entity_exposed(entity): + return self.Response("Entity not found", status=404) + + # Parse the request into requested "on" status and brightness + parsed = parse_hue_api_put_light_body(request_json, entity) + + if parsed is None: + return self.Response("Bad request", status=400) + + result, brightness = parsed + + # Convert the resulting "on" status into the service we need to call + service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF + + # Construct what we need to send to the service + data = {ATTR_ENTITY_ID: entity_id} + + if brightness is not None: + data[ATTR_BRIGHTNESS] = brightness + + if entity.domain.lower() in config.off_maps_to_on_domains: + # Map the off command to on + service = SERVICE_TURN_ON + + # Caching is required because things like scripts and scenes won't + # report as "off" to Alexa if an "off" command is received, because + # they'll map to "on". Thus, instead of reporting its actual + # status, we report what Alexa will want to see, which is the same + # as the actual requested command. + self.cached_states[entity_id] = (result, brightness) + + # Perform the requested action + self.hass.services.call(core.DOMAIN, service, data, blocking=True) + + json_response = \ + [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] + + if brightness is not None: + json_response.append(create_hue_success_response( + entity_id, HUE_API_STATE_BRI, brightness)) + + return self.json(json_response) + + def is_entity_exposed(self, entity): + """Determine if an entity should be exposed on the emulated bridge.""" + config = self.config + + if entity.attributes.get('view') is not None: + # Ignore entities that are views + return False + + domain = entity.domain.lower() + explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None) + + domain_exposed_by_default = \ + config.expose_by_default and domain in config.exposed_domains + + # Expose an entity if the entity's domain is exposed by default and + # the configuration doesn't explicitly exclude it from being + # exposed, or if the entity is explicitly exposed + is_default_exposed = \ + domain_exposed_by_default and explicit_expose is not False + + return is_default_exposed or explicit_expose + + +def parse_hue_api_put_light_body(request_json, entity): + """Parse the body of a request to change the state of a light.""" + if HUE_API_STATE_ON in request_json: + if not isinstance(request_json[HUE_API_STATE_ON], bool): + return None + + if request_json['on']: + # Echo requested device be turned on + brightness = None + report_brightness = False + result = True + else: + # Echo requested device be turned off + brightness = None + report_brightness = False + result = False + + if HUE_API_STATE_BRI in request_json: + # Make sure the entity actually supports brightness + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS: + try: + # Clamp brightness from 0 to 255 + brightness = \ + max(0, min(int(request_json[HUE_API_STATE_BRI]), 255)) + except ValueError: + return None + + report_brightness = True + result = (brightness > 0) + + return (result, brightness) if report_brightness else (result, None) + + +def entity_to_json(entity, is_on=None, brightness=None): + """Convert an entity to its Hue bridge JSON representation.""" + if is_on is None: + is_on = entity.state == STATE_ON + + if brightness is None: + brightness = 255 if is_on else 0 + + name = entity.attributes.get( + ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME]) + + return { + 'state': + { + HUE_API_STATE_ON: is_on, + HUE_API_STATE_BRI: brightness, + 'reachable': True + }, + 'type': 'Dimmable light', + 'name': name, + 'modelid': 'HASS123', + 'uniqueid': entity.entity_id, + 'swversion': '123' + } + + +def create_hue_success_response(entity_id, attr, value): + """Create a success response for an attribute set on a light.""" + success_key = '/lights/{}/state/{}'.format(entity_id, attr) + return {'success': {success_key: value}} + + +class UPNPResponderThread(threading.Thread): + """Handle responding to UPNP/SSDP discovery requests.""" + + _interrupted = False + + def __init__(self, host_ip_addr, listen_port): + """Initialize the class.""" + threading.Thread.__init__(self) + + self.host_ip_addr = host_ip_addr + self.listen_port = listen_port + + # Note that the double newline at the end of + # this string is required per the SSDP spec + resp_template = """HTTP/1.1 200 OK +CACHE-CONTROL: max-age=60 +EXT: +LOCATION: http://{0}:{1}/description.xml +SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1 +ST: urn:schemas-upnp-org:device:basic:1 +USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 + +""" + + self.upnp_response = resp_template.format(host_ip_addr, listen_port) \ + .replace("\n", "\r\n") \ + .encode('utf-8') + + # Set up a pipe for signaling to the receiver that it's time to + # shutdown. Essentially, we place the SSDP socket into nonblocking + # mode and use select() to wait for data to arrive on either the SSDP + # socket or the pipe. If data arrives on either one, select() returns + # and tells us which filenos have data ready to read. + # + # When we want to stop the responder, we write data to the pipe, which + # causes the select() to return and indicate that said pipe has data + # ready to be read, which indicates to us that the responder needs to + # be shutdown. + self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe() + + def run(self): + """Run the server.""" + # Listen for UDP port 1900 packets sent to SSDP multicast address + ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + ssdp_socket.setblocking(False) + + # Required for receiving multicast + ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + ssdp_socket.setsockopt( + socket.SOL_IP, + socket.IP_MULTICAST_IF, + socket.inet_aton(self.host_ip_addr)) + + ssdp_socket.setsockopt( + socket.SOL_IP, + socket.IP_ADD_MEMBERSHIP, + socket.inet_aton("239.255.255.250") + + socket.inet_aton(self.host_ip_addr)) + + ssdp_socket.bind(("239.255.255.250", 1900)) + + while True: + if self._interrupted: + clean_socket_close(ssdp_socket) + return + + try: + read, _, _ = select.select( + [self._interrupted_read_pipe, ssdp_socket], [], + [ssdp_socket]) + + if self._interrupted_read_pipe in read: + # Implies self._interrupted is True + clean_socket_close(ssdp_socket) + return + elif ssdp_socket in read: + data, addr = ssdp_socket.recvfrom(1024) + else: + continue + except socket.error as ex: + if self._interrupted: + clean_socket_close(ssdp_socket) + return + + _LOGGER.error("UPNP Responder socket exception occured: %s", + ex.__str__) + + if "M-SEARCH" in data.decode('utf-8'): + # SSDP M-SEARCH method received, respond to it with our info + resp_socket = socket.socket( + socket.AF_INET, socket.SOCK_DGRAM) + + resp_socket.sendto(self.upnp_response, addr) + resp_socket.close() + + def stop(self): + """Stop the server.""" + # Request for server + self._interrupted = True + os.write(self._interrupted_write_pipe, bytes([0])) + self.join() + + +def clean_socket_close(sock): + """Close a socket connection and logs its closure.""" + _LOGGER.info("UPNP responder shutting down.") + + sock.close() diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index eea06307f7d..3e947e55a4d 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -12,7 +12,7 @@ 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'] +REQUIREMENTS = ['pyenvisalink==1.7', 'pydispatcher==2.0.5'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'envisalink' @@ -34,12 +34,14 @@ CONF_PARTITIONS = 'partitions' CONF_ZONENAME = 'name' CONF_ZONETYPE = 'type' CONF_PARTITIONNAME = 'name' +CONF_PANIC = 'panic_type' DEFAULT_PORT = 4025 DEFAULT_EVL_VERSION = 3 DEFAULT_KEEPALIVE = 60 DEFAULT_ZONEDUMP_INTERVAL = 30 DEFAULT_ZONETYPE = 'opening' +DEFAULT_PANIC = 'Police' SIGNAL_ZONE_UPDATE = 'zones_updated' SIGNAL_PARTITION_UPDATE = 'partition_updated' @@ -60,6 +62,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASS): cv.string, vol.Required(CONF_CODE): cv.string, + vol.Optional(CONF_PANIC, default=DEFAULT_PANIC): 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): cv.port, @@ -89,6 +92,7 @@ def setup(hass, base_config): _port = config.get(CONF_EVL_PORT) _code = config.get(CONF_CODE) _panel_type = config.get(CONF_PANEL_TYPE) + _panic_type = config.get(CONF_PANIC) _version = config.get(CONF_EVL_VERSION) _user = config.get(CONF_USERNAME) _pass = config.get(CONF_PASS) @@ -104,7 +108,8 @@ def setup(hass, base_config): _user, _pass, _zone_dump, - _keep_alive) + _keep_alive, + hass.loop) def login_fail_callback(data): """Callback for when the evl rejects our login.""" @@ -149,7 +154,7 @@ def setup(hass, base_config): def start_envisalink(event): """Startup process for the Envisalink.""" - EVL_CONTROLLER.start() + hass.loop.call_soon_threadsafe(EVL_CONTROLLER.start) for _ in range(10): if 'success' in _connect_status: hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) @@ -177,14 +182,15 @@ def setup(hass, base_config): # Load sub-components for Envisalink if _partitions: load_platform(hass, 'alarm_control_panel', 'envisalink', - {'partitions': _partitions, - 'code': _code}, config) + {CONF_PARTITIONS: _partitions, + CONF_CODE: _code, + CONF_PANIC: _panic_type}, config) load_platform(hass, 'sensor', 'envisalink', - {'partitions': _partitions, - 'code': _code}, config) + {CONF_PARTITIONS: _partitions, + CONF_CODE: _code}, config) if _zones: load_platform(hass, 'binary_sensor', 'envisalink', - {'zones': _zones}, config) + {CONF_ZONES: _zones}, config) return True diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py new file mode 100644 index 00000000000..2deb938d337 --- /dev/null +++ b/homeassistant/components/fan/isy994.py @@ -0,0 +1,120 @@ +""" +Support for ISY994 fans. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/fan.isy994/ +""" +import logging +from typing import Callable + +from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF, + SPEED_LOW, SPEED_MED, + SPEED_HIGH) +import homeassistant.components.isy994 as isy +from homeassistant.const import STATE_UNKNOWN, STATE_ON, STATE_OFF +from homeassistant.helpers.typing import ConfigType + +_LOGGER = logging.getLogger(__name__) + +VALUE_TO_STATE = { + 0: SPEED_OFF, + 63: SPEED_LOW, + 64: SPEED_LOW, + 190: SPEED_MED, + 191: SPEED_MED, + 255: SPEED_HIGH, +} + +STATE_TO_VALUE = {} +for key in VALUE_TO_STATE: + STATE_TO_VALUE[VALUE_TO_STATE[key]] = key + +STATES = [SPEED_OFF, SPEED_LOW, SPEED_MED, SPEED_HIGH] + + +# pylint: disable=unused-argument +def setup_platform(hass, config: ConfigType, + add_devices: Callable[[list], None], discovery_info=None): + """Setup the ISY994 fan platform.""" + if isy.ISY is None or not isy.ISY.connected: + _LOGGER.error('A connection has not been made to the ISY controller.') + return False + + devices = [] + + for node in isy.filter_nodes(isy.NODES, states=STATES): + devices.append(ISYFanDevice(node)) + + for program in isy.PROGRAMS.get(DOMAIN, []): + try: + status = program[isy.KEY_STATUS] + actions = program[isy.KEY_ACTIONS] + assert actions.dtype == 'program', 'Not a program' + except (KeyError, AssertionError): + pass + else: + devices.append(ISYFanProgram(program.name, status, actions)) + + add_devices(devices) + + +class ISYFanDevice(isy.ISYDevice, FanEntity): + """Representation of an ISY994 fan device.""" + + def __init__(self, node) -> None: + """Initialize the ISY994 fan device.""" + isy.ISYDevice.__init__(self, node) + self.speed = self.state + + @property + def state(self) -> str: + """Get the state of the ISY994 fan device.""" + return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN) + + def set_speed(self, speed: str) -> None: + """Send the set speed command to the ISY994 fan device.""" + if not self._node.on(val=STATE_TO_VALUE.get(speed, 0)): + _LOGGER.debug('Unable to set fan speed') + else: + self.speed = self.state + + def turn_on(self, speed: str=None, **kwargs) -> None: + """Send the turn on command to the ISY994 fan device.""" + self.set_speed(speed) + + def turn_off(self, **kwargs) -> None: + """Send the turn off command to the ISY994 fan device.""" + if not self._node.off(): + _LOGGER.debug('Unable to set fan speed') + else: + self.speed = self.state + + +class ISYFanProgram(ISYFanDevice): + """Representation of an ISY994 fan program.""" + + def __init__(self, name: str, node, actions) -> None: + """Initialize the ISY994 fan program.""" + ISYFanDevice.__init__(self, node) + self._name = name + self._actions = actions + self.speed = STATE_ON if self.is_on else STATE_OFF + + @property + def state(self) -> str: + """Get the state of the ISY994 fan program.""" + return STATE_ON if bool(self.value) else STATE_OFF + + def turn_off(self, **kwargs) -> None: + """Send the turn on command to ISY994 fan program.""" + if not self._actions.runThen(): + _LOGGER.error('Unable to open the cover') + else: + self.speed = STATE_ON if self.is_on else STATE_OFF + + def turn_on(self, **kwargs) -> None: + """Send the turn off command to ISY994 fan program.""" + if not self._actions.runElse(): + _LOGGER.error('Unable to close the cover') + else: + self.speed = STATE_ON if self.is_on else STATE_OFF diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 9d824a715c2..b6703e22c19 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -5,17 +5,16 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.mqtt/ """ import logging -from functools import partial import voluptuous as vol import homeassistant.components.mqtt as mqtt -from homeassistant.const import (CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, - STATE_ON, STATE_OFF) +from homeassistant.const import ( + CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, + CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template import render_with_possible_json_value from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, @@ -23,33 +22,31 @@ from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_MEDIUM, _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ["mqtt"] +DEPENDENCIES = ['mqtt'] -CONF_STATE_VALUE_TEMPLATE = "state_value_template" -CONF_SPEED_STATE_TOPIC = "speed_state_topic" -CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" -CONF_SPEED_VALUE_TEMPLATE = "speed_value_template" -CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" -CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" -CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" -CONF_PAYLOAD_ON = "payload_on" -CONF_PAYLOAD_OFF = "payload_off" -CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on" -CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" -CONF_PAYLOAD_LOW_SPEED = "payload_low_speed" -CONF_PAYLOAD_MEDIUM_SPEED = "payload_medium_speed" -CONF_PAYLOAD_HIGH_SPEED = "payload_high_speed" -CONF_SPEED_LIST = "speeds" +CONF_STATE_VALUE_TEMPLATE = 'state_value_template' +CONF_SPEED_STATE_TOPIC = 'speed_state_topic' +CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic' +CONF_SPEED_VALUE_TEMPLATE = 'speed_value_template' +CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic' +CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic' +CONF_OSCILLATION_VALUE_TEMPLATE = 'oscillation_value_template' +CONF_PAYLOAD_OSCILLATION_ON = 'payload_oscillation_on' +CONF_PAYLOAD_OSCILLATION_OFF = 'payload_oscillation_off' +CONF_PAYLOAD_LOW_SPEED = 'payload_low_speed' +CONF_PAYLOAD_MEDIUM_SPEED = 'payload_medium_speed' +CONF_PAYLOAD_HIGH_SPEED = 'payload_high_speed' +CONF_SPEED_LIST = 'speeds' -DEFAULT_NAME = "MQTT Fan" -DEFAULT_PAYLOAD_ON = "ON" -DEFAULT_PAYLOAD_OFF = "OFF" +DEFAULT_NAME = 'MQTT Fan' +DEFAULT_PAYLOAD_ON = 'ON' +DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False -OSCILLATE_ON_PAYLOAD = "oscillate_on" -OSCILLATE_OFF_PAYLOAD = "oscillate_off" +OSCILLATE_ON_PAYLOAD = 'oscillate_on' +OSCILLATE_OFF_PAYLOAD = 'oscillate_off' -OSCILLATION = "oscillation" +OSCILLATION = 'oscillation' PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -77,11 +74,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup MQTT fan platform.""" - add_devices_callback([MqttFan( + add_devices([MqttFan( hass, - config[CONF_NAME], + config.get(CONF_NAME), { key: config.get(key) for key in ( CONF_STATE_TOPIC, @@ -97,19 +94,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE), OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE) }, - config[CONF_QOS], - config[CONF_RETAIN], + config.get(CONF_QOS), + config.get(CONF_RETAIN), { - STATE_ON: config[CONF_PAYLOAD_ON], - STATE_OFF: config[CONF_PAYLOAD_OFF], - OSCILLATE_ON_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_ON], - OSCILLATE_OFF_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_OFF], - SPEED_LOW: config[CONF_PAYLOAD_LOW_SPEED], - SPEED_MEDIUM: config[CONF_PAYLOAD_MEDIUM_SPEED], - SPEED_HIGH: config[CONF_PAYLOAD_HIGH_SPEED], + STATE_ON: config.get(CONF_PAYLOAD_ON), + STATE_OFF: config.get(CONF_PAYLOAD_OFF), + OSCILLATE_ON_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_ON), + OSCILLATE_OFF_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_OFF), + SPEED_LOW: config.get(CONF_PAYLOAD_LOW_SPEED), + SPEED_MEDIUM: config.get(CONF_PAYLOAD_MEDIUM_SPEED), + SPEED_HIGH: config.get(CONF_PAYLOAD_HIGH_SPEED), }, - config[CONF_SPEED_LIST], - config[CONF_OPTIMISTIC], + config.get(CONF_SPEED_LIST), + config.get(CONF_OPTIMISTIC), )]) @@ -120,7 +117,7 @@ class MqttFan(FanEntity): # pylint: disable=too-many-arguments def __init__(self, hass, name, topic, templates, qos, retain, payload, speed_list, optimistic): - """Initialize MQTT fan.""" + """Initialize the MQTT fan.""" self._hass = hass self._name = name self._topic = topic @@ -129,11 +126,10 @@ class MqttFan(FanEntity): self._payload = payload self._speed_list = speed_list self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None - self._optimistic_oscillation = (optimistic or - topic[CONF_OSCILLATION_STATE_TOPIC] - is None) - self._optimistic_speed = (optimistic or - topic[CONF_SPEED_STATE_TOPIC] is None) + self._optimistic_oscillation = ( + optimistic or topic[CONF_OSCILLATION_STATE_TOPIC] is None) + self._optimistic_speed = ( + optimistic or topic[CONF_SPEED_STATE_TOPIC] is None) self._state = False self._supported_features = 0 self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC] @@ -141,9 +137,12 @@ class MqttFan(FanEntity): self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) - templates = {key: ((lambda value: value) if tpl is None else - partial(render_with_possible_json_value, hass, tpl)) - for key, tpl in templates.items()} + for key, tpl in list(templates.items()): + if tpl is None: + templates[key] = lambda value: value + else: + tpl.hass = hass + templates[key] = tpl.render_with_possible_json_value def state_received(topic, payload, qos): """A new MQTT message has been received.""" diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py new file mode 100644 index 00000000000..0ba015a4660 --- /dev/null +++ b/homeassistant/components/ffmpeg.py @@ -0,0 +1,70 @@ +""" +Component that will help set the ffmpeg component. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ffmpeg/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'ffmpeg' +REQUIREMENTS = ["ha-ffmpeg==0.13"] + +_LOGGER = logging.getLogger(__name__) + +CONF_INPUT = 'input' +CONF_FFMPEG_BIN = 'ffmpeg_bin' +CONF_EXTRA_ARGUMENTS = 'extra_arguments' +CONF_OUTPUT = 'output' +CONF_RUN_TEST = 'run_test' + +DEFAULT_BINARY = 'ffmpeg' +DEFAULT_RUN_TEST = True + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_FFMPEG_BIN, default=DEFAULT_BINARY): cv.string, + vol.Optional(CONF_RUN_TEST, default=DEFAULT_RUN_TEST): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +FFMPEG_CONFIG = { + CONF_FFMPEG_BIN: DEFAULT_BINARY, + CONF_RUN_TEST: DEFAULT_RUN_TEST, +} +FFMPEG_TEST_CACHE = {} + + +def setup(hass, config): + """Setup the FFmpeg component.""" + if DOMAIN in config: + FFMPEG_CONFIG.update(config.get(DOMAIN)) + return True + + +def get_binary(): + """Return ffmpeg binary from config.""" + return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN) + + +def run_test(input_source): + """Run test on this input. TRUE is deactivate or run correct.""" + from haffmpeg import Test + + if FFMPEG_CONFIG.get(CONF_RUN_TEST): + # if in cache + if input_source in FFMPEG_TEST_CACHE: + return FFMPEG_TEST_CACHE[input_source] + + # run test + test = Test(get_binary()) + if not test.run_test(input_source): + _LOGGER.error("FFmpeg '%s' test fails!", input_source) + FFMPEG_TEST_CACHE[input_source] = False + return False + FFMPEG_TEST_CACHE[input_source] = True + return True diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 796d91cade8..151bc954cfa 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,14 +1,14 @@ """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" FINGERPRINTS = { - "core.js": "1fd10c1fcdf56a61f60cf861d5a0368c", - "frontend.html": "20defe06c11b2fa2f076dc92b6c3b0dd", - "mdi.html": "710b84acc99b32514f52291aba9cd8e8", - "panels/ha-panel-dev-event.html": "3cc881ae8026c0fba5aa67d334a3ab2b", + "core.js": "78862c0984279b6876f594ffde45177c", + "frontend.html": "c1753e1ce530f978036742477c96d2fd", + "mdi.html": "6bd013a8252e19b3c1f1de52994cfbe4", + "panels/ha-panel-dev-event.html": "c4a5f70eece9f92616a65e8d26be803e", "panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169", - "panels/ha-panel-dev-service.html": "bb5c587ada694e0fd42ceaaedd6fe6aa", - "panels/ha-panel-dev-state.html": "4608326978256644c42b13940c028e0a", - "panels/ha-panel-dev-template.html": "0a099d4589636ed3038a3e9f020468a7", + "panels/ha-panel-dev-service.html": "07e83c6b7f79d78a59258f6dba477b54", + "panels/ha-panel-dev-state.html": "fd8eb946856b1346a87a51d0c86854ff", + "panels/ha-panel-dev-template.html": "7cff8a2ef3f44fdaf357a0d41696bf6d", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40", diff --git a/homeassistant/components/frontend/www_static/core.js b/homeassistant/components/frontend/www_static/core.js index 336aab04ffe..d6ab58ba0ef 100644 --- a/homeassistant/components/frontend/www_static/core.js +++ b/homeassistant/components/frontend/www_static/core.js @@ -1,4 +1,4 @@ -!function(){"use strict";function t(t){return t&&"object"==typeof t&&"default"in t?t["default"]:t}function e(t,e){return e={exports:{}},t(e,e.exports),e.exports}function n(t,e){var n=e.authToken,r=e.host;return je({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function r(){return Ne.getInitialState()}function i(t,e){var n=e.errorMessage;return t.withMutations(function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)})}function o(t,e){var n=e.authToken,r=e.host;return Ue({authToken:n,host:r})}function u(){return Pe.getInitialState()}function a(t,e){var n=e.rememberAuth;return n}function s(t){return t.withMutations(function(t){t.set("isStreaming",!0).set("useStreaming",!0).set("hasError",!1)})}function c(t){return t.withMutations(function(t){t.set("isStreaming",!1).set("useStreaming",!1).set("hasError",!1)})}function f(t){return t.withMutations(function(t){t.set("isStreaming",!1).set("hasError",!0)})}function h(){return Ke.getInitialState()}function l(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?$e({}):t.get(o),a=Array.isArray(r)?r:[r],s=n.fromJSON||$e;return t.set(o,u.withMutations(function(t){for(var e=0;e199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function A(t,e){var n=e.message;return t.set(t.size,n)}function D(){return Dn.getInitialState()}function C(t,e){t.dispatch(wn.NOTIFICATION_CREATED,{message:e})}function z(t){t.registerStores({notifications:Dn})}function R(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 L(t,e){return!!t&&("group"===t.domain?"on"===t.state||"off"===t.state:R(t.domain,e))}function M(t,e){return[er(t),function(t){return!!t&&t.services.has(e)}]}function j(t){return[bn.byId(t),tr,L]}function N(t,e,n){function r(){var c=(new Date).getTime()-a;c0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function k(t,e){var n=e.component;return t.push(n)}function U(t,e){var n=e.components;return pr(n)}function P(){return _r.getInitialState()}function H(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 vr({latitude:n,longitude:r,location_name:i,temperature_unit:o,time_zone:u,serverVersion:a})}function x(){return yr.getInitialState()}function V(t,e){t.dispatch(hr.SERVER_CONFIG_LOADED,e)}function q(t){fn(t,"GET","config").then(function(e){return V(t,e)})}function F(t,e){t.dispatch(hr.COMPONENT_LOADED,{component:e})}function G(t){return[["serverComponent"],function(e){return e.contains(t)}]}function K(t){t.registerStores({serverComponent:_r,serverConfig:yr})}function Y(t,e){var n=e.pane;return n}function B(){return Dr.getInitialState()}function J(t,e){var n=e.panels;return zr(n)}function W(){return Rr.getInitialState()}function X(t,e){var n=e.show;return!!n}function Q(){return Mr.getInitialState()}function Z(t,e){t.dispatch(Tr.SHOW_SIDEBAR,{show:e})}function $(t,e){t.dispatch(Tr.NAVIGATE,{pane:e})}function tt(t,e){t.dispatch(Tr.PANELS_LOADED,{panels:e})}function et(t,e){var n=e.entityId;return n}function nt(){return qr.getInitialState()}function rt(t,e){t.dispatch(xr.SELECT_ENTITY,{entityId:e})}function it(t){t.dispatch(xr.SELECT_ENTITY,{entityId:null})}function ot(t){return!t||(new Date).getTime()-t>6e4}function ut(t,e){var n=e.date;return n.toISOString()}function at(){return Yr.getInitialState()}function st(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,Jr({})):t.withMutations(function(t){r.forEach(function(e){return t.setIn([n,e[0].entity_id],Jr(e.map(dn.fromJSON)))})})}function ct(){return Wr.getInitialState()}function ft(t,e){var n=e.stateHistory;return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,$r(e.map(dn.fromJSON)))})})}function ht(){return ti.getInitialState()}function lt(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(ri,r)})}function pt(){return ii.getInitialState()}function _t(t,e){t.dispatch(Gr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function dt(t,e){void 0===e&&(e=null),t.dispatch(Gr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),fn(t,"GET",n).then(function(e){return t.dispatch(Gr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})},function(){return t.dispatch(Gr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})})}function vt(t,e){return t.dispatch(Gr.ENTITY_HISTORY_FETCH_START,{date:e}),fn(t,"GET","history/period/"+e).then(function(n){return t.dispatch(Gr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})},function(){return t.dispatch(Gr.ENTITY_HISTORY_FETCH_ERROR,{})})}function yt(t){var e=t.evaluate(ai);return vt(t,e)}function St(t){t.registerStores({currentEntityHistoryDate:Yr,entityHistory:Wr,isLoadingEntityHistory:Qr,recentEntityHistory:ti,recentEntityHistoryUpdated:ii})}function gt(t){t.registerStores({moreInfoEntityId:qr})}function mt(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;oru}function ae(t){t.registerStores({currentLogbookDate:Fo,isLoadingLogbookEntries:Ko,logbookEntries:Qo,logbookEntriesUpdated:tu})}function se(t){return t.set("active",!0)}function ce(t){return t.set("active",!1)}function fe(){return du.getInitialState()}function he(t){return navigator.serviceWorker.getRegistration().then(function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})}).then(function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",fn(t,"POST","notify.html5",{subscription:e,browser:n}).then(function(){return t.dispatch(lu.PUSH_NOTIFICATIONS_SUBSCRIBE,{})}).then(function(){return!0})})["catch"](function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),Mn.createNotification(t,n),!1})}function le(t){return navigator.serviceWorker.getRegistration().then(function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})}).then(function(e){return fn(t,"DELETE","notify.html5",{subscription:e}).then(function(){return e.unsubscribe()}).then(function(){return t.dispatch(lu.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})}).then(function(){return!0})})["catch"](function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),Mn.createNotification(t,n),!1})}function pe(t){t.registerStores({pushNotifications:du})}function _e(t,e){return fn(t,"POST","template",{template:e})}function de(t){return t.set("isListening",!0)}function ve(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 ye(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 Se(){return Ru.getInitialState()}function ge(){return Ru.getInitialState()}function me(){return Ru.getInitialState()}function Ee(t){return Lu[t.hassId]}function Ie(t){var e=Ee(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(Du.VOICE_TRANSMITTING,{finalTranscript:n}),ir.callService(t,"conversation","process",{text:n}).then(function(){t.dispatch(Du.VOICE_DONE)},function(){t.dispatch(Du.VOICE_ERROR)})}}function be(t){var e=Ee(t);e&&(e.recognition.stop(),Lu[t.hassId]=!1)}function Oe(t){Ie(t),be(t)}function we(t){var e=Oe.bind(null,t);e();var n=new webkitSpeechRecognition;Lu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(Du.VOICE_START)},n.onerror=function(){return t.dispatch(Du.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=Ee(t);if(n){for(var r="",i="",o=e.resultIndex;o=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:t<0?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:L(t)}function v(t){return!(!t||!t[dn])}function y(t){return!(!t||!t[vn])}function S(t){return!(!t||!t[yn])}function g(t){return y(t)||S(t)}function m(t){return!(!t||!t[Sn])}function E(t){this.next=t}function I(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 b(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(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[bn]);if("function"==typeof e)return e}function D(t){return t&&"number"==typeof t.length}function C(t){return null===t||void 0===t?P():v(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?P().toKeyedSeq():v(t)?y(t)?t.toSeq():t.fromEntrySeq():H(t)}function R(t){return null===t||void 0===t?P():v(t)?y(t)?t.entrySeq():t.toIndexedSeq():x(t)}function L(t){return(null===t||void 0===t?P():v(t)?y(t)?t.entrySeq():t:x(t)).toSetSeq()}function M(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function N(t){this._iterable=t,this.size=t.length||t.size}function k(t){this._iterator=t,this._iteratorCache=[]}function U(t){return!(!t||!t[wn])}function P(){return Tn||(Tn=new M([]))}function H(t){var e=Array.isArray(t)?new M(t).fromEntrySeq():w(t)?new k(t).fromEntrySeq():O(t)?new N(t).fromEntrySeq():"object"==typeof t?new j(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"==typeof t&&new j(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 M(t):w(t)?new k(t):O(t)?new N(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;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?b():I(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=typeof 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>jn?nt(t):rt(t):"function"==typeof t.hashCode?t.hashCode():it(t)}function nt(t){var e=Un[t];return void 0===e&&(e=rt(t),kn===Nn&&(kn=0,Un={}),kn++,Un[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=Mt(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=jt,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===En){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===mn?gn:mn,n)},e}function pt(t,e,n){var r=Mt(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,ln);return o===ln?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(En,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return I(r,a,e.call(n,u[1],a,t),i)})},r}function _t(t,e){var n=Mt(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=jt,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=Mt(t);return r&&(i.has=function(r){var i=t.get(r,ln);return i!==ln&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,ln);return o!==ln&&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){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)},o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(En,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 I(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)?be():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=Lt(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=l<0?0:l);var p=Mt(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&&eh)return b();var t=i.next();return r||e===mn?t:e===gn?I(e,a-1,void 0,t):I(e,a-1,t.value[1],t)})},p}function gt(t,e,n){var r=Mt(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(En,i),a=!0;return new E(function(){if(!a)return b();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===En?t:I(r,s,c,t):(a=!1,b())})},r}function mt(t,e,n,r){var i=Mt(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){if(!a||!(a=e.call(n,t,o,c)))return 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(En,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===mn?t:i===gn?I(i,c++,void 0,t):I(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===En?t:I(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?H(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 M(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 It(t,e,n){var r=Mt(t);return r.__iterateUncached=function(r,i){function o(t,s){var c=this;t.__iterate(function(t,i){return(!e||s0}function Dt(t,e,n){var r=Mt(t);return r.size=new M(n).map(function(t){return t.size}).min(),r.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(mn,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},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?b():I(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 Lt(t){return y(t)?p:S(t)?_:d}function Mt(t){return Object.create((y(t)?z:S(t)?R:L).prototype)}function jt(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):C.prototype.cacheResult.call(this)}function Nt(t,e){return t>e?1:t>>n)&hn,a=(0===n?r:r>>>n)&hn,s=u===a?[Zt(t,e,n+cn,r,i)]:(o=new Ft(e,r,i),u>>=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;a0&&ro?0:o-n,c=u-n;return c>fn&&(c=fn),function(){if(i===c)return Bn;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>fn&&(f=fn),function(){for(;;){if(a){var t=a();if(t!==Bn)return t;a=null}if(c===f)return Bn;var o=e?--f:c++;a=n(s&&s[o],r-cn,i+(o<=t.size||n<0)return t.withMutations(function(t){n<0?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(_n);return n>=Ie(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&hn,s=t&&a0){var f=t&&t.array[a],h=ye(f,e,r-cn,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>=Ie(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&hn],r-=cn;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:n<0?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;a+h<0;)f=new le(f&&f.array.length?[void 0,f]:[],i),c+=cn,h+=1<=1<l?new le([],i):_;if(_&&p>l&&acn;y-=cn){var S=l>>>y&hn;v=v.array[S]=Se(v.array[S],i)}v.array[l>>>cn&hn]=_}if(s=p)a-=p,s-=p,c=cn,f=null,d=d&&d.removeBefore(i,0,a);else if(a>o||p>>c&hn;if(g!==p>>>c&hn)break;g&&(h+=(1<o&&(f=f.removeBefore(i,c,a-h)),f&&pi&&(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 Ie(t){return t>>cn<=fn&&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):we(r,i)}function De(t){return null===t||void 0===t?Re():Ce(t)?t:Re().unshiftAll(t)}function Ce(t){return!(!t||!t[Wn])}function ze(t,e,n,r){var i=Object.create(Xn);return i.size=t,i._head=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Re(){return Qn||(Qn=ze(0))}function Le(t){return null===t||void 0===t?ke():Me(t)&&!m(t)?t:ke().withMutations(function(e){var n=d(t);at(n.size),n.forEach(function(t){return e.add(t)})})}function Me(t){return!(!t||!t[Zn])}function je(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($n);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function ke(){return tr||(tr=Ne(Jt()))}function Ue(t){return null===t||void 0===t?xe():Pe(t)?t:xe().withMutations(function(e){var n=d(t);at(n.size),n.forEach(function(t){return e.add(t)})})}function Pe(t){return Me(t)&&m(t)}function He(t,e){var n=Object.create(er);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function xe(){return nr||(nr=He(Te()))}function Ve(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);Ge(i,u),i.size=u.length,i._name=e,i._keys=u,i._defaultValues=t}this._map=Ut(o)},i=r.prototype=Object.create(rr);return i.constructor=r,r}function qe(t,e,n){var r=Object.create(Object.getPrototypeOf(t));return r._map=e,r.__ownerID=n,r}function Fe(t){return t._name||t.constructor.name||"Record"}function Ge(t,e){try{e.forEach(Ke.bind(void 0,t))}catch(n){}}function Ke(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 Ye(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){if(n?!t.has(e):i?!W(e,t.get(r,ln)):!W(t.get(r,ln),e))return u=!1,!1});return u&&t.size===a}function Be(t,e,n){if(!(this instanceof Be))return new Be(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),ee?-1:0}function rn(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+un(et(t),et(e))|0}:function(t,e){r=r+un(et(t),et(e))|0}:e?function(t){r=31*r+et(t)|0}:function(t){r=r+et(t)|0});return on(i,r)}function on(t,e){return e=Dn(e,3432918353),e=Dn(e<<15|e>>>-15,461845907),e=Dn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Dn(e^e>>>16,2246822507),e=Dn(e^e>>>13,3266489909),e=tt(e^e>>>16)}function un(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var an=Array.prototype.slice,sn="delete",cn=5,fn=1<r?b():I(t,i,n[e?r-i++:i++])})},t(j,z),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.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?b():I(t,u,n[u])})},j.prototype[Sn]=!0,t(N,R),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},N.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new E(b);var i=0;return new E(function(){var e=r.next();return e.done?e:I(t,i++,e.value)})},t(k,R),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return I(t,i,r[i++])})};var Tn;t(K,l),t(Y,K),t(B,K),t(J,K),K.Keyed=Y,K.Indexed=B,K.Set=J;var An,Dn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-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},Cn=Object.isExtensible,zn=function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}}(),Rn="function"==typeof WeakMap;Rn&&(An=new WeakMap);var Ln=0,Mn="__immutablehash__";"function"==typeof Symbol&&(Mn=Symbol(Mn));var jn=16,Nn=255,kn=0,Un={};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(mn,e),r=e?Rt(this):0;return new E(function(){var i=n.next();return i.done?i:I(t,e?--r:r++,i.value,i)})},st.prototype[Sn]=!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(mn,e),r=0;return new E(function(){var e=n.next();return e.done?e:I(t,r++,e.value,e)})},t(ft,L),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(mn,e);return new E(function(){var e=n.next();return e.done?e:I(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(mn,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 I(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=jt,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,ln,function(){return e})},Ut.prototype.remove=function(t){return Wt(this,t,ln)},Ut.prototype.deleteIn=function(t){return this.updateIn(t,function(){return ln})},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,kt(t),e,n);return r===ln?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=an.call(arguments,1);return ne(this,t,e)},Ut.prototype.mergeIn=function(t){var e=an.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=an.call(arguments,1);return ne(this,re(t),e)},Ut.prototype.mergeDeepIn=function(t){var e=an.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 be(wt(this,t))},Ut.prototype.sortBy=function(t,e){return be(wt(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=Pt;var Pn="@@__IMMUTABLE_MAP__@@",Hn=Ut.prototype;Hn[Pn]=!0,Hn[sn]=Hn.remove,Hn.removeIn=Hn.deleteIn,Ht.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Vn)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 Ht(t,d)}},xt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=1<<((0===t?e:e>>>t)&hn),o=this.bitmap;return 0===(o&i)?r:this.nodes[ue(o&i-1)].get(t+cn,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)&hn,s=1<=qn)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)&hn,o=this.nodes[i];return o?o.get(t+cn,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)&hn,s=i===ln,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Xt(f,t,e+cn,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&hn;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-cn,n),i===u&&o)return this}if(o&&!i)return this;var a=Se(this,t);if(!o)for(var s=0;s>>e&hn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-cn,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 Yn,Bn={};t(be,Ut),be.of=function(){return this(arguments)},be.prototype.toString=function(){return this.__toString("OrderedMap {","}")},be.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):Te()},be.prototype.set=function(t,e){return Ae(this,t,e)},be.prototype.remove=function(t){return Ae(this,t,ln)},be.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},be.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate(function(e){return e&&t(e[1],e[0],n)},e)},be.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},be.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?we(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},be.isOrderedMap=Oe,be.prototype[Sn]=!0,be.prototype[sn]=be.prototype.remove;var Jn;t(De,B),De.of=function(){return this(arguments)},De.prototype.toString=function(){return this.__toString("Stack [","]")},De.prototype.get=function(t,e){var n=this._head;for(t=u(this,t);n&&t--;)n=n.next;return n?n.value:e},De.prototype.peek=function(){return this._head&&this._head.value},De.prototype.push=function(){var t=arguments;if(0===arguments.length)return this;for(var e=this.size+arguments.length,n=this._head,r=arguments.length-1;r>=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):ze(e,n)},De.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):ze(e,n)},De.prototype.pop=function(){return this.slice(1)},De.prototype.unshift=function(){return this.push.apply(this,arguments)},De.prototype.unshiftAll=function(t){return this.pushAll(t)},De.prototype.shift=function(){return this.pop.apply(this,arguments)},De.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):Re()},De.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):ze(i,o)},De.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?ze(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},De.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},De.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,I(t,n++,e)}return b()})},De.isStack=Ce;var Wn="@@__IMMUTABLE_STACK__@@",Xn=De.prototype;Xn[Wn]=!0,Xn.withMutations=Hn.withMutations,Xn.asMutable=Hn.asMutable,Xn.asImmutable=Hn.asImmutable,Xn.wasAltered=Hn.wasAltered;var Qn;t(Le,J),Le.of=function(){return this(arguments)},Le.fromKeys=function(t){return this(p(t).keySeq())},Le.prototype.toString=function(){return this.__toString("Set {","}")},Le.prototype.has=function(t){return this._map.has(t)},Le.prototype.add=function(t){return je(this,this._map.set(t,!0))},Le.prototype.remove=function(t){return je(this,this._map.remove(t))},Le.prototype.clear=function(){return je(this,this._map.clear())},Le.prototype.union=function(){var t=an.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:"")+" ]"},Be.prototype.get=function(t,e){return this.has(t)?this._start+u(this,t)*this._step:e},Be.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&e=0&&nn?b():I(t,o++,u)})},Be.prototype.equals=function(t){return t instanceof Be?this._start===t._start&&this._end===t._end&&this._step===t._step:Ye(this,t)};var ir;t(Je,R),Je.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Je.prototype.get=function(t,e){return this.has(t)?this._value:e},Je.prototype.includes=function(t){return W(this._value,t)},Je.prototype.slice=function(t,e){var n=this.size;return s(t,e,n)?this:new Je(this._value,f(e,n)-c(t,n))},Je.prototype.reverse=function(){return this},Je.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Je.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Je.prototype.__iterate=function(t,e){for(var n=this,r=0;rthis.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"!=typeof 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=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){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(var t=this;this.__unwatchFns.length;)t.__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 L({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 b(t,[n])})}),I(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 b(t,i)});return I(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=w["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 b(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=w["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,w["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)});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 b(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",w["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&&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],w["default"].Map({value:n,storeStates:u,dispatchId:i}))}function E(t,e){var n=y(e);return t.getIn(["cache",n,"value"])}function I(t){return t.update("dispatchId",function(t){return t+1})}function b(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 O=n(3),w=r(O),T=n(9),A=r(T),D=n(5),C=n(10),z=n(11),R=n(4),L=w["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}])})}),Ce=t(De),ze=e(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}),Re=t(ze),Le=Re({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),Me=Ce.Store,je=Ce.toImmutable,Ne=new Me({getInitialState:function(){return je({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Le.VALIDATING_AUTH_TOKEN,n),this.on(Le.VALID_AUTH_TOKEN,r),this.on(Le.INVALID_AUTH_TOKEN,i)}}),ke=Ce.Store,Ue=Ce.toImmutable,Pe=new ke({getInitialState:function(){return Ue({authToken:null,host:""})},initialize:function(){this.on(Le.VALID_AUTH_TOKEN,o),this.on(Le.LOG_OUT,u)}}),He=Ce.Store,xe=new He({getInitialState:function(){return!0},initialize:function(){this.on(Le.VALID_AUTH_TOKEN,a)}}),Ve=Re({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),qe="object"==typeof window&&"EventSource"in window,Fe=Ce.Store,Ge=Ce.toImmutable,Ke=new Fe({getInitialState:function(){return Ge({isSupported:qe,isStreaming:!1,useStreaming:!0,hasError:!1})},initialize:function(){this.on(Ve.STREAM_START,s),this.on(Ve.STREAM_STOP,c),this.on(Ve.STREAM_ERROR,f),this.on(Ve.LOG_OUT,h)}}),Ye=Re({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),Be=Ce.Store,Je=new Be({getInitialState:function(){return!0},initialize:function(){this.on(Ye.API_FETCH_ALL_START,function(){return!0}),this.on(Ye.API_FETCH_ALL_SUCCESS,function(){return!1}),this.on(Ye.API_FETCH_ALL_FAIL,function(){return!1}),this.on(Ye.LOG_OUT,function(){return!1})}}),We=Ce.Store,Xe=new We({getInitialState:function(){return!1},initialize:function(){this.on(Ye.SYNC_SCHEDULED,function(){return!0}),this.on(Ye.SYNC_SCHEDULE_CANCELLED,function(){return!1}),this.on(Ye.LOG_OUT,function(){return!1})}}),Qe=Re({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}),Ze=Ce.Store,$e=Ce.toImmutable,tn=new Ze({getInitialState:function(){return $e({})},initialize:function(){var t=this;this.on(Qe.API_FETCH_SUCCESS,l),this.on(Qe.API_SAVE_SUCCESS,l),this.on(Qe.API_DELETE_SUCCESS,p),this.on(Qe.LOG_OUT,function(){return t.getInitialState()})}}),en=e(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;n<10;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=arguments,s=e(t),c=1;c199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?(u.setRequestHeader("Content-Type","application/json;charset=UTF-8"),u.send(JSON.stringify(r))):u.send()})}function C(t,e){var n=e.message;return t.set(t.size,n)}function z(){return zn.getInitialState()}function R(t,e){t.dispatch(An.NOTIFICATION_CREATED,{message:e})}function M(t){t.registerStores({notifications:zn})}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 j(t,e){return!!t&&("group"===t.domain?"on"===t.state||"off"===t.state:L(t.domain,e))}function N(t,e){return[rr(t),function(t){return!!t&&t.services.has(e)}]}function k(t){return[wn.byId(t),nr,j]}function U(t,e,n){function r(){var c=(new Date).getTime()-a;c0?i=setTimeout(r,e-c):(i=null,n||(s=t.apply(u,o),i||(u=o=null)))}var i,o,u,a,s;null==e&&(e=100);var c=function(){u=this,o=arguments,a=(new Date).getTime();var c=n&&!i;return i||(i=setTimeout(r,e)),c&&(s=t.apply(u,o),u=o=null),s};return c.clear=function(){i&&(clearTimeout(i),i=null)},c}function P(t,e){var n=e.component;return t.push(n)}function H(t,e){var n=e.components;return dr(n)}function x(){return vr.getInitialState()}function V(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 Sr({latitude:n,longitude:r,location_name:i,temperature_unit:o,time_zone:u,serverVersion:a})}function q(){return gr.getInitialState()}function F(t,e){t.dispatch(pr.SERVER_CONFIG_LOADED,e)}function G(t){ln(t,"GET","config").then((function(e){return F(t,e)}))}function K(t,e){t.dispatch(pr.COMPONENT_LOADED,{component:e})}function Y(t){return[["serverComponent"],function(e){return e.contains(t)}]}function B(t){t.registerStores({serverComponent:vr,serverConfig:gr})}function J(t,e){var n=e.pane;return n}function W(){return zr.getInitialState()}function X(t,e){var n=e.panels;return Mr(n)}function Q(){return Lr.getInitialState()}function Z(t,e){var n=e.show;return!!n}function $(){return Nr.getInitialState()}function tt(t,e){t.dispatch(Dr.SHOW_SIDEBAR,{show:e})}function et(t,e){t.dispatch(Dr.NAVIGATE,{pane:e})}function nt(t,e){t.dispatch(Dr.PANELS_LOADED,{panels:e})}function rt(t,e){var n=e.entityId;return n}function it(){return Gr.getInitialState()}function ot(t,e){t.dispatch(qr.SELECT_ENTITY,{entityId:e})}function ut(t){t.dispatch(qr.SELECT_ENTITY,{entityId:null})}function at(t){return!t||(new Date).getTime()-t>6e4}function st(t,e){var n=e.date;return n.toISOString()}function ct(){return Jr.getInitialState()}function ft(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,Xr({})):t.withMutations((function(t){r.forEach((function(e){return t.setIn([n,e[0].entity_id],Xr(e.map(yn.fromJSON)))}))}))}function ht(){return Qr.getInitialState()}function lt(t,e){var n=e.stateHistory;return t.withMutations((function(t){n.forEach((function(e){return t.set(e[0].entity_id,ei(e.map(yn.fromJSON)))}))}))}function pt(){return ni.getInitialState()}function _t(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(oi,r)}))}function dt(){return ui.getInitialState()}function vt(t,e){t.dispatch(Yr.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function yt(t,e){void 0===e&&(e=null),t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),ln(t,"GET",n).then((function(e){return t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})}),(function(){return t.dispatch(Yr.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})}))}function St(t,e){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_START,{date:e}),ln(t,"GET","history/period/"+e).then((function(n){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})}),(function(){return t.dispatch(Yr.ENTITY_HISTORY_FETCH_ERROR,{})}))}function gt(t){var e=t.evaluate(ci);return St(t,e)}function mt(t){t.registerStores({currentEntityHistoryDate:Jr,entityHistory:Qr,isLoadingEntityHistory:$r,recentEntityHistory:ni,recentEntityHistoryUpdated:ui})}function Et(t){t.registerStores({moreInfoEntityId:Gr})}function It(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;ouu}function se(t){t.registerStores({currentLogbookDate:Yo,isLoadingLogbookEntries:Jo,logbookEntries:tu,logbookEntriesUpdated:ru})}function ce(t){return t.set("active",!0)}function fe(t){return t.set("active",!1)}function he(){return Su.getInitialState()}function le(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered.");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){var n;return n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1?"firefox":"chrome",ln(t,"POST","notify.html5",{subscription:e,browser:n}).then((function(){return t.dispatch(du.PUSH_NOTIFICATIONS_SUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n;return n=e.message&&e.message.indexOf("gcm_sender_id")!==-1?"Please setup the notify.html5 platform.":"Notification registration failed.",console.error(e),Nn.createNotification(t,n),!1}))}function pe(t){return navigator.serviceWorker.getRegistration().then((function(t){if(!t)throw new Error("No service worker registered");return t.pushManager.subscribe({userVisibleOnly:!0})})).then((function(e){return ln(t,"DELETE","notify.html5",{subscription:e}).then((function(){return e.unsubscribe()})).then((function(){return t.dispatch(du.PUSH_NOTIFICATIONS_UNSUBSCRIBE,{})})).then((function(){return!0}))})).catch((function(e){var n="Failed unsubscribing for push notifications.";return console.error(e),Nn.createNotification(t,n),!1}))}function _e(t){t.registerStores({pushNotifications:Su})}function de(t,e){return ln(t,"POST","template",{template:e})}function ve(t){return t.set("isListening",!0)}function ye(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 Se(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 ge(){return ju.getInitialState()}function me(){return ju.getInitialState()}function Ee(){return ju.getInitialState()}function Ie(t){return Nu[t.hassId]}function be(t){var e=Ie(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(Ru.VOICE_TRANSMITTING,{finalTranscript:n}),ur.callService(t,"conversation","process",{text:n}).then((function(){t.dispatch(Ru.VOICE_DONE)}),(function(){t.dispatch(Ru.VOICE_ERROR)}))}}function Oe(t){var e=Ie(t);e&&(e.recognition.stop(),Nu[t.hassId]=!1)}function we(t){be(t),Oe(t)}function Te(t){var e=we.bind(null,t);e();var n=new webkitSpeechRecognition;Nu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(Ru.VOICE_START)},n.onerror=function(){return t.dispatch(Ru.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=Ie(t);if(n){for(var r="",i="",o=e.resultIndex;o=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:t<0?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[dn])}function y(t){return!(!t||!t[vn])}function S(t){return!(!t||!t[yn])}function g(t){return y(t)||S(t)}function m(t){return!(!t||!t[Sn])}function E(t){this.next=t}function I(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 b(){return{value:void 0,done:!0}}function O(t){return!!A(t)}function w(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[bn]);if("function"==typeof e)return e}function D(t){return t&&"number"==typeof t.length}function C(t){return null===t||void 0===t?P():v(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?P().toKeyedSeq():v(t)?y(t)?t.toSeq():t.fromEntrySeq():H(t)}function R(t){return null===t||void 0===t?P():v(t)?y(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?P():v(t)?y(t)?t.entrySeq():t:x(t)).toSetSeq()}function L(t){this._array=t,this.size=t.length}function j(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function N(t){this._iterable=t,this.size=t.length||t.size}function k(t){this._iterator=t,this._iteratorCache=[]}function U(t){return!(!t||!t[wn])}function P(){return Tn||(Tn=new L([]))}function H(t){var e=Array.isArray(t)?new L(t).fromEntrySeq():w(t)?new k(t).fromEntrySeq():O(t)?new N(t).fromEntrySeq():"object"==typeof t?new j(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"==typeof t&&new j(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 L(t):w(t)?new k(t):O(t)?new N(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;u<=o;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?b():I(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=typeof 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>jn?nt(t):rt(t):"function"==typeof t.hashCode?t.hashCode():it(t)}function nt(t){var e=Un[t];return void 0===e&&(e=rt(t),kn===Nn&&(kn=0,Un={}),kn++,Un[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=Lt(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=jt,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===En){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===mn?gn:mn,n)},e}function pt(t,e,n){var r=Lt(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,ln);return o===ln?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(En,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return I(r,a,e.call(n,u[1],a,t),i)})},r}function _t(t,e){var n=Lt(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=jt,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=Lt(t);return r&&(i.has=function(r){var i=t.get(r,ln);return i!==ln&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,ln);return o!==ln&&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){if(e.call(n,t,o,s))return a++,i(t,r?o:a-1,u)}),o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(En,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 I(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)?be():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=l<0?0:l);var p=Lt(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&&eh)return b();var t=i.next();return r||e===mn?t:e===gn?I(e,a-1,void 0,t):I(e,a-1,t.value[1],t)})},p}function gt(t,e,n){var r=Lt(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(En,i),a=!0;return new E(function(){if(!a)return b();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===En?t:I(r,s,c,t):(a=!1,b())})},r}function mt(t,e,n,r){var i=Lt(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){if(!a||!(a=e.call(n,t,o,c)))return 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(En,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===mn?t:i===gn?I(i,c++,void 0,t):I(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===En?t:I(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?H(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 L(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 It(t,e,n){var r=Lt(t);return r.__iterateUncached=function(r,i){function o(t,s){var c=this;t.__iterate((function(t,i){return(!e||s0}function Dt(t,e,n){var r=Lt(t);return r.size=new L(n).map((function(t){return t.size})).min(),r.__iterate=function(t,e){for(var n,r=this,i=this.__iterator(mn,e),o=0;!(n=i.next()).done&&t(n.value,o++,r)!==!1;);return o},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?b():I(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 Lt(t){return Object.create((y(t)?z:S(t)?R:M).prototype)}function jt(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):C.prototype.cacheResult.call(this)}function Nt(t,e){return t>e?1:t>>n)&hn,a=(0===n?r:r>>>n)&hn,s=u===a?[Zt(t,e,n+cn,r,i)]:(o=new Ft(e,r,i),u>>=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;a0&&ro?0:o-n,c=u-n;return c>fn&&(c=fn),function(){if(i===c)return Bn;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>fn&&(f=fn),function(){for(;;){if(a){var t=a();if(t!==Bn)return t;a=null}if(c===f)return Bn;var o=e?--f:c++;a=n(s&&s[o],r-cn,i+(o<=t.size||n<0)return t.withMutations((function(t){n<0?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(_n);return n>=Ie(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&hn,s=t&&a0){var f=t&&t.array[a],h=ye(f,e,r-cn,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>=Ie(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&hn],r-=cn;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:n<0?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;a+h<0;)f=new le(f&&f.array.length?[void 0,f]:[],i),c+=cn,h+=1<=1<l?new le([],i):_;if(_&&p>l&&acn;y-=cn){var S=l>>>y&hn;v=v.array[S]=Se(v.array[S],i)}v.array[l>>>cn&hn]=_}if(s=p)a-=p,s-=p,c=cn,f=null,d=d&&d.removeBefore(i,0,a);else if(a>o||p>>c&hn;if(g!==p>>>c&hn)break;g&&(h+=(1<o&&(f=f.removeBefore(i,c,a-h)),f&&pi&&(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 Ie(t){return t>>cn<=fn&&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):we(r,i)}function De(t){return null===t||void 0===t?Re():Ce(t)?t:Re().unshiftAll(t)}function Ce(t){return!(!t||!t[Wn])}function ze(t,e,n,r){var i=Object.create(Xn);return i.size=t,i._head=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Re(){return Qn||(Qn=ze(0))}function Me(t){return null===t||void 0===t?ke():Le(t)&&!m(t)?t:ke().withMutations((function(e){var n=d(t);at(n.size),n.forEach((function(t){return e.add(t)}))}))}function Le(t){return!(!t||!t[Zn])}function je(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($n);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function ke(){return tr||(tr=Ne(Jt()))}function Ue(t){return null===t||void 0===t?xe():Pe(t)?t:xe().withMutations((function(e){var n=d(t);at(n.size),n.forEach((function(t){return e.add(t)}))}))}function Pe(t){return Le(t)&&m(t)}function He(t,e){var n=Object.create(er);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function xe(){return nr||(nr=He(Te()))}function Ve(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);Ge(i,u),i.size=u.length,i._name=e,i._keys=u,i._defaultValues=t}this._map=Ut(o)},i=r.prototype=Object.create(rr);return i.constructor=r,r}function qe(t,e,n){var r=Object.create(Object.getPrototypeOf(t));return r._map=e,r.__ownerID=n,r}function Fe(t){return t._name||t.constructor.name||"Record"}function Ge(t,e){try{e.forEach(Ke.bind(void 0,t))}catch(t){}}function Ke(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 Ye(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){if(n?!t.has(e):i?!W(e,t.get(r,ln)):!W(t.get(r,ln),e))return u=!1,!1}));return u&&t.size===a}function Be(t,e,n){if(!(this instanceof Be))return new Be(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),ee?-1:0}function rn(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+un(et(t),et(e))|0}:function(t,e){r=r+un(et(t),et(e))|0}:e?function(t){r=31*r+et(t)|0}:function(t){r=r+et(t)|0});return on(i,r)}function on(t,e){return e=Dn(e,3432918353),e=Dn(e<<15|e>>>-15,461845907),e=Dn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Dn(e^e>>>16,2246822507),e=Dn(e^e>>>13,3266489909),e=tt(e^e>>>16)}function un(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var an=Array.prototype.slice,sn="delete",cn=5,fn=1<r?b():I(t,i,n[e?r-i++:i++])})},t(j,z),j.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},j.prototype.has=function(t){return this._object.hasOwnProperty(t)},j.prototype.__iterate=function(t,e){for(var n=this,r=this._object,i=this._keys,o=i.length-1,u=0;u<=o;u++){var a=i[e?o-u:u];if(t(r[a],a,n)===!1)return u+1}return u},j.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?b():I(t,u,n[u])})},j.prototype[Sn]=!0,t(N,R),N.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);var r=this._iterable,i=T(r),o=0;if(w(i))for(var u;!(u=i.next()).done&&t(u.value,o++,n)!==!1;);return o},N.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!w(r))return new E(b);var i=0;return new E(function(){var e=r.next();return e.done?e:I(t,i++,e.value)})},t(k,R),k.prototype.__iterateUncached=function(t,e){var n=this;if(e)return this.cacheResult().__iterate(t,e);for(var r=this._iterator,i=this._iteratorCache,o=0;o=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return I(t,i,r[i++])})};var Tn;t(K,l),t(Y,K),t(B,K),t(J,K),K.Keyed=Y,K.Indexed=B,K.Set=J;var An,Dn="function"==typeof Math.imul&&Math.imul(4294967295,2)===-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},Cn=Object.isExtensible,zn=(function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}})(),Rn="function"==typeof WeakMap;Rn&&(An=new WeakMap);var Mn=0,Ln="__immutablehash__";"function"==typeof Symbol&&(Ln=Symbol(Ln));var jn=16,Nn=255,kn=0,Un={};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(mn,e),r=e?Rt(this):0;return new E(function(){var i=n.next();return i.done?i:I(t,e?--r:r++,i.value,i)})},st.prototype[Sn]=!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(mn,e),r=0;return new E(function(){var e=n.next();return e.done?e:I(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(mn,e);return new E(function(){var e=n.next();return e.done?e:I(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(mn,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 I(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=jt,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,ln,(function(){return e}))},Ut.prototype.remove=function(t){return Wt(this,t,ln)},Ut.prototype.deleteIn=function(t){return this.updateIn(t,(function(){return ln}))},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,kt(t),e,n);return r===ln?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=an.call(arguments,1);return ne(this,t,e)},Ut.prototype.mergeIn=function(t){var e=an.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=an.call(arguments,1);return ne(this,re(t),e)},Ut.prototype.mergeDeepIn=function(t){var e=an.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 be(wt(this,t))},Ut.prototype.sortBy=function(t,e){return be(wt(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=Pt;var Pn="@@__IMMUTABLE_MAP__@@",Hn=Ut.prototype;Hn[Pn]=!0,Hn[sn]=Hn.remove,Hn.removeIn=Hn.deleteIn,Ht.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;o=Vn)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 Ht(t,d)}},xt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=1<<((0===t?e:e>>>t)&hn),o=this.bitmap;return 0===(o&i)?r:this.nodes[ue(o&i-1)].get(t+cn,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)&hn,s=1<=qn)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)&hn,o=this.nodes[i];return o?o.get(t+cn,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)&hn,s=i===ln,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Xt(f,t,e+cn,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,l=0&&t>>e&hn;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-cn,n),i===u&&o)return this}if(o&&!i)return this;var a=Se(this,t);if(!o)for(var s=0;s>>e&hn;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-cn,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 Yn,Bn={};t(be,Ut),be.of=function(){return this(arguments)},be.prototype.toString=function(){return this.__toString("OrderedMap {","}")},be.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},be.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):Te()},be.prototype.set=function(t,e){return Ae(this,t,e)},be.prototype.remove=function(t){return Ae(this,t,ln)},be.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},be.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate((function(e){return e&&t(e[1],e[0],n)}),e)},be.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},be.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?we(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},be.isOrderedMap=Oe,be.prototype[Sn]=!0,be.prototype[sn]=be.prototype.remove;var Jn;t(De,B),De.of=function(){return this(arguments)},De.prototype.toString=function(){return this.__toString("Stack [","]")},De.prototype.get=function(t,e){var n=this._head;for(t=u(this,t);n&&t--;)n=n.next;return n?n.value:e},De.prototype.peek=function(){return this._head&&this._head.value},De.prototype.push=function(){var t=arguments;if(0===arguments.length)return this;for(var e=this.size+arguments.length,n=this._head,r=arguments.length-1;r>=0;r--)n={value:t[r],next:n};return this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):ze(e,n)},De.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):ze(e,n)},De.prototype.pop=function(){return this.slice(1)},De.prototype.unshift=function(){return this.push.apply(this,arguments)},De.prototype.unshiftAll=function(t){return this.pushAll(t)},De.prototype.shift=function(){return this.pop.apply(this,arguments)},De.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):Re()},De.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):ze(i,o)},De.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?ze(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},De.prototype.__iterate=function(t,e){var n=this;if(e)return this.reverse().__iterate(t);for(var r=0,i=this._head;i&&t(i.value,r++,n)!==!1;)i=i.next;return r},De.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,I(t,n++,e)}return b()})},De.isStack=Ce;var Wn="@@__IMMUTABLE_STACK__@@",Xn=De.prototype;Xn[Wn]=!0,Xn.withMutations=Hn.withMutations,Xn.asMutable=Hn.asMutable,Xn.asImmutable=Hn.asImmutable,Xn.wasAltered=Hn.wasAltered;var Qn;t(Me,J),Me.of=function(){return this(arguments)},Me.fromKeys=function(t){return this(p(t).keySeq())},Me.prototype.toString=function(){return this.__toString("Set {","}")},Me.prototype.has=function(t){return this._map.has(t)},Me.prototype.add=function(t){return je(this,this._map.set(t,!0))},Me.prototype.remove=function(t){return je(this,this._map.remove(t))},Me.prototype.clear=function(){return je(this,this._map.clear())},Me.prototype.union=function(){var t=an.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:"")+" ]"},Be.prototype.get=function(t,e){return this.has(t)?this._start+u(this,t)*this._step:e},Be.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&e=0&&nn?b():I(t,o++,u)})},Be.prototype.equals=function(t){return t instanceof Be?this._start===t._start&&this._end===t._end&&this._step===t._step:Ye(this,t)};var ir;t(Je,R),Je.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Je.prototype.get=function(t,e){return this.has(t)?this._value:e},Je.prototype.includes=function(t){return W(this._value,t)},Je.prototype.slice=function(t,e){var n=this.size;return s(t,e,n)?this:new Je(this._value,f(e,n)-c(t,n))},Je.prototype.reverse=function(){return this},Je.prototype.indexOf=function(t){return W(this._value,t)?0:-1},Je.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},Je.prototype.__iterate=function(t,e){for(var n=this,r=0;rthis.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"!=typeof 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=typeof t;return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments,n=arguments.length;if(!t||n<2)return t||{};for(var r=1;r0)){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(var t=this;this.__unwatchFns.length;)t.__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 b(t,[n])}))})),I(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(e){throw A.default.dispatchError(t,e.message),e}if(void 0===s&&f(t,"throwOnUndefinedStoreReturnValue")){var c="Store handler must return a value, did you forget a return statement";throw A.default.dispatchError(t,c),new Error(c)}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 b(t,i)}));return I(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=w.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 b(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=w.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,w.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)}));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 b(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",w.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&&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],w.default.Map({value:n,storeStates:u,dispatchId:i}))}function E(t,e){var n=y(e);return t.getIn(["cache",n,"value"])}function I(t){return t.update("dispatchId",(function(t){return t+1}))}function b(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 O=n(3),w=r(O),T=n(9),A=r(T),D=n(5),C=n(10),z=n(11),R=n(4),M=w.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}])}))})),ze=t(Ce),Re=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},Me=Re,Le=Me({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),je=ze.Store,Ne=ze.toImmutable,ke=new je({getInitialState:function(){return Ne({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(Le.VALIDATING_AUTH_TOKEN,n),this.on(Le.VALID_AUTH_TOKEN,r),this.on(Le.INVALID_AUTH_TOKEN,i)}}),Ue=ze.Store,Pe=ze.toImmutable,He=new Ue({getInitialState:function(){return Pe({authToken:null,host:""})},initialize:function(){this.on(Le.VALID_AUTH_TOKEN,o),this.on(Le.LOG_OUT,u)}}),xe=ze.Store,Ve=new xe({getInitialState:function(){return!0},initialize:function(){this.on(Le.VALID_AUTH_TOKEN,a)}}),qe=Me({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Fe="object"==typeof window&&"EventSource"in window,Ge=ze.Store,Ke=ze.toImmutable,Ye=new Ge({getInitialState:function(){return Ke({isSupported:Fe,isStreaming:!1,useStreaming:!0,hasError:!1})},initialize:function(){this.on(qe.STREAM_START,s),this.on(qe.STREAM_STOP,c),this.on(qe.STREAM_ERROR,f),this.on(qe.LOG_OUT,h)}}),Be=Me({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),Je=ze.Store,We=new Je({getInitialState:function(){return!0},initialize:function(){this.on(Be.API_FETCH_ALL_START,(function(){return!0})),this.on(Be.API_FETCH_ALL_SUCCESS,(function(){return!1})),this.on(Be.API_FETCH_ALL_FAIL,(function(){return!1})),this.on(Be.LOG_OUT,(function(){return!1}))}}),Xe=ze.Store,Qe=new Xe({getInitialState:function(){return!1},initialize:function(){this.on(Be.SYNC_SCHEDULED,(function(){return!0})),this.on(Be.SYNC_SCHEDULE_CANCELLED,(function(){return!1})),this.on(Be.LOG_OUT,(function(){return!1}))}}),Ze=Me({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}),$e=ze.Store,tn=ze.toImmutable,en=new $e({getInitialState:function(){return tn({})},initialize:function(){var t=this;this.on(Ze.API_FETCH_SUCCESS,l),this.on(Ze.API_SAVE_SUCCESS,l),this.on(Ze.API_DELETE_SUCCESS,p),this.on(Ze.LOG_OUT,(function(){return t.getInitialState()}))}}),nn=Object.prototype.hasOwnProperty,rn=Object.prototype.propertyIsEnumerable,on=d()?Object.assign:function(t,e){for(var n,r,i=arguments,o=_(t),u=1;u \ No newline at end of file +var r=t.propertyDataFromStyles(n._styles,this),i=!this.__notStyleScopeCacheable;i&&(r.key.customStyle=this.customStyle,e=n._styleCache.retrieve(this.is,r.key,this._styles));var a=Boolean(e);a?this._styleProperties=e._styleProperties:this._computeStyleProperties(r.properties),this._computeOwnStyleProperties(),a||(e=o.retrieve(this.is,this._ownStyleProperties,this._styles));var l=Boolean(e)&&!a,h=this._applyStyleProperties(e);a||(h=h&&s?h.cloneNode(!0):h,e={style:h,_scopeSelector:this._scopeSelector,_styleProperties:this._styleProperties},i&&(r.key.customStyle={},this.mixin(r.key.customStyle,this.customStyle),n._styleCache.store(this.is,e,r.key,this._styles)),l||o.store(this.is,Object.create(e),this._ownStyleProperties,this._styles))},_computeStyleProperties:function(e){var n=this._findStyleHost();n._styleProperties||n._computeStyleProperties();var r=Object.create(n._styleProperties),s=t.hostAndRootPropertiesForScope(this);this.mixin(r,s.hostProps),e=e||t.propertyDataFromStyles(n._styles,this).properties,this.mixin(r,e),this.mixin(r,s.rootProps),t.mixinCustomStyle(r,this.customStyle),t.reify(r),this._styleProperties=r},_computeOwnStyleProperties:function(){for(var e,t={},n=0;n0&&l.push(t);return[{removed:a,added:l}]}},Polymer.Collection.get=function(e){return Polymer._collections.get(e)||new Polymer.Collection(e)},Polymer.Collection.applySplices=function(e,t){var n=Polymer._collections.get(e);return n?n._applySplices(t):null},Polymer({is:"dom-repeat",extends:"template",_template:null,properties:{items:{type:Array},as:{type:String,value:"item"},indexAs:{type:String,value:"index"},sort:{type:Function,observer:"_sortChanged"},filter:{type:Function,observer:"_filterChanged"},observe:{type:String,observer:"_observeChanged"},delay:Number,renderedItemCount:{type:Number,notify:!0,readOnly:!0},initialCount:{type:Number,observer:"_initializeChunking"},targetFramerate:{type:Number,value:20},_targetFrameTime:{type:Number,computed:"_computeFrameTime(targetFramerate)"}},behaviors:[Polymer.Templatizer],observers:["_itemsChanged(items.*)"],created:function(){this._instances=[],this._pool=[],this._limit=1/0;var e=this;this._boundRenderChunk=function(){e._renderChunk()}},detached:function(){this.__isDetached=!0;for(var e=0;e=0;t--){var n=this._instances[t];n.isPlaceholder&&t=this._limit&&(n=this._downgradeInstance(t,n.__key__)),e[n.__key__]=t,n.isPlaceholder||n.__setProperty(this.indexAs,t,!0)}this._pool.length=0,this._setRenderedItemCount(this._instances.length),this.fire("dom-change"),this._tryRenderChunk()},_applyFullRefresh:function(){var e,t=this.collection;if(this._sortFn)e=t?t.getKeys():[];else{e=[];var n=this.items;if(n)for(var r=0;r=r;a--)this._detachAndRemoveInstance(a)},_numericSort:function(e,t){return e-t},_applySplicesUserSort:function(e){for(var t,n,r=this.collection,s={},i=0;i=0;i--){var h=a[i];void 0!==h&&this._detachAndRemoveInstance(h)}var c=this;if(l.length){this._filterFn&&(l=l.filter(function(e){return c._filterFn(r.getItem(e))})),l.sort(function(e,t){return c._sortFn(r.getItem(e),r.getItem(t))});var u=0;for(i=0;i>1,a=this._instances[o].__key__,l=this._sortFn(n.getItem(a),r);if(l<0)e=o+1;else{if(!(l>0)){i=o;break}s=o-1}}return i<0&&(i=s+1),this._insertPlaceholder(i,t),i},_applySplicesArrayOrder:function(e){for(var t,n=0;n=0?(e=this.as+"."+e.substring(n+1),i._notifyPath(e,t,!0)):i.__setProperty(this.as,t,!0))}},itemForElement:function(e){var t=this.modelForElement(e);return t&&t[this.as]},keyForElement:function(e){var t=this.modelForElement(e);return t&&t.__key__},indexForElement:function(e){var t=this.modelForElement(e);return t&&t[this.indexAs]}}),Polymer({is:"array-selector",_template:null,properties:{items:{type:Array,observer:"clearSelection"},multi:{type:Boolean,value:!1,observer:"clearSelection"},selected:{type:Object,notify:!0},selectedItem:{type:Object,notify:!0},toggle:{type:Boolean,value:!1}},clearSelection:function(){if(Array.isArray(this.selected))for(var e=0;e \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 3c052db8e0c..3f49436d6c0 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 ba588fc779d..0a4454c68f3 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit ba588fc779d34a2fbf7cc9a23103c38e3e3e0356 +Subproject commit 0a4454c68f3c29c77cd60f4315d410d8b3737543 diff --git a/homeassistant/components/frontend/www_static/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index ff359e902b3..0f1aa609e46 100644 --- a/homeassistant/components/frontend/www_static/mdi.html +++ b/homeassistant/components/frontend/www_static/mdi.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/mdi.html.gz b/homeassistant/components/frontend/www_static/mdi.html.gz index 09cadf26e6b..2be36b99edb 100644 Binary files a/homeassistant/components/frontend/www_static/mdi.html.gz and b/homeassistant/components/frontend/www_static/mdi.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html index bdf7f206ba3..6babc87e2ef 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz index 83efff86001..09b6fecfc09 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index 21db62aab76..bd1998a7949 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index 85ebe5f75f9..0683f460f7d 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html index 35caacb76b9..90a36bccc6a 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz index edfb67103e0..e40d047db48 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html index bf2ff97c528..b9aba112b63 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html @@ -1,2 +1,2 @@ -