diff --git a/.gitmodules b/.gitmodules index 5cfe7de0098..b9cf022a8f4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "homeassistant/external/netdisco"] path = homeassistant/external/netdisco url = https://github.com/balloob/netdisco.git +[submodule "homeassistant/external/noop"] + path = homeassistant/external/noop + url = https://github.com/balloob/noop.git diff --git a/config/custom_components/example.py b/config/custom_components/example.py index ee422174377..a972e3ab576 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -34,7 +34,6 @@ SERVICE_FLASH = 'flash' _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument def setup(hass, config): """ Setup example component. """ diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index bb8afbc97fc..a51bb6b47a8 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -426,9 +426,11 @@ class State(object): state: the state of the entity attributes: extra information on entity and state last_changed: last time the state was changed, not the attributes. + last_updated: last time this object was updated. """ - __slots__ = ['entity_id', 'state', 'attributes', 'last_changed'] + __slots__ = ['entity_id', 'state', 'attributes', + 'last_changed', 'last_updated'] def __init__(self, entity_id, state, attributes=None, last_changed=None): if not ENTITY_ID_PATTERN.match(entity_id): @@ -439,13 +441,14 @@ class State(object): self.entity_id = entity_id self.state = state self.attributes = attributes or {} + self.last_updated = dt.datetime.now() # Strip microsecond from last_changed else we cannot guarantee # state == State.from_dict(state.as_dict()) # This behavior occurs because to_dict uses datetime_to_str # which does not preserve microseconds self.last_changed = util.strip_microseconds( - last_changed or dt.datetime.now()) + last_changed or self.last_updated) def copy(self): """ Creates a copy of itself. """ @@ -527,15 +530,12 @@ class StateMachine(object): def get_since(self, point_in_time): """ Returns all states that have been changed since point_in_time. - - Note: States keep track of last_changed -without- microseconds. - Therefore your point_in_time will also be stripped of microseconds. """ point_in_time = util.strip_microseconds(point_in_time) with self._lock: return [state for state in self._states.values() - if state.last_changed >= point_in_time] + if state.last_updated >= point_in_time] def is_state(self, entity_id, state): """ Returns True if entity exists and is specified state. """ diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 8226e1fdc84..2b48882712c 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,24 +1,19 @@ """ Starts home assistant. """ +from __future__ import print_function import sys import os import argparse import importlib -try: - from homeassistant import bootstrap -except ImportError: - # This is to add support to load Home Assistant using - # `python3 homeassistant` instead of `python3 -m homeassistant` +def validate_python(): + """ Validate we're running the right Python version. """ + major, minor = sys.version_info[:2] - # Insert the parent directory of this file into the module search path - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - - from homeassistant import bootstrap - -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.components import http, demo + if major < 3 or (major == 3 and minor < 4): + print("Home Assistant requires atleast Python 3.4") + sys.exit() def validate_dependencies(): @@ -39,6 +34,34 @@ def validate_dependencies(): sys.exit() +def ensure_path_and_load_bootstrap(): + """ Ensure sys load path is correct and load Home Assistant bootstrap. """ + try: + from homeassistant import bootstrap + + except ImportError: + # This is to add support to load Home Assistant using + # `python3 homeassistant` instead of `python3 -m homeassistant` + + # Insert the parent directory of this file into the module search path + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + + from homeassistant import bootstrap + + return bootstrap + + +def validate_git_submodules(): + """ Validate the git submodules are cloned. """ + try: + # pylint: disable=no-name-in-module, unused-variable + from homeassistant.external.noop import WORKING # noqa + except ImportError: + print("Repository submodules have not been initialized") + print("Please run: git submodule update --init --recursive") + sys.exit() + + def ensure_config_path(config_dir): """ Gets the path to the configuration file. Creates one if it not exists. """ @@ -65,9 +88,8 @@ def ensure_config_path(config_dir): return config_path -def main(): - """ Starts Home Assistant. Will create demo config if no config found. """ - +def get_arguments(): + """ Get parsed passed in arguments. """ parser = argparse.ArgumentParser() parser.add_argument( '-c', '--config', @@ -83,15 +105,26 @@ def main(): action='store_true', help='Open the webinterface in a browser') - args = parser.parse_args() + return parser.parse_args() + +def main(): + """ Starts Home Assistant. """ + validate_python() validate_dependencies() - config_dir = os.path.join(os.getcwd(), args.config) + bootstrap = ensure_path_and_load_bootstrap() + validate_git_submodules() + + args = get_arguments() + + config_dir = os.path.join(os.getcwd(), args.config) config_path = ensure_config_path(config_dir) if args.demo_mode: + from homeassistant.components import http, demo + # Demo mode only requires http and demo components. hass = bootstrap.from_config_dict({ http.DOMAIN: {}, @@ -101,7 +134,8 @@ def main(): hass = bootstrap.from_config_file(config_path) if args.open_ui: - # pylint: disable=unused-argument + from homeassistant.const import EVENT_HOMEASSISTANT_START + def open_browser(event): """ Open the webinterface in a browser. """ if hass.local_api is not None: diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 6720ae2a2d9..0b757766bc0 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -70,7 +70,6 @@ def turn_off(hass, entity_id=None, **service_data): hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) -# pylint: disable=unused-argument def setup(hass, config): """ Setup general services related to homeassistant. """ diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index d2be5a54006..c8adfe95bbe 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -26,7 +26,6 @@ def register(hass, config, action): from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO, MATCH_ALL) - # pylint: disable=unused-argument def state_automation_listener(entity, from_s, to_s): """ Listens for state changes and calls action. """ action() diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index b1fca4121e6..7e38960534d 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -17,7 +17,6 @@ def register(hass, config, action): minutes = convert(config.get(CONF_MINUTES), int) seconds = convert(config.get(CONF_SECONDS), int) - # pylint: disable=unused-argument def time_automation_listener(now): """ Listens for time changes and calls action. """ action() diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py index dc3fc568fde..c5a55afad40 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser.py @@ -11,7 +11,6 @@ DEPENDENCIES = [] SERVICE_BROWSE_URL = "browse_url" -# pylint: disable=unused-argument def setup(hass, config): """ Listen for browse_url events and open the url in the default webbrowser. """ diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py index 1736f0d2444..2f619007365 100644 --- a/homeassistant/components/chromecast.py +++ b/homeassistant/components/chromecast.py @@ -158,7 +158,6 @@ def setup(hass, config): for host in hosts: setup_chromecast(casts, host) - # pylint: disable=unused-argument def chromecast_discovered(service, info): """ Called when a Chromecast has been discovered. """ logger.info("New Chromecast discovered: %s", info[0]) @@ -212,7 +211,7 @@ def setup(hass, config): hass.states.set(entity_id, state, state_attr) - def update_chromecast_states(time): # pylint: disable=unused-argument + def update_chromecast_states(time): """ Updates all chromecast states. """ if casts: logger.info("Updating Chromecast status") @@ -298,7 +297,7 @@ def setup(hass, config): pychromecast.play_youtube_video(video_id, cast.host) update_chromecast_state(entity_id, cast) - hass.track_time_change(update_chromecast_states) + hass.track_time_change(update_chromecast_states, second=[0, 15, 30, 45]) hass.services.register(DOMAIN, SERVICE_TURN_OFF, turn_off_service) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py new file mode 100644 index 00000000000..fdd3c571601 --- /dev/null +++ b/homeassistant/components/configurator.py @@ -0,0 +1,190 @@ +""" +homeassistant.components.configurator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A component to allow pieces of code to request configuration from the user. + +Initiate a request by calling the `request_config` method with a callback. +This will return a request id that has to be used for future calls. +A callback has to be provided to `request_config` which will be called when +the user has submitted configuration information. +""" +import logging + +from homeassistant.helpers import generate_entity_id +from homeassistant.const import EVENT_TIME_CHANGED + +DOMAIN = "configurator" +DEPENDENCIES = [] +ENTITY_ID_FORMAT = DOMAIN + ".{}" + +SERVICE_CONFIGURE = "configure" + +STATE_CONFIGURE = "configure" +STATE_CONFIGURED = "configured" + +ATTR_CONFIGURE_ID = "configure_id" +ATTR_DESCRIPTION = "description" +ATTR_DESCRIPTION_IMAGE = "description_image" +ATTR_SUBMIT_CAPTION = "submit_caption" +ATTR_FIELDS = "fields" +ATTR_ERRORS = "errors" + +_REQUESTS = {} +_INSTANCES = {} +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=too-many-arguments +def request_config( + hass, name, callback, description=None, description_image=None, + submit_caption=None, fields=None): + """ Create a new request for config. + Will return an ID to be used for sequent calls. """ + + instance = _get_instance(hass) + + request_id = instance.request_config( + name, callback, + description, description_image, submit_caption, fields) + + _REQUESTS[request_id] = instance + + return request_id + + +def notify_errors(request_id, error): + """ Add errors to a config request. """ + try: + _REQUESTS[request_id].notify_errors(request_id, error) + except KeyError: + # If request_id does not exist + pass + + +def request_done(request_id): + """ Mark a config request as done. """ + try: + _REQUESTS.pop(request_id).request_done(request_id) + except KeyError: + # If request_id does not exist + pass + + +def setup(hass, config): + """ Set up Configurator. """ + return True + + +def _get_instance(hass): + """ Get an instance per hass object. """ + try: + return _INSTANCES[hass] + except KeyError: + _INSTANCES[hass] = Configurator(hass) + + if DOMAIN not in hass.components: + hass.components.append(DOMAIN) + + return _INSTANCES[hass] + + +class Configurator(object): + """ + Class to keep track of current configuration requests. + """ + + def __init__(self, hass): + self.hass = hass + self._cur_id = 0 + self._requests = {} + hass.services.register( + DOMAIN, SERVICE_CONFIGURE, self.handle_service_call) + + # pylint: disable=too-many-arguments + def request_config( + self, name, callback, + description, description_image, submit_caption, fields): + """ Setup a request for configuration. """ + + entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass) + + if fields is None: + fields = [] + + request_id = self._generate_unique_id() + + self._requests[request_id] = (entity_id, fields, callback) + + data = { + ATTR_CONFIGURE_ID: request_id, + ATTR_FIELDS: fields, + } + + data.update({ + key: value for key, value in [ + (ATTR_DESCRIPTION, description), + (ATTR_DESCRIPTION_IMAGE, description_image), + (ATTR_SUBMIT_CAPTION, submit_caption), + ] if value is not None + }) + + self.hass.states.set(entity_id, STATE_CONFIGURE, data) + + return request_id + + def notify_errors(self, request_id, error): + """ Update the state with errors. """ + if not self._validate_request_id(request_id): + return + + entity_id = self._requests[request_id][0] + + state = self.hass.states.get(entity_id) + + new_data = state.attributes + new_data[ATTR_ERRORS] = error + + self.hass.states.set(entity_id, STATE_CONFIGURE, new_data) + + def request_done(self, request_id): + """ Remove the config request. """ + if not self._validate_request_id(request_id): + return + + entity_id = self._requests.pop(request_id)[0] + + # If we remove the state right away, it will not be included with + # the result fo the service call (current design limitation). + # Instead, we will set it to configured to give as feedback but delete + # it shortly after so that it is deleted when the client updates. + self.hass.states.set(entity_id, STATE_CONFIGURED) + + def deferred_remove(event): + """ Remove the request state. """ + self.hass.states.remove(entity_id) + + self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove) + + def handle_service_call(self, call): + """ Handle a configure service call. """ + request_id = call.data.get(ATTR_CONFIGURE_ID) + + if not self._validate_request_id(request_id): + return + + # pylint: disable=unused-variable + entity_id, fields, callback = self._requests[request_id] + + # field validation goes here? + + callback(call.data.get(ATTR_FIELDS, {})) + + def _generate_unique_id(self): + """ Generates a unique configurator id. """ + self._cur_id += 1 + return "{}-{}".format(id(self), self._cur_id) + + def _validate_request_id(self, request_id): + """ Validate that the request belongs to this instance. """ + return request_id in self._requests diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 4076ca63159..0dcc1a41bf7 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -5,6 +5,7 @@ homeassistant.components.demo Sets up a demo environment that mimics interaction with devices """ import random +import time import homeassistant as ha import homeassistant.loader as loader @@ -28,6 +29,7 @@ DEPENDENCIES = [] def setup(hass, config): """ Setup a demo environment. """ group = loader.get_component('group') + configurator = loader.get_component('configurator') config.setdefault(ha.DOMAIN, {}) config.setdefault(DOMAIN, {}) @@ -170,4 +172,30 @@ def setup(hass, config): ATTR_AWAY_MODE: STATE_OFF }) + configurator_ids = [] + + def hue_configuration_callback(data): + """ Fake callback, mark config as done. """ + time.sleep(2) + + # First time it is called, pretend it failed. + if len(configurator_ids) == 1: + configurator.notify_errors( + configurator_ids[0], + "Failed to register, please try again.") + + configurator_ids.append(0) + else: + configurator.request_done(configurator_ids[0]) + + request_id = configurator.request_config( + hass, "Philips Hue", hue_configuration_callback, + description=("Press the button on the bridge to register Philips Hue " + "with Home Assistant."), + description_image="/static/images/config_philips_hue.jpg", + submit_caption="I have pressed the button" + ) + + configurator_ids.append(request_id) + return True diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index c9668853c73..90d06e6a3c1 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -66,7 +66,6 @@ def setup(hass, config): else: return None - # pylint: disable=unused-argument def schedule_light_on_sun_rise(entity, old_state, new_state): """The moment sun sets we want to have all the lights on. We will schedule to have each light start after one another diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 04cea3e047b..009cf15ad20 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -24,9 +24,8 @@ DEPENDENCIES = [] SERVICE_DEVICE_TRACKER_RELOAD = "reload_devices_csv" -GROUP_NAME_ALL_DEVICES = 'all_devices' -ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format( - GROUP_NAME_ALL_DEVICES) +GROUP_NAME_ALL_DEVICES = 'all devices' +ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format('all_devices') ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -114,7 +113,6 @@ class DeviceTracker(object): dev_group = group.Group( hass, GROUP_NAME_ALL_DEVICES, user_defined=False) - # pylint: disable=unused-argument def reload_known_devices_service(service): """ Reload known devices file. """ self._read_known_devices_file() @@ -128,7 +126,8 @@ class DeviceTracker(object): if self.invalid_known_devices_file: return - hass.track_time_change(update_device_state) + hass.track_time_change( + update_device_state, second=[0, 12, 24, 36, 48]) hass.services.register(DOMAIN, SERVICE_DEVICE_TRACKER_RELOAD, diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 9dbf503fc94..637c48ddf26 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -17,7 +17,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a Luci scanner. """ if not validate_config(config, diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 14a01b7d658..aac09536746 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -14,7 +14,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a Netgear scanner. """ if not validate_config(config, diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 4d914d5c1c0..a4fd5f6cfff 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -20,7 +20,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a Nmap scanner. """ if not validate_config(config, {DOMAIN: [CONF_HOSTS]}, diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 81755f42c66..265dcf84b57 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -20,7 +20,6 @@ CONF_HTTP_ID = "http_id" _LOGGER = logging.getLogger(__name__) -# pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a Tomato scanner. """ if not validate_config(config, diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index fde6061dfaa..195203e8134 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -75,7 +75,6 @@ def setup(hass, config): ATTR_DISCOVERED: info }) - # pylint: disable=unused-argument def start_discovery(event): """ Start discovering. """ netdisco = DiscoveryService(SCAN_INTERVAL) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 05c69b6e230..bcf7f3fe8b4 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -163,7 +163,6 @@ class Group(object): self.hass.bus.remove_listener( ha.EVENT_STATE_CHANGED, self._update_group_state) - # pylint: disable=unused-argument def _update_group_state(self, entity_id, old_state, new_state): """ Updates the group state based on a state change by a tracked entity. """ diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index f2b5eefa5a1..67030407f5f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -351,7 +351,6 @@ class RequestHandler(SimpleHTTPRequestHandler): """ DELETE request handler. """ self._handle_request('DELETE') - # pylint: disable=unused-argument def _handle_get_root(self, path_match, data): """ Renders the debug interface. """ @@ -390,17 +389,14 @@ class RequestHandler(SimpleHTTPRequestHandler): "" "").format(app_url, auth)) - # pylint: disable=unused-argument def _handle_get_api(self, path_match, data): """ Renders the debug interface. """ self._json_message("API running.") - # pylint: disable=unused-argument def _handle_get_api_states(self, path_match, data): """ Returns a dict containing all entity ids and their state. """ self._write_json(self.server.hass.states.all()) - # pylint: disable=unused-argument def _handle_get_api_states_entity(self, path_match, data): """ Returns the state of a specific entity. """ entity_id = path_match.group('entity_id') diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py index 99a5a35a739..cd61782ef8a 100644 --- a/homeassistant/components/http/frontend.py +++ b/homeassistant/components/http/frontend.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "1d7f65f99c286d2c897b900518ebb663" +VERSION = "43699d5ec727d3444985a1028d21e0d9" diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html index 3c2f344a2a0..7a0accc1921 100644 --- a/homeassistant/components/http/www_static/frontend.html +++ b/homeassistant/components/http/www_static/frontend.html @@ -6,6 +6,7 @@ b.events&&Object.keys(a).length>0&&console.log("[%s] addHostListeners:",this.loc }else if(!!(match=isoDurationRegex.exec(input))){sign=match[1]==="-"?-1:1;parseIso=function(inp){var res=inp&&parseFloat(inp.replace(",","."));return(isNaN(res)?0:res)*sign};duration={y:parseIso(match[2]),M:parseIso(match[3]),d:parseIso(match[4]),h:parseIso(match[5]),m:parseIso(match[6]),s:parseIso(match[7]),w:parseIso(match[8])}}else if(typeof duration==="object"&&("from"in duration||"to"in duration)){diffRes=momentsDifference(moment(duration.from),moment(duration.to));duration={};duration.ms=diffRes.milliseconds;duration.M=diffRes.months}ret=new Duration(duration);if(moment.isDuration(input)&&hasOwnProp(input,"_locale")){ret._locale=input._locale}return ret};moment.version=VERSION;moment.defaultFormat=isoFormat;moment.ISO_8601=function(){};moment.momentProperties=momentProperties;moment.updateOffset=function(){};moment.relativeTimeThreshold=function(threshold,limit){if(relativeTimeThresholds[threshold]===undefined){return false}if(limit===undefined){return relativeTimeThresholds[threshold]}relativeTimeThresholds[threshold]=limit;return true};moment.lang=deprecate("moment.lang is deprecated. Use moment.locale instead.",function(key,value){return moment.locale(key,value)});moment.locale=function(key,values){var data;if(key){if(typeof values!=="undefined"){data=moment.defineLocale(key,values)}else{data=moment.localeData(key)}if(data){moment.duration._locale=moment._locale=data}}return moment._locale._abbr};moment.defineLocale=function(name,values){if(values!==null){values.abbr=name;if(!locales[name]){locales[name]=new Locale}locales[name].set(values);moment.locale(name);return locales[name]}else{delete locales[name];return null}};moment.langData=deprecate("moment.langData is deprecated. Use moment.localeData instead.",function(key){return moment.localeData(key)});moment.localeData=function(key){var locale;if(key&&key._locale&&key._locale._abbr){key=key._locale._abbr}if(!key){return moment._locale}if(!isArray(key)){locale=loadLocale(key);if(locale){return locale}key=[key]}return chooseLocale(key)};moment.isMoment=function(obj){return obj instanceof Moment||obj!=null&&hasOwnProp(obj,"_isAMomentObject")};moment.isDuration=function(obj){return obj instanceof Duration};for(i=lists.length-1;i>=0;--i){makeList(lists[i])}moment.normalizeUnits=function(units){return normalizeUnits(units)};moment.invalid=function(flags){var m=moment.utc(NaN);if(flags!=null){extend(m._pf,flags)}else{m._pf.userInvalidated=true}return m};moment.parseZone=function(){return moment.apply(null,arguments).parseZone()};moment.parseTwoDigitYear=function(input){return toInt(input)+(toInt(input)>68?1900:2e3)};extend(moment.fn=Moment.prototype,{clone:function(){return moment(this)},valueOf:function(){return+this._d+(this._offset||0)*6e4},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var m=moment(this).utc();if(00}return false},parsingFlags:function(){return extend({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(keepLocalTime){return this.zone(0,keepLocalTime)},local:function(keepLocalTime){if(this._isUTC){this.zone(0,keepLocalTime);this._isUTC=false;if(keepLocalTime){this.add(this._dateTzOffset(),"m")}}return this},format:function(inputString){var output=formatMoment(this,inputString||moment.defaultFormat);return this.localeData().postformat(output)},add:createAdder(1,"add"),subtract:createAdder(-1,"subtract"),diff:function(input,units,asFloat){var that=makeAs(input,this),zoneDiff=(this.zone()-that.zone())*6e4,diff,output,daysAdjust;units=normalizeUnits(units);if(units==="year"||units==="month"){diff=(this.daysInMonth()+that.daysInMonth())*432e5;output=(this.year()-that.year())*12+(this.month()-that.month());daysAdjust=this-moment(this).startOf("month")-(that-moment(that).startOf("month"));daysAdjust-=(this.zone()-moment(this).startOf("month").zone()-(that.zone()-moment(that).startOf("month").zone()))*6e4;output+=daysAdjust/diff;if(units==="year"){output=output/12}}else{diff=this-that;output=units==="second"?diff/1e3:units==="minute"?diff/6e4:units==="hour"?diff/36e5:units==="day"?(diff-zoneDiff)/864e5:units==="week"?(diff-zoneDiff)/6048e5:diff}return asFloat?output:absRound(output)},from:function(time,withoutSuffix){return moment.duration({to:this,from:time}).locale(this.locale()).humanize(!withoutSuffix)},fromNow:function(withoutSuffix){return this.from(moment(),withoutSuffix)},calendar:function(time){var now=time||moment(),sod=makeAs(now,this).startOf("day"),diff=this.diff(sod,"days",true),format=diff<-6?"sameElse":diff<-1?"lastWeek":diff<0?"lastDay":diff<1?"sameDay":diff<2?"nextDay":diff<7?"nextWeek":"sameElse";return this.format(this.localeData().calendar(format,this,moment(now)))},isLeapYear:function(){return isLeapYear(this.year())},isDST:function(){return this.zone()+input}else{inputMs=moment.isMoment(input)?+input:+moment(input);return inputMs<+this.clone().startOf(units)}},isBefore:function(input,units){var inputMs;units=normalizeUnits(typeof units!=="undefined"?units:"millisecond");if(units==="millisecond"){input=moment.isMoment(input)?input:moment(input);return+this<+input}else{inputMs=moment.isMoment(input)?+input:+moment(input);return+this.clone().endOf(units)this?this:other}),zone:function(input,keepLocalTime){var offset=this._offset||0,localAdjust;if(input!=null){if(typeof input==="string"){input=timezoneMinutesFromString(input)}if(Math.abs(input)<16){input=input*60}if(!this._isUTC&&keepLocalTime){localAdjust=this._dateTzOffset()}this._offset=input;this._isUTC=true;if(localAdjust!=null){this.subtract(localAdjust,"m")}if(offset!==input){if(!keepLocalTime||this._changeInProgress){addOrSubtractDurationFromMoment(this,moment.duration(offset-input,"m"),1,false)}else if(!this._changeInProgress){this._changeInProgress=true;moment.updateOffset(this,true);this._changeInProgress=null}}}else{return this._isUTC?offset:this._dateTzOffset()}return this},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){if(this._tzm){this.zone(this._tzm)}else if(typeof this._i==="string"){this.zone(this._i)}return this},hasAlignedHourOffset:function(input){if(!input){input=0}else{input=moment(input).zone()}return(this.zone()-input)%60===0},daysInMonth:function(){return daysInMonth(this.year(),this.month())},dayOfYear:function(input){var dayOfYear=round((moment(this).startOf("day")-moment(this).startOf("year"))/864e5)+1;return input==null?dayOfYear:this.add(input-dayOfYear,"d")},quarter:function(input){return input==null?Math.ceil((this.month()+1)/3):this.month((input-1)*3+this.month()%3)},weekYear:function(input){var year=weekOfYear(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return input==null?year:this.add(input-year,"y")},isoWeekYear:function(input){var year=weekOfYear(this,1,4).year;return input==null?year:this.add(input-year,"y")},week:function(input){var week=this.localeData().week(this);return input==null?week:this.add((input-week)*7,"d")},isoWeek:function(input){var week=weekOfYear(this,1,4).week;return input==null?week:this.add((input-week)*7,"d")},weekday:function(input){var weekday=(this.day()+7-this.localeData()._week.dow)%7;return input==null?weekday:this.add(input-weekday,"d")},isoWeekday:function(input){return input==null?this.day()||7:this.day(this.day()%7?input:input-7)},isoWeeksInYear:function(){return weeksInYear(this.year(),1,4)},weeksInYear:function(){var weekInfo=this.localeData()._week;return weeksInYear(this.year(),weekInfo.dow,weekInfo.doy)},get:function(units){units=normalizeUnits(units);return this[units]()},set:function(units,value){units=normalizeUnits(units);if(typeof this[units]==="function"){this[units](value)}return this},locale:function(key){var newLocaleData;if(key===undefined){return this._locale._abbr}else{newLocaleData=moment.localeData(key);if(newLocaleData!=null){this._locale=newLocaleData}return this}},lang:deprecate("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(key){if(key===undefined){return this.localeData()}else{return this.locale(key)}}),localeData:function(){return this._locale},_dateTzOffset:function(){return Math.round(this._d.getTimezoneOffset()/15)*15}});function rawMonthSetter(mom,value){var dayOfMonth;if(typeof value==="string"){value=mom.localeData().monthsParse(value);if(typeof value!=="number"){return mom}}dayOfMonth=Math.min(mom.date(),daysInMonth(mom.year(),value));mom._d["set"+(mom._isUTC?"UTC":"")+"Month"](value,dayOfMonth);return mom}function rawGetter(mom,unit){return mom._d["get"+(mom._isUTC?"UTC":"")+unit]()}function rawSetter(mom,unit,value){if(unit==="Month"){return rawMonthSetter(mom,value)}else{return mom._d["set"+(mom._isUTC?"UTC":"")+unit](value)}}function makeAccessor(unit,keepTime){return function(value){if(value!=null){rawSetter(this,unit,value);moment.updateOffset(this,keepTime);return this}else{return rawGetter(this,unit)}}}moment.fn.millisecond=moment.fn.milliseconds=makeAccessor("Milliseconds",false);moment.fn.second=moment.fn.seconds=makeAccessor("Seconds",false);moment.fn.minute=moment.fn.minutes=makeAccessor("Minutes",false);moment.fn.hour=moment.fn.hours=makeAccessor("Hours",true);moment.fn.date=makeAccessor("Date",true);moment.fn.dates=deprecate("dates accessor is deprecated. Use date instead.",makeAccessor("Date",true));moment.fn.year=makeAccessor("FullYear",true);moment.fn.years=deprecate("years accessor is deprecated. Use year instead.",makeAccessor("FullYear",true));moment.fn.days=moment.fn.day;moment.fn.months=moment.fn.month;moment.fn.weeks=moment.fn.week;moment.fn.isoWeeks=moment.fn.isoWeek;moment.fn.quarters=moment.fn.quarter;moment.fn.toJSON=moment.fn.toISOString;function daysToYears(days){return days*400/146097}function yearsToDays(years){return years*146097/400}extend(moment.duration.fn=Duration.prototype,{_bubble:function(){var milliseconds=this._milliseconds,days=this._days,months=this._months,data=this._data,seconds,minutes,hours,years=0;data.milliseconds=milliseconds%1e3;seconds=absRound(milliseconds/1e3);data.seconds=seconds%60;minutes=absRound(seconds/60);data.minutes=minutes%60;hours=absRound(minutes/60);data.hours=hours%24;days+=absRound(hours/24);years=absRound(daysToYears(days));days-=absRound(yearsToDays(years));months+=absRound(days/30);days%=30;years+=absRound(months/12);months%=12;data.days=days;data.months=months;data.years=years},abs:function(){this._milliseconds=Math.abs(this._milliseconds);this._days=Math.abs(this._days);this._months=Math.abs(this._months);this._data.milliseconds=Math.abs(this._data.milliseconds);this._data.seconds=Math.abs(this._data.seconds);this._data.minutes=Math.abs(this._data.minutes);this._data.hours=Math.abs(this._data.hours);this._data.months=Math.abs(this._data.months);this._data.years=Math.abs(this._data.years);return this},weeks:function(){return absRound(this.days()/7)},valueOf:function(){return this._milliseconds+this._days*864e5+this._months%12*2592e6+toInt(this._months/12)*31536e6},humanize:function(withSuffix){var output=relativeTime(this,!withSuffix,this.localeData());if(withSuffix){output=this.localeData().pastFuture(+this,output)}return this.localeData().postformat(output)},add:function(input,val){var dur=moment.duration(input,val);this._milliseconds+=dur._milliseconds;this._days+=dur._days;this._months+=dur._months;this._bubble();return this},subtract:function(input,val){var dur=moment.duration(input,val);this._milliseconds-=dur._milliseconds;this._days-=dur._days;this._months-=dur._months;this._bubble();return this},get:function(units){units=normalizeUnits(units);return this[units.toLowerCase()+"s"]()},as:function(units){var days,months;units=normalizeUnits(units);if(units==="month"||units==="year"){days=this._days+this._milliseconds/864e5;months=this._months+daysToYears(days)*12;return units==="month"?months:months/12}else{days=this._days+Math.round(yearsToDays(this._months/12));switch(units){case"week":return days/7+this._milliseconds/6048e5;case"day":return days+this._milliseconds/864e5;case"hour":return days*24+this._milliseconds/36e5;case"minute":return days*24*60+this._milliseconds/6e4;case"second":return days*24*60*60+this._milliseconds/1e3;case"millisecond":return Math.floor(days*24*60*60*1e3)+this._milliseconds;default:throw new Error("Unknown unit "+units)}}},lang:moment.fn.lang,locale:moment.fn.locale,toIsoString:deprecate("toIsoString() is deprecated. Please use toISOString() instead "+"(notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var years=Math.abs(this.years()),months=Math.abs(this.months()),days=Math.abs(this.days()),hours=Math.abs(this.hours()),minutes=Math.abs(this.minutes()),seconds=Math.abs(this.seconds()+this.milliseconds()/1e3);if(!this.asSeconds()){return"P0D"}return(this.asSeconds()<0?"-":"")+"P"+(years?years+"Y":"")+(months?months+"M":"")+(days?days+"D":"")+(hours||minutes||seconds?"T":"")+(hours?hours+"H":"")+(minutes?minutes+"M":"")+(seconds?seconds+"S":"")},localeData:function(){return this._locale}});moment.duration.fn.toString=moment.duration.fn.toISOString;function makeDurationGetter(name){moment.duration.fn[name]=function(){return this._data[name]}}for(i in unitMillisecondFactors){if(hasOwnProp(unitMillisecondFactors,i)){makeDurationGetter(i.toLowerCase())}}moment.duration.fn.asMilliseconds=function(){return this.as("ms")};moment.duration.fn.asSeconds=function(){return this.as("s")};moment.duration.fn.asMinutes=function(){return this.as("m")};moment.duration.fn.asHours=function(){return this.as("h")};moment.duration.fn.asDays=function(){return this.as("d")};moment.duration.fn.asWeeks=function(){return this.as("weeks")};moment.duration.fn.asMonths=function(){return this.as("M")};moment.duration.fn.asYears=function(){return this.as("y")};moment.locale("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(number){var b=number%10,output=toInt(number%100/10)===1?"th":b===1?"st":b===2?"nd":b===3?"rd":"th";return number+output}});function makeGlobal(shouldDeprecate){if(typeof ender!=="undefined"){return}oldGlobalMoment=globalScope.moment;if(shouldDeprecate){globalScope.moment=deprecate("Accessing Moment through the global scope is "+"deprecated, and will be removed in an upcoming "+"release.",moment)}else{globalScope.moment=moment}}if(hasModule){module.exports=moment}else if(typeof define==="function"&&define.amd){define("moment",function(require,exports,module){if(module.config&&module.config()&&module.config().noGlobal===true){globalScope.moment=oldGlobalMoment}return moment});makeGlobal(true)}else{makeGlobal()}}).call(this);