diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..a7c337f80e --- /dev/null +++ b/.clang-format @@ -0,0 +1,137 @@ +Language: Cpp +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 2000 +PointerAlignment: Right +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: true +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Auto +TabWidth: 2 +UseTab: Never diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000000..5e486e6a0c --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,127 @@ +--- +Checks: >- + *, + -abseil-*, + -android-*, + -boost-*, + -bugprone-macro-parentheses, + -cert-dcl50-cpp, + -cert-err58-cpp, + -clang-analyzer-core.CallAndMessage, + -clang-analyzer-osx.*, + -clang-analyzer-security.*, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-c-copy-assignment-signature, + -cppcoreguidelines-owning-memory, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-const-cast, + -cppcoreguidelines-pro-type-cstyle-cast, + -cppcoreguidelines-pro-type-member-init, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-static-cast-downcast, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-pro-type-vararg, + -cppcoreguidelines-special-member-functions, + -fuchsia-*, + -fuchsia-default-arguments, + -fuchsia-multiple-inheritance, + -fuchsia-overloaded-operator, + -fuchsia-statically-constructed-objects, + -google-build-using-namespace, + -google-explicit-constructor, + -google-readability-braces-around-statements, + -google-readability-casting, + -google-readability-todo, + -google-runtime-int, + -google-runtime-references, + -hicpp-*, + -llvm-header-guard, + -llvm-include-order, + -misc-unconventional-assign-operator, + -misc-unused-parameters, + -modernize-deprecated-headers, + -modernize-pass-by-value, + -modernize-pass-by-value, + -modernize-return-braced-init-list, + -modernize-use-auto, + -modernize-use-default-member-init, + -modernize-use-equals-default, + -mpi-*, + -objc-*, + -performance-unnecessary-value-param, + -readability-braces-around-statements, + -readability-else-after-return, + -readability-implicit-bool-conversion, + -readability-named-parameter, + -readability-redundant-member-init, + -warnings-as-errors, + -zircon-* +WarningsAsErrors: '*' +HeaderFilterRegex: '^.*/src/esphome/.*' +AnalyzeTemporaryDtors: false +FormatStyle: google +CheckOptions: + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: readability-identifier-naming.LocalVariableCase + value: 'lower_case' + - key: readability-identifier-naming.ClassCase + value: 'CamelCase' + - key: readability-identifier-naming.StructCase + value: 'CamelCase' + - key: readability-identifier-naming.EnumCase + value: 'CamelCase' + - key: readability-identifier-naming.EnumConstantCase + value: 'UPPER_CASE' + - key: readability-identifier-naming.StaticConstantCase + value: 'UPPER_CASE' + - key: readability-identifier-naming.StaticVariableCase + value: 'UPPER_CASE' + - key: readability-identifier-naming.GlobalConstantCase + value: 'UPPER_CASE' + - key: readability-identifier-naming.ParameterCase + value: 'lower_case' + - key: readability-identifier-naming.PrivateMemberPrefix + value: 'NO_PRIVATE_MEMBERS_ALWAYS_USE_PROTECTED' + - key: readability-identifier-naming.PrivateMethodPrefix + value: 'NO_PRIVATE_METHODS_ALWAYS_USE_PROTECTED' + - key: readability-identifier-naming.ClassMemberCase + value: 'lower_case' + - key: readability-identifier-naming.ClassMemberCase + value: 'lower_case' + - key: readability-identifier-naming.ProtectedMemberCase + value: 'lower_case' + - key: readability-identifier-naming.ProtectedMemberSuffix + value: '_' + - key: readability-identifier-naming.FunctionCase + value: 'lower_case' + - key: readability-identifier-naming.ClassMethodCase + value: 'lower_case' + - key: readability-identifier-naming.ProtectedMethodCase + value: 'lower_case' + - key: readability-identifier-naming.ProtectedMethodSuffix + value: '_' + - key: readability-identifier-naming.VirtualMethodCase + value: 'lower_case' + - key: readability-identifier-naming.VirtualMethodSuffix + value: '' diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..f24d70487a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +root = true + +# general +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +# python +[*.{py}] +indent_style = space +indent_size = 4 + +# C++ +[*.{cpp,h,tcc}] +indent_style = space +indent_size = 2 + +# Web +[*.{js,html,css}] +indent_style = space +indent_size = 2 + +# YAML +[*.{yaml,yml}] +indent_style = space +indent_size = 2 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 64780a340d..94a5b7284e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,11 +4,10 @@ **Related issue (if applicable):** fixes **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs# -**Pull request in [esphome-core](https://github.com/esphome/esphome-core) with C++ framework changes (if applicable):** esphome/esphome-core# ## Checklist: - [ ] The code change is tested and works locally. - [ ] Tests have been added to verify that the new code works (under `tests/` folder). If user exposed functionality or configuration variables are added/changed: - - [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs). + - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). diff --git a/.gitignore b/.gitignore index c8a36dfd3a..6002612c13 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,19 @@ __pycache__/ # C extensions *.so +# Hide sublime text stuff +*.sublime-project +*.sublime-workspace + +# Hide some OS X stuff +.DS_Store +.AppleDouble +.LSOverride +Icon + +# Thumbnails +._* + # Distribution / packaging .Python build/ @@ -25,12 +38,6 @@ wheels/ *.egg MANIFEST -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - # Installer logs pip-log.txt pip-delete-this-directory.txt @@ -51,36 +58,9 @@ coverage.xml *.mo *.pot -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - # pyenv .python-version -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - # Environments .env .venv @@ -90,19 +70,46 @@ ENV/ env.bak/ venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - # mypy .mypy_cache/ +.pioenvs +.piolibdeps +.vscode +CMakeListsPrivate.txt +CMakeLists.txt + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/dynamic.xml + +# CMake +cmake-build-debug/ +cmake-build-release/ + +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +/*.cbp + +.clang_complete +.gcc-flags.json + config/ tests/build/ tests/.esphome/ +/.temp-clang-tidy.cpp diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f4e2fa40af..969a53b311 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,8 @@ variables: DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375/ + BASE_VERSION: '1.5.1' + TZ: UTC stages: - lint @@ -10,23 +12,20 @@ stages: - deploy .lint: &lint - image: esphome/esphome-base-amd64 + image: esphome/esphome-lint:latest stage: lint before_script: - - pip install -e . - - pip install flake8==3.6.0 pylint==1.9.4 pillow + - script/setup tags: - docker .test: &test - image: esphome/esphome-base-amd64 + image: esphome/esphome-lint:latest stage: test before_script: - - pip install -e . + - script/setup tags: - docker - variables: - TZ: UTC .docker-base: &docker-base image: esphome/esphome-base-builder @@ -41,11 +40,11 @@ stages: - | if [[ "${IS_HASSIO}" == "YES" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.4.3 + BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:${BASE_VERSION} BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH} DOCKERFILE=docker/Dockerfile.hassio else - BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.4.3 + BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:${BASE_VERSION} if [[ "${BUILD_ARCH}" == "amd64" ]]; then BUILD_TO=esphome/esphome else @@ -94,15 +93,32 @@ stages: - docker stage: deploy -flake8: +lint-custom: <<: *lint script: - - flake8 esphome + - script/ci-custom.py -pylint: +lint-python: <<: *lint script: - - pylint esphome + - script/lint-python + +lint-tidy: + <<: *lint + script: + - pio init --ide atom + - | + if ! patch -R -p0 -s -f --dry-run ")); + + request->send(stream); +} + +#ifdef USE_SENSOR +void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { + this->events_.send(this->sensor_json(obj, state).c_str(), "state"); +} +void WebServer::handle_sensor_request(AsyncWebServerRequest *request, UrlMatch match) { + for (sensor::Sensor *obj : App.get_sensors()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->sensor_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +std::string WebServer::sensor_json(sensor::Sensor *obj, float value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "sensor-" + obj->get_object_id(); + std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); + if (!obj->get_unit_of_measurement().empty()) + state += " " + obj->get_unit_of_measurement(); + root["state"] = state; + root["value"] = value; + }); +} +#endif + +#ifdef USE_TEXT_SENSOR +void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) { + this->events_.send(this->text_sensor_json(obj, state).c_str(), "state"); +} +void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, UrlMatch match) { + for (text_sensor::TextSensor *obj : App.get_text_sensors()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->text_sensor_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "text_sensor-" + obj->get_object_id(); + root["state"] = value; + root["value"] = value; + }); +} +#endif + +#ifdef USE_SWITCH +void WebServer::on_switch_update(switch_::Switch *obj, bool state) { + this->events_.send(this->switch_json(obj, state).c_str(), "state"); +} +std::string WebServer::switch_json(switch_::Switch *obj, bool value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "switch-" + obj->get_object_id(); + root["state"] = value ? "ON" : "OFF"; + root["value"] = value; + }); +} +void WebServer::handle_switch_request(AsyncWebServerRequest *request, UrlMatch match) { + for (switch_::Switch *obj : App.get_switches()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->switch_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + } else if (match.method == "toggle") { + this->defer([obj]() { obj->toggle(); }); + request->send(200); + } else if (match.method == "turn_on") { + this->defer([obj]() { obj->turn_on(); }); + request->send(200); + } else if (match.method == "turn_off") { + this->defer([obj]() { obj->turn_off(); }); + request->send(200); + } else { + request->send(404); + } + return; + } + request->send(404); +} +#endif + +#ifdef USE_BINARY_SENSOR +void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { + if (obj->is_internal()) + return; + this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state"); +} +std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "binary_sensor-" + obj->get_object_id(); + root["state"] = value ? "ON" : "OFF"; + root["value"] = value; + }); +} +void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, UrlMatch match) { + for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->binary_sensor_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +#endif + +#ifdef USE_FAN +void WebServer::on_fan_update(fan::FanState *obj) { + if (obj->is_internal()) + return; + this->events_.send(this->fan_json(obj).c_str(), "state"); +} +std::string WebServer::fan_json(fan::FanState *obj) { + return json::build_json([obj](JsonObject &root) { + root["id"] = "fan-" + obj->get_object_id(); + root["state"] = obj->state ? "ON" : "OFF"; + root["value"] = obj->state; + if (obj->get_traits().supports_speed()) { + switch (obj->speed) { + case fan::FAN_SPEED_LOW: + root["speed"] = "low"; + break; + case fan::FAN_SPEED_MEDIUM: + root["speed"] = "medium"; + break; + case fan::FAN_SPEED_HIGH: + root["speed"] = "high"; + break; + } + } + if (obj->get_traits().supports_oscillation()) + root["oscillation"] = obj->oscillating; + }); +} +void WebServer::handle_fan_request(AsyncWebServerRequest *request, UrlMatch match) { + for (fan::FanState *obj : App.get_fans()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->fan_json(obj); + request->send(200, "text/json", data.c_str()); + } else if (match.method == "toggle") { + this->defer([obj]() { obj->toggle().perform(); }); + request->send(200); + } else if (match.method == "turn_on") { + auto call = obj->turn_on(); + if (request->hasParam("speed")) { + String speed = request->getParam("speed")->value(); + call.set_speed(speed.c_str()); + } + if (request->hasParam("oscillation")) { + String speed = request->getParam("oscillation")->value(); + auto val = parse_on_off(speed.c_str()); + switch (val) { + case PARSE_ON: + call.set_oscillating(true); + break; + case PARSE_OFF: + call.set_oscillating(false); + break; + case PARSE_TOGGLE: + call.set_oscillating(!obj->oscillating); + break; + case PARSE_NONE: + request->send(404); + return; + } + } + this->defer([call]() { call.perform(); }); + request->send(200); + } else if (match.method == "turn_off") { + this->defer([obj]() { obj->turn_off().perform(); }); + request->send(200); + } else { + request->send(404); + } + return; + } + request->send(404); +} +#endif + +#ifdef USE_LIGHT +void WebServer::on_light_update(light::LightState *obj) { + if (obj->is_internal()) + return; + this->events_.send(this->light_json(obj).c_str(), "state"); +} +void WebServer::handle_light_request(AsyncWebServerRequest *request, UrlMatch match) { + for (light::LightState *obj : App.get_lights()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->light_json(obj); + request->send(200, "text/json", data.c_str()); + } else if (match.method == "toggle") { + this->defer([obj]() { obj->toggle().perform(); }); + request->send(200); + } else if (match.method == "turn_on") { + auto call = obj->turn_on(); + if (request->hasParam("brightness")) + call.set_brightness(request->getParam("brightness")->value().toFloat() / 255.0f); + if (request->hasParam("r")) + call.set_red(request->getParam("r")->value().toFloat() / 255.0f); + if (request->hasParam("g")) + call.set_green(request->getParam("g")->value().toFloat() / 255.0f); + if (request->hasParam("b")) + call.set_blue(request->getParam("b")->value().toFloat() / 255.0f); + if (request->hasParam("white_value")) + call.set_white(request->getParam("white_value")->value().toFloat() / 255.0f); + if (request->hasParam("color_temp")) + call.set_color_temperature(request->getParam("color_temp")->value().toFloat()); + + if (request->hasParam("flash")) + call.set_flash_length((uint32_t) request->getParam("flash")->value().toFloat() * 1000); + + if (request->hasParam("transition")) + call.set_transition_length((uint32_t) request->getParam("transition")->value().toFloat() * 1000); + + if (request->hasParam("effect")) { + const char *effect = request->getParam("effect")->value().c_str(); + call.set_effect(effect); + } + + this->defer([call]() mutable { call.perform(); }); + request->send(200); + } else if (match.method == "turn_off") { + auto call = obj->turn_off(); + if (request->hasParam("transition")) { + auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000; + call.set_transition_length(length); + } + this->defer([call]() mutable { call.perform(); }); + request->send(200); + } else { + request->send(404); + } + return; + } + request->send(404); +} +std::string WebServer::light_json(light::LightState *obj) { + return json::build_json([obj](JsonObject &root) { + root["id"] = "light-" + obj->get_object_id(); + root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; + obj->dump_json(root); + }); +} +#endif + +bool WebServer::canHandle(AsyncWebServerRequest *request) { + if (request->url() == "/") + return true; + + if (request->url() == "/update" && request->method() == HTTP_POST) + return true; + + UrlMatch match = match_url(request->url().c_str(), true); + if (!match.valid) + return false; +#ifdef USE_SENSOR + if (request->method() == HTTP_GET && match.domain == "sensor") + return true; +#endif + +#ifdef USE_SWITCH + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "switch") + return true; +#endif + +#ifdef USE_BINARY_SENSOR + if (request->method() == HTTP_GET && match.domain == "binary_sensor") + return true; +#endif + +#ifdef USE_FAN + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "fan") + return true; +#endif + +#ifdef USE_LIGHT + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "light") + return true; +#endif + +#ifdef USE_TEXT_SENSOR + if (request->method() == HTTP_GET && match.domain == "text_sensor") + return true; +#endif + + return false; +} +void WebServer::handleRequest(AsyncWebServerRequest *request) { + if (request->url() == "/") { + this->handle_index_request(request); + return; + } + + if (request->url() == "/update") { + this->handle_update_request(request); + return; + } + + UrlMatch match = match_url(request->url().c_str()); +#ifdef USE_SENSOR + if (match.domain == "sensor") { + this->handle_sensor_request(request, match); + return; + } +#endif + +#ifdef USE_SWITCH + if (match.domain == "switch") { + this->handle_switch_request(request, match); + return; + } +#endif + +#ifdef USE_BINARY_SENSOR + if (match.domain == "binary_sensor") { + this->handle_binary_sensor_request(request, match); + return; + } +#endif + +#ifdef USE_FAN + if (match.domain == "fan") { + this->handle_fan_request(request, match); + return; + } +#endif + +#ifdef USE_LIGHT + if (match.domain == "light") { + this->handle_light_request(request, match); + return; + } +#endif + +#ifdef USE_TEXT_SENSOR + if (match.domain == "text_sensor") { + this->handle_text_sensor_request(request, match); + return; + } +#endif +} + +bool WebServer::isRequestHandlerTrivial() { return false; } + +} // namespace web_server +} // namespace esphome diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h new file mode 100644 index 0000000000..7840a81dce --- /dev/null +++ b/esphome/components/web_server/web_server.h @@ -0,0 +1,141 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/controller.h" + +#include +#include + +namespace esphome { +namespace web_server { + +/// Internal helper struct that is used to parse incoming URLs +struct UrlMatch { + std::string domain; ///< The domain of the component, for example "sensor" + std::string id; ///< The id of the device that's being accessed, for example "living_room_fan" + std::string method; ///< The method that's being called, for example "turn_on" + bool valid; ///< Whether this match is valid +}; + +/** This class allows users to create a web server with their ESP nodes. + * + * Behind the scenes it's using AsyncWebServer to set up the server. It exposes 3 things: + * an index page under '/' that's used to show a simple web interface (the css/js is hosted + * by esphome.io by default), an event source under '/events' that automatically sends + * all state updates in real time + the debug log. Lastly, there's an REST API available + * under the '/light/...', '/sensor/...', ... URLs. A full documentation for this API + * can be found under https://esphome.io/web-api/index.html. + */ +class WebServer : public Controller, public Component, public AsyncWebHandler { + public: + void set_port(uint16_t port) { port_ = port; } + + /** Set the URL to the CSS that's sent to each client. Defaults to + * https://esphome.io/_static/webserver-v1.min.css + * + * @param css_url The url to the web server stylesheet. + */ + void set_css_url(const char *css_url); + + /** Set the URL to the script that's embedded in the index page. Defaults to + * https://esphome.io/_static/webserver-v1.min.js + * + * @param js_url The url to the web server script. + */ + void set_js_url(const char *js_url); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Setup the internal web server and register handlers. + void setup() override; + + void dump_config() override; + + /// MQTT setup priority. + float get_setup_priority() const override; + + /// Handle an index request under '/'. + void handle_index_request(AsyncWebServerRequest *request); + + void handle_update_request(AsyncWebServerRequest *request); + +#ifdef USE_SENSOR + void on_sensor_update(sensor::Sensor *obj, float state) override; + /// Handle a sensor request under '/sensor/'. + void handle_sensor_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the sensor state with its value as a JSON string. + std::string sensor_json(sensor::Sensor *obj, float value); +#endif + +#ifdef USE_SWITCH + void on_switch_update(switch_::Switch *obj, bool state) override; + + /// Handle a switch request under '/switch//'. + void handle_switch_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the switch state with its value as a JSON string. + std::string switch_json(switch_::Switch *obj, bool value); +#endif + +#ifdef USE_BINARY_SENSOR + void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; + + /// Handle a binary sensor request under '/binary_sensor/'. + void handle_binary_sensor_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the binary sensor state with its value as a JSON string. + std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value); +#endif + +#ifdef USE_FAN + void on_fan_update(fan::FanState *obj) override; + + /// Handle a fan request under '/fan//'. + void handle_fan_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the fan state as a JSON string. + std::string fan_json(fan::FanState *obj); +#endif + +#ifdef USE_LIGHT + void on_light_update(light::LightState *obj) override; + + /// Handle a light request under '/light//'. + void handle_light_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the light state as a JSON string. + std::string light_json(light::LightState *obj); +#endif + +#ifdef USE_TEXT_SENSOR + void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) override; + + /// Handle a text sensor request under '/text_sensor/'. + void handle_text_sensor_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the text sensor state with its value as a JSON string. + std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value); +#endif + + /// Override the web handler's canHandle method. + bool canHandle(AsyncWebServerRequest *request) override; + /// Override the web handler's handleRequest method. + void handleRequest(AsyncWebServerRequest *request) override; + void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, + bool final) override; + /// This web handle is not trivial. + bool isRequestHandlerTrivial() override; + + protected: + uint16_t port_; + AsyncWebServer *server_; + AsyncEventSource events_{"/events"}; + const char *css_url_{nullptr}; + const char *js_url_{nullptr}; + uint32_t last_ota_progress_{0}; + uint32_t ota_read_length_{0}; +}; + +} // namespace web_server +} // namespace esphome diff --git a/esphome/components/wifi.py b/esphome/components/wifi.py deleted file mode 100644 index e9932d06ea..0000000000 --- a/esphome/components/wifi.py +++ /dev/null @@ -1,203 +0,0 @@ -import voluptuous as vol - -from esphome.automation import CONDITION_REGISTRY, Condition -import esphome.config_validation as cv -from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ - CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ - CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ - CONF_SUBNET, CONF_USE_ADDRESS -from esphome.core import CORE, HexInt -from esphome.cpp_generator import Pvariable, StructInitializer, add, variable -from esphome.cpp_types import App, Component, esphome_ns, global_ns - -IPAddress = global_ns.class_('IPAddress') -ManualIP = esphome_ns.struct('ManualIP') -WiFiComponent = esphome_ns.class_('WiFiComponent', Component) -WiFiAP = esphome_ns.struct('WiFiAP') - -WiFiPowerSaveMode = esphome_ns.enum('WiFiPowerSaveMode') -WIFI_POWER_SAVE_MODES = { - 'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE, - 'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT, - 'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, -} -WiFiConnectedCondition = esphome_ns.class_('WiFiConnectedCondition', Condition) - - -def validate_password(value): - value = cv.string_strict(value) - if not value: - return value - if len(value) < 8: - raise vol.Invalid(u"WPA password must be at least 8 characters long") - if len(value) > 64: - raise vol.Invalid(u"WPA password must be at most 64 characters long") - return value - - -def validate_channel(value): - value = cv.positive_int(value) - if value < 1: - raise vol.Invalid("Minimum WiFi channel is 1") - if value > 14: - raise vol.Invalid("Maximum WiFi channel is 14") - return value - - -AP_MANUAL_IP_SCHEMA = cv.Schema({ - vol.Required(CONF_STATIC_IP): cv.ipv4, - vol.Required(CONF_GATEWAY): cv.ipv4, - vol.Required(CONF_SUBNET): cv.ipv4, -}) - -STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ - vol.Optional(CONF_DNS1, default="1.1.1.1"): cv.ipv4, - vol.Optional(CONF_DNS2, default="1.0.0.1"): cv.ipv4, -}) - -WIFI_NETWORK_BASE = cv.Schema({ - cv.GenerateID(): cv.declare_variable_id(WiFiAP), - vol.Optional(CONF_SSID): cv.ssid, - vol.Optional(CONF_PASSWORD): validate_password, - vol.Optional(CONF_CHANNEL): validate_channel, - vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, -}) - -WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({ - -}) - -WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ - vol.Optional(CONF_BSSID): cv.mac_address, - vol.Optional(CONF_HIDDEN): cv.boolean, -}) - - -def validate(config): - if CONF_PASSWORD in config and CONF_SSID not in config: - raise vol.Invalid("Cannot have WiFi password without SSID!") - - if CONF_SSID in config: - network = {CONF_SSID: config.pop(CONF_SSID)} - if CONF_PASSWORD in config: - network[CONF_PASSWORD] = config.pop(CONF_PASSWORD) - if CONF_NETWORKS in config: - raise vol.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please " - "copy your network into the 'networks:' key") - config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) - - if (CONF_NETWORKS not in config) and (CONF_AP not in config): - raise vol.Invalid("Please specify at least an SSID or an Access Point " - "to create.") - - if config.get(CONF_FAST_CONNECT, False): - networks = config.get(CONF_NETWORKS, []) - if not networks: - raise vol.Invalid("At least one network required for fast_connect!") - if len(networks) != 1: - raise vol.Invalid("Fast connect can only be used with one network!") - - if CONF_USE_ADDRESS not in config: - if CONF_MANUAL_IP in config: - use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP]) - else: - use_address = CORE.name + config[CONF_DOMAIN] - config[CONF_USE_ADDRESS] = use_address - - return config - - -CONFIG_SCHEMA = vol.All(cv.Schema({ - cv.GenerateID(): cv.declare_variable_id(WiFiComponent), - vol.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), - - vol.Optional(CONF_SSID): cv.ssid, - vol.Optional(CONF_PASSWORD): validate_password, - vol.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, - - vol.Optional(CONF_AP): WIFI_NETWORK_AP, - vol.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, - vol.Optional(CONF_REBOOT_TIMEOUT): cv.positive_time_period_milliseconds, - vol.Optional(CONF_POWER_SAVE_MODE): cv.one_of(*WIFI_POWER_SAVE_MODES, upper=True), - vol.Optional(CONF_FAST_CONNECT): cv.boolean, - vol.Optional(CONF_USE_ADDRESS): cv.string_strict, - - vol.Optional('hostname'): cv.invalid("The hostname option has been removed in 1.11.0"), -}), validate) - - -def safe_ip(ip): - if ip is None: - return IPAddress(0, 0, 0, 0) - return IPAddress(*ip.args) - - -def manual_ip(config): - if config is None: - return None - return StructInitializer( - ManualIP, - ('static_ip', safe_ip(config[CONF_STATIC_IP])), - ('gateway', safe_ip(config[CONF_GATEWAY])), - ('subnet', safe_ip(config[CONF_SUBNET])), - ('dns1', safe_ip(config.get(CONF_DNS1))), - ('dns2', safe_ip(config.get(CONF_DNS2))), - ) - - -def wifi_network(config, static_ip): - ap = variable(config[CONF_ID], WiFiAP()) - if CONF_SSID in config: - add(ap.set_ssid(config[CONF_SSID])) - if CONF_PASSWORD in config: - add(ap.set_password(config[CONF_PASSWORD])) - if CONF_BSSID in config: - add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) - if CONF_HIDDEN in config: - add(ap.set_hidden(config[CONF_HIDDEN])) - if CONF_CHANNEL in config: - add(ap.set_channel(config[CONF_CHANNEL])) - if static_ip is not None: - add(ap.set_manual_ip(manual_ip(static_ip))) - - return ap - - -def to_code(config): - rhs = App.init_wifi() - wifi = Pvariable(config[CONF_ID], rhs) - add(wifi.set_use_address(config[CONF_USE_ADDRESS])) - - for network in config.get(CONF_NETWORKS, []): - add(wifi.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP)))) - - if CONF_AP in config: - add(wifi.set_ap(wifi_network(config[CONF_AP], config.get(CONF_MANUAL_IP)))) - - if CONF_REBOOT_TIMEOUT in config: - add(wifi.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - - if CONF_POWER_SAVE_MODE in config: - add(wifi.set_power_save_mode(WIFI_POWER_SAVE_MODES[config[CONF_POWER_SAVE_MODE]])) - - if CONF_FAST_CONNECT in config: - add(wifi.set_fast_connect(config[CONF_FAST_CONNECT])) - - -def lib_deps(config): - if CORE.is_esp8266: - return 'ESP8266WiFi' - if CORE.is_esp32: - return None - raise NotImplementedError - - -CONF_WIFI_CONNECTED = 'wifi.connected' -WIFI_CONNECTED_CONDITION_SCHEMA = cv.Schema({}) - - -@CONDITION_REGISTRY.register(CONF_WIFI_CONNECTED, WIFI_CONNECTED_CONDITION_SCHEMA) -def wifi_connected_to_code(config, condition_id, template_arg, args): - rhs = WiFiConnectedCondition.new(template_arg) - type = WiFiConnectedCondition.template(template_arg) - yield Pvariable(condition_id, rhs, type=type) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py new file mode 100644 index 0000000000..9ac8b74ef2 --- /dev/null +++ b/esphome/components/wifi/__init__.py @@ -0,0 +1,194 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import Condition +from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ + CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ + CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ + CONF_SUBNET, CONF_USE_ADDRESS +from esphome.core import CORE, HexInt, coroutine_with_priority + +AUTO_LOAD = ['network'] + +wifi_ns = cg.esphome_ns.namespace('wifi') +IPAddress = cg.global_ns.class_('IPAddress') +ManualIP = wifi_ns.struct('ManualIP') +WiFiComponent = wifi_ns.class_('WiFiComponent', cg.Component) +WiFiAP = wifi_ns.struct('WiFiAP') + +WiFiPowerSaveMode = wifi_ns.enum('WiFiPowerSaveMode') +WIFI_POWER_SAVE_MODES = { + 'NONE': WiFiPowerSaveMode.WIFI_POWER_SAVE_NONE, + 'LIGHT': WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT, + 'HIGH': WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, +} +WiFiConnectedCondition = wifi_ns.class_('WiFiConnectedCondition', Condition) + + +def validate_password(value): + value = cv.string_strict(value) + if not value: + return value + if len(value) < 8: + raise cv.Invalid(u"WPA password must be at least 8 characters long") + if len(value) > 64: + raise cv.Invalid(u"WPA password must be at most 64 characters long") + return value + + +def validate_channel(value): + value = cv.positive_int(value) + if value < 1: + raise cv.Invalid("Minimum WiFi channel is 1") + if value > 14: + raise cv.Invalid("Maximum WiFi channel is 14") + return value + + +AP_MANUAL_IP_SCHEMA = cv.Schema({ + cv.Required(CONF_STATIC_IP): cv.ipv4, + cv.Required(CONF_GATEWAY): cv.ipv4, + cv.Required(CONF_SUBNET): cv.ipv4, +}) + +STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ + cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4, + cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, +}) + +WIFI_NETWORK_BASE = cv.Schema({ + cv.GenerateID(): cv.declare_id(WiFiAP), + cv.Optional(CONF_SSID): cv.ssid, + cv.Optional(CONF_PASSWORD): validate_password, + cv.Optional(CONF_CHANNEL): validate_channel, + cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, +}) + +WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({ + +}) + +WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ + cv.Optional(CONF_BSSID): cv.mac_address, + cv.Optional(CONF_HIDDEN): cv.boolean, +}) + + +def validate(config): + if CONF_PASSWORD in config and CONF_SSID not in config: + raise cv.Invalid("Cannot have WiFi password without SSID!") + + if CONF_SSID in config: + network = {CONF_SSID: config.pop(CONF_SSID)} + if CONF_PASSWORD in config: + network[CONF_PASSWORD] = config.pop(CONF_PASSWORD) + if CONF_NETWORKS in config: + raise cv.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please " + "copy your network into the 'networks:' key") + config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) + + if (CONF_NETWORKS not in config) and (CONF_AP not in config): + raise cv.Invalid("Please specify at least an SSID or an Access Point " + "to create.") + + if config.get(CONF_FAST_CONNECT, False): + networks = config.get(CONF_NETWORKS, []) + if not networks: + raise cv.Invalid("At least one network required for fast_connect!") + if len(networks) != 1: + raise cv.Invalid("Fast connect can only be used with one network!") + + if CONF_USE_ADDRESS not in config: + if CONF_MANUAL_IP in config: + use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP]) + else: + use_address = CORE.name + config[CONF_DOMAIN] + config[CONF_USE_ADDRESS] = use_address + + return config + + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(WiFiComponent), + cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), + + cv.Optional(CONF_SSID): cv.ssid, + cv.Optional(CONF_PASSWORD): validate_password, + cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + + cv.Optional(CONF_AP): WIFI_NETWORK_AP, + cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, + cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_POWER_SAVE_MODE, default='NONE'): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), + cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, + cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + + cv.Optional('hostname'): cv.invalid("The hostname option has been removed in 1.11.0"), +}), validate) + + +def safe_ip(ip): + if ip is None: + return IPAddress(0, 0, 0, 0) + return IPAddress(*ip.args) + + +def manual_ip(config): + if config is None: + return None + return cg.StructInitializer( + ManualIP, + ('static_ip', safe_ip(config[CONF_STATIC_IP])), + ('gateway', safe_ip(config[CONF_GATEWAY])), + ('subnet', safe_ip(config[CONF_SUBNET])), + ('dns1', safe_ip(config.get(CONF_DNS1))), + ('dns2', safe_ip(config.get(CONF_DNS2))), + ) + + +def wifi_network(config, static_ip): + ap = cg.variable(config[CONF_ID], WiFiAP()) + if CONF_SSID in config: + cg.add(ap.set_ssid(config[CONF_SSID])) + if CONF_PASSWORD in config: + cg.add(ap.set_password(config[CONF_PASSWORD])) + if CONF_BSSID in config: + cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) + if CONF_HIDDEN in config: + cg.add(ap.set_hidden(config[CONF_HIDDEN])) + if CONF_CHANNEL in config: + cg.add(ap.set_channel(config[CONF_CHANNEL])) + if static_ip is not None: + cg.add(ap.set_manual_ip(manual_ip(static_ip))) + + return ap + + +@coroutine_with_priority(60.0) +def to_code(config): + rhs = WiFiComponent.new() + wifi = cg.Pvariable(config[CONF_ID], rhs) + cg.add(wifi.set_use_address(config[CONF_USE_ADDRESS])) + + for network in config.get(CONF_NETWORKS, []): + cg.add(wifi.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP)))) + + if CONF_AP in config: + cg.add(wifi.set_ap(wifi_network(config[CONF_AP], config.get(CONF_MANUAL_IP)))) + + cg.add(wifi.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) + cg.add(wifi.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) + cg.add(wifi.set_fast_connect(config[CONF_FAST_CONNECT])) + + if CORE.is_esp8266: + cg.add_library('ESP8266WiFi', None) + + cg.add_define('USE_WIFI') + + # Register at end for OTA safe mode + yield cg.register_component(wifi, config) + + +@automation.register_condition('wifi.connected', WiFiConnectedCondition, cv.Schema({})) +def wifi_connected_to_code(config, condition_id, template_arg, args): + yield cg.new_Pvariable(condition_id, template_arg) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp new file mode 100644 index 0000000000..882d45f793 --- /dev/null +++ b/esphome/components/wifi/wifi_component.cpp @@ -0,0 +1,541 @@ +#include "wifi_component.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include +#endif + +#include +#include +#include "lwip/err.h" +#include "lwip/dns.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/esphal.h" +#include "esphome/core/util.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace wifi { + +static const char *TAG = "wifi"; + +float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } + +void WiFiComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up WiFi..."); + + this->wifi_register_callbacks_(); + + bool ret = this->wifi_mode_(this->has_sta(), false); + if (!ret) { + this->mark_failed(); + return; + } + + if (this->has_sta()) { + this->wifi_disable_auto_connect_(); + delay(10); + + this->wifi_apply_power_save_(); + + if (this->fast_connect_) { + this->selected_ap_ = this->sta_[0]; + this->start_connecting(this->selected_ap_, false); + } else { + this->start_scanning(); + } + } else if (this->has_ap()) { + this->setup_ap_config_(); + } + + this->wifi_apply_hostname_(); + network_setup_mdns(); +} + +void WiFiComponent::loop() { + const uint32_t now = millis(); + + if (this->has_sta()) { + switch (this->state_) { + case WIFI_COMPONENT_STATE_COOLDOWN: { + this->status_set_warning(); + if (millis() - this->action_started_ > 5000) { + if (this->fast_connect_) { + this->start_connecting(this->sta_[0], false); + } else { + this->start_scanning(); + } + } + break; + } + case WIFI_COMPONENT_STATE_STA_SCANNING: { + this->status_set_warning(); + this->check_scanning_finished(); + break; + } + case WIFI_COMPONENT_STATE_STA_CONNECTING: + case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { + this->status_set_warning(); + this->check_connecting_finished(); + break; + } + + case WIFI_COMPONENT_STATE_STA_CONNECTED: { + if (!this->is_connected()) { + ESP_LOGW(TAG, "WiFi Connection lost... Reconnecting..."); + this->retry_connect(); + } else { + this->status_clear_warning(); + this->last_connected_ = now; + } + break; + } + case WIFI_COMPONENT_STATE_OFF: + case WIFI_COMPONENT_STATE_AP: + break; + } + + if (!this->has_ap() && this->reboot_timeout_ != 0) { + if (now - this->last_connected_ > this->reboot_timeout_) { + ESP_LOGE(TAG, "Can't connect to WiFi, rebooting..."); + App.reboot(); + } + } + } + + network_tick_mdns(); +} + +WiFiComponent::WiFiComponent() { global_wifi_component = this; } + +bool WiFiComponent::has_ap() const { return !this->ap_.get_ssid().empty(); } +bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } +void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; } +IPAddress WiFiComponent::get_ip_address() { + if (this->has_sta()) + return this->wifi_sta_ip_(); + if (this->has_ap()) + return this->wifi_soft_ap_ip_(); + return {}; +} +std::string WiFiComponent::get_use_address() const { + if (this->use_address_.empty()) { + return App.get_name() + ".local"; + } + return this->use_address_; +} +void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void WiFiComponent::setup_ap_config_() { + this->wifi_mode_({}, true); + + if (this->ap_setup_) + return; + + ESP_LOGCONFIG(TAG, "Setting up AP..."); + + ESP_LOGCONFIG(TAG, " AP SSID: '%s'", this->ap_.get_ssid().c_str()); + ESP_LOGCONFIG(TAG, " AP Password: '%s'", this->ap_.get_password().c_str()); + if (this->ap_.get_manual_ip().has_value()) { + auto manual = *this->ap_.get_manual_ip(); + ESP_LOGCONFIG(TAG, " AP Static IP: '%s'", manual.static_ip.toString().c_str()); + ESP_LOGCONFIG(TAG, " AP Gateway: '%s'", manual.gateway.toString().c_str()); + ESP_LOGCONFIG(TAG, " AP Subnet: '%s'", manual.subnet.toString().c_str()); + } + + this->ap_setup_ = this->wifi_start_ap_(this->ap_); + ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip_().toString().c_str()); + + if (!this->has_sta()) { + this->state_ = WIFI_COMPONENT_STATE_AP; + } +} + +float WiFiComponent::get_loop_priority() const { + return 10.0f; // before other loop components +} +void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; } +void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } + +void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { + ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str()); +#ifdef ESPHOME_LOG_HAS_VERBOSE + ESP_LOGV(TAG, "Connection Params:"); + ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); + if (ap.get_bssid().has_value()) { + bssid_t b = *ap.get_bssid(); + ESP_LOGV(TAG, " BSSID: %02X:%02X:%02X:%02X:%02X:%02X", b[0], b[1], b[2], b[3], b[4], b[5]); + } else { + ESP_LOGV(TAG, " BSSID: Not Set"); + } + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); + if (ap.get_channel().has_value()) { + ESP_LOGV(TAG, " Channel: %u", *ap.get_channel()); + } else { + ESP_LOGV(TAG, " Channel: Not Set"); + } + if (ap.get_manual_ip().has_value()) { + ManualIP m = *ap.get_manual_ip(); + ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.toString().c_str(), + m.gateway.toString().c_str(), m.subnet.toString().c_str(), m.dns1.toString().c_str(), + m.dns2.toString().c_str()); + } else { + ESP_LOGV(TAG, " Using DHCP IP"); + } + ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); +#endif + + if (!this->wifi_sta_connect_(ap)) { + ESP_LOGE(TAG, "wifi_sta_connect_ failed!"); + this->retry_connect(); + return; + } + + if (!two) { + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + } else { + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + } + this->action_started_ = millis(); +} + +void print_signal_bars(int8_t rssi, char *buf) { + // LOWER ONE QUARTER BLOCK + // Unicode: U+2582, UTF-8: E2 96 82 + // LOWER HALF BLOCK + // Unicode: U+2584, UTF-8: E2 96 84 + // LOWER THREE QUARTERS BLOCK + // Unicode: U+2586, UTF-8: E2 96 86 + // FULL BLOCK + // Unicode: U+2588, UTF-8: E2 96 88 + if (rssi >= -50) { + sprintf(buf, "\033[0;32m" // green + "\xe2\x96\x82" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); + } else if (rssi >= -65) { + sprintf(buf, "\033[0;33m" // yellow + "\xe2\x96\x82" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\033[0;37m" + "\xe2\x96\x88" + "\033[0m"); + } else if (rssi >= -85) { + sprintf(buf, "\033[0;33m" // yellow + "\xe2\x96\x82" + "\xe2\x96\x84" + "\033[0;37m" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); + } else { + sprintf(buf, "\033[0;31m" // red + "\xe2\x96\x82" + "\033[0;37m" + "\xe2\x96\x84" + "\xe2\x96\x86" + "\xe2\x96\x88" + "\033[0m"); + } +} + +void WiFiComponent::print_connect_params_() { + uint8_t *bssid = WiFi.BSSID(); + ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), WiFi.SSID().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", WiFi.localIP().toString().c_str()); + ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3], + bssid[4], bssid[5]); + ESP_LOGCONFIG(TAG, " Hostname: '%s'", App.get_name().c_str()); + char signal_bars[50]; + int8_t rssi = WiFi.RSSI(); + print_signal_bars(rssi, signal_bars); + ESP_LOGCONFIG(TAG, " Signal strength: %d dB %s", rssi, signal_bars); + ESP_LOGCONFIG(TAG, " Channel: %d", WiFi.channel()); + ESP_LOGCONFIG(TAG, " Subnet: %s", WiFi.subnetMask().toString().c_str()); + ESP_LOGCONFIG(TAG, " Gateway: %s", WiFi.gatewayIP().toString().c_str()); + ESP_LOGCONFIG(TAG, " DNS1: %s", WiFi.dnsIP(0).toString().c_str()); + ESP_LOGCONFIG(TAG, " DNS2: %s", WiFi.dnsIP(1).toString().c_str()); +} + +void WiFiComponent::start_scanning() { + this->action_started_ = millis(); + ESP_LOGD(TAG, "Starting scan..."); + this->wifi_scan_start_(); + this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; +} + +void WiFiComponent::check_scanning_finished() { + if (!this->scan_done_) { + if (millis() - this->action_started_ > 30000) { + ESP_LOGE(TAG, "Scan timeout!"); + this->retry_connect(); + } + return; + } + this->scan_done_ = false; + + ESP_LOGD(TAG, "Found networks:"); + if (this->scan_result_.empty()) { + ESP_LOGD(TAG, " No network found!"); + this->retry_connect(); + return; + } + + for (auto &res : this->scan_result_) { + for (auto &ap : this->sta_) { + if (res.matches(ap)) { + res.set_matches(true); + break; + } + } + } + + std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(), + [](const WiFiScanResult &a, const WiFiScanResult &b) { + if (a.get_matches() && !b.get_matches()) + return true; + if (!a.get_matches() && b.get_matches()) + return false; + + return a.get_rssi() > b.get_rssi(); + }); + + for (auto &res : this->scan_result_) { + char bssid_s[18]; + auto bssid = res.get_bssid(); + sprintf(bssid_s, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + char signal_bars[50]; + print_signal_bars(res.get_rssi(), signal_bars); + + if (res.get_matches()) { + ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), + res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, signal_bars); + ESP_LOGD(TAG, " Channel: %u", res.get_channel()); + ESP_LOGD(TAG, " RSSI: %d dB", res.get_rssi()); + } else { + ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, signal_bars); + } + } + + if (!this->scan_result_[0].get_matches()) { + ESP_LOGW(TAG, "No matching network found!"); + this->retry_connect(); + return; + } + + WiFiAP connect_params; + WiFiScanResult scan_res = this->scan_result_[0]; + for (auto &config : this->sta_) { + // search for matching STA config, at least one will match (from checks before) + if (!scan_res.matches(config)) { + continue; + } + + if (config.get_hidden()) { + // selected network is hidden, we use the data from the config + connect_params.set_hidden(true); + connect_params.set_ssid(config.get_ssid()); + // don't set BSSID and channel, there might be multiple hidden networks + // but we can't know which one is the correct one. Rely on probe-req with just SSID. + } else { + // selected network is visible, we use the data from the scan + // limit the connect params to only connect to exactly this network + // (network selection is done during scan phase). + connect_params.set_hidden(false); + connect_params.set_ssid(scan_res.get_ssid()); + connect_params.set_channel(scan_res.get_channel()); + connect_params.set_bssid(scan_res.get_bssid()); + } + // set manual IP+password (if any) + connect_params.set_manual_ip(config.get_manual_ip()); + connect_params.set_password(config.get_password()); + break; + } + + yield(); + + this->selected_ap_ = connect_params; + this->start_connecting(connect_params, false); +} + +void WiFiComponent::dump_config() { + ESP_LOGCONFIG(TAG, "WiFi:"); + this->print_connect_params_(); +} + +void WiFiComponent::check_connecting_finished() { + wl_status_t status = this->wifi_sta_status_(); + + if (status == WL_CONNECTED) { + ESP_LOGI(TAG, "WiFi connected!"); + this->print_connect_params_(); + + if (this->has_ap()) { + ESP_LOGD(TAG, "Disabling AP..."); + this->wifi_mode_({}, false); + } + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; + this->num_retried_ = 0; + return; + } + + uint32_t now = millis(); + if (now - this->action_started_ > 30000) { + ESP_LOGW(TAG, "Timeout while connecting to WiFi."); + this->retry_connect(); + return; + } + + if (this->error_from_callback_) { + ESP_LOGW(TAG, "Error while connecting to network."); + this->retry_connect(); + return; + } + + if (status == WL_IDLE_STATUS || status == WL_DISCONNECTED || status == WL_CONNECTION_LOST) { + // WL_DISCONNECTED is set while not connected yet. + // WL_IDLE_STATUS is set while we're waiting for the IP address. + // WL_CONNECTION_LOST happens on the ESP32 + return; + } + + if (status == WL_NO_SSID_AVAIL) { + ESP_LOGW(TAG, "WiFi network can not be found anymore."); + this->retry_connect(); + return; + } + + if (status == WL_CONNECT_FAILED) { + ESP_LOGW(TAG, "Connecting to WiFi network failed. Are the credentials wrong?"); + this->retry_connect(); + return; + } + + ESP_LOGW(TAG, "WiFi Unknown connection status %d", status); +} + +void WiFiComponent::retry_connect() { + if (this->num_retried_ > 5 || this->error_from_callback_) { + // If retry failed for more than 5 times, let's restart STA + ESP_LOGW(TAG, "Restarting WiFi adapter..."); + this->wifi_mode_(false, {}); + delay(100); + this->num_retried_ = 0; + } else { + this->num_retried_++; + } + this->error_from_callback_ = false; + if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { + yield(); + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + this->start_connecting(this->selected_ap_, true); + return; + } + + if (this->has_ap()) { + this->setup_ap_config_(); + } + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); +} + +bool WiFiComponent::can_proceed() { + if (this->has_ap() && !this->has_sta()) { + return true; + } + return this->is_connected(); +} +void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } +bool WiFiComponent::is_connected() { + return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_status_() == WL_CONNECTED && + !this->error_from_callback_; +} +bool WiFiComponent::ready_for_ota() { + if (this->has_ap()) + return true; + return this->is_connected(); +} +void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } + +std::string WiFiComponent::format_mac_addr(const uint8_t *mac) { + char buf[20]; + sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return buf; +} + +bool sta_field_equal(const uint8_t *field_a, const uint8_t *field_b, int len) { + for (int i = 0; i < len; i++) { + uint8_t a = field_a[i]; + uint8_t b = field_b[i]; + if (a == b && a == 0) + break; + if (a == b) + continue; + + return false; + } + + return true; +} + +void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } +void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } +void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } +void WiFiAP::set_password(const std::string &password) { this->password_ = password; } +void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } +void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } +void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } +const std::string &WiFiAP::get_ssid() const { return this->ssid_; } +const optional &WiFiAP::get_bssid() const { return this->bssid_; } +const std::string &WiFiAP::get_password() const { return this->password_; } +const optional &WiFiAP::get_channel() const { return this->channel_; } +const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } +bool WiFiAP::get_hidden() const { return this->hidden_; } + +WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const std::string &ssid, uint8_t channel, int8_t rssi, + bool with_auth, bool is_hidden) + : bssid_(bssid), ssid_(ssid), channel_(channel), rssi_(rssi), with_auth_(with_auth), is_hidden_(is_hidden) {} +bool WiFiScanResult::matches(const WiFiAP &config) { + if (config.get_hidden()) { + // User configured a hidden network, only match actually hidden networks + // don't match SSID + if (!this->is_hidden_) + return false; + } else if (!config.get_ssid().empty()) { + // check if SSID matches + if (config.get_ssid() != this->ssid_) + return false; + } else { + // network is configured without SSID - match other settings + } + // If BSSID configured, only match for correct BSSIDs + if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_) + return false; + // If PW given, only match for networks with auth (and vice versa) + if (config.get_password().empty() == this->with_auth_) + return false; + // If channel configured, only match networks on that channel. + if (config.get_channel().has_value() && *config.get_channel() != this->channel_) { + return false; + } + return true; +} +bool WiFiScanResult::get_matches() const { return this->matches_; } +void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; } +const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; } +const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; } +uint8_t WiFiScanResult::get_channel() const { return this->channel_; } +int8_t WiFiScanResult::get_rssi() const { return this->rssi_; } +bool WiFiScanResult::get_with_auth() const { return this->with_auth_; } +bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; } + +WiFiComponent *global_wifi_component; + +} // namespace wifi +} // namespace esphome diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h new file mode 100644 index 0000000000..8e6418791c --- /dev/null +++ b/esphome/components/wifi/wifi_component.h @@ -0,0 +1,234 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 +#include +#include +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include + +#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +extern "C" { +#include +}; +#endif +#endif + +namespace esphome { +namespace wifi { + +enum WiFiComponentState { + /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ + WIFI_COMPONENT_STATE_OFF = 0, + /** WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of time. */ + WIFI_COMPONENT_STATE_COOLDOWN, + /** WiFi is in STA-only mode and currently scanning for APs. */ + WIFI_COMPONENT_STATE_STA_SCANNING, + /** WiFi is in STA(+AP) mode and currently connecting to an AP. */ + WIFI_COMPONENT_STATE_STA_CONNECTING, + /** WiFi is in STA(+AP) mode and currently connecting to an AP a second time. + * + * This is required because for some reason ESPs don't like to connect to WiFi APs directly after + * a scan. + * */ + WIFI_COMPONENT_STATE_STA_CONNECTING_2, + /** WiFi is in STA(+AP) mode and successfully connected. */ + WIFI_COMPONENT_STATE_STA_CONNECTED, + /** WiFi is in AP-only mode and internal AP is already enabled. */ + WIFI_COMPONENT_STATE_AP, +}; + +/// Struct for setting static IPs in WiFiComponent. +struct ManualIP { + IPAddress static_ip; + IPAddress gateway; + IPAddress subnet; + IPAddress dns1; ///< The first DNS server. 0.0.0.0 for default. + IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. +}; + +using bssid_t = std::array; + +class WiFiAP { + public: + void set_ssid(const std::string &ssid); + void set_bssid(bssid_t bssid); + void set_bssid(optional bssid); + void set_password(const std::string &password); + void set_channel(optional channel); + void set_manual_ip(optional manual_ip); + void set_hidden(bool hidden); + const std::string &get_ssid() const; + const optional &get_bssid() const; + const std::string &get_password() const; + const optional &get_channel() const; + const optional &get_manual_ip() const; + bool get_hidden() const; + + protected: + std::string ssid_; + optional bssid_; + std::string password_; + optional channel_; + optional manual_ip_; + bool hidden_{false}; +}; + +class WiFiScanResult { + public: + WiFiScanResult(const bssid_t &bssid, const std::string &ssid, uint8_t channel, int8_t rssi, bool with_auth, + bool is_hidden); + + bool matches(const WiFiAP &config); + + bool get_matches() const; + void set_matches(bool matches); + const bssid_t &get_bssid() const; + const std::string &get_ssid() const; + uint8_t get_channel() const; + int8_t get_rssi() const; + bool get_with_auth() const; + bool get_is_hidden() const; + + protected: + bool matches_{false}; + bssid_t bssid_; + std::string ssid_; + uint8_t channel_; + int8_t rssi_; + bool with_auth_; + bool is_hidden_; +}; + +enum WiFiPowerSaveMode { + WIFI_POWER_SAVE_NONE = 0, + WIFI_POWER_SAVE_LIGHT, + WIFI_POWER_SAVE_HIGH, +}; + +/// This component is responsible for managing the ESP WiFi interface. +class WiFiComponent : public Component { + public: + /// Construct a WiFiComponent. + WiFiComponent(); + + void add_sta(const WiFiAP &ap); + + /** Setup an Access Point that should be created if no connection to a station can be made. + * + * This can also be used without set_sta(). Then the AP will always be active. + * + * If both STA and AP are defined, then both will be enabled at startup, but if a connection to a station + * can be made, the AP will be turned off again. + */ + void set_ap(const WiFiAP &ap); + + void start_scanning(); + void check_scanning_finished(); + void start_connecting(const WiFiAP &ap, bool two); + void set_fast_connect(bool fast_connect); + + void check_connecting_finished(); + + void retry_connect(); + + bool can_proceed() override; + + bool ready_for_ota(); + + void set_reboot_timeout(uint32_t reboot_timeout); + + bool is_connected(); + + void set_power_save_mode(WiFiPowerSaveMode power_save); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Setup WiFi interface. + void setup() override; + void dump_config() override; + /// WIFI setup_priority. + float get_setup_priority() const override; + float get_loop_priority() const override; + + /// Reconnect WiFi if required. + void loop() override; + + bool has_sta() const; + bool has_ap() const; + + IPAddress get_ip_address(); + std::string get_use_address() const; + void set_use_address(const std::string &use_address); + + protected: + static std::string format_mac_addr(const uint8_t mac[6]); + void setup_ap_config_(); + void print_connect_params_(); + + bool wifi_mode_(optional sta, optional ap); + bool wifi_disable_auto_connect_(); + bool wifi_apply_power_save_(); + bool wifi_sta_ip_config_(optional manual_ip); + IPAddress wifi_sta_ip_(); + bool wifi_apply_hostname_(); + bool wifi_sta_connect_(WiFiAP ap); + void wifi_register_callbacks_(); + wl_status_t wifi_sta_status_(); + bool wifi_scan_start_(); + bool wifi_ap_ip_config_(optional manual_ip); + bool wifi_start_ap_(const WiFiAP &ap); + IPAddress wifi_soft_ap_ip_(); + +#ifdef ARDUINO_ARCH_ESP8266 + static void wifi_event_callback(System_Event_t *event); + void wifi_scan_done_callback_(void *arg, STATUS status); + static void s_wifi_scan_done_callback(void *arg, STATUS status); +#endif + +#ifdef ARDUINO_ARCH_ESP32 + void wifi_event_callback_(system_event_id_t event, system_event_info_t info); + void wifi_scan_done_callback_(); +#endif + + std::string use_address_; + std::vector sta_; + WiFiAP selected_ap_; + bool fast_connect_{false}; + + WiFiAP ap_; + WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; + uint32_t action_started_; + uint8_t num_retried_{0}; + uint32_t last_connected_{0}; + uint32_t reboot_timeout_{300000}; + WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + bool error_from_callback_{false}; + std::vector scan_result_; + bool scan_done_{false}; + bool ap_setup_{false}; +}; + +extern WiFiComponent *global_wifi_component; + +template class WiFiConnectedCondition : public Condition { + public: + bool check(Ts... x) override; +}; + +template bool WiFiConnectedCondition::check(Ts... x) { + return global_wifi_component->is_connected(); +} + +} // namespace wifi +} // namespace esphome diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp new file mode 100644 index 0000000000..6b118cf0ae --- /dev/null +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -0,0 +1,529 @@ +#include "wifi_component.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include + +#include +#include +#include "lwip/err.h" +#include "lwip/dns.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/esphal.h" +#include "esphome/core/application.h" +#include "esphome/core/util.h" + +namespace esphome { +namespace wifi { + +static const char *TAG = "wifi_esp32"; + +bool WiFiComponent::wifi_mode_(optional sta, optional ap) { + uint8_t current_mode = WiFi.getMode(); + bool current_sta = current_mode & 0b01; + bool current_ap = current_mode & 0b10; + bool sta_ = sta.value_or(current_sta); + bool ap_ = ap.value_or(current_ap); + if (current_sta == sta_ && current_ap == ap_) + return true; + + if (sta_ && !current_sta) { + ESP_LOGV(TAG, "Enabling STA."); + } else if (!sta_ && current_sta) { + ESP_LOGV(TAG, "Disabling STA."); + } + if (ap_ && !current_ap) { + ESP_LOGV(TAG, "Enabling AP."); + } else if (!ap_ && current_ap) { + ESP_LOGV(TAG, "Disabling AP."); + } + + uint8_t mode = 0; + if (sta_) + mode |= 0b01; + if (ap_) + mode |= 0b10; + bool ret = WiFi.mode(static_cast(mode)); + + if (!ret) { + ESP_LOGW(TAG, "Setting WiFi mode failed!"); + } + + return ret; +} +bool WiFiComponent::wifi_disable_auto_connect_() { + WiFi.setAutoReconnect(false); + return true; +} +bool WiFiComponent::wifi_apply_power_save_() { + wifi_ps_type_t power_save; + switch (this->power_save_) { + case WIFI_POWER_SAVE_LIGHT: + power_save = WIFI_PS_MIN_MODEM; + break; + case WIFI_POWER_SAVE_HIGH: + power_save = WIFI_PS_MAX_MODEM; + break; + case WIFI_POWER_SAVE_NONE: + default: + power_save = WIFI_PS_NONE; + break; + } + return esp_wifi_set_ps(power_save) == ESP_OK; +} +bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + tcpip_adapter_dhcp_status_t dhcp_status; + tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status); + if (!manual_ip.has_value()) { + // Use DHCP client + if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) { + esp_err_t err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); + } + return err == ESP_OK; + } + return true; + } + + tcpip_adapter_ip_info_t info; + memset(&info, 0, sizeof(info)); + info.ip.addr = static_cast(manual_ip->static_ip); + info.gw.addr = static_cast(manual_ip->gateway); + info.netmask.addr = static_cast(manual_ip->subnet); + + esp_err_t dhcp_stop_ret = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA); + if (dhcp_stop_ret != ESP_OK) { + ESP_LOGV(TAG, "Stopping DHCP client failed! %d", dhcp_stop_ret); + } + + esp_err_t wifi_set_info_ret = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &info); + if (wifi_set_info_ret != ESP_OK) { + ESP_LOGV(TAG, "Setting manual IP info failed! %s", esp_err_to_name(wifi_set_info_ret)); + } + + ip_addr_t dns; + dns.type = IPADDR_TYPE_V4; + if (uint32_t(manual_ip->dns1) != 0) { + dns.u_addr.ip4.addr = static_cast(manual_ip->dns1); + dns_setserver(0, &dns); + } + if (uint32_t(manual_ip->dns2) != 0) { + dns.u_addr.ip4.addr = static_cast(manual_ip->dns2); + dns_setserver(1, &dns); + } + + return true; +} + +IPAddress WiFiComponent::wifi_sta_ip_() { + if (!this->has_sta()) + return IPAddress(); + tcpip_adapter_ip_info_t ip; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); + return IPAddress(ip.ip.addr); +} + +bool WiFiComponent::wifi_apply_hostname_() { + esp_err_t err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Setting hostname failed: %d", err); + return false; + } + return true; +} +bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + wifi_config_t conf; + memset(&conf, 0, sizeof(conf)); + strcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str()); + strcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str()); + + if (ap.get_bssid().has_value()) { + conf.sta.bssid_set = 1; + memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6); + } else { + conf.sta.bssid_set = 0; + } + if (ap.get_channel().has_value()) { + conf.sta.channel = *ap.get_channel(); + } + + esp_err_t err = esp_wifi_disconnect(); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_disconnect failed! %d", err); + return false; + } + + err = esp_wifi_set_config(WIFI_IF_STA, &conf); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); + } + + if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { + return false; + } + + this->wifi_apply_hostname_(); + + err = esp_wifi_connect(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_connect failed! %d", err); + return false; + } + + return true; +} +const char *get_auth_mode_str(uint8_t mode) { + switch (mode) { + case WIFI_AUTH_OPEN: + return "OPEN"; + case WIFI_AUTH_WEP: + return "WEP"; + case WIFI_AUTH_WPA_PSK: + return "WPA PSK"; + case WIFI_AUTH_WPA2_PSK: + return "WPA2 PSK"; + case WIFI_AUTH_WPA_WPA2_PSK: + return "WPA/WPA2 PSK"; + case WIFI_AUTH_WPA2_ENTERPRISE: + return "WPA2 Enterprise"; + default: + return "UNKNOWN"; + } +} +std::string format_ip4_addr(const ip4_addr_t &ip) { + char buf[20]; + sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), + uint8_t(ip.addr >> 24)); + return buf; +} +const char *get_op_mode_str(uint8_t mode) { + switch (mode) { + case WIFI_OFF: + return "OFF"; + case WIFI_STA: + return "STA"; + case WIFI_AP: + return "AP"; + case WIFI_AP_STA: + return "AP+STA"; + default: + return "UNKNOWN"; + } +} +const char *get_disconnect_reason_str(uint8_t reason) { + switch (reason) { + case WIFI_REASON_AUTH_EXPIRE: + return "Auth Expired"; + case WIFI_REASON_AUTH_LEAVE: + return "Auth Leave"; + case WIFI_REASON_ASSOC_EXPIRE: + return "Association Expired"; + case WIFI_REASON_ASSOC_TOOMANY: + return "Too Many Associations"; + case WIFI_REASON_NOT_AUTHED: + return "Not Authenticated"; + case WIFI_REASON_NOT_ASSOCED: + return "Not Associated"; + case WIFI_REASON_ASSOC_LEAVE: + return "Association Leave"; + case WIFI_REASON_ASSOC_NOT_AUTHED: + return "Association not Authenticated"; + case WIFI_REASON_DISASSOC_PWRCAP_BAD: + return "Disassociate Power Cap Bad"; + case WIFI_REASON_DISASSOC_SUPCHAN_BAD: + return "Disassociate Supported Channel Bad"; + case WIFI_REASON_IE_INVALID: + return "IE Invalid"; + case WIFI_REASON_MIC_FAILURE: + return "Mic Failure"; + case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: + return "4-Way Handshake Timeout"; + case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT: + return "Group Key Update Timeout"; + case WIFI_REASON_IE_IN_4WAY_DIFFERS: + return "IE In 4-Way Handshake Differs"; + case WIFI_REASON_GROUP_CIPHER_INVALID: + return "Group Cipher Invalid"; + case WIFI_REASON_PAIRWISE_CIPHER_INVALID: + return "Pairwise Cipher Invalid"; + case WIFI_REASON_AKMP_INVALID: + return "AKMP Invalid"; + case WIFI_REASON_UNSUPP_RSN_IE_VERSION: + return "Unsupported RSN IE version"; + case WIFI_REASON_INVALID_RSN_IE_CAP: + return "Invalid RSN IE Cap"; + case WIFI_REASON_802_1X_AUTH_FAILED: + return "802.1x Authentication Failed"; + case WIFI_REASON_CIPHER_SUITE_REJECTED: + return "Cipher Suite Rejected"; + case WIFI_REASON_BEACON_TIMEOUT: + return "Beacon Timeout"; + case WIFI_REASON_NO_AP_FOUND: + return "AP Not Found"; + case WIFI_REASON_AUTH_FAIL: + return "Authentication Failed"; + case WIFI_REASON_ASSOC_FAIL: + return "Association Failed"; + case WIFI_REASON_HANDSHAKE_TIMEOUT: + return "Handshake Failed"; + case WIFI_REASON_UNSPECIFIED: + default: + return "Unspecified"; + } +} +void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_info_t info) { + switch (event) { + case SYSTEM_EVENT_WIFI_READY: { + ESP_LOGV(TAG, "Event: WiFi ready"); + break; + } + case SYSTEM_EVENT_SCAN_DONE: { + auto it = info.scan_done; + ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id); + break; + } + case SYSTEM_EVENT_STA_START: { + ESP_LOGV(TAG, "Event: WiFi STA start"); + break; + } + case SYSTEM_EVENT_STA_STOP: { + ESP_LOGV(TAG, "Event: WiFi STA stop"); + break; + } + case SYSTEM_EVENT_STA_CONNECTED: { + auto it = info.connected; + char buf[33]; + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf, + format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode)); + break; + } + case SYSTEM_EVENT_STA_DISCONNECTED: { + auto it = info.disconnected; + char buf[33]; + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason=%s", buf, + format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + break; + } + case SYSTEM_EVENT_STA_AUTHMODE_CHANGE: { + auto it = info.auth_change; + ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), + get_auth_mode_str(it.new_mode)); + break; + } + case SYSTEM_EVENT_STA_GOT_IP: { + auto it = info.got_ip.ip_info; + ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), + format_ip4_addr(it.gw).c_str()); + break; + } + case SYSTEM_EVENT_STA_LOST_IP: { + ESP_LOGV(TAG, "Event: Lost IP"); + break; + } + case SYSTEM_EVENT_AP_START: { + ESP_LOGV(TAG, "Event: WiFi AP start"); + break; + } + case SYSTEM_EVENT_AP_STOP: { + ESP_LOGV(TAG, "Event: WiFi AP stop"); + break; + } + case SYSTEM_EVENT_AP_STACONNECTED: { + auto it = info.sta_connected; + ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + break; + } + case SYSTEM_EVENT_AP_STADISCONNECTED: { + auto it = info.sta_disconnected; + ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + break; + } + case SYSTEM_EVENT_AP_STAIPASSIGNED: { + ESP_LOGV(TAG, "Event: AP client assigned IP"); + break; + } + case SYSTEM_EVENT_AP_PROBEREQRECVED: { + auto it = info.ap_probereqrecved; + ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + break; + } + default: + break; + } + + if (event == SYSTEM_EVENT_STA_DISCONNECTED) { + uint8_t reason = info.disconnected.reason; + if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT || + reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL || + reason == WIFI_REASON_HANDSHAKE_TIMEOUT) { + err_t err = esp_wifi_disconnect(); + if (err != ESP_OK) { + ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err)); + } + this->error_from_callback_ = true; + } + } + if (event == SYSTEM_EVENT_SCAN_DONE) { + this->wifi_scan_done_callback_(); + } +} +void WiFiComponent::wifi_register_callbacks_() { + auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2); + WiFi.onEvent(f); + WiFi.persistent(false); +} +wl_status_t WiFiComponent::wifi_sta_status_() { return WiFi.status(); } +bool WiFiComponent::wifi_scan_start_() { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + // need to use WiFi because of WiFiScanClass allocations :( + int16_t err = WiFi.scanNetworks(true, true, false, 200); + if (err != WIFI_SCAN_RUNNING) { + ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); + return false; + } + + return true; +} +void WiFiComponent::wifi_scan_done_callback_() { + this->scan_result_.clear(); + + int16_t num = WiFi.scanComplete(); + if (num < 0) + return; + + this->scan_result_.reserve(static_cast(num)); + for (int i = 0; i < num; i++) { + String ssid = WiFi.SSID(i); + wifi_auth_mode_t authmode = WiFi.encryptionType(i); + int32_t rssi = WiFi.RSSI(i); + uint8_t *bssid = WiFi.BSSID(i); + int32_t channel = WiFi.channel(i); + + WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()), + channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0); + this->scan_result_.push_back(scan); + } + WiFi.scanDelete(); + this->scan_done_ = true; +} +bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { + esp_err_t err; + + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + tcpip_adapter_ip_info_t info; + memset(&info, 0, sizeof(info)); + if (manual_ip.has_value()) { + info.ip.addr = static_cast(manual_ip->static_ip); + info.gw.addr = static_cast(manual_ip->gateway); + info.netmask.addr = static_cast(manual_ip->subnet); + } else { + info.ip.addr = static_cast(IPAddress(192, 168, 4, 1)); + info.gw.addr = static_cast(IPAddress(192, 168, 4, 1)); + info.netmask.addr = static_cast(IPAddress(255, 255, 255, 0)); + } + tcpip_adapter_dhcp_status_t dhcp_status; + tcpip_adapter_dhcps_get_status(TCPIP_ADAPTER_IF_AP, &dhcp_status); + err = tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcps_stop failed! %d", err); + return false; + } + + err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info); + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed! %d", err); + return false; + } + + dhcps_lease_t lease; + lease.enable = true; + IPAddress start_address = info.ip.addr; + start_address[3] += 99; + lease.start_ip.addr = static_cast(start_address); + ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.toString().c_str()); + start_address[3] += 100; + lease.end_ip.addr = static_cast(start_address); + ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.toString().c_str()); + err = tcpip_adapter_dhcps_option(TCPIP_ADAPTER_OP_SET, TCPIP_ADAPTER_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); + + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcps_option failed! %d", err); + return false; + } + + err = tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP); + + if (err != ESP_OK) { + ESP_LOGV(TAG, "tcpip_adapter_dhcps_start failed! %d", err); + return false; + } + + return true; +} +bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + wifi_config_t conf; + memset(&conf, 0, sizeof(conf)); + strcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str()); + conf.ap.channel = ap.get_channel().value_or(1); + conf.ap.ssid_hidden = ap.get_ssid().size(); + conf.ap.max_connection = 5; + conf.ap.beacon_interval = 100; + + if (ap.get_password().empty()) { + conf.ap.authmode = WIFI_AUTH_OPEN; + *conf.ap.password = 0; + } else { + conf.ap.authmode = WIFI_AUTH_WPA2_PSK; + strcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str()); + } + + esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); + return false; + } + + yield(); + + if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + return false; + } + + return true; +} +IPAddress WiFiComponent::wifi_soft_ap_ip_() { + tcpip_adapter_ip_info_t ip; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); + return IPAddress(ip.ip.addr); +} + +} // namespace wifi +} // namespace esphome + +#endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp new file mode 100644 index 0000000000..11981b801b --- /dev/null +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -0,0 +1,596 @@ +#include "wifi_component.h" + +#ifdef ARDUINO_ARCH_ESP8266 + +#include + +#include +#include +#include "lwip/err.h" +#include "lwip/dns.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "esphome/core/esphal.h" +#include "esphome/core/util.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace wifi { + +static const char *TAG = "wifi_esp8266"; + +bool WiFiComponent::wifi_mode_(optional sta, optional ap) { + uint8_t current_mode = wifi_get_opmode(); + bool current_sta = current_mode & 0b01; + bool current_ap = current_mode & 0b10; + bool target_sta = sta.value_or(current_sta); + bool target_ap = ap.value_or(current_ap); + if (current_sta == target_sta && current_ap == target_ap) + return true; + + if (target_sta && !current_sta) { + ESP_LOGV(TAG, "Enabling STA."); + } else if (!target_sta && current_sta) { + ESP_LOGV(TAG, "Disabling STA."); + // Stop DHCP client when disabling STA + // See https://github.com/esp8266/Arduino/pull/5703 + wifi_station_dhcpc_stop(); + } + if (target_ap && !current_ap) { + ESP_LOGV(TAG, "Enabling AP."); + } else if (!target_ap && current_ap) { + ESP_LOGV(TAG, "Disabling AP."); + } + + ETS_UART_INTR_DISABLE(); + uint8_t mode = 0; + if (target_sta) + mode |= 0b01; + if (target_ap) + mode |= 0b10; + bool ret = wifi_set_opmode_current(mode); + ETS_UART_INTR_ENABLE(); + + if (!ret) { + ESP_LOGW(TAG, "Setting WiFi mode failed!"); + } + + return ret; +} +bool WiFiComponent::wifi_disable_auto_connect_() { + bool ret1, ret2; + ETS_UART_INTR_DISABLE(); + ret1 = wifi_station_set_auto_connect(0); + ret2 = wifi_station_set_reconnect_policy(false); + ETS_UART_INTR_ENABLE(); + + if (!ret1 || !ret2) { + ESP_LOGV(TAG, "Disabling Auto-Connect failed!"); + } + + return ret1 && ret2; +} +bool WiFiComponent::wifi_apply_power_save_() { + sleep_type_t power_save; + switch (this->power_save_) { + case WIFI_POWER_SAVE_LIGHT: + power_save = LIGHT_SLEEP_T; + break; + case WIFI_POWER_SAVE_HIGH: + power_save = MODEM_SLEEP_T; + break; + case WIFI_POWER_SAVE_NONE: + default: + power_save = NONE_SLEEP_T; + break; + } + return wifi_set_sleep_type(power_save); +} +bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + enum dhcp_status dhcp_status = wifi_station_dhcpc_status(); + if (!manual_ip.has_value()) { + // Use DHCP client + if (dhcp_status != DHCP_STARTED) { + bool ret = wifi_station_dhcpc_start(); + if (!ret) { + ESP_LOGV(TAG, "Starting DHCP client failed!"); + } + return ret; + } + return true; + } + + bool ret = true; + + struct ip_info info {}; + info.ip.addr = static_cast(manual_ip->static_ip); + info.gw.addr = static_cast(manual_ip->gateway); + info.netmask.addr = static_cast(manual_ip->subnet); + + if (dhcp_status == DHCP_STARTED) { + bool dhcp_stop_ret = wifi_station_dhcpc_stop(); + if (!dhcp_stop_ret) { + ESP_LOGV(TAG, "Stopping DHCP client failed!"); + ret = false; + } + } + bool wifi_set_info_ret = wifi_set_ip_info(STATION_IF, &info); + if (!wifi_set_info_ret) { + ESP_LOGV(TAG, "Setting manual IP info failed!"); + ret = false; + } + + ip_addr_t dns; + if (uint32_t(manual_ip->dns1) != 0) { + dns.addr = static_cast(manual_ip->dns1); + dns_setserver(0, &dns); + } + if (uint32_t(manual_ip->dns2) != 0) { + dns.addr = static_cast(manual_ip->dns2); + dns_setserver(1, &dns); + } + + return ret; +} + +IPAddress WiFiComponent::wifi_sta_ip_() { + if (!this->has_sta()) + return {}; + struct ip_info ip {}; + wifi_get_ip_info(STATION_IF, &ip); + return {ip.ip.addr}; +} +bool WiFiComponent::wifi_apply_hostname_() { + bool ret = wifi_station_set_hostname(const_cast(App.get_name().c_str())); + if (!ret) { + ESP_LOGV(TAG, "Setting WiFi Hostname failed!"); + } + return ret; +} + +bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + ETS_UART_INTR_DISABLE(); + wifi_station_disconnect(); + ETS_UART_INTR_ENABLE(); + + struct station_config conf {}; + memset(&conf, 0, sizeof(conf)); + strcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str()); + strcpy(reinterpret_cast(conf.password), ap.get_password().c_str()); + + if (ap.get_bssid().has_value()) { + conf.bssid_set = 1; + memcpy(conf.bssid, ap.get_bssid()->data(), 6); + } else { + conf.bssid_set = 0; + } + +#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 + if (ap.get_password().empty()) { + conf.threshold.authmode = AUTH_OPEN; + } else { + conf.threshold.authmode = AUTH_WPA_PSK; + } + conf.threshold.rssi = -127; +#endif + + ETS_UART_INTR_DISABLE(); + bool ret = wifi_station_set_config_current(&conf); + ETS_UART_INTR_ENABLE(); + + if (!ret) { + ESP_LOGV(TAG, "Setting WiFi Station config failed!"); + return false; + } + + if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { + return false; + } + + this->wifi_apply_hostname_(); + + ETS_UART_INTR_DISABLE(); + ret = wifi_station_connect(); + ETS_UART_INTR_ENABLE(); + if (!ret) { + ESP_LOGV(TAG, "wifi_station_connect failed!"); + return false; + } + + if (ap.get_channel().has_value()) { + ret = wifi_set_channel(*ap.get_channel()); + if (!ret) { + ESP_LOGV(TAG, "wifi_set_channel failed!"); + return false; + } + } + + return true; +} + +class WiFiMockClass : public ESP8266WiFiGenericClass { + public: + static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT +}; + +const char *get_auth_mode_str(uint8_t mode) { + switch (mode) { + case AUTH_OPEN: + return "OPEN"; + case AUTH_WEP: + return "WEP"; + case AUTH_WPA_PSK: + return "WPA PSK"; + case AUTH_WPA2_PSK: + return "WPA2 PSK"; + case AUTH_WPA_WPA2_PSK: + return "WPA/WPA2 PSK"; + default: + return "UNKNOWN"; + } +} +#ifdef ipv4_addr +std::string format_ip_addr(struct ipv4_addr ip) { + char buf[20]; + sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), + uint8_t(ip.addr >> 24)); + return buf; +} +#else +std::string format_ip_addr(struct ip_addr ip) { + char buf[20]; + sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16), + uint8_t(ip.addr >> 24)); + return buf; +} +#endif +const char *get_op_mode_str(uint8_t mode) { + switch (mode) { + case WIFI_OFF: + return "OFF"; + case WIFI_STA: + return "STA"; + case WIFI_AP: + return "AP"; + case WIFI_AP_STA: + return "AP+STA"; + default: + return "UNKNOWN"; + } +} +const char *get_disconnect_reason_str(uint8_t reason) { + switch (reason) { + case REASON_AUTH_EXPIRE: + return "Auth Expired"; + case REASON_AUTH_LEAVE: + return "Auth Leave"; + case REASON_ASSOC_EXPIRE: + return "Association Expired"; + case REASON_ASSOC_TOOMANY: + return "Too Many Associations"; + case REASON_NOT_AUTHED: + return "Not Authenticated"; + case REASON_NOT_ASSOCED: + return "Not Associated"; + case REASON_ASSOC_LEAVE: + return "Association Leave"; + case REASON_ASSOC_NOT_AUTHED: + return "Association not Authenticated"; + case REASON_DISASSOC_PWRCAP_BAD: + return "Disassociate Power Cap Bad"; + case REASON_DISASSOC_SUPCHAN_BAD: + return "Disassociate Supported Channel Bad"; + case REASON_IE_INVALID: + return "IE Invalid"; + case REASON_MIC_FAILURE: + return "Mic Failure"; + case REASON_4WAY_HANDSHAKE_TIMEOUT: + return "4-Way Handshake Timeout"; + case REASON_GROUP_KEY_UPDATE_TIMEOUT: + return "Group Key Update Timeout"; + case REASON_IE_IN_4WAY_DIFFERS: + return "IE In 4-Way Handshake Differs"; + case REASON_GROUP_CIPHER_INVALID: + return "Group Cipher Invalid"; + case REASON_PAIRWISE_CIPHER_INVALID: + return "Pairwise Cipher Invalid"; + case REASON_AKMP_INVALID: + return "AKMP Invalid"; + case REASON_UNSUPP_RSN_IE_VERSION: + return "Unsupported RSN IE version"; + case REASON_INVALID_RSN_IE_CAP: + return "Invalid RSN IE Cap"; + case REASON_802_1X_AUTH_FAILED: + return "802.1x Authentication Failed"; + case REASON_CIPHER_SUITE_REJECTED: + return "Cipher Suite Rejected"; + case REASON_BEACON_TIMEOUT: + return "Beacon Timeout"; + case REASON_NO_AP_FOUND: + return "AP Not Found"; + case REASON_AUTH_FAIL: + return "Authentication Failed"; + case REASON_ASSOC_FAIL: + return "Association Failed"; + case REASON_HANDSHAKE_TIMEOUT: + return "Handshake Failed"; + case REASON_UNSPECIFIED: + default: + return "Unspecified"; + } +} + +void WiFiComponent::wifi_event_callback(System_Event_t *event) { +#ifdef ESPHOME_LOG_HAS_VERBOSE + // TODO: this callback is called while in cont context, so delay will fail + // We need to defer the log messages until we're out of this context + // only affects verbose log level + // reproducible by enabling verbose log level and letting the ESP disconnect and + // then reconnect to WiFi. + switch (event->event) { + case EVENT_STAMODE_CONNECTED: { + auto it = event->event_info.connected; + char buf[33]; + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), + it.channel); + break; + } + case EVENT_STAMODE_DISCONNECTED: { + auto it = event->event_info.disconnected; + char buf[33]; + memcpy(buf, it.ssid, it.ssid_len); + buf[it.ssid_len] = '\0'; + ESP_LOGV(TAG, "Event: Disconnected ssid='%s' bssid=%s reason='%s'", buf, format_mac_addr(it.bssid).c_str(), + get_disconnect_reason_str(it.reason)); + break; + } + case EVENT_STAMODE_AUTHMODE_CHANGE: { + auto it = event->event_info.auth_change; + ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", get_auth_mode_str(it.old_mode), + get_auth_mode_str(it.new_mode)); + break; + } + case EVENT_STAMODE_GOT_IP: { + auto it = event->event_info.got_ip; + ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s netmask=%s", format_ip_addr(it.ip).c_str(), + format_ip_addr(it.gw).c_str(), format_ip_addr(it.mask).c_str()); + break; + } + case EVENT_STAMODE_DHCP_TIMEOUT: { + ESP_LOGW(TAG, "Event: Getting IP address timeout"); + break; + } + case EVENT_SOFTAPMODE_STACONNECTED: { + auto it = event->event_info.sta_connected; + ESP_LOGV(TAG, "Event: AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + break; + } + case EVENT_SOFTAPMODE_STADISCONNECTED: { + auto it = event->event_info.sta_disconnected; + ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid); + break; + } + case EVENT_SOFTAPMODE_PROBEREQRECVED: { + auto it = event->event_info.ap_probereqrecved; + ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi); + break; + } +#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 + case EVENT_OPMODE_CHANGED: { + auto it = event->event_info.opmode_changed; + ESP_LOGV(TAG, "Event: Changed Mode old=%s new=%s", get_op_mode_str(it.old_opmode), + get_op_mode_str(it.new_opmode)); + break; + } + case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: { + auto it = event->event_info.distribute_sta_ip; + ESP_LOGV(TAG, "Event: AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(), + format_ip_addr(it.ip).c_str(), it.aid); + break; + } +#endif + default: + break; + } +#endif + + if (event->event == EVENT_STAMODE_DISCONNECTED) { + global_wifi_component->error_from_callback_ = true; + } + + WiFiMockClass::_event_callback(event); +} + +void WiFiComponent::wifi_register_callbacks_() { wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback); } +wl_status_t WiFiComponent::wifi_sta_status_() { + station_status_t status = wifi_station_get_connect_status(); + switch (status) { + case STATION_GOT_IP: + return WL_CONNECTED; + case STATION_NO_AP_FOUND: + return WL_NO_SSID_AVAIL; + case STATION_CONNECT_FAIL: + case STATION_WRONG_PASSWORD: + return WL_CONNECT_FAILED; + case STATION_IDLE: + return WL_IDLE_STATUS; + case STATION_CONNECTING: + default: + return WL_DISCONNECTED; + } +} +bool WiFiComponent::wifi_scan_start_() { + static bool FIRST_SCAN = false; + + // enable STA + if (!this->wifi_mode_(true, {})) + return false; + + station_status_t sta_status = wifi_station_get_connect_status(); + if (sta_status != STATION_GOT_IP && sta_status != STATION_IDLE) { + wifi_station_disconnect(); + } + + struct scan_config config {}; + memset(&config, 0, sizeof(config)); + config.ssid = nullptr; + config.bssid = nullptr; + config.channel = 0; + config.show_hidden = 1; +#ifndef ARDUINO_ESP8266_RELEASE_2_3_0 + config.scan_type = WIFI_SCAN_TYPE_ACTIVE; + if (FIRST_SCAN) { + config.scan_time.active.min = 100; + config.scan_time.active.max = 200; + } else { + config.scan_time.active.min = 400; + config.scan_time.active.max = 500; + } +#endif + FIRST_SCAN = false; + bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); + if (!ret) { + ESP_LOGV(TAG, "wifi_station_scan failed!"); + return false; + } + + return ret; +} +void WiFiComponent::s_wifi_scan_done_callback(void *arg, STATUS status) { + global_wifi_component->wifi_scan_done_callback_(arg, status); +} + +void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { + this->scan_result_.clear(); + + if (status != OK) { + ESP_LOGV(TAG, "Scan failed! %d", status); + return; + } + auto *head = reinterpret_cast(arg); + for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { + WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, + std::string(reinterpret_cast(it->ssid), it->ssid_len), it->channel, it->rssi, + it->authmode != AUTH_OPEN, it->is_hidden != 0); + this->scan_result_.push_back(res); + } + this->scan_done_ = true; +} +bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + struct ip_info info {}; + if (manual_ip.has_value()) { + info.ip.addr = static_cast(manual_ip->static_ip); + info.gw.addr = static_cast(manual_ip->gateway); + info.netmask.addr = static_cast(manual_ip->subnet); + } else { + info.ip.addr = static_cast(IPAddress(192, 168, 4, 1)); + info.gw.addr = static_cast(IPAddress(192, 168, 4, 1)); + info.netmask.addr = static_cast(IPAddress(255, 255, 255, 0)); + } + + if (wifi_softap_dhcps_status() == DHCP_STARTED) { + if (!wifi_softap_dhcps_stop()) { + ESP_LOGV(TAG, "Stopping DHCP server failed!"); + } + } + + if (!wifi_set_ip_info(SOFTAP_IF, &info)) { + ESP_LOGV(TAG, "Setting SoftAP info failed!"); + return false; + } + + struct dhcps_lease lease {}; + IPAddress start_address = info.ip.addr; + start_address[3] += 99; + lease.start_ip.addr = static_cast(start_address); + ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.toString().c_str()); + start_address[3] += 100; + lease.end_ip.addr = static_cast(start_address); + ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.toString().c_str()); + if (!wifi_softap_set_dhcps_lease(&lease)) { + ESP_LOGV(TAG, "Setting SoftAP DHCP lease failed!"); + return false; + } + + // lease time 1440 minutes (=24 hours) + if (!wifi_softap_set_dhcps_lease_time(1440)) { + ESP_LOGV(TAG, "Setting SoftAP DHCP lease time failed!"); + return false; + } + + uint8_t mode = 1; + // bit0, 1 enables router information from ESP8266 SoftAP DHCP server. + if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) { + ESP_LOGV(TAG, "wifi_softap_set_dhcps_offer_option failed!"); + return false; + } + + if (!wifi_softap_dhcps_start()) { + ESP_LOGV(TAG, "Starting SoftAP DHCPS failed!"); + return false; + } + + return true; +} +bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { + // enable AP + if (!this->wifi_mode_({}, true)) + return false; + + struct softap_config conf {}; + strcpy(reinterpret_cast(conf.ssid), ap.get_ssid().c_str()); + conf.ssid_len = static_cast(ap.get_ssid().size()); + conf.channel = ap.get_channel().value_or(1); + conf.ssid_hidden = 0; + conf.max_connection = 5; + conf.beacon_interval = 100; + + if (ap.get_password().empty()) { + conf.authmode = AUTH_OPEN; + *conf.password = 0; + } else { + conf.authmode = AUTH_WPA2_PSK; + strcpy(reinterpret_cast(conf.password), ap.get_password().c_str()); + } + + ETS_UART_INTR_DISABLE(); + bool ret = wifi_softap_set_config_current(&conf); + ETS_UART_INTR_ENABLE(); + + if (!ret) { + ESP_LOGV(TAG, "wifi_softap_set_config_current failed!"); + return false; + } + + if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!"); + return false; + } + + return true; +} +IPAddress WiFiComponent::wifi_soft_ap_ip_() { + struct ip_info ip {}; + wifi_get_ip_info(SOFTAP_IF, &ip); + return {ip.ip.addr}; +} + +} // namespace wifi +} // namespace esphome + +#endif diff --git a/esphome/components/wifi_info/__init__.py b/esphome/components/wifi_info/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py new file mode 100644 index 0000000000..81ee787848 --- /dev/null +++ b/esphome/components/wifi_info/text_sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_BSSID, CONF_ID, CONF_IP_ADDRESS, CONF_SSID +from esphome.core import coroutine + +DEPENDENCIES = ['wifi'] + +wifi_info_ns = cg.esphome_ns.namespace('wifi_info') +IPAddressWiFiInfo = wifi_info_ns.class_('IPAddressWiFiInfo', text_sensor.TextSensor, cg.Component) +SSIDWiFiInfo = wifi_info_ns.class_('SSIDWiFiInfo', text_sensor.TextSensor, cg.Component) +BSSIDWiFiInfo = wifi_info_ns.class_('BSSIDWiFiInfo', text_sensor.TextSensor, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.Optional(CONF_IP_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(IPAddressWiFiInfo), + }), + cv.Optional(CONF_SSID): text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SSIDWiFiInfo), + }), + cv.Optional(CONF_BSSID): text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(BSSIDWiFiInfo), + }), +}) + + +@coroutine +def setup_conf(config, key): + if key in config: + conf = config[key] + var = cg.new_Pvariable(conf[CONF_ID]) + yield cg.register_component(var, conf) + yield text_sensor.register_text_sensor(var, conf) + + +def to_code(config): + yield setup_conf(config, CONF_IP_ADDRESS) + yield setup_conf(config, CONF_SSID) + yield setup_conf(config, CONF_BSSID) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h new file mode 100644 index 0000000000..13e632bde1 --- /dev/null +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/wifi/wifi_component.h" + +namespace esphome { +namespace wifi_info { + +class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { + public: + void loop() override { + IPAddress ip = WiFi.localIP(); + if (ip != this->last_ip_) { + this->last_ip_ = ip; + this->publish_state(ip.toString().c_str()); + } + } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + IPAddress last_ip_; +}; + +class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { + public: + void loop() override { + String ssid = WiFi.SSID(); + if (this->last_ssid_ != ssid.c_str()) { + this->last_ssid_ = std::string(ssid.c_str()); + this->publish_state(this->last_ssid_); + } + } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + std::string last_ssid_; +}; + +class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { + public: + void loop() override { + uint8_t *bssid = WiFi.BSSID(); + if (memcmp(bssid, this->last_bssid_.data(), 6) != 0) { + std::copy(bssid, bssid + 6, this->last_bssid_.data()); + char buf[30]; + sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + this->publish_state(buf); + } + } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + protected: + wifi::bssid_t last_bssid_; +}; + +} // namespace wifi_info +} // namespace esphome diff --git a/esphome/components/wifi_signal/__init__.py b/esphome/components/wifi_signal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py new file mode 100644 index 0000000000..1cc58009af --- /dev/null +++ b/esphome/components/wifi_signal/sensor.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, ICON_WIFI, UNIT_DECIBEL + +DEPENDENCIES = ['wifi'] +wifi_signal_ns = cg.esphome_ns.namespace('wifi_signal') +WiFiSignalSensor = wifi_signal_ns.class_('WiFiSignalSensor', sensor.Sensor, cg.PollingComponent) + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_DECIBEL, ICON_WIFI, 0).extend({ + cv.GenerateID(): cv.declare_id(WiFiSignalSensor), +}).extend(cv.polling_component_schema('60s')) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.cpp b/esphome/components/wifi_signal/wifi_signal_sensor.cpp new file mode 100644 index 0000000000..7b2f010c07 --- /dev/null +++ b/esphome/components/wifi_signal/wifi_signal_sensor.cpp @@ -0,0 +1,12 @@ +#include "wifi_signal_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace wifi_signal { + +static const char *TAG = "wifi_signal.sensor"; + +void WiFiSignalSensor::dump_config() { LOG_SENSOR("", "WiFi Signal", this); } + +} // namespace wifi_signal +} // namespace esphome diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h new file mode 100644 index 0000000000..8fe108a530 --- /dev/null +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/wifi/wifi_component.h" + +namespace esphome { +namespace wifi_signal { + +class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { + public: + void update() override { this->publish_state(WiFi.RSSI()); } + void dump_config() override; + + std::string unique_id() override { return get_mac_address() + "-wifisignal"; } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } +}; + +} // namespace wifi_signal +} // namespace esphome diff --git a/esphome/components/xiaomi_ble/__init__.py b/esphome/components/xiaomi_ble/__init__.py new file mode 100644 index 0000000000..2b36090293 --- /dev/null +++ b/esphome/components/xiaomi_ble/__init__.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] + +xiaomi_ble_ns = cg.esphome_ns.namespace('xiaomi_ble') +XiaomiListener = xiaomi_ble_ns.class_('XiaomiListener', esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiListener), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp new file mode 100644 index 0000000000..7431b84491 --- /dev/null +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -0,0 +1,145 @@ +#include "xiaomi_ble.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_ble { + +static const char *TAG = "xiaomi_ble"; + +bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result) { + switch (data_type) { + case 0x0D: { // temperature+humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % + if (data_length != 4) + return false; + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + result.temperature = temperature / 10.0f; + result.humidity = humidity / 10.0f; + return true; + } + case 0x0A: { // battery, 1 byte, 8-bit unsigned integer, 1 % + if (data_length != 1) + return false; + result.battery_level = data[0]; + return true; + } + case 0x06: { // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % + if (data_length != 2) + return false; + const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.humidity = humidity / 10.0f; + return true; + } + case 0x04: { // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C + if (data_length != 2) + return false; + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.temperature = temperature / 10.0f; + return true; + } + case 0x09: { // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm + if (data_length != 2) + return false; + const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.conductivity = conductivity; + return true; + } + case 0x07: { // illuminance, 3 bytes, 24-bit unsigned integer (LE), 1 lx + if (data_length != 3) + return false; + const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + result.illuminance = illuminance; + return true; + } + case 0x08: { // soil moisture, 1 byte, 8-bit unsigned integer, 1 % + if (data_length != 1) + return false; + result.moisture = data[0]; + return true; + } + default: + return false; + } +} +optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { + if (!device.get_service_data_uuid().has_value()) { + // ESP_LOGVV(TAG, "Xiaomi no service data"); + return {}; + } + + if (!device.get_service_data_uuid()->contains(0x95, 0xFE)) { + // ESP_LOGVV(TAG, "Xiaomi no service data UUID magic bytes"); + return {}; + } + + const auto *raw = reinterpret_cast(device.get_service_data().data()); + + if (device.get_service_data().size() < 14) { + // ESP_LOGVV(TAG, "Xiaomi service data too short!"); + return {}; + } + + bool is_mijia = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; + bool is_miflora = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; + + if (!is_mijia && !is_miflora) { + // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); + return {}; + } + + uint8_t raw_offset = is_mijia ? 11 : 12; + + const uint8_t raw_type = raw[raw_offset]; + const uint8_t data_length = raw[raw_offset + 2]; + const uint8_t *data = &raw[raw_offset + 3]; + const uint8_t expected_length = data_length + raw_offset + 3; + const uint8_t actual_length = device.get_service_data().size(); + if (expected_length != actual_length) { + // ESP_LOGV(TAG, "Xiaomi %s data length mismatch (%u != %d)", type, expected_length, actual_length); + return {}; + } + XiaomiParseResult result; + result.type = is_miflora ? XiaomiParseResult::TYPE_MIFLORA : XiaomiParseResult::TYPE_MIJIA; + bool success = parse_xiaomi_data_byte(raw_type, data, data_length, result); + if (!success) + return {}; + return result; +} + +bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + auto res = parse_xiaomi(device); + if (!res.has_value()) + return false; + + const char *name = res->type == XiaomiParseResult::TYPE_MIFLORA ? "Mi Flora" : "Mi Jia"; + + ESP_LOGD(TAG, "Got Xiaomi %s:", name); + + if (res->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.1f°C", *res->temperature); + } + if (res->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.1f%%", *res->humidity); + } + if (res->battery_level.has_value()) { + ESP_LOGD(TAG, " Battery Level: %.0f%%", *res->battery_level); + } + if (res->conductivity.has_value()) { + ESP_LOGD(TAG, " Conductivity: %.0fµS/cm", *res->conductivity); + } + if (res->illuminance.has_value()) { + ESP_LOGD(TAG, " Illuminance: %.0flx", *res->illuminance); + } + if (res->moisture.has_value()) { + ESP_LOGD(TAG, " Moisture: %.0f%%", *res->moisture); + } + + return true; +} + +} // namespace xiaomi_ble +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h new file mode 100644 index 0000000000..058a89927b --- /dev/null +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_ble { + +struct XiaomiParseResult { + enum { TYPE_MIJIA, TYPE_MIFLORA } type; + optional temperature; + optional humidity; + optional battery_level; + optional conductivity; + optional illuminance; + optional moisture; +}; + +bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result); + +optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device); + +class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace xiaomi_ble +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_miflora/__init__.py b/esphome/components/xiaomi_miflora/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_miflora/sensor.py b/esphome/components/xiaomi_miflora/sensor.py new file mode 100644 index 0000000000..8be06a93f3 --- /dev/null +++ b/esphome/components/xiaomi_miflora/sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ + UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_miflora_ns = cg.esphome_ns.namespace('xiaomi_miflora') +XiaomiMiflora = xiaomi_miflora_ns.class_('XiaomiMiflora', esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiMiflora), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp b/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp new file mode 100644 index 0000000000..966c78a1a6 --- /dev/null +++ b/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp @@ -0,0 +1,23 @@ +#include "xiaomi_miflora.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_miflora { + +static const char *TAG = "xiaomi_miflora"; + +void XiaomiMiflora::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi Mijia"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +} // namespace xiaomi_miflora +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.h b/esphome/components/xiaomi_miflora/xiaomi_miflora.h new file mode 100644 index 0000000000..d1f05cdcc7 --- /dev/null +++ b/esphome/components/xiaomi_miflora/xiaomi_miflora.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_miflora { + +class XiaomiMiflora : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = xiaomi_ble::parse_xiaomi(device); + if (!res.has_value()) + return false; + + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_miflora +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mijia/__init__.py b/esphome/components/xiaomi_mijia/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_mijia/sensor.py b/esphome/components/xiaomi_mijia/sensor.py new file mode 100644 index 0000000000..995a6cbf25 --- /dev/null +++ b/esphome/components/xiaomi_mijia/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mijia_ns = cg.esphome_ns.namespace('xiaomi_mijia') +XiaomiMijia = xiaomi_mijia_ns.class_('XiaomiMijia', esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiMijia), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp b/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp new file mode 100644 index 0000000000..544af32d7b --- /dev/null +++ b/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp @@ -0,0 +1,21 @@ +#include "xiaomi_mijia.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mijia { + +static const char *TAG = "xiaomi_mijia"; + +void XiaomiMijia::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi Mijia"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +} // namespace xiaomi_mijia +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.h b/esphome/components/xiaomi_mijia/xiaomi_mijia.h new file mode 100644 index 0000000000..814e33fa75 --- /dev/null +++ b/esphome/components/xiaomi_mijia/xiaomi_mijia.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mijia { + +class XiaomiMijia : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = xiaomi_ble::parse_xiaomi(device); + if (!res.has_value()) + return false; + + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_mijia +} // namespace esphome + +#endif diff --git a/esphome/config.py b/esphome/config.py index 469b1305a6..9b34d53200 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,24 +1,29 @@ from __future__ import print_function -from collections import OrderedDict +import collections import importlib import logging import re +import os.path + +# pylint: disable=unused-import, wrong-import-order +import sys +from contextlib import contextmanager import voluptuous as vol from esphome import core, core_config, yaml_util from esphome.components import substitutions +from esphome.components.substitutions import CONF_SUBSTITUTIONS from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS -from esphome.core import CORE, EsphomeError +from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent -from esphome.py_compat import text_type -from esphome.util import safe_print +from esphome.py_compat import text_type, IS_PY2 +from esphome.util import safe_print, OrderedDict -# pylint: disable=unused-import, wrong-import-order from typing import List, Optional, Tuple, Union # noqa from esphome.core import ConfigType # noqa -from esphome.yaml_util import is_secret +from esphome.yaml_util import is_secret, ESPHomeDataBase from esphome.voluptuous_schema import ExtraKeysInvalid _LOGGER = logging.getLogger(__name__) @@ -26,107 +31,256 @@ _LOGGER = logging.getLogger(__name__) _COMPONENT_CACHE = {} -def get_component(domain): +class ComponentManifest(object): + def __init__(self, module, base_components_path, is_core=False, is_platform=False): + self.module = module + self._is_core = is_core + self.is_platform = is_platform + self.base_components_path = base_components_path + + @property + def is_platform_component(self): + return getattr(self.module, 'IS_PLATFORM_COMPONENT', False) + + @property + def config_schema(self): + return getattr(self.module, 'CONFIG_SCHEMA', None) + + @property + def is_multi_conf(self): + return getattr(self.module, 'MULTI_CONF', False) + + @property + def to_code(self): + return getattr(self.module, 'to_code', None) + + @property + def esp_platforms(self): + return getattr(self.module, 'ESP_PLATFORMS', ESP_PLATFORMS) + + @property + def dependencies(self): + return getattr(self.module, 'DEPENDENCIES', []) + + @property + def conflicts_with(self): + return getattr(self.module, 'CONFLICTS_WITH', []) + + @property + def auto_load(self): + return getattr(self.module, 'AUTO_LOAD', []) + + def _get_flags_set(self, name, config): + if not hasattr(self.module, name): + return set() + obj = getattr(self.module, name) + if callable(obj): + obj = obj(config) + if obj is None: + return set() + if not isinstance(obj, (list, tuple, set)): + obj = [obj] + return set(obj) + + @property + def source_files(self): + if self._is_core: + core_p = os.path.abspath(os.path.join(os.path.dirname(__file__), 'core')) + source_files = core.find_source_files(os.path.join(core_p, 'dummy')) + ret = {} + for f in source_files: + ret['esphome/core/{}'.format(f)] = os.path.join(core_p, f) + return ret + + source_files = core.find_source_files(self.module.__file__) + ret = {} + # Make paths absolute + directory = os.path.abspath(os.path.dirname(self.module.__file__)) + for x in source_files: + full_file = os.path.join(directory, x) + rel = os.path.relpath(full_file, self.base_components_path) + # Always use / for C++ include names + rel = rel.replace(os.sep, '/') + target_file = 'esphome/components/{}'.format(rel) + ret[target_file] = full_file + return ret + + +CORE_COMPONENTS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), 'components')) +_UNDEF = object() +CUSTOM_COMPONENTS_PATH = _UNDEF + + +def _mount_config_dir(): + global CUSTOM_COMPONENTS_PATH + if CUSTOM_COMPONENTS_PATH is not _UNDEF: + return + custom_path = os.path.abspath(os.path.join(CORE.config_dir, 'custom_components')) + if not os.path.isdir(custom_path): + CUSTOM_COMPONENTS_PATH = None + return + init_path = os.path.join(custom_path, '__init__.py') + if IS_PY2 and not os.path.isfile(init_path): + _LOGGER.warning("Found 'custom_components' folder, but file __init__.py was not found. " + "ESPHome will automatically create it now....") + with open(init_path, 'w') as f: + f.write('\n') + if CORE.config_dir not in sys.path: + sys.path.insert(0, CORE.config_dir) + CUSTOM_COMPONENTS_PATH = custom_path + + +def _lookup_module(domain, is_platform): if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] - path = 'esphome.components.{}'.format(domain) + _mount_config_dir() + # First look for custom_components try: - module = importlib.import_module(path) - except (ImportError, ValueError) as err: - _LOGGER.debug(err) + module = importlib.import_module('custom_components.{}'.format(domain)) + except ImportError as e: + # ImportError when no such module + if 'No module named' not in str(e): + _LOGGER.warning("Unable to import custom component %s:", domain, exc_info=True) + except Exception: # pylint: disable=broad-except + # Other error means component has an issue + _LOGGER.error("Unable to load custom component %s:", domain, exc_info=True) + return None else: - _COMPONENT_CACHE[domain] = module - return module + # Found in custom components + manif = ComponentManifest(module, CUSTOM_COMPONENTS_PATH, is_platform=is_platform) + _COMPONENT_CACHE[domain] = manif + return manif - _LOGGER.error("Unable to find component %s", domain) - return None + try: + module = importlib.import_module('esphome.components.{}'.format(domain)) + except ImportError as e: + if 'No module named' not in str(e): + _LOGGER.error("Unable to import component %s:", domain, exc_info=True) + return None + except Exception: # pylint: disable=broad-except + _LOGGER.error("Unable to load component %s:", domain, exc_info=True) + return None + else: + manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform) + _COMPONENT_CACHE[domain] = manif + return manif + + +def get_component(domain): + assert '.' not in domain + return _lookup_module(domain, False) def get_platform(domain, platform): - return get_component("{}.{}".format(domain, platform)) + full = '{}.{}'.format(platform, domain) + return _lookup_module(full, True) -def is_platform_component(component): - return hasattr(component, 'PLATFORM_SCHEMA') +_COMPONENT_CACHE['esphome'] = ComponentManifest( + core_config, CORE_COMPONENTS_PATH, is_core=True, is_platform=False, +) def iter_components(config): for domain, conf in config.items(): - if domain == CONF_ESPHOME: - yield CONF_ESPHOME, core_config, conf - continue component = get_component(domain) - if getattr(component, 'MULTI_CONF', False): + if component.is_multi_conf: for conf_ in conf: yield domain, component, conf_ else: yield domain, component, conf - if is_platform_component(component): + if component.is_platform_component: for p_config in conf: p_name = u"{}.{}".format(domain, p_config[CONF_PLATFORM]) - platform = get_component(p_name) + platform = get_platform(domain, p_config[CONF_PLATFORM]) yield p_name, platform, p_config ConfigPath = List[Union[str, int]] -def _path_begins_with_(path, other): # type: (ConfigPath, ConfigPath) -> bool +def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool if len(path) < len(other): return False return path[:len(other)] == other -def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool - ret = _path_begins_with_(path, other) - return ret - - class Config(OrderedDict): def __init__(self): super(Config, self).__init__() - self.errors = [] # type: List[Tuple[basestring, ConfigPath]] - self.domains = [] # type: List[Tuple[ConfigPath, basestring]] + # A list of voluptuous errors + self.errors = [] # type: List[vol.Invalid] + # A list of paths that should be fully outputted + # The values will be the paths to all "domain", for example (['logger'], 'logger') + # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic') + self.output_paths = [] # type: List[Tuple[ConfigPath, unicode]] - def add_error(self, message, path): + def add_error(self, error): + # type: (vol.Invalid) -> None + if isinstance(error, vol.MultipleInvalid): + for err in error.errors: + self.add_error(err) + return + self.errors.append(error) + + @contextmanager + def catch_error(self, path=None): + path = path or [] + try: + yield + except vol.Invalid as e: + e.prepend(path) + self.add_error(e) + + def add_str_error(self, message, path): # type: (basestring, ConfigPath) -> None - if not isinstance(message, text_type): - message = text_type(message) - self.errors.append((message, path)) + self.add_error(vol.Invalid(message, path)) - def add_domain(self, path, name): - # type: (ConfigPath, basestring) -> None - self.domains.append((path, name)) + def add_output_path(self, path, domain): + # type: (ConfigPath, unicode) -> None + self.output_paths.append((path, domain)) - def remove_domain(self, path, name): - self.domains.remove((path, name)) - - def lookup_domain(self, path): - # type: (ConfigPath) -> Optional[basestring] - best_len = 0 - best_domain = None - for d_path, domain in self.domains: - if len(d_path) < best_len: - continue - if _path_begins_with(path, d_path): - best_len = len(d_path) - best_domain = domain - return best_domain + def remove_output_path(self, path, domain): + # type: (ConfigPath, unicode) -> None + self.output_paths.remove((path, domain)) def is_in_error_path(self, path): - for _, p in self.errors: - if _path_begins_with(p, path): + # type: (ConfigPath) -> bool + for err in self.errors: + if _path_begins_with(err.path, path): return True return False + def set_by_path(self, path, value): + conf = self + for key in path[:-1]: + conf = conf[key] + conf[path[-1]] = value + def get_error_for_path(self, path): - for msg, p in self.errors: - if self.nested_item_path(p) == path: - return msg + # type: (ConfigPath) -> Optional[vol.Invalid] + for err in self.errors: + if self.get_deepest_path(err.path) == path: + return err return None - def nested_item(self, path): + def get_deepest_document_range_for_path(self, path): + # type: (ConfigPath) -> Optional[ESPHomeDataBase] + data = self + doc_range = None + for item_index in path: + try: + data = data[item_index] + except (KeyError, IndexError, TypeError): + return doc_range + if isinstance(data, ESPHomeDataBase) and data.esp_range is not None: + doc_range = data.esp_range + + return doc_range + + def get_nested_item(self, path): + # type: (ConfigPath) -> ConfigType data = self for item_index in path: try: @@ -135,7 +289,9 @@ class Config(OrderedDict): return {} return data - def nested_item_path(self, path): + def get_deepest_path(self, path): + # type: (ConfigPath) -> ConfigPath + """Return the path that is the deepest reachable by following path.""" data = self part = [] for item_index in path: @@ -166,20 +322,27 @@ def iter_ids(config, path=None): def do_id_pass(result): # type: (Config) -> None from esphome.cpp_generator import MockObjClass + from esphome.cpp_types import Component declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]] searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]] for id, path in iter_ids(result): if id.is_declaration: - if id.id is not None and any(v[0].id == id.id for v in declare_ids): - result.add_error(u"ID {} redefined!".format(id.id), path) - continue + if id.id is not None: + # Look for duplicate definitions + match = next((v for v in declare_ids if v[0].id == id.id), None) + if match is not None: + opath = u'->'.join(text_type(v) for v in match[1]) + result.add_str_error(u"ID {} redefined! Check {}".format(id.id, opath), path) + continue declare_ids.append((id, path)) else: searching_ids.append((id, path)) # Resolve default ids after manual IDs for id, _ in declare_ids: id.resolve([v[0].id for v in declare_ids]) + if isinstance(id.type, MockObjClass) and id.type.inherits_from(Component): + CORE.component_ids.add(id.id) # Check searched IDs for id, path in searching_ids: @@ -188,14 +351,22 @@ def do_id_pass(result): # type: (Config) -> None match = next((v[0] for v in declare_ids if v[0].id == id.id), None) if match is None: # No declared ID with this name - result.add_error("Couldn't find ID '{}'".format(id.id), path) + import difflib + error = ("Couldn't find ID '{}'. Please check you have defined " + "an ID with that name in your configuration.".format(id.id)) + # Find candidates + matches = difflib.get_close_matches(id.id, [v[0].id for v in declare_ids]) + if matches: + matches_s = ', '.join('"{}"'.format(x) for x in matches) + error += " These IDs look similar: {}.".format(matches_s) + result.add_str_error(error, path) continue if not isinstance(match.type, MockObjClass) or not isinstance(id.type, MockObjClass): continue if not match.type.inherits_from(id.type): - result.add_error("ID '{}' of type {} doesn't inherit from {}. Please double check " - "your ID is pointing to the correct value" - "".format(id.id, match.type, id.type), path) + result.add_str_error("ID '{}' of type {} doesn't inherit from {}. Please " + "double check your ID is pointing to the correct value" + "".format(id.id, match.type, id.type), path) if id.id is None and id.type is not None: for v in declare_ids: @@ -206,179 +377,189 @@ def do_id_pass(result): # type: (Config) -> None id.id = v[0].id break else: - result.add_error("Couldn't resolve ID for type '{}'".format(id.type), path) + result.add_str_error("Couldn't resolve ID for type '{}'".format(id.type), path) def validate_config(config): result = Config() - def _comp_error(ex, path): - # type: (vol.Invalid, List[basestring]) -> None - if isinstance(ex, vol.MultipleInvalid): - errors = ex.errors - else: - errors = [ex] + # 1. Load substitutions + if CONF_SUBSTITUTIONS in config: + result[CONF_SUBSTITUTIONS] = config[CONF_SUBSTITUTIONS] + result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) + try: + substitutions.do_substitution_pass(config) + except vol.Invalid as err: + result.add_error(err) + return result - for e in errors: - path_ = path + e.path - domain = result.lookup_domain(path_) or '' - result.add_error(_format_vol_invalid(e, config, path, domain), path_) - - skip_paths = list() # type: List[ConfigPath] - - # Step 1: Load everything - result.add_domain([CONF_ESPHOME], CONF_ESPHOME) + # 2. Load partial core config result[CONF_ESPHOME] = config[CONF_ESPHOME] + result.add_output_path([CONF_ESPHOME], CONF_ESPHOME) + try: + core_config.preload_core_config(config) + except vol.Invalid as err: + result.add_error(err) + return result + # Remove temporary esphome config path again, it will be reloaded later + result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME) + # 3. Load components. + # Load components (also AUTO_LOAD) and set output paths of result + # Queue of items to load, FIFO + load_queue = collections.deque() for domain, conf in config.items(): - domain = str(domain) - if domain == CONF_ESPHOME or domain.startswith(u'.'): - skip_paths.append([domain]) + load_queue.append((domain, conf)) + + # List of items to enter next stage + check_queue = [] # type: List[Tuple[ConfigPath, str, ConfigType, ComponentManifest]] + + # This step handles: + # - Adding output path + # - Auto Load + # - Loading configs into result + + while load_queue: + domain, conf = load_queue.popleft() + domain = text_type(domain) + if domain.startswith(u'.'): + # Ignore top-level keys starting with a dot continue - result.add_domain([domain], domain) + result.add_output_path([domain], domain) result[domain] = conf - if conf is None: - result[domain] = conf = {} component = get_component(domain) + path = [domain] if component is None: - result.add_error(u"Component not found: {}".format(domain), [domain]) - skip_paths.append([domain]) + result.add_str_error(u"Component not found: {}".format(domain), path) + continue + CORE.loaded_integrations.add(domain) + + # Process AUTO_LOAD + for load in component.auto_load: + if load not in config: + load_conf = core.AutoLoad() + config[load] = load_conf + load_queue.append((load, load_conf)) + + if not component.is_platform_component: + check_queue.append(([domain], domain, conf, component)) continue - if not isinstance(conf, list) and getattr(component, 'MULTI_CONF', False): - result[domain] = conf = [conf] + # This is a platform component, proceed to reading platform entries + # Remove this is as an output path + result.remove_output_path([domain], domain) - success = True - dependencies = getattr(component, 'DEPENDENCIES', []) - for dependency in dependencies: - if dependency not in config: - result.add_error(u"Component {} requires component {}".format(domain, dependency), - [domain]) - success = False - if not success: - skip_paths.append([domain]) - continue - - success = True - conflicts_with = getattr(component, 'CONFLICTS_WITH', []) - for conflict in conflicts_with: - if conflict in config: - result.add_error(u"Component {} cannot be used together with component {}" - u"".format(domain, conflict), [domain]) - success = False - if not success: - skip_paths.append([domain]) - continue - - esp_platforms = getattr(component, 'ESP_PLATFORMS', ESP_PLATFORMS) - if CORE.esp_platform not in esp_platforms: - result.add_error(u"Component {} doesn't support {}.".format(domain, CORE.esp_platform), - [domain]) - skip_paths.append([domain]) - continue - - if not hasattr(component, 'PLATFORM_SCHEMA'): - continue - - result.remove_domain([domain], domain) - - if not isinstance(conf, list) and conf: + # Ensure conf is a list + if not conf: + result[domain] = conf = [] + elif not isinstance(conf, list): result[domain] = conf = [conf] for i, p_config in enumerate(conf): + path = [domain, i] + # Construct temporary unknown output path + p_domain = u'{}.unknown'.format(domain) + result.add_output_path(path, p_domain) + result[domain][i] = p_config if not isinstance(p_config, dict): - result.add_error(u"Platform schemas must have 'platform:' key", [domain, i]) - skip_paths.append([domain, i]) + result.add_str_error(u"Platform schemas must be key-value pairs.", path) continue p_name = p_config.get('platform') if p_name is None: - result.add_error(u"No platform specified for {}".format(domain), [domain, i]) - skip_paths.append([domain, i]) + result.add_str_error(u"No platform specified! See 'platform' key.", path) continue + # Remove temp output path and construct new one + result.remove_output_path(path, p_domain) p_domain = u'{}.{}'.format(domain, p_name) - result.add_domain([domain, i], p_domain) + result.add_output_path(path, p_domain) + # Try Load platform platform = get_platform(domain, p_name) if platform is None: - result.add_error(u"Platform not found: '{}'".format(p_domain), [domain, i]) - skip_paths.append([domain, i]) + result.add_str_error(u"Platform not found: '{}'".format(p_domain), path) continue + CORE.loaded_integrations.add(p_name) - success = True - dependencies = getattr(platform, 'DEPENDENCIES', []) - for dependency in dependencies: - if dependency not in config: - result.add_error(u"Platform {} requires component {}" - u"".format(p_domain, dependency), [domain, i]) - success = False - if not success: - skip_paths.append([domain, i]) - continue + # Process AUTO_LOAD + for load in platform.auto_load: + if load not in config: + load_conf = core.AutoLoad() + config[load] = load_conf + load_queue.append((load, load_conf)) - success = True - conflicts_with = getattr(platform, 'CONFLICTS_WITH', []) - for conflict in conflicts_with: - if conflict in config: - result.add_error(u"Platform {} cannot be used together with component {}" - u"".format(p_domain, conflict), [domain, i]) - success = False - if not success: - skip_paths.append([domain, i]) - continue + check_queue.append((path, p_domain, p_config, platform)) - esp_platforms = getattr(platform, 'ESP_PLATFORMS', ESP_PLATFORMS) - if CORE.esp_platform not in esp_platforms: - result.add_error(u"Platform {} doesn't support {}." - u"".format(p_domain, CORE.esp_platform), [domain, i]) - skip_paths.append([domain, i]) - continue + # 4. Validate component metadata, including + # - Transformation (nullable, multi conf) + # - Dependencies + # - Conflicts + # - Supported ESP Platform - # Step 2: Validate configuration - try: - result[CONF_ESPHOME] = core_config.CONFIG_SCHEMA(result[CONF_ESPHOME]) - except vol.Invalid as ex: - _comp_error(ex, [CONF_ESPHOME]) + # List of items to proceed to next stage + validate_queue = [] # type: List[Tuple[ConfigPath, ConfigType, ComponentManifest]] + for path, domain, conf, comp in check_queue: + if conf is None: + result[domain] = conf = {} - for domain, conf in result.items(): - domain = str(domain) - if [domain] in skip_paths: + success = True + for dependency in comp.dependencies: + if dependency not in config: + result.add_str_error(u"Component {} requires component {}" + u"".format(domain, dependency), path) + success = False + if not success: continue - component = get_component(domain) - if hasattr(component, 'CONFIG_SCHEMA'): - multi_conf = getattr(component, 'MULTI_CONF', False) + success = True + for conflict in comp.conflicts_with: + if conflict in config: + result.add_str_error(u"Component {} cannot be used together with component {}" + u"".format(domain, conflict), path) + success = False + if not success: + continue - if multi_conf: - for i, conf_ in enumerate(conf): - try: - validated = component.CONFIG_SCHEMA(conf_) - result[domain][i] = validated - except vol.Invalid as ex: - _comp_error(ex, [domain, i]) + if CORE.esp_platform not in comp.esp_platforms: + result.add_str_error(u"Component {} doesn't support {}.".format(domain, + CORE.esp_platform), + path) + continue + + if not comp.is_platform_component and comp.config_schema is None and \ + not isinstance(conf, core.AutoLoad): + result.add_str_error(u"Component {} cannot be loaded via YAML " + u"(no CONFIG_SCHEMA).".format(domain), path) + continue + + if comp.is_multi_conf: + if not isinstance(conf, list): + result[domain] = conf = [conf] + for i, part_conf in enumerate(conf): + validate_queue.append((path + [i], part_conf, comp)) + continue + + validate_queue.append((path, conf, comp)) + + # 5. Validate configuration schema + for path, conf, comp in validate_queue: + if comp.config_schema is None: + continue + with result.catch_error(path): + if comp.is_platform: + # Remove 'platform' key for validation + input_conf = OrderedDict(conf) + platform_val = input_conf.pop('platform') + validated = comp.config_schema(input_conf) + # Ensure result is OrderedDict so we can call move_to_end + if not isinstance(validated, OrderedDict): + validated = OrderedDict(validated) + validated['platform'] = platform_val + validated.move_to_end('platform', last=False) + result.set_by_path(path, validated) else: - try: - validated = component.CONFIG_SCHEMA(conf) - result[domain] = validated - except vol.Invalid as ex: - _comp_error(ex, [domain]) - continue - - if not hasattr(component, 'PLATFORM_SCHEMA'): - continue - - for i, p_config in enumerate(conf): - if [domain, i] in skip_paths: - continue - p_name = p_config['platform'] - platform = get_platform(domain, p_name) - - if hasattr(platform, 'PLATFORM_SCHEMA'): - try: - p_validated = platform.PLATFORM_SCHEMA(p_config) - except vol.Invalid as ex: - _comp_error(ex, [domain, i]) - continue - result[domain][i] = p_validated + validated = comp.config_schema(conf) + result.set_by_path(path, validated) + # 6. If no validation errors, check IDs if not result.errors: # Only parse IDs if no validation error. Otherwise # user gets confusing messages @@ -396,9 +577,6 @@ def _nested_getitem(data, path): def humanize_error(config, validation_error): - offending_item_summary = _nested_getitem(config, validation_error.path) - if isinstance(offending_item_summary, dict): - offending_item_summary = None validation_error = text_type(validation_error) m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error) if m is not None: @@ -406,19 +584,26 @@ def humanize_error(config, validation_error): validation_error = validation_error.strip() if not validation_error.endswith(u'.'): validation_error += u'.' - if offending_item_summary is None or is_secret(offending_item_summary): - return validation_error - - return u"{} Got '{}'".format(validation_error, offending_item_summary) + return validation_error -def _format_vol_invalid(ex, config, path, domain): - # type: (vol.Invalid, ConfigType, ConfigPath, basestring) -> unicode +def _get_parent_name(path, config): + if not path: + return '' + for domain_path, domain in config.output_paths: + if _path_begins_with(path, domain_path): + if len(path) > len(domain_path): + # Sub-item + break + return domain + return path[-1] + + +def _format_vol_invalid(ex, config): + # type: (vol.Invalid, Config) -> unicode message = u'' - try: - paren = ex.path[-2] - except IndexError: - paren = domain + + paren = _get_parent_name(ex.path[:-1], config) if isinstance(ex, ExtraKeysInvalid): if ex.candidates: @@ -427,25 +612,30 @@ def _format_vol_invalid(ex, config, path, domain): else: message += u'[{}] is an invalid option for [{}]. Please check the indentation.'.format( ex.path[-1], paren) - elif u'extra keys not allowed' in ex.error_message: + elif u'extra keys not allowed' in text_type(ex): message += u'[{}] is an invalid option for [{}].'.format(ex.path[-1], paren) - elif u'required key not provided' in ex.error_message: + elif u'required key not provided' in text_type(ex): message += u"'{}' is a required option for [{}].".format(ex.path[-1], paren) else: - message += humanize_error(_nested_getitem(config, path), ex) + message += humanize_error(config, ex) return message -def load_config(): +class InvalidYAMLError(EsphomeError): + def __init__(self, base_exc): + message = u"Invalid YAML syntax. Please see YAML syntax reference or use an " \ + u"online YAML syntax validator:\n\n{}".format(base_exc) + super(InvalidYAMLError, self).__init__(message) + self.base_exc = base_exc + + +def _load_config(): try: config = yaml_util.load_yaml(CORE.config_path) - except OSError: - raise EsphomeError(u"Invalid YAML at {}. Please see YAML syntax reference or use an online " - u"YAML syntax validator".format(CORE.config_path)) + except EsphomeError as e: + raise InvalidYAMLError(e) CORE.raw_config = config - config = substitutions.do_substitution_pass(config) - core_config.preload_core_config(config) try: result = validate_config(config) @@ -458,13 +648,21 @@ def load_config(): return result +def load_config(): + try: + return _load_config() + except vol.Invalid as err: + raise EsphomeError("Error while parsing config: {}".format(err)) + + def line_info(obj, highlight=True): """Display line config source.""" if not highlight: return None - if hasattr(obj, '__config_file__'): - return color('cyan', "[source {}:{}]" - .format(obj.__config_file__, obj.__line__ or '?')) + if isinstance(obj, ESPHomeDataBase) and obj.esp_range is not None: + mark = obj.esp_range.start_mark + source = u"[source {}:{}]".format(mark.document, mark.line + 1) + return color('cyan', source) return None @@ -480,14 +678,14 @@ def _print_on_next_line(obj): def dump_dict(config, path, at_root=True): # type: (Config, ConfigPath, bool) -> Tuple[unicode, bool] - conf = config.nested_item(path) + conf = config.get_nested_item(path) ret = u'' multiline = False if at_root: error = config.get_error_for_path(path) if error is not None: - ret += u'\n' + color('bold_red', error) + u'\n' + ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' if isinstance(conf, (list, tuple)): multiline = True @@ -499,14 +697,14 @@ def dump_dict(config, path, at_root=True): path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: - ret += u'\n' + color('bold_red', error) + u'\n' + ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' sep = u'- ' if config.is_in_error_path(path_): sep = color('red', sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) - inf = line_info(config.nested_item(path_), highlight=config.is_in_error_path(path_)) + inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if inf is not None: msg = inf + u'\n' + msg elif msg: @@ -522,14 +720,14 @@ def dump_dict(config, path, at_root=True): path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: - ret += u'\n' + color('bold_red', error) + u'\n' + ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' st = u'{}: '.format(k) if config.is_in_error_path(path_): st = color('red', st) msg, m = dump_dict(config, path_, at_root=False) - inf = line_info(config.nested_item(path_), highlight=config.is_in_error_path(path_)) + inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if m: msg = u'\n' + indent(msg) @@ -574,7 +772,7 @@ def strip_default_ids(config): to_remove = [] for i, x in enumerate(config): x = config[i] = strip_default_ids(x) - if isinstance(x, core.ID) and not x.is_manual: + if (isinstance(x, core.ID) and not x.is_manual) or isinstance(x, core.AutoLoad): to_remove.append(x) for x in to_remove: config.remove(x) @@ -582,7 +780,7 @@ def strip_default_ids(config): to_remove = [] for k, v in config.items(): v = config[k] = strip_default_ids(v) - if isinstance(v, core.ID) and not v.is_manual: + if (isinstance(v, core.ID) and not v.is_manual) or isinstance(v, core.AutoLoad): to_remove.append(k) for k in to_remove: config.pop(k) @@ -602,12 +800,12 @@ def read_config(verbose): safe_print(color('bold_red', u"Failed config")) safe_print('') - for path, domain in res.domains: + for path, domain in res.output_paths: if not res.is_in_error_path(path): continue safe_print(color('bold_red', u'{}:'.format(domain)) + u' ' + - (line_info(res.nested_item(path)) or u'')) + (line_info(res.get_nested_item(path)) or u'')) safe_print(indent(dump_dict(res, path)[0])) return None return OrderedDict(res) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py new file mode 100644 index 0000000000..ddad36f8a8 --- /dev/null +++ b/esphome/config_helpers.py @@ -0,0 +1,29 @@ +from __future__ import print_function + +import codecs +import json +import os + +from esphome.core import CORE, EsphomeError +from esphome.py_compat import safe_input + + +def read_config_file(path): + # type: (basestring) -> unicode + if CORE.vscode and (not CORE.ace or + os.path.abspath(path) == os.path.abspath(CORE.config_path)): + print(json.dumps({ + 'type': 'read_file', + 'path': path, + })) + data = json.loads(safe_input()) + assert data['type'] == 'file_response' + return data['content'] + + try: + with codecs.open(path, encoding='utf-8') as handle: + return handle.read() + except IOError as exc: + raise EsphomeError(u"Error accessing file {}: {}".format(path, exc)) + except UnicodeDecodeError as exc: + raise EsphomeError(u"Unable to read file {}: {}".format(path, exc)) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a78f0ad503..50d30c3ea6 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -5,18 +5,22 @@ from __future__ import print_function import logging import os import re +from contextlib import contextmanager import uuid as uuid_ +from datetime import datetime +from string import ascii_letters, digits import voluptuous as vol from esphome import core from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \ - CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PLATFORM, \ - CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, ESP_PLATFORM_ESP32, \ - ESP_PLATFORM_ESP8266 + CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, \ + CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \ + CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes -from esphome.py_compat import integer_types, string_types, text_type +from esphome.helpers import list_starts_with +from esphome.py_compat import integer_types, string_types, text_type, IS_PY2, decode_text from esphome.voluptuous_schema import _Schema _LOGGER = logging.getLogger(__name__) @@ -24,13 +28,20 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=invalid-name Schema = _Schema -port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) -float_ = vol.Coerce(float) -positive_float = vol.All(float_, vol.Range(min=0)) -zero_to_one_float = vol.All(float_, vol.Range(min=0, max=1)) -negative_one_to_one_float = vol.All(float_, vol.Range(min=-1, max=1)) -positive_int = vol.All(vol.Coerce(int), vol.Range(min=0)) -positive_not_null_int = vol.All(vol.Coerce(int), vol.Range(min=0, min_included=False)) +All = vol.All +Coerce = vol.Coerce +Range = vol.Range +Invalid = vol.Invalid +MultipleInvalid = vol.MultipleInvalid +Any = vol.Any +Lower = vol.Lower +Upper = vol.Upper +Length = vol.Length +Exclusive = vol.Exclusive +Inclusive = vol.Inclusive +ALLOW_EXTRA = vol.ALLOW_EXTRA +UNDEFINED = vol.UNDEFINED +RequiredFieldInvalid = vol.RequiredFieldInvalid ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' @@ -50,15 +61,51 @@ RESERVED_IDS = [ 'App', 'pinMode', 'delay', 'delayMicroseconds', 'digitalRead', 'digitalWrite', 'INPUT', 'OUTPUT', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', + 'close', 'pause', 'sleep', 'open', ] +class Optional(vol.Optional): + """Mark a field as optional and optionally define a default for the field. + + When no default is defined, the validated config will not contain the key. + You can check if the key is defined with 'CONF_ in config'. Or to access + the key and return None if it does not exist, call config.get(CONF_) + + If a default *is* set, the resulting validated config will always contain the + default value. You can therefore directly access the value using the + 'config[CONF_]' syntax. + + In ESPHome, all configuration defaults should be defined with the Optional class + during config validation - specifically *not* in the C++ code or the code generation + phase. + """ + def __init__(self, key, default=UNDEFINED): + super(Optional, self).__init__(key, default=default) + + +class Required(vol.Required): + """Define a field to be required to be set. The validated configuration is guaranteed + to contain this key. + + All required values should be acceessed with the `config[CONF_]` syntax in code + - *not* the `config.get(CONF_)` syntax. + """ + def __init__(self, key): + super(Required, self).__init__(key) + + +def check_not_templatable(value): + if isinstance(value, Lambda): + raise Invalid("This option is not templatable!") + + def alphanumeric(value): if value is None: - raise vol.Invalid("string value is None") + raise Invalid("string value is None") value = text_type(value) if not value.isalnum(): - raise vol.Invalid("string value is not alphanumeric") + raise Invalid("string value is not alphanumeric") return value @@ -66,133 +113,215 @@ def valid_name(value): value = string_strict(value) for c in value: if c not in ALLOWED_NAME_CHARS: - raise vol.Invalid(u"'{}' is an invalid character for names. Valid characters are: {}" - u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS)) + raise Invalid(u"'{}' is an invalid character for names. Valid characters are: {}" + u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS)) return value def string(value): + """Validate that a configuration value is a string. If not, automatically converts to a string. + + Note that this can be lossy, for example the input value 60.00 (float) will be turned into + "60.0" (string). For values where this could be a problem `string_string` has to be used. + """ + check_not_templatable(value) if isinstance(value, (dict, list)): - raise vol.Invalid("string value cannot be dictionary or list.") + raise Invalid("string value cannot be dictionary or list.") + if isinstance(value, bool): + raise Invalid("Auto-converted this value to boolean, please wrap the value in quotes.") + if isinstance(value, text_type): + return value if value is not None: return text_type(value) - raise vol.Invalid("string value is None") + raise Invalid("string value is None") def string_strict(value): - """Strictly only allow strings.""" - if isinstance(value, string_types): + """Like string, but only allows strings, and does not automatically convert other types to + strings.""" + check_not_templatable(value) + if isinstance(value, text_type): return value - raise vol.Invalid("Must be string, got {}. did you forget putting quotes " - "around the value?".format(type(value))) + if isinstance(value, string_types): + return text_type(value) + raise Invalid("Must be string, got {}. did you forget putting quotes " + "around the value?".format(type(value))) def icon(value): - """Validate icon.""" + """Validate that a given config value is a valid icon.""" value = string_strict(value) + if not value: + return value if value.startswith('mdi:'): return value - raise vol.Invalid('Icons should start with prefix "mdi:"') + raise Invalid('Icons should start with prefix "mdi:"') def boolean(value): - """Validate and coerce a boolean value.""" - if isinstance(value, str): + """Validate the given config option to be a boolean. + + This option allows a bunch of different ways of expressing boolean values: + - instance of boolean + - 'true'/'false' + - 'yes'/'no' + - 'enable'/disable + """ + check_not_templatable(value) + if isinstance(value, bool): + return value + if isinstance(value, string_types): value = value.lower() - if value in ('1', 'true', 'yes', 'on', 'enable'): + if value in ('true', 'yes', 'on', 'enable'): return True - if value in ('0', 'false', 'no', 'off', 'disable'): + if value in ('false', 'no', 'off', 'disable'): return False - raise vol.Invalid('invalid boolean value {}'.format(value)) - return bool(value) + raise Invalid(u"Expected boolean value, but cannot convert {} to a boolean. " + u"Please use 'true' or 'false'".format(value)) def ensure_list(*validators): - """Wrap value in list if it is not one.""" - user = vol.All(*validators) + """Validate this configuration option to be a list. + + If the config value is not a list, it is automatically converted to a + single-item list. + + None and empty dictionaries are converted to empty lists. + """ + user = All(*validators) def validator(value): + check_not_templatable(value) if value is None or (isinstance(value, dict) and not value): return [] if not isinstance(value, list): return [user(value)] ret = [] + errs = [] for i, val in enumerate(value): try: - ret.append(user(val)) - except vol.Invalid as err: - err.prepend([i]) - raise err + with prepend_path([i]): + ret.append(user(val)) + except MultipleInvalid as err: + errs.extend(err.errors) + except Invalid as err: + errs.append(err) + if errs: + raise MultipleInvalid(errs) return ret return validator -def ensure_list_not_empty(value): - if isinstance(value, list): - return value - return [value] - - -def ensure_dict(value): - if value is None: - return {} - if not isinstance(value, dict): - raise vol.Invalid("Expected a dictionary") - return value - - -def hex_int_(value): - if isinstance(value, integer_types): - return HexInt(value) - value = string_strict(value).lower() - if value.startswith('0x'): - return HexInt(int(value, 16)) - return HexInt(int(value)) +def hex_int(value): + """Validate the given value to be a hex integer. This is mostly for cosmetic + purposes of the generated code. + """ + return HexInt(int_(value)) def int_(value): + """Validate that the config option is an integer. + + Automatically also converts strings to ints. + """ + check_not_templatable(value) if isinstance(value, integer_types): return value + if isinstance(value, float): + if int(value) == value: + return int(value) + raise Invalid("This option only accepts integers with no fractional part. Please remove " + "the fractional part from {}".format(value)) value = string_strict(value).lower() + base = 10 if value.startswith('0x'): - return int(value, 16) - return int(value) + base = 16 + try: + return int(value, base) + except ValueError: + raise Invalid(u"Expected integer, but cannot parse {} as an integer".format(value)) -hex_int = vol.Coerce(hex_int_) +def int_range(min=None, max=None, min_included=True, max_included=True): + """Validate that the config option is an integer in the given range.""" + if min is not None: + assert isinstance(min, integer_types) + if max is not None: + assert isinstance(max, integer_types) + return All(int_, Range(min=min, max=max, min_included=min_included, max_included=max_included)) + + +def hex_int_range(min=None, max=None, min_included=True, max_included=True): + """Validate that the config option is an integer in the given range.""" + return All(hex_int, + Range(min=min, max=max, min_included=min_included, max_included=max_included)) + + +def float_range(min=None, max=None, min_included=True, max_included=True): + """Validate that the config option is a floating point number in the given range.""" + if min is not None: + assert isinstance(min, (int, float)) + if max is not None: + assert isinstance(max, (int, float)) + return All(float_, Range(min=min, max=max, min_included=min_included, + max_included=max_included)) + + +port = int_range(min=1, max=65535) +float_ = Coerce(float) +positive_float = float_range(min=0) +zero_to_one_float = float_range(min=0, max=1) +negative_one_to_one_float = float_range(min=-1, max=1) +positive_int = int_range(min=0) +positive_not_null_int = int_range(min=0, min_included=False) def validate_id_name(value): + """Validate that the given value would be a valid C++ identifier name.""" value = string(value) if not value: - raise vol.Invalid("ID must not be empty") + raise Invalid("ID must not be empty") if value[0].isdigit(): - raise vol.Invalid("First character in ID cannot be a digit.") + raise Invalid("First character in ID cannot be a digit.") if '-' in value: - raise vol.Invalid("Dashes are not supported in IDs, please use underscores instead.") + raise Invalid("Dashes are not supported in IDs, please use underscores instead.") + valid_chars = ascii_letters + digits + '_' for char in value: - if char != '_' and not char.isalnum(): - raise vol.Invalid(u"IDs must only consist of upper/lowercase characters, the underscore" - u"character and numbers. The character '{}' cannot be used" - u"".format(char)) + if char not in valid_chars: + raise Invalid(u"IDs must only consist of upper/lowercase characters, the underscore" + u"character and numbers. The character '{}' cannot be used" + u"".format(char)) if value in RESERVED_IDS: - raise vol.Invalid(u"ID {} is reserved internally and cannot be used".format(value)) + raise Invalid(u"ID '{}' is reserved internally and cannot be used".format(value)) + if value in CORE.loaded_integrations: + raise Invalid(u"ID '{}' conflicts with the name of an esphome integration, please use " + u"another ID name.".format(value)) return value -def use_variable_id(type): +def use_id(type): + """Declare that this configuration option should point to an ID with the given type.""" def validator(value): + check_not_templatable(value) if value is None: return core.ID(None, is_declaration=False, type=type) + if isinstance(value, core.ID) and value.is_declaration is False and value.type is type: + return value return core.ID(validate_id_name(value), is_declaration=False, type=type) return validator -def declare_variable_id(type): +def declare_id(type): + """Declare that this configuration option should be used to declare a variable ID + with the given type. + + If two IDs with the same name exist, a validation error is thrown. + """ def validator(value): + check_not_templatable(value) if value is None: return core.ID(None, is_declaration=True, type=type) @@ -202,72 +331,83 @@ def declare_variable_id(type): def templatable(other_validators): + """Validate that the configuration option can (optionally) be templated. + + The user can declare a value as template by using the '!lambda' tag. In that case, + validation is skipped. Otherwise (if the value is not templated) the validator given + as the first argument to this method is called. + """ + schema = Schema(other_validators) + def validator(value): if isinstance(value, Lambda): - return value + return returning_lambda(value) if isinstance(other_validators, dict): - return Schema(other_validators)(value) - return other_validators(value) + return schema(value) + return schema(value) return validator def only_on(platforms): + """Validate that this option can only be specified on the given ESP platforms.""" if not isinstance(platforms, list): platforms = [platforms] def validator_(obj): if CORE.esp_platform not in platforms: - raise vol.Invalid(u"This feature is only available on {}".format(platforms)) + raise Invalid(u"This feature is only available on {}".format(platforms)) return obj return validator_ -only_on_esp32 = only_on(ESP_PLATFORM_ESP32) -only_on_esp8266 = only_on(ESP_PLATFORM_ESP8266) +only_on_esp32 = only_on('ESP32') +only_on_esp8266 = only_on('ESP8266') # Adapted from: # https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666 def has_at_least_one_key(*keys): - """Validate that at least one key exists.""" + """Validate that at least one of the given keys exist in the config.""" def validate(obj): """Test keys exist in dict.""" if not isinstance(obj, dict): - raise vol.Invalid('expected dictionary') + raise Invalid('expected dictionary') if not any(k in keys for k in obj): - raise vol.Invalid('Must contain at least one of {}.'.format(', '.join(keys))) + raise Invalid('Must contain at least one of {}.'.format(', '.join(keys))) return obj return validate def has_exactly_one_key(*keys): + """Validate that exactly one of the given keys exist in the config.""" def validate(obj): if not isinstance(obj, dict): - raise vol.Invalid('expected dictionary') + raise Invalid('expected dictionary') number = sum(k in keys for k in obj) if number > 1: - raise vol.Invalid("Cannot specify more than one of {}.".format(', '.join(keys))) + raise Invalid("Cannot specify more than one of {}.".format(', '.join(keys))) if number < 1: - raise vol.Invalid('Must contain exactly one of {}.'.format(', '.join(keys))) + raise Invalid('Must contain exactly one of {}.'.format(', '.join(keys))) return obj return validate def has_at_most_one_key(*keys): + """Validate that at most one of the given keys exist in the config.""" def validate(obj): if not isinstance(obj, dict): - raise vol.Invalid('expected dictionary') + raise Invalid('expected dictionary') number = sum(k in keys for k in obj) if number > 1: - raise vol.Invalid("Cannot specify more than one of {}.".format(', '.join(keys))) + raise Invalid("Cannot specify more than one of {}.".format(', '.join(keys))) return obj return validate @@ -275,31 +415,31 @@ def has_at_most_one_key(*keys): TIME_PERIOD_ERROR = "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h" -time_period_dict = vol.All( - dict, Schema({ - 'days': float_, - 'hours': float_, - 'minutes': float_, - 'seconds': float_, - 'milliseconds': float_, - 'microseconds': float_, +time_period_dict = All( + Schema({ + Optional('days'): float_, + Optional('hours'): float_, + Optional('minutes'): float_, + Optional('seconds'): float_, + Optional('milliseconds'): float_, + Optional('microseconds'): float_, }), - has_at_least_one_key('days', 'hours', 'minutes', - 'seconds', 'milliseconds', 'microseconds'), - lambda value: TimePeriod(**value)) + has_at_least_one_key('days', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds'), + lambda value: TimePeriod(**value) +) def time_period_str_colon(value): """Validate and transform time offset with format HH:MM[:SS].""" if isinstance(value, int): - raise vol.Invalid('Make sure you wrap time values in quotes') + raise Invalid('Make sure you wrap time values in quotes') if not isinstance(value, str): - raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + raise Invalid(TIME_PERIOD_ERROR.format(value)) try: parsed = [int(x) for x in value.split(':')] except ValueError: - raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + raise Invalid(TIME_PERIOD_ERROR.format(value)) if len(parsed) == 2: hour, minute = parsed @@ -307,18 +447,20 @@ def time_period_str_colon(value): elif len(parsed) == 3: hour, minute, second = parsed else: - raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + raise Invalid(TIME_PERIOD_ERROR.format(value)) return TimePeriod(hours=hour, minutes=minute, seconds=second) def time_period_str_unit(value): """Validate and transform time period with time unit and integer value.""" + check_not_templatable(value) + if isinstance(value, int): - raise vol.Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean " - "'{0}s'?".format(value)) + raise Invalid("Don't know what '{0}' means as it has no time *unit*! Did you mean " + "'{0}s'?".format(value)) if not isinstance(value, string_types): - raise vol.Invalid("Expected string for time period with unit.") + raise Invalid("Expected string for time period with unit.") unit_to_kwarg = { 'us': 'microseconds', @@ -339,8 +481,8 @@ def time_period_str_unit(value): match = re.match(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*)$", value) if match is None: - raise vol.Invalid(u"Expected time period with unit, " - u"got {}".format(value)) + raise Invalid(u"Expected time period with unit, " + u"got {}".format(value)) kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))] return TimePeriod(**{kwarg: float(match.group(1))}) @@ -348,7 +490,7 @@ def time_period_str_unit(value): def time_period_in_milliseconds_(value): if value.microseconds is not None and value.microseconds != 0: - raise vol.Invalid("Maximum precision is milliseconds") + raise Invalid("Maximum precision is milliseconds") return TimePeriodMilliseconds(**value.as_dict()) @@ -358,19 +500,19 @@ def time_period_in_microseconds_(value): def time_period_in_seconds_(value): if value.microseconds is not None and value.microseconds != 0: - raise vol.Invalid("Maximum precision is seconds") + raise Invalid("Maximum precision is seconds") if value.milliseconds is not None and value.milliseconds != 0: - raise vol.Invalid("Maximum precision is seconds") + raise Invalid("Maximum precision is seconds") return TimePeriodSeconds(**value.as_dict()) def time_period_in_minutes_(value): if value.microseconds is not None and value.microseconds != 0: - raise vol.Invalid("Maximum precision is minutes") + raise Invalid("Maximum precision is minutes") if value.milliseconds is not None and value.milliseconds != 0: - raise vol.Invalid("Maximum precision is minutes") + raise Invalid("Maximum precision is minutes") if value.seconds is not None and value.seconds != 0: - raise vol.Invalid("Maximum precision is minutes") + raise Invalid("Maximum precision is minutes") return TimePeriodMinutes(**value.as_dict()) @@ -380,36 +522,53 @@ def update_interval(value): return positive_time_period_milliseconds(value) -time_period = vol.Any(time_period_str_unit, time_period_str_colon, time_period_dict) -positive_time_period = vol.All(time_period, vol.Range(min=TimePeriod())) -positive_time_period_milliseconds = vol.All(positive_time_period, time_period_in_milliseconds_) -positive_time_period_seconds = vol.All(positive_time_period, time_period_in_seconds_) -positive_time_period_minutes = vol.All(positive_time_period, time_period_in_minutes_) -time_period_microseconds = vol.All(time_period, time_period_in_microseconds_) -positive_time_period_microseconds = vol.All(positive_time_period, time_period_in_microseconds_) -positive_not_null_time_period = vol.All(time_period, - vol.Range(min=TimePeriod(), min_included=False)) +time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict) +positive_time_period = All(time_period, Range(min=TimePeriod())) +positive_time_period_milliseconds = All(positive_time_period, time_period_in_milliseconds_) +positive_time_period_seconds = All(positive_time_period, time_period_in_seconds_) +positive_time_period_minutes = All(positive_time_period, time_period_in_minutes_) +time_period_microseconds = All(time_period, time_period_in_microseconds_) +positive_time_period_microseconds = All(positive_time_period, time_period_in_microseconds_) +positive_not_null_time_period = All(time_period, + Range(min=TimePeriod(), min_included=False)) + + +def time_of_day(value): + value = string(value) + try: + date = datetime.strptime(value, '%H:%M:%S') + except ValueError as err: + try: + date = datetime.strptime(value, '%H:%M:%S %p') + except ValueError: + raise Invalid("Invalid time of day: {}".format(err)) + + return { + CONF_HOUR: date.hour, + CONF_MINUTE: date.minute, + CONF_SECOND: date.second, + } def mac_address(value): value = string_strict(value) parts = value.split(':') if len(parts) != 6: - raise vol.Invalid("MAC Address must consist of 6 : (colon) separated parts") + raise Invalid("MAC Address must consist of 6 : (colon) separated parts") parts_int = [] if any(len(part) != 2 for part in parts): - raise vol.Invalid("MAC Address must be format XX:XX:XX:XX:XX:XX") + raise Invalid("MAC Address must be format XX:XX:XX:XX:XX:XX") for part in parts: try: parts_int.append(int(part, 16)) except ValueError: - raise vol.Invalid("MAC Address parts must be hexadecimal values from 00 to FF") + raise Invalid("MAC Address parts must be hexadecimal values from 00 to FF") return core.MACAddress(*parts_int) def uuid(value): - return vol.Coerce(uuid_.UUID)(value) + return Coerce(uuid_.UUID)(value) METRIC_SUFFIXES = { @@ -419,18 +578,23 @@ METRIC_SUFFIXES = { } -def float_with_unit(quantity, regex_suffix): - pattern = re.compile(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*?)" + regex_suffix + "$") +def float_with_unit(quantity, regex_suffix, optional_unit=False): + pattern = re.compile(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*?)" + regex_suffix + r"$", re.UNICODE) def validator(value): + if optional_unit: + try: + return float_(value) + except Invalid: + pass match = pattern.match(string(value)) if match is None: - raise vol.Invalid(u"Expected {} with unit, got {}".format(quantity, value)) + raise Invalid(u"Expected {} with unit, got {}".format(quantity, value)) mantissa = float(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise vol.Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2))) + raise Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] return mantissa * multiplier @@ -438,12 +602,64 @@ def float_with_unit(quantity, regex_suffix): return validator -frequency = float_with_unit("frequency", r"(Hz|HZ|hz)?") -resistance = float_with_unit("resistance", r"(Ω|Ω|ohm|Ohm|OHM)?") -current = float_with_unit("current", r"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") -voltage = float_with_unit("voltage", r"(v|V|volt|Volts)?") -distance = float_with_unit("distance", r"(m)") -framerate = float_with_unit("framerate", r"(FPS|fps|Fps|FpS|Hz)") +frequency = float_with_unit("frequency", u"(Hz|HZ|hz)?") +resistance = float_with_unit("resistance", u"(Ω|Ω|ohm|Ohm|OHM)?") +current = float_with_unit("current", u"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") +voltage = float_with_unit("voltage", u"(v|V|volt|Volts)?") +distance = float_with_unit("distance", u"(m)") +framerate = float_with_unit("framerate", u"(FPS|fps|Fps|FpS|Hz)") +angle = float_with_unit("angle", u"(°|deg)", optional_unit=True) +_temperature_c = float_with_unit("temperature", u"(°C|° C|°|C)?") +_temperature_k = float_with_unit("temperature", u"(° K|° K|K)?") +_temperature_f = float_with_unit("temperature", u"(°F|° F|F)?") + +if IS_PY2: + # Override voluptuous invalid to unicode for py2 + def _vol_invalid_unicode(self): + path = u' @ data[%s]' % u']['.join(map(repr, self.path)) \ + if self.path else u'' + # pylint: disable=no-member + output = decode_text(self.message) + if self.error_type: + output += u' for ' + self.error_type + return output + path + + Invalid.__unicode__ = _vol_invalid_unicode + + +def temperature(value): + try: + return _temperature_c(value) + except Invalid as orig_err: # noqa + pass + + try: + kelvin = _temperature_k(value) + return kelvin - 273.15 + except Invalid: + pass + + try: + fahrenheit = _temperature_f(value) + return (fahrenheit - 32) * (5 / 9) + except Invalid: + pass + + raise orig_err # noqa + + +_color_temperature_mireds = float_with_unit('Color Temperature', r'(mireds|Mireds)') +_color_temperature_kelvin = float_with_unit('Color Temperature', r'(K|Kelvin)') + + +def color_temperature(value): + try: + val = _color_temperature_mireds(value) + except Invalid: + val = 1000000.0 / _color_temperature_kelvin(value) + if val < 0: + raise Invalid("Color temperature cannot be negative") + return val def validate_bytes(value): @@ -451,25 +667,25 @@ def validate_bytes(value): match = re.match(r"^([0-9]+)\s*(\w*?)(?:byte|B|b)?s?$", value) if match is None: - raise vol.Invalid(u"Expected number of bytes with unit, got {}".format(value)) + raise Invalid(u"Expected number of bytes with unit, got {}".format(value)) mantissa = int(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise vol.Invalid(u"Invalid metric suffix {}".format(match.group(2))) + raise Invalid(u"Invalid metric suffix {}".format(match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] if multiplier < 1: - raise vol.Invalid(u"Only suffixes with positive exponents are supported. " - u"Got {}".format(match.group(2))) + raise Invalid(u"Only suffixes with positive exponents are supported. " + u"Got {}".format(match.group(2))) return int(mantissa * multiplier) def hostname(value): value = string(value) if len(value) > 63: - raise vol.Invalid("Hostnames can only be 63 characters long") + raise Invalid("Hostnames can only be 63 characters long") for c in value: if not (c.isalnum() or c in '_-'): - raise vol.Invalid("Hostname can only have alphanumeric characters and _ or -") + raise Invalid("Hostname can only have alphanumeric characters and _ or -") return value @@ -479,8 +695,8 @@ def domain(value): return value try: return str(ipv4(value)) - except vol.Invalid: - raise vol.Invalid("Invalid domain: {}".format(value)) + except Invalid: + raise Invalid("Invalid domain: {}".format(value)) def domain_name(value): @@ -488,21 +704,21 @@ def domain_name(value): if not value: return value if not value.startswith('.'): - raise vol.Invalid("Domain name must start with .") + raise Invalid("Domain name must start with .") if value.startswith('..'): - raise vol.Invalid("Domain name must start with single .") + raise Invalid("Domain name must start with single .") for c in value: if not (c.isalnum() or c in '._-'): - raise vol.Invalid("Domain name can only have alphanumeric characters and _ or -") + raise Invalid("Domain name can only have alphanumeric characters and _ or -") return value def ssid(value): value = string_strict(value) if not value: - raise vol.Invalid("SSID can't be empty.") + raise Invalid("SSID can't be empty.") if len(value) > 32: - raise vol.Invalid("SSID can't be longer than 32 characters") + raise Invalid("SSID can't be longer than 32 characters") return value @@ -514,34 +730,34 @@ def ipv4(value): elif isinstance(value, IPAddress): return value else: - raise vol.Invalid("IPv4 address must consist of either string or " - "integer list") + raise Invalid("IPv4 address must consist of either string or " + "integer list") if len(parts) != 4: - raise vol.Invalid("IPv4 address must consist of four point-separated " - "integers") + raise Invalid("IPv4 address must consist of four point-separated " + "integers") parts_ = list(map(int, parts)) if not all(0 <= x < 256 for x in parts_): - raise vol.Invalid("IPv4 address parts must be in range from 0 to 255") + raise Invalid("IPv4 address parts must be in range from 0 to 255") return IPAddress(*parts_) def _valid_topic(value): """Validate that this is a valid topic name/filter.""" if isinstance(value, dict): - raise vol.Invalid("Can't use dictionary with topic") + raise Invalid("Can't use dictionary with topic") value = string(value) try: raw_value = value.encode('utf-8') except UnicodeError: - raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") + raise Invalid("MQTT topic name/filter must be valid UTF-8 string.") if not raw_value: - raise vol.Invalid("MQTT topic name/filter must not be empty.") + raise Invalid("MQTT topic name/filter must not be empty.") if len(raw_value) > 65535: - raise vol.Invalid("MQTT topic name/filter must not be longer than " - "65535 encoded bytes.") + raise Invalid("MQTT topic name/filter must not be longer than " + "65535 encoded bytes.") if '\0' in value: - raise vol.Invalid("MQTT topic name/filter must not contain null " - "character.") + raise Invalid("MQTT topic name/filter must not contain null " + "character.") return value @@ -551,18 +767,18 @@ def subscribe_topic(value): for i in (i for i, c in enumerate(value) if c == '+'): if (i > 0 and value[i - 1] != '/') or \ (i < len(value) - 1 and value[i + 1] != '/'): - raise vol.Invalid("Single-level wildcard must occupy an entire " - "level of the filter") + raise Invalid("Single-level wildcard must occupy an entire " + "level of the filter") index = value.find('#') if index != -1: if index != len(value) - 1: # If there are multiple wildcards, this will also trigger - raise vol.Invalid("Multi-level wildcard must be the last " - "character in the topic filter.") + raise Invalid("Multi-level wildcard must be the last " + "character in the topic filter.") if len(value) > 1 and value[index - 1] != '/': - raise vol.Invalid("Multi-level wildcard must be after a topic " - "level separator.") + raise Invalid("Multi-level wildcard must be after a topic " + "level separator.") return value @@ -571,7 +787,7 @@ def publish_topic(value): """Validate that we can publish using this MQTT topic.""" value = _valid_topic(value) if '+' in value or '#' in value: - raise vol.Invalid("Wildcards can not be used in topic names") + raise Invalid("Wildcards can not be used in topic names") return value @@ -585,29 +801,34 @@ def mqtt_qos(value): try: value = int(value) except (TypeError, ValueError): - raise vol.Invalid(u"MQTT Quality of Service must be integer, got {}".format(value)) + raise Invalid(u"MQTT Quality of Service must be integer, got {}".format(value)) return one_of(0, 1, 2)(value) def requires_component(comp): + """Validate that this option can only be specified when the component `comp` is loaded.""" def validator(value): if comp not in CORE.raw_config: - raise vol.Invalid("This option requires component {}".format(comp)) + raise Invalid("This option requires component {}".format(comp)) return value return validator -uint8_t = vol.All(int_, vol.Range(min=0, max=255)) -uint16_t = vol.All(int_, vol.Range(min=0, max=65535)) -uint32_t = vol.All(int_, vol.Range(min=0, max=4294967295)) -hex_uint8_t = vol.All(hex_int, vol.Range(min=0, max=255)) -hex_uint16_t = vol.All(hex_int, vol.Range(min=0, max=65535)) -hex_uint32_t = vol.All(hex_int, vol.Range(min=0, max=4294967295)) +uint8_t = int_range(min=0, max=255) +uint16_t = int_range(min=0, max=65535) +uint32_t = int_range(min=0, max=4294967295) +hex_uint8_t = hex_int_range(min=0, max=255) +hex_uint16_t = hex_int_range(min=0, max=65535) +hex_uint32_t = hex_int_range(min=0, max=4294967295) i2c_address = hex_uint8_t def percentage(value): + """Validate that the value is a percentage. + + The resulting value is an integer in the range 0.0 to 1.0. + """ value = possibly_negative_percentage(value) return zero_to_one_float(value) @@ -620,12 +841,12 @@ def possibly_negative_percentage(value): msg = "Percentage must not be higher than 100%." if not has_percent_sign: msg += " Please put a percent sign after the number!" - raise vol.Invalid(msg) + raise Invalid(msg) if value < -1: msg = "Percentage must not be smaller than -100%." if not has_percent_sign: msg += " Please put a percent sign after the number!" - raise vol.Invalid(msg) + raise Invalid(msg) return negative_one_to_one_float(value) @@ -636,8 +857,11 @@ def percentage_int(value): def invalid(message): + """Mark this value as invalid. Each time *any* value is passed here it will result in a + validation error with the given message. + """ def validator(value): - raise vol.Invalid(message) + raise Invalid(message) return validator @@ -646,13 +870,56 @@ def valid(value): return value +@contextmanager +def prepend_path(path): + """A contextmanager helper to prepend a path to all voluptuous errors.""" + if not isinstance(path, (list, tuple)): + path = [path] + try: + yield + except vol.Invalid as e: + e.prepend(path) + raise e + + +@contextmanager +def remove_prepend_path(path): + """A contextmanager helper to remove a path from a voluptuous error.""" + if not isinstance(path, (list, tuple)): + path = [path] + try: + yield + except vol.Invalid as e: + if list_starts_with(e.path, path): + # Can't set e.path (namedtuple + for _ in range(len(path)): + e.path.pop(0) + raise e + + def one_of(*values, **kwargs): + """Validate that the config option is one of the given values. + + :param values: The valid values for this type + + :Keyword Arguments: + - *lower* (``bool``, default=False): Whether to convert the incoming values to lowercase + strings. + - *upper* (``bool``, default=False): Whether to convert the incoming values to uppercase + strings. + - *int* (``bool``, default=False): Whether to convert the incoming values to integers. + - *float* (``bool``, default=False): Whether to convert the incoming values to floats. + - *space* (``str``, default=' '): What to convert spaces in the input string to. + """ options = u', '.join(u"'{}'".format(x) for x in values) - lower = kwargs.get('lower', False) - upper = kwargs.get('upper', False) - string_ = kwargs.get('string', False) or lower or upper - to_int = kwargs.get('int', False) - space = kwargs.get('space', ' ') + lower = kwargs.pop('lower', False) + upper = kwargs.pop('upper', False) + string_ = kwargs.pop('string', False) or lower or upper + to_int = kwargs.pop('int', False) + to_float = kwargs.pop('float', False) + space = kwargs.pop('space', ' ') + if kwargs: + raise ValueError def validator(value): if string_: @@ -660,60 +927,120 @@ def one_of(*values, **kwargs): value = value.replace(' ', space) if to_int: value = int_(value) + if to_float: + value = float_(value) if lower: - value = vol.Lower(value) + value = Lower(value) if upper: - value = vol.Upper(value) + value = Upper(value) if value not in values: - raise vol.Invalid(u"Unknown value '{}', must be one of {}".format(value, options)) + import difflib + options_ = [text_type(x) for x in values] + option = text_type(value) + matches = difflib.get_close_matches(option, options_) + if matches: + raise Invalid(u"Unknown value '{}', did you mean {}?" + u"".format(value, u", ".join(u"'{}'".format(x) for x in matches))) + raise Invalid(u"Unknown value '{}', valid options are {}.".format(value, options)) return value return validator -def lambda_(value): - if isinstance(value, Lambda): +def enum(mapping, **kwargs): + """Validate this config option against an enum mapping. + + The mapping should be a dictionary with the key representing the config value name and + a value representing the expression to set during code generation. + + Accepts all kwargs of one_of. + """ + assert isinstance(mapping, dict) + one_of_validator = one_of(*mapping, **kwargs) + + def validator(value): + from esphome.yaml_util import make_data_base + + value = make_data_base(one_of_validator(value)) + cls = value.__class__ + value.__class__ = cls.__class__(cls.__name__ + "Enum", (cls, core.EnumValue), {}) + value.enum_value = mapping[value] return value - return Lambda(string_strict(value)) + + return validator + + +LAMBDA_ENTITY_ID_PROG = re.compile(r'id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)') + + +def lambda_(value): + """Coerce this configuration option to a lambda.""" + if not isinstance(value, Lambda): + value = Lambda(string_strict(value)) + entity_id_parts = re.split(LAMBDA_ENTITY_ID_PROG, value.value) + if len(entity_id_parts) != 1: + entity_ids = ' '.join("'{}'".format(entity_id_parts[i]) + for i in range(1, len(entity_id_parts), 2)) + raise Invalid("Lambda contains reference to entity-id-style ID {}. " + "The id() wrapper only works for ESPHome-internal types. For importing " + "states from Home Assistant use the 'homeassistant' sensor platforms." + "".format(entity_ids)) + return value + + +def returning_lambda(value): + """Coerce this configuration option to a lambda. + + Additionally, make sure the lambda returns something. + """ + value = lambda_(value) + if u'return' not in value.value: + raise Invalid("Lambda doesn't contain a 'return' statement, but the lambda " + "is expected to return a value. \n" + "Please make sure the lambda contains at least one " + "return statement.") + return value def dimensions(value): if isinstance(value, list): if len(value) != 2: - raise vol.Invalid(u"Dimensions must have a length of two, not {}".format(len(value))) + raise Invalid(u"Dimensions must have a length of two, not {}".format(len(value))) try: width, height = int(value[0]), int(value[1]) except ValueError: - raise vol.Invalid(u"Width and height dimensions must be integers") + raise Invalid(u"Width and height dimensions must be integers") if width <= 0 or height <= 0: - raise vol.Invalid(u"Width and height must at least be 1") + raise Invalid(u"Width and height must at least be 1") return [width, height] value = string(value) match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: - raise vol.Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") + raise Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") return dimensions([match.group(1), match.group(2)]) def directory(value): value = string(value) - path = CORE.relative_path(value) + path = CORE.relative_config_path(value) if not os.path.exists(path): - raise vol.Invalid(u"Could not find directory '{}'. Please make sure it exists.".format( - path)) + raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." + u"".format(path, os.path.abspath(path))) if not os.path.isdir(path): - raise vol.Invalid(u"Path '{}' is not a directory.".format(path)) + raise Invalid(u"Path '{}' is not a directory (full path: {})." + u"".format(path, os.path.abspath(path))) return value def file_(value): value = string(value) - path = CORE.relative_path(value) + path = CORE.relative_config_path(value) if not os.path.exists(path): - raise vol.Invalid(u"Could not find file '{}'. Please make sure it exists.".format( - path)) + raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." + u"".format(path, os.path.abspath(path))) if not os.path.isfile(path): - raise vol.Invalid(u"Path '{}' is not a file.".format(path)) + raise Invalid(u"Path '{}' is not a file (full path: {})." + u"".format(path, os.path.abspath(path))) return value @@ -721,62 +1048,217 @@ ENTITY_ID_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789_' def entity_id(value): + """Validate that this option represents a valid Home Assistant entity id. + + Should only be used for 'homeassistant' platforms. + """ value = string_strict(value).lower() if value.count('.') != 1: - raise vol.Invalid("Entity ID must have exactly one dot in it") + raise Invalid("Entity ID must have exactly one dot in it") for x in value.split('.'): for c in x: if c not in ENTITY_ID_CHARACTERS: - raise vol.Invalid("Invalid character for entity ID: {}".format(c)) + raise Invalid("Invalid character for entity ID: {}".format(c)) return value -class GenerateID(vol.Optional): - def __init__(self, key=CONF_ID): - super(GenerateID, self).__init__(key, default=lambda: None) +def extract_keys(schema): + """Extract the names of the keys from the given schema.""" + if isinstance(schema, Schema): + schema = schema.schema + assert isinstance(schema, dict) + keys = [] + for skey in list(schema.keys()): + if isinstance(skey, string_types): + keys.append(skey) + elif isinstance(skey, vol.Marker) and isinstance(skey.schema, string_types): + keys.append(skey.schema) + else: + raise ValueError() + keys.sort() + return keys -def nameable(*schemas): - def validator(config): - config = vol.All(*schemas)(config) - if CONF_NAME not in config and CONF_ID not in config: - raise vol.Invalid("At least one of 'id:' or 'name:' is required!") - if CONF_NAME not in config: - id = config[CONF_ID] - if not id.is_manual: - raise vol.Invalid("At least one of 'id:' or 'name:' is required!") - config[CONF_NAME] = id.id - config[CONF_INTERNAL] = True - return config - return config +def typed_schema(schemas, **kwargs): + """Create a schema that has a key to distinguish between schemas""" + key = kwargs.pop('key', CONF_TYPE) + key_validator = one_of(*schemas, **kwargs) + + def validator(value): + if not isinstance(value, dict): + raise Invalid("Value must be dict") + if CONF_TYPE not in value: + raise Invalid("type not specified!") + value = value.copy() + key_v = key_validator(value.pop(key)) + value = schemas[key_v](value) + value[key] = key_v + return value return validator -PLATFORM_SCHEMA = Schema({ - vol.Required(CONF_PLATFORM): valid, -}) +class GenerateID(Optional): + """Mark this key as being an auto-generated ID key.""" + def __init__(self, key=CONF_ID): + super(GenerateID, self).__init__(key, default=lambda: None) + + +class SplitDefault(Optional): + """Mark this key to have a split default for ESP8266/ESP32.""" + def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED): + super(SplitDefault, self).__init__(key) + self._esp8266_default = vol.default_factory(esp8266) + self._esp32_default = vol.default_factory(esp32) + + @property + def default(self): + if CORE.is_esp8266: + return self._esp8266_default + if CORE.is_esp32: + return self._esp32_default + raise ValueError + + @default.setter + def default(self, value): + # Ignore default set from vol.Optional + pass + + +class OnlyWith(Optional): + """Set the default value only if the given component is loaded.""" + def __init__(self, key, component, default=None): + super(OnlyWith, self).__init__(key) + self._component = component + self._default = vol.default_factory(default) + + @property + def default(self): + if self._component not in CORE.raw_config: + return vol.UNDEFINED + return self._default + + @default.setter + def default(self, value): + # Ignore default set from vol.Optional + pass + + +def _nameable_validator(config): + if CONF_NAME not in config and CONF_ID not in config: + raise Invalid("At least one of 'id:' or 'name:' is required!") + if CONF_NAME not in config: + id = config[CONF_ID] + if not id.is_manual: + raise Invalid("At least one of 'id:' or 'name:' is required!") + config[CONF_NAME] = id.id + config[CONF_INTERNAL] = True + return config + return config + + +def ensure_schema(schema): + if not isinstance(schema, vol.Schema): + return Schema(schema) + return schema + + +def validate_registry_entry(name, registry): + base_schema = ensure_schema(registry.base_schema).extend({ + Optional(CONF_TYPE_ID): valid, + }, extra=ALLOW_EXTRA) + ignore_keys = extract_keys(base_schema) + + def validator(value): + if isinstance(value, string_types): + value = {value: {}} + if not isinstance(value, dict): + raise Invalid(u"{} must consist of key-value mapping! Got {}" + u"".format(name.title(), value)) + value = base_schema(value) + key = next((x for x in value if x not in ignore_keys), None) + if key is None: + raise Invalid(u"Key missing from {}! Got {}".format(name, value)) + if key not in registry: + raise Invalid(u"Unable to find {} with the name '{}'".format(name, key), [key]) + key2 = next((x for x in value if x != key and x not in ignore_keys), None) + if key2 is not None: + raise Invalid(u"Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " + u"Did you forget to indent the block inside the {0}?" + u"".format(name, key, key2)) + + if value[key] is None: + value[key] = {} + + registry_entry = registry[key] + + with prepend_path([key]): + value[key] = registry_entry.schema(value[key]) + + if registry_entry.type_id is not None: + my_base_schema = base_schema.extend({ + GenerateID(CONF_TYPE_ID): declare_id(registry_entry.type_id) + }) + value = my_base_schema(value) + + return value + + return validator + + +def validate_registry(name, registry): + return ensure_list(validate_registry_entry(name, registry)) + + +def maybe_simple_value(*validators, **kwargs): + key = kwargs.pop('key', CONF_VALUE) + validator = All(*validators) + + def validate(value): + if isinstance(value, dict) and key in value: + return validator(value) + return validator({key: value}) + + return validate + MQTT_COMPONENT_AVAILABILITY_SCHEMA = Schema({ - vol.Required(CONF_TOPIC): subscribe_topic, - vol.Optional(CONF_PAYLOAD_AVAILABLE, default='online'): mqtt_payload, - vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, default='offline'): mqtt_payload, + Required(CONF_TOPIC): subscribe_topic, + Optional(CONF_PAYLOAD_AVAILABLE, default='online'): mqtt_payload, + Optional(CONF_PAYLOAD_NOT_AVAILABLE, default='offline'): mqtt_payload, }) MQTT_COMPONENT_SCHEMA = Schema({ - vol.Optional(CONF_NAME): string, - vol.Optional(CONF_RETAIN): vol.All(requires_component('mqtt'), boolean), - vol.Optional(CONF_DISCOVERY): vol.All(requires_component('mqtt'), boolean), - vol.Optional(CONF_STATE_TOPIC): vol.All(requires_component('mqtt'), publish_topic), - vol.Optional(CONF_AVAILABILITY): vol.All(requires_component('mqtt'), - vol.Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)), - vol.Optional(CONF_INTERNAL): boolean, + Optional(CONF_NAME): string, + Optional(CONF_RETAIN): All(requires_component('mqtt'), boolean), + Optional(CONF_DISCOVERY): All(requires_component('mqtt'), boolean), + Optional(CONF_STATE_TOPIC): All(requires_component('mqtt'), publish_topic), + Optional(CONF_AVAILABILITY): All(requires_component('mqtt'), + Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)), + Optional(CONF_INTERNAL): boolean, }) +MQTT_COMPONENT_SCHEMA.add_extra(_nameable_validator) MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend({ - vol.Optional(CONF_COMMAND_TOPIC): vol.All(requires_component('mqtt'), subscribe_topic), + Optional(CONF_COMMAND_TOPIC): All(requires_component('mqtt'), subscribe_topic), }) COMPONENT_SCHEMA = Schema({ - vol.Optional(CONF_SETUP_PRIORITY): float_ + Optional(CONF_SETUP_PRIORITY): float_ }) + + +def polling_component_schema(default_update_interval): + """Validate that this component represents a PollingComponent with a configurable + update_interval. + + :param default_update_interval: The default update interval to set for the integration. + """ + if default_update_interval is None: + return COMPONENT_SCHEMA.extend({ + Required(CONF_UPDATE_INTERVAL): default_update_interval, + }) + assert isinstance(default_update_interval, string_types) + return COMPONENT_SCHEMA.extend({ + Optional(CONF_UPDATE_INTERVAL, default=default_update_interval): update_interval, + }) diff --git a/esphome/const.py b/esphome/const.py index ba1cdc247d..2408ca18fb 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,431 +1,16 @@ +# coding=utf-8 """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 12 -PATCH_VERSION = '2' +MINOR_VERSION = 13 +PATCH_VERSION = '0b7' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) -ESPHOME_CORE_VERSION = '1.12.2' ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] -APB_CLOCK_FREQ = 80000000 - -CONF_ESPHOME = 'esphome' -CONF_NAME = 'name' -CONF_PLATFORM = 'platform' -CONF_BOARD = 'board' -CONF_ESPHOME_CORE_VERSION = 'esphome_core_version' -CONF_USE_CUSTOM_CODE = 'use_custom_code' -CONF_ARDUINO_VERSION = 'arduino_version' -CONF_LOCAL = 'local' -CONF_REPOSITORY = 'repository' -CONF_COMMIT = 'commit' -CONF_SERVICES = 'services' -CONF_TAG = 'tag' -CONF_BRANCH = 'branch' -CONF_LOGGER = 'logger' -CONF_WIFI = 'wifi' -CONF_SSID = 'ssid' -CONF_IP_ADDRESS = 'ip_address' -CONF_BSSID = 'bssid' -CONF_PASSWORD = 'password' -CONF_MANUAL_IP = 'manual_ip' -CONF_STATIC_IP = 'static_ip' -CONF_GATEWAY = 'gateway' -CONF_SUBNET = 'subnet' -CONF_OTA = 'ota' -CONF_MQTT = 'mqtt' -CONF_BROKER = 'broker' -CONF_USERNAME = 'username' -CONF_MIN_LEVEL = 'min_level' -CONF_IDLE_LEVEL = 'idle_level' -CONF_MAX_LEVEL = 'max_level' -CONF_POWER_SUPPLY = 'power_supply' -CONF_ID = 'id' -CONF_MQTT_ID = 'mqtt_id' -CONF_SENSOR_ID = 'sensor_id' -CONF_TRIGGER_ID = 'trigger_id' -CONF_ACTION_ID = 'action_id' -CONF_CONDITION_ID = 'condition_id' -CONF_MAKE_ID = 'make_id' -CONF_AUTOMATION_ID = 'automation_id' -CONF_DELAY = 'delay' -CONF_PIN = 'pin' -CONF_NUMBER = 'number' -CONF_INVERTED = 'inverted' -CONF_I2C = 'i2c' -CONF_SDA = 'sda' -CONF_SCL = 'scl' -CONF_FREQUENCY = 'frequency' -CONF_PCA9685 = 'pca9685' -CONF_PCA9685_ID = 'pca9685_id' -CONF_OUTPUT = 'output' -CONF_CHANNEL = 'channel' -CONF_CHANNELS = 'channels' -CONF_LIGHT = 'light' -CONF_RED = 'red' -CONF_GREEN = 'green' -CONF_BLUE = 'blue' -CONF_SENSOR = 'sensor' -CONF_TEMPERATURE = 'temperature' -CONF_HUMIDITY = 'humidity' -CONF_MODEL = 'model' -CONF_BINARY_SENSOR = 'binary_sensor' -CONF_DEVICE_CLASS = 'device_class' -CONF_GPIO = 'gpio' -CONF_DHT = 'dht' -CONF_SAFE_MODE = 'safe_mode' -CONF_MODE = 'mode' -CONF_GAMMA_CORRECT = 'gamma_correct' -CONF_RETAIN = 'retain' -CONF_DISCOVERY = 'discovery' -CONF_DISCOVERY_PREFIX = 'discovery_prefix' -CONF_STATE_TOPIC = 'state_topic' -CONF_COMMAND_TOPIC = 'command_topic' -CONF_AVAILABILITY = 'availability' -CONF_TOPIC = 'topic' -CONF_PAYLOAD_AVAILABLE = 'payload_available' -CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' -CONF_DEFAULT_TRANSITION_LENGTH = 'default_transition_length' -CONF_TRANSITION_LENGTH = 'transition_length' -CONF_FLASH_LENGTH = 'flash_length' -CONF_BRIGHTNESS = 'brightness' -CONF_EFFECT = 'effect' -CONF_ABOVE = 'above' -CONF_BELOW = 'below' -CONF_ON = 'on' -CONF_IF = 'if' -CONF_WHILE = 'while' -CONF_WAIT_UNTIL = 'wait_until' -CONF_THEN = 'then' -CONF_BINARY = 'binary' -CONF_WHITE = 'white' -CONF_RGBW = 'rgbw' -CONF_MAX_POWER = 'max_power' -CONF_BIT_DEPTH = 'bit_depth' -CONF_BAUD_RATE = 'baud_rate' -CONF_LOG_TOPIC = 'log_topic' -CONF_TX_BUFFER_SIZE = 'tx_buffer_size' -CONF_LEVEL = 'level' -CONF_LOGS = 'logs' -CONF_PORT = 'port' -CONF_WILL_MESSAGE = 'will_message' -CONF_BIRTH_MESSAGE = 'birth_message' -CONF_SHUTDOWN_MESSAGE = 'shutdown_message' -CONF_PAYLOAD = 'payload' -CONF_QOS = 'qos' -CONF_DISCOVERY_RETAIN = 'discovery_retain' -CONF_TOPIC_PREFIX = 'topic_prefix' -CONF_PHASE_BALANCER = 'phase_balancer' -CONF_ADDRESS = 'address' -CONF_ENABLE_TIME = 'enable_time' -CONF_KEEP_ON_TIME = 'keep_on_time' -CONF_DNS1 = 'dns1' -CONF_DNS2 = 'dns2' -CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' -CONF_ICON = 'icon' -CONF_ACCURACY_DECIMALS = 'accuracy_decimals' -CONF_EXPIRE_AFTER = 'expire_after' -CONF_FILTERS = 'filters' -CONF_OFFSET = 'offset' -CONF_MULTIPLY = 'multiply' -CONF_FILTER_OUT = 'filter_out' -CONF_SLIDING_WINDOW_MOVING_AVERAGE = 'sliding_window_moving_average' -CONF_EXPONENTIAL_MOVING_AVERAGE = 'exponential_moving_average' -CONF_WINDOW_SIZE = 'window_size' -CONF_SEND_EVERY = 'send_every' -CONF_ALPHA = 'alpha' -CONF_LAMBDA = 'lambda' -CONF_THROTTLE = 'throttle' -CONF_DELTA = 'delta' -CONF_OR = 'or' -CONF_CALIBRATE_LINEAR = 'calibrate_linear' -CONF_AND = 'and' -CONF_RANGE = 'range' -CONF_UNIQUE = 'unique' -CONF_HEARTBEAT = 'heartbeat' -CONF_DEBOUNCE = 'debounce' -CONF_UPDATE_INTERVAL = 'update_interval' -CONF_PULL_MODE = 'pull_mode' -CONF_COUNT_MODE = 'count_mode' -CONF_RISING_EDGE = 'rising_edge' -CONF_FALLING_EDGE = 'falling_edge' -CONF_INTERNAL_FILTER = 'internal_filter' -CONF_DALLAS_ID = 'dallas_id' -CONF_INDEX = 'index' -CONF_RESOLUTION = 'resolution' -CONF_ATTENUATION = 'attenuation' -CONF_PRESSURE = 'pressure' -CONF_TRIGGER_PIN = 'trigger_pin' -CONF_ECHO_PIN = 'echo_pin' -CONF_TIMEOUT = 'timeout' -CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' -CONF_NEC = 'nec' -CONF_COMMAND = 'command' -CONF_DATA = 'data' -CONF_NBITS = 'nbits' -CONF_JVC = 'jvc' -CONF_RC5 = 'rc5' -CONF_LG = 'lg' -CONF_SAMSUNG = 'samsung' -CONF_SONY = 'sony' -CONF_PANASONIC = 'panasonic' -CONF_REPEAT = 'repeat' -CONF_TIMES = 'times' -CONF_WAIT_TIME = 'wait_time' -CONF_OSCILLATION_OUTPUT = 'oscillation_output' -CONF_SPEED = 'speed' -CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic' -CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic' -CONF_OSCILLATING = 'oscillating' -CONF_SPEED_STATE_TOPIC = 'speed_state_topic' -CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic' -CONF_LOW = 'low' -CONF_MEDIUM = 'medium' -CONF_HIGH = 'high' -CONF_NUM_ATTEMPTS = 'num_attempts' -CONF_CLIENT_ID = 'client_id' -CONF_RAW = 'raw' -CONF_CARRIER_FREQUENCY = 'carrier_frequency' -CONF_RATE = 'rate' -CONF_ADS1115_ID = 'ads1115_id' -CONF_MULTIPLEXER = 'multiplexer' -CONF_GAIN = 'gain' -CONF_SLEEP_DURATION = 'sleep_duration' -CONF_WAKEUP_PIN = 'wakeup_pin' -CONF_RUN_CYCLES = 'run_cycles' -CONF_RUN_DURATION = 'run_duration' -CONF_AP = 'ap' -CONF_CSS_URL = 'css_url' -CONF_JS_URL = 'js_url' -CONF_SSL_FINGERPRINTS = 'ssl_fingerprints' -CONF_PCF8574 = 'pcf8574' -CONF_MCP23017 = 'mcp23017' -CONF_PCF8575 = 'pcf8575' -CONF_SCAN = 'scan' -CONF_KEEPALIVE = 'keepalive' -CONF_INTEGRATION_TIME = 'integration_time' -CONF_RECEIVE_TIMEOUT = 'receive_timeout' -CONF_SCAN_INTERVAL = 'scan_interval' -CONF_MAC_ADDRESS = 'mac_address' -CONF_SETUP_MODE = 'setup_mode' -CONF_SETUP_PRIORITY = 'setup_priority' -CONF_IIR_FILTER = 'iir_filter' -CONF_MEASUREMENT_DURATION = 'measurement_duration' -CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' -CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' -CONF_VOLTAGE_ATTENUATION = 'voltage_attenuation' -CONF_THRESHOLD = 'threshold' -CONF_OVERSAMPLING = 'oversampling' -CONF_GAS_RESISTANCE = 'gas_resistance' -CONF_NUM_LEDS = 'num_leds' -CONF_MAX_REFRESH_RATE = 'max_refresh_rate' -CONF_CHIPSET = 'chipset' -CONF_DATA_PIN = 'data_pin' -CONF_CLOCK_PIN = 'clock_pin' -CONF_RGB_ORDER = 'rgb_order' -CONF_ACCURACY = 'accuracy' -CONF_BOARD_FLASH_MODE = 'board_flash_mode' -CONF_ON_PRESS = 'on_press' -CONF_ON_RELEASE = 'on_release' -CONF_ON_STATE = 'on_state' -CONF_ON_CLICK = 'on_click' -CONF_ON_DOUBLE_CLICK = 'on_double_click' -CONF_ON_MULTI_CLICK = 'on_multi_click' -CONF_MIN_LENGTH = 'min_length' -CONF_MAX_LENGTH = 'max_length' -CONF_ON_VALUE = 'on_value' -CONF_ON_RAW_VALUE = 'on_raw_value' -CONF_ON_VALUE_RANGE = 'on_value_range' -CONF_ON_MESSAGE = 'on_message' -CONF_CS_PIN = 'cs_pin' -CONF_CLK_PIN = 'clk_pin' -CONF_MISO_PIN = 'miso_pin' -CONF_MOSI_PIN = 'mosi_pin' -CONF_TURN_ON_ACTION = 'turn_on_action' -CONF_TURN_OFF_ACTION = 'turn_off_action' -CONF_OPEN_ACTION = 'open_action' -CONF_CLOSE_ACTION = 'close_action' -CONF_STOP_ACTION = 'stop_action' -CONF_DOMAIN = 'domain' -CONF_OPTIMISTIC = 'optimistic' -CONF_ASSUMED_STATE = 'assumed_state' -CONF_ON_BOOT = 'on_boot' -CONF_ON_SHUTDOWN = 'on_shutdown' -CONF_PRIORITY = 'priority' -CONF_DUMP = 'dump' -CONF_BUFFER_SIZE = 'buffer_size' -CONF_TOLERANCE = 'tolerance' -CONF_FILTER = 'filter' -CONF_IDLE = 'idle' -CONF_NETWORKS = 'networks' -CONF_INTERNAL = 'internal' -CONF_BUILD_PATH = 'build_path' -CONF_PLATFORMIO_OPTIONS = 'platformio_options' -CONF_REBOOT_TIMEOUT = 'reboot_timeout' -CONF_INVERT = 'invert' -CONF_DELAYED_ON = 'delayed_on' -CONF_DELAYED_OFF = 'delayed_off' -CONF_UUID = 'uuid' -CONF_TYPE = 'type' -CONF_SPI_ID = 'spi_id' -CONF_HARDWARE_UART = 'hardware_uart' -CONF_UART_ID = 'uart_id' -CONF_UID = 'uid' -CONF_TX_PIN = 'tx_pin' -CONF_RX_PIN = 'rx_pin' -CONF_CO2 = 'co2' -CONF_SHUNT_RESISTANCE = 'shunt_resistance' -CONF_MAX_CURRENT = 'max_current' -CONF_MAX_VOLTAGE = 'max_voltage' -CONF_CURRENT = 'current' -CONF_POWER = 'power' -CONF_BUS_VOLTAGE = 'bus_voltage' -CONF_SHUNT_VOLTAGE = 'shunt_voltage' -CONF_CONDITION = 'condition' -CONF_ELSE = 'else' -CONF_EFFECTS = 'effects' -CONF_RANDOM = 'random' -CONF_EFFECT_ID = 'effect_id' -CONF_COLORS = 'colors' -CONF_STATE = 'state' -CONF_DURATION = 'duration' -CONF_WIDTH = 'width' -CONF_ILLUMINANCE = 'illuminance' -CONF_COLOR_TEMPERATURE = 'color_temperature' -CONF_BATTERY_LEVEL = 'battery_level' -CONF_MOISTURE = 'moisture' -CONF_CONDUCTIVITY = 'conductivity' -CONF_RC_SWITCH_RAW = 'rc_switch_raw' -CONF_RC_SWITCH_TYPE_A = 'rc_switch_type_a' -CONF_RC_SWITCH_TYPE_B = 'rc_switch_type_b' -CONF_RC_SWITCH_TYPE_C = 'rc_switch_type_c' -CONF_RC_SWITCH_TYPE_D = 'rc_switch_type_d' -CONF_CODE = 'code' -CONF_PROTOCOL = 'protocol' -CONF_PULSE_LENGTH = 'pulse_length' -CONF_SYNC = 'sync' -CONF_ZERO = 'zero' -CONF_ONE = 'one' -CONF_GROUP = 'group' -CONF_DEVICE = 'device' -CONF_FAMILY = 'family' -CONF_FILE = 'file' -CONF_GLYPHS = 'glyphs' -CONF_SIZE = 'size' -CONF_RESIZE = 'resize' -CONF_ROTATION = 'rotation' -CONF_DC_PIN = 'dc_pin' -CONF_RESET_PIN = 'reset_pin' -CONF_BUSY_PIN = 'busy_pin' -CONF_ESP8266_RESTORE_FROM_FLASH = 'esp8266_restore_from_flash' -CONF_FULL_UPDATE_EVERY = 'full_update_every' -CONF_DATA_PINS = 'data_pins' -CONF_ENABLE_PIN = 'enable_pin' -CONF_RS_PIN = 'rs_pin' -CONF_RW_PIN = 'rw_pin' -CONF_DIMENSIONS = 'dimensions' -CONF_NUM_CHIPS = 'num_chips' -CONF_INTENSITY = 'intensity' -CONF_EXTERNAL_VCC = 'external_vcc' -CONF_TIMEZONE = 'timezone' -CONF_SERVERS = 'servers' -CONF_HEATER = 'heater' -CONF_VOLTAGE = 'voltage' -CONF_CURRENT_RESISTOR = 'current_resistor' -CONF_VOLTAGE_DIVIDER = 'voltage_divider' -CONF_SEL_PIN = 'sel_pin' -CONF_CF_PIN = 'cf_pin' -CONF_CF1_PIN = 'cf1_pin' -CONF_CHANGE_MODE_EVERY = 'change_mode_every' -CONF_PAGE_ID = 'page_id' -CONF_COMPONENT_ID = 'component_id' -CONF_COLD_WHITE = 'cold_white' -CONF_PAGES = 'pages' -CONF_WARM_WHITE = 'warm_white' -CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature' -CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature' -CONF_HIDDEN = 'hidden' -CONF_ON_LOOP = 'on_loop' -CONF_ON_TIME = 'on_time' -CONF_SECONDS = 'seconds' -CONF_MINUTES = 'minutes' -CONF_HOURS = 'hours' -CONF_DAYS_OF_MONTH = 'days_of_month' -CONF_MONTHS = 'months' -CONF_DAYS_OF_WEEK = 'days_of_week' -CONF_CRON = 'cron' -CONF_POWER_SAVE_MODE = 'power_save_mode' -CONF_POWER_ON_VALUE = 'power_on_value' -CONF_PM_1_0 = 'pm_1_0' -CONF_PM_2_5 = 'pm_2_5' -CONF_PM_10_0 = 'pm_10_0' -CONF_FORMALDEHYDE = 'formaldehyde' -CONF_ON_TAG = 'on_tag' -CONF_ARGS = 'args' -CONF_FORMAT = 'format' -CONF_FOR = 'for' -CONF_COLOR_CORRECT = 'color_correct' -CONF_ON_JSON_MESSAGE = 'on_json_message' -CONF_ACCELERATION = 'acceleration' -CONF_DECELERATION = 'deceleration' -CONF_MAX_SPEED = 'max_speed' -CONF_TARGET = 'target' -CONF_POSITION = 'position' -CONF_STEP_PIN = 'step_pin' -CONF_DIR_PIN = 'dir_pin' -CONF_SLEEP_PIN = 'sleep_pin' -CONF_SEND_FIRST_AT = 'send_first_at' -CONF_TIME_ID = 'time_id' -CONF_RESTORE_STATE = 'restore_state' -CONF_TIMING = 'timing' -CONF_INVALID_COOLDOWN = 'invalid_cooldown' -CONF_MY9231_ID = 'my9231_id' -CONF_NUM_CHANNELS = 'num_channels' -CONF_UPDATE_ON_BOOT = 'update_on_boot' -CONF_INITIAL_VALUE = 'initial_value' -CONF_RESTORE_VALUE = 'restore_value' -CONF_PINS = 'pins' -CONF_SENSORS = 'sensors' -CONF_BINARY_SENSORS = 'binary_sensors' -CONF_OUTPUTS = 'outputs' -CONF_SWITCHES = 'switches' -CONF_TEXT_SENSORS = 'text_sensors' -CONF_INCLUDES = 'includes' -CONF_LIBRARIES = 'libraries' -CONF_PIN_A = 'pin_a' -CONF_PIN_B = 'pin_b' -CONF_PIN_C = 'pin_c' -CONF_PIN_D = 'pin_d' -CONF_SLEEP_WHEN_DONE = 'sleep_when_done' -CONF_STEP_MODE = 'step_mode' -CONF_COMPONENTS = 'components' -CONF_DATA_TEMPLATE = 'data_template' -CONF_VARIABLES = 'variables' -CONF_SERVICE = 'service' -CONF_ENTITY_ID = 'entity_id' -CONF_RESTORE_MODE = 'restore_mode' -CONF_INTERVAL = 'interval' -CONF_DIRECTION = 'direction' -CONF_VARIANT = 'variant' -CONF_METHOD = 'method' -CONF_FAST_CONNECT = 'fast_connect' -CONF_INTERLOCK = 'interlock' -CONF_ON_TURN_ON = 'on_turn_on' -CONF_ON_TURN_OFF = 'on_turn_off' -CONF_USE_ADDRESS = 'use_address' -CONF_FROM = 'from' -CONF_TO = 'to' -CONF_SEGMENTS = 'segments' -CONF_MIN_POWER = 'min_power' -CONF_MIN_VALUE = 'min_value' -CONF_MAX_VALUE = 'max_value' -CONF_RX_ONLY = 'rx_only' - - ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0' @@ -433,4 +18,496 @@ ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0' ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \ '/stage' ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.0' +ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0' +ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.0' ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0' +SOURCE_FILE_EXTENSIONS = {'.cpp', '.hpp', '.h', '.c', '.tcc', '.ino'} +HEADER_FILE_EXTENSIONS = {'.h', '.hpp', '.tcc'} + +CONF_ABOVE = 'above' +CONF_ACCELERATION = 'acceleration' +CONF_ACCURACY = 'accuracy' +CONF_ACCURACY_DECIMALS = 'accuracy_decimals' +CONF_ACTION_ID = 'action_id' +CONF_ADDRESS = 'address' +CONF_ALPHA = 'alpha' +CONF_AND = 'and' +CONF_AP = 'ap' +CONF_ARDUINO_VERSION = 'arduino_version' +CONF_ARGS = 'args' +CONF_ASSUMED_STATE = 'assumed_state' +CONF_AT = 'at' +CONF_ATTENUATION = 'attenuation' +CONF_AUTOMATION_ID = 'automation_id' +CONF_AVAILABILITY = 'availability' +CONF_AWAY = 'away' +CONF_AWAY_CONFIG = 'away_config' +CONF_BATTERY_LEVEL = 'battery_level' +CONF_BAUD_RATE = 'baud_rate' +CONF_BELOW = 'below' +CONF_BINARY = 'binary' +CONF_BINARY_SENSOR = 'binary_sensor' +CONF_BINARY_SENSORS = 'binary_sensors' +CONF_BIRTH_MESSAGE = 'birth_message' +CONF_BIT_DEPTH = 'bit_depth' +CONF_BLUE = 'blue' +CONF_BOARD = 'board' +CONF_BOARD_FLASH_MODE = 'board_flash_mode' +CONF_BRANCH = 'branch' +CONF_BRIGHTNESS = 'brightness' +CONF_BROKER = 'broker' +CONF_BSSID = 'bssid' +CONF_BUFFER_SIZE = 'buffer_size' +CONF_BUILD_PATH = 'build_path' +CONF_BUSY_PIN = 'busy_pin' +CONF_BUS_VOLTAGE = 'bus_voltage' +CONF_CALIBRATE_LINEAR = 'calibrate_linear' +CONF_CALIBRATION = 'calibration' +CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' +CONF_CARRIER_FREQUENCY = 'carrier_frequency' +CONF_CHANGE_MODE_EVERY = 'change_mode_every' +CONF_CHANNEL = 'channel' +CONF_CHANNELS = 'channels' +CONF_CHIPSET = 'chipset' +CONF_CLIENT_ID = 'client_id' +CONF_CLK_PIN = 'clk_pin' +CONF_CLOCK_PIN = 'clock_pin' +CONF_CLOSE_ACTION = 'close_action' +CONF_CLOSE_DURATION = 'close_duration' +CONF_CLOSE_ENDSTOP = 'close_endstop' +CONF_CO2 = 'co2' +CONF_CODE = 'code' +CONF_COLD_WHITE = 'cold_white' +CONF_COLD_WHITE_COLOR_TEMPERATURE = 'cold_white_color_temperature' +CONF_COLORS = 'colors' +CONF_COLOR_CORRECT = 'color_correct' +CONF_COLOR_TEMPERATURE = 'color_temperature' +CONF_COMMAND = 'command' +CONF_COMMAND_TOPIC = 'command_topic' +CONF_COMMIT = 'commit' +CONF_COMPONENTS = 'components' +CONF_COMPONENT_ID = 'component_id' +CONF_CONDITION = 'condition' +CONF_CONDITION_ID = 'condition_id' +CONF_CONDUCTIVITY = 'conductivity' +CONF_COOL_ACTION = 'cool_action' +CONF_COUNT_MODE = 'count_mode' +CONF_CRON = 'cron' +CONF_CSS_URL = 'css_url' +CONF_CS_PIN = 'cs_pin' +CONF_CURRENT = 'current' +CONF_CURRENT_OPERATION = 'current_operation' +CONF_CURRENT_RESISTOR = 'current_resistor' +CONF_DALLAS_ID = 'dallas_id' +CONF_DATA = 'data' +CONF_DATA_PIN = 'data_pin' +CONF_DATA_PINS = 'data_pins' +CONF_DATA_TEMPLATE = 'data_template' +CONF_DAYS_OF_MONTH = 'days_of_month' +CONF_DAYS_OF_WEEK = 'days_of_week' +CONF_DC_PIN = 'dc_pin' +CONF_DEBOUNCE = 'debounce' +CONF_DECELERATION = 'deceleration' +CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = 'default_target_temperature_high' +CONF_DEFAULT_TARGET_TEMPERATURE_LOW = 'default_target_temperature_low' +CONF_DEFAULT_TRANSITION_LENGTH = 'default_transition_length' +CONF_DELAY = 'delay' +CONF_DELTA = 'delta' +CONF_DEVICE = 'device' +CONF_DEVICE_CLASS = 'device_class' +CONF_DIMENSIONS = 'dimensions' +CONF_DIRECTION = 'direction' +CONF_DIR_PIN = 'dir_pin' +CONF_DISCOVERY = 'discovery' +CONF_DISCOVERY_PREFIX = 'discovery_prefix' +CONF_DISCOVERY_RETAIN = 'discovery_retain' +CONF_DNS1 = 'dns1' +CONF_DNS2 = 'dns2' +CONF_DOMAIN = 'domain' +CONF_DUMP = 'dump' +CONF_DURATION = 'duration' +CONF_ECHO_PIN = 'echo_pin' +CONF_EFFECT = 'effect' +CONF_EFFECTS = 'effects' +CONF_ELSE = 'else' +CONF_ENABLE_PIN = 'enable_pin' +CONF_ENABLE_TIME = 'enable_time' +CONF_ENTITY_ID = 'entity_id' +CONF_ESP8266_RESTORE_FROM_FLASH = 'esp8266_restore_from_flash' +CONF_ESPHOME = 'esphome' +CONF_ESPHOME_CORE_VERSION = 'esphome_core_version' +CONF_EXPIRE_AFTER = 'expire_after' +CONF_EXTERNAL_VCC = 'external_vcc' +CONF_FALLING_EDGE = 'falling_edge' +CONF_FAMILY = 'family' +CONF_FAST_CONNECT = 'fast_connect' +CONF_FILE = 'file' +CONF_FILTER = 'filter' +CONF_FILTERS = 'filters' +CONF_FILTER_OUT = 'filter_out' +CONF_FLASH_LENGTH = 'flash_length' +CONF_FOR = 'for' +CONF_FORMALDEHYDE = 'formaldehyde' +CONF_FORMAT = 'format' +CONF_FREQUENCY = 'frequency' +CONF_FROM = 'from' +CONF_FULL_UPDATE_EVERY = 'full_update_every' +CONF_GAIN = 'gain' +CONF_GAMMA_CORRECT = 'gamma_correct' +CONF_GAS_RESISTANCE = 'gas_resistance' +CONF_GATEWAY = 'gateway' +CONF_GLYPHS = 'glyphs' +CONF_GPIO = 'gpio' +CONF_GREEN = 'green' +CONF_GROUP = 'group' +CONF_HARDWARE_UART = 'hardware_uart' +CONF_HEARTBEAT = 'heartbeat' +CONF_HEATER = 'heater' +CONF_HEAT_ACTION = 'heat_action' +CONF_HIDDEN = 'hidden' +CONF_HIGH = 'high' +CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' +CONF_HOUR = 'hour' +CONF_HOURS = 'hours' +CONF_HUMIDITY = 'humidity' +CONF_I2C = 'i2c' +CONF_I2C_ID = 'i2c_id' +CONF_ICON = 'icon' +CONF_ID = 'id' +CONF_IDLE = 'idle' +CONF_IDLE_ACTION = 'idle_action' +CONF_IDLE_LEVEL = 'idle_level' +CONF_IF = 'if' +CONF_IIR_FILTER = 'iir_filter' +CONF_ILLUMINANCE = 'illuminance' +CONF_INCLUDES = 'includes' +CONF_INDEX = 'index' +CONF_INITIAL_VALUE = 'initial_value' +CONF_INTEGRATION_TIME = 'integration_time' +CONF_INTENSITY = 'intensity' +CONF_INTERLOCK = 'interlock' +CONF_INTERNAL = 'internal' +CONF_INTERNAL_FILTER = 'internal_filter' +CONF_INTERVAL = 'interval' +CONF_INVALID_COOLDOWN = 'invalid_cooldown' +CONF_INVERT = 'invert' +CONF_INVERTED = 'inverted' +CONF_IP_ADDRESS = 'ip_address' +CONF_JS_URL = 'js_url' +CONF_JVC = 'jvc' +CONF_KEEPALIVE = 'keepalive' +CONF_KEEP_ON_TIME = 'keep_on_time' +CONF_LAMBDA = 'lambda' +CONF_LEVEL = 'level' +CONF_LG = 'lg' +CONF_LIBRARIES = 'libraries' +CONF_LIGHT = 'light' +CONF_LOADED_INTEGRATIONS = 'loaded_integrations' +CONF_LOCAL = 'local' +CONF_LOGGER = 'logger' +CONF_LOGS = 'logs' +CONF_LOG_TOPIC = 'log_topic' +CONF_LOW = 'low' +CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' +CONF_MAC_ADDRESS = 'mac_address' +CONF_MAKE_ID = 'make_id' +CONF_MANUAL_IP = 'manual_ip' +CONF_MAX_CURRENT = 'max_current' +CONF_MAX_DURATION = 'max_duration' +CONF_MAX_LENGTH = 'max_length' +CONF_MAX_LEVEL = 'max_level' +CONF_MAX_POWER = 'max_power' +CONF_MAX_REFRESH_RATE = 'max_refresh_rate' +CONF_MAX_SPEED = 'max_speed' +CONF_MAX_TEMPERATURE = 'max_temperature' +CONF_MAX_VALUE = 'max_value' +CONF_MAX_VOLTAGE = 'max_voltage' +CONF_MEASUREMENT_DURATION = 'measurement_duration' +CONF_MEDIUM = 'medium' +CONF_METHOD = 'method' +CONF_MINUTE = 'minute' +CONF_MINUTES = 'minutes' +CONF_MIN_LENGTH = 'min_length' +CONF_MIN_LEVEL = 'min_level' +CONF_MIN_POWER = 'min_power' +CONF_MIN_TEMPERATURE = 'min_temperature' +CONF_MIN_VALUE = 'min_value' +CONF_MISO_PIN = 'miso_pin' +CONF_MODE = 'mode' +CONF_MODEL = 'model' +CONF_MOISTURE = 'moisture' +CONF_MONTHS = 'months' +CONF_MOSI_PIN = 'mosi_pin' +CONF_MQTT = 'mqtt' +CONF_MQTT_ID = 'mqtt_id' +CONF_MULTIPLEXER = 'multiplexer' +CONF_MULTIPLY = 'multiply' +CONF_NAME = 'name' +CONF_NBITS = 'nbits' +CONF_NEC = 'nec' +CONF_NETWORKS = 'networks' +CONF_NUMBER = 'number' +CONF_NUM_ATTEMPTS = 'num_attempts' +CONF_NUM_CHANNELS = 'num_channels' +CONF_NUM_CHIPS = 'num_chips' +CONF_NUM_LEDS = 'num_leds' +CONF_OFFSET = 'offset' +CONF_ON = 'on' +CONF_ONE = 'one' +CONF_ON_BOOT = 'on_boot' +CONF_ON_CLICK = 'on_click' +CONF_ON_DOUBLE_CLICK = 'on_double_click' +CONF_ON_JSON_MESSAGE = 'on_json_message' +CONF_ON_LOOP = 'on_loop' +CONF_ON_MESSAGE = 'on_message' +CONF_ON_MULTI_CLICK = 'on_multi_click' +CONF_ON_PRESS = 'on_press' +CONF_ON_RAW_VALUE = 'on_raw_value' +CONF_ON_RELEASE = 'on_release' +CONF_ON_SHUTDOWN = 'on_shutdown' +CONF_ON_STATE = 'on_state' +CONF_ON_TAG = 'on_tag' +CONF_ON_TIME = 'on_time' +CONF_ON_TURN_OFF = 'on_turn_off' +CONF_ON_TURN_ON = 'on_turn_on' +CONF_ON_VALUE = 'on_value' +CONF_ON_VALUE_RANGE = 'on_value_range' +CONF_OPEN_ACTION = 'open_action' +CONF_OPEN_DURATION = 'open_duration' +CONF_OPEN_ENDSTOP = 'open_endstop' +CONF_OPTIMISTIC = 'optimistic' +CONF_OR = 'or' +CONF_OSCILLATING = 'oscillating' +CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic' +CONF_OSCILLATION_OUTPUT = 'oscillation_output' +CONF_OSCILLATION_STATE_TOPIC = 'oscillation_state_topic' +CONF_OTA = 'ota' +CONF_OUTPUT = 'output' +CONF_OUTPUTS = 'outputs' +CONF_OUTPUT_ID = 'output_id' +CONF_OVERSAMPLING = 'oversampling' +CONF_PAGES = 'pages' +CONF_PAGE_ID = 'page_id' +CONF_PANASONIC = 'panasonic' +CONF_PASSWORD = 'password' +CONF_PAYLOAD = 'payload' +CONF_PAYLOAD_AVAILABLE = 'payload_available' +CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' +CONF_PHASE_BALANCER = 'phase_balancer' +CONF_PIN = 'pin' +CONF_PINS = 'pins' +CONF_PIN_A = 'pin_a' +CONF_PIN_B = 'pin_b' +CONF_PIN_C = 'pin_c' +CONF_PIN_D = 'pin_d' +CONF_PLATFORM = 'platform' +CONF_PLATFORMIO_OPTIONS = 'platformio_options' +CONF_PM_10_0 = 'pm_10_0' +CONF_PM_1_0 = 'pm_1_0' +CONF_PM_2_5 = 'pm_2_5' +CONF_PORT = 'port' +CONF_POSITION = 'position' +CONF_POWER = 'power' +CONF_POWER_ON_VALUE = 'power_on_value' +CONF_POWER_SAVE_MODE = 'power_save_mode' +CONF_POWER_SUPPLY = 'power_supply' +CONF_PRESSURE = 'pressure' +CONF_PRIORITY = 'priority' +CONF_PROTOCOL = 'protocol' +CONF_PULL_MODE = 'pull_mode' +CONF_PULSE_LENGTH = 'pulse_length' +CONF_QOS = 'qos' +CONF_RANDOM = 'random' +CONF_RANGE = 'range' +CONF_RANGE_FROM = 'range_from' +CONF_RANGE_TO = 'range_to' +CONF_RATE = 'rate' +CONF_RAW = 'raw' +CONF_REBOOT_TIMEOUT = 'reboot_timeout' +CONF_RECEIVE_TIMEOUT = 'receive_timeout' +CONF_RED = 'red' +CONF_REPEAT = 'repeat' +CONF_REPOSITORY = 'repository' +CONF_RESET_PIN = 'reset_pin' +CONF_RESIZE = 'resize' +CONF_RESOLUTION = 'resolution' +CONF_RESTORE = 'restore' +CONF_RESTORE_MODE = 'restore_mode' +CONF_RESTORE_STATE = 'restore_state' +CONF_RESTORE_VALUE = 'restore_value' +CONF_RETAIN = 'retain' +CONF_RGBW = 'rgbw' +CONF_RGB_ORDER = 'rgb_order' +CONF_RISING_EDGE = 'rising_edge' +CONF_ROTATION = 'rotation' +CONF_RS_PIN = 'rs_pin' +CONF_RUN_CYCLES = 'run_cycles' +CONF_RUN_DURATION = 'run_duration' +CONF_RW_PIN = 'rw_pin' +CONF_RX_ONLY = 'rx_only' +CONF_RX_PIN = 'rx_pin' +CONF_SAFE_MODE = 'safe_mode' +CONF_SAMSUNG = 'samsung' +CONF_SCAN = 'scan' +CONF_SCAN_INTERVAL = 'scan_interval' +CONF_SCL = 'scl' +CONF_SCL_PIN = 'scl_pin' +CONF_SDA = 'sda' +CONF_SDO_PIN = 'sdo_pin' +CONF_SECOND = 'second' +CONF_SECONDS = 'seconds' +CONF_SEGMENTS = 'segments' +CONF_SEL_PIN = 'sel_pin' +CONF_SEND_EVERY = 'send_every' +CONF_SEND_FIRST_AT = 'send_first_at' +CONF_SENSOR = 'sensor' +CONF_SENSORS = 'sensors' +CONF_SENSOR_ID = 'sensor_id' +CONF_SERVERS = 'servers' +CONF_SERVICE = 'service' +CONF_SERVICES = 'services' +CONF_SETUP_MODE = 'setup_mode' +CONF_SETUP_PRIORITY = 'setup_priority' +CONF_SEQUENCE = 'sequence' +CONF_SHUNT_RESISTANCE = 'shunt_resistance' +CONF_SHUNT_VOLTAGE = 'shunt_voltage' +CONF_SHUTDOWN_MESSAGE = 'shutdown_message' +CONF_SIZE = 'size' +CONF_SLEEP_DURATION = 'sleep_duration' +CONF_SLEEP_PIN = 'sleep_pin' +CONF_SLEEP_WHEN_DONE = 'sleep_when_done' +CONF_SONY = 'sony' +CONF_SPEED = 'speed' +CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic' +CONF_SPEED_STATE_TOPIC = 'speed_state_topic' +CONF_SPI_ID = 'spi_id' +CONF_SSID = 'ssid' +CONF_SSL_FINGERPRINTS = 'ssl_fingerprints' +CONF_STATE = 'state' +CONF_STATE_TOPIC = 'state_topic' +CONF_STATIC_IP = 'static_ip' +CONF_STEP_MODE = 'step_mode' +CONF_STEP_PIN = 'step_pin' +CONF_STOP = 'stop' +CONF_STOP_ACTION = 'stop_action' +CONF_SUBNET = 'subnet' +CONF_SWITCHES = 'switches' +CONF_SYNC = 'sync' +CONF_TAG = 'tag' +CONF_TARGET = 'target' +CONF_TARGET_TEMPERATURE = 'target_temperature' +CONF_TARGET_TEMPERATURE_HIGH = 'target_temperature_high' +CONF_TARGET_TEMPERATURE_LOW = 'target_temperature_low' +CONF_TEMPERATURE = 'temperature' +CONF_TEMPERATURE_STEP = 'temperature_step' +CONF_TEXT_SENSORS = 'text_sensors' +CONF_THEN = 'then' +CONF_THRESHOLD = 'threshold' +CONF_THROTTLE = 'throttle' +CONF_TILT = 'tilt' +CONF_TIME = 'time' +CONF_TIMEOUT = 'timeout' +CONF_TIMES = 'times' +CONF_TIMEZONE = 'timezone' +CONF_TIME_ID = 'time_id' +CONF_TIMING = 'timing' +CONF_TO = 'to' +CONF_TOLERANCE = 'tolerance' +CONF_TOPIC = 'topic' +CONF_TOPIC_PREFIX = 'topic_prefix' +CONF_TRANSITION_LENGTH = 'transition_length' +CONF_TRIGGER_ID = 'trigger_id' +CONF_TRIGGER_PIN = 'trigger_pin' +CONF_TURN_OFF_ACTION = 'turn_off_action' +CONF_TURN_ON_ACTION = 'turn_on_action' +CONF_TX_BUFFER_SIZE = 'tx_buffer_size' +CONF_TX_PIN = 'tx_pin' +CONF_TYPE = 'type' +CONF_TYPE_ID = 'type_id' +CONF_UART_ID = 'uart_id' +CONF_UID = 'uid' +CONF_UNIQUE = 'unique' +CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' +CONF_UPDATE_INTERVAL = 'update_interval' +CONF_UPDATE_ON_BOOT = 'update_on_boot' +CONF_USERNAME = 'username' +CONF_USE_ADDRESS = 'use_address' +CONF_UUID = 'uuid' +CONF_VALUE = 'value' +CONF_VARIABLES = 'variables' +CONF_VARIANT = 'variant' +CONF_VISUAL = 'visual' +CONF_VOLTAGE = 'voltage' +CONF_VOLTAGE_ATTENUATION = 'voltage_attenuation' +CONF_VOLTAGE_DIVIDER = 'voltage_divider' +CONF_WAIT_TIME = 'wait_time' +CONF_WAIT_UNTIL = 'wait_until' +CONF_WAKEUP_PIN = 'wakeup_pin' +CONF_WARM_WHITE = 'warm_white' +CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature' +CONF_WHILE = 'while' +CONF_WHITE = 'white' +CONF_WIDTH = 'width' +CONF_WIFI = 'wifi' +CONF_WILL_MESSAGE = 'will_message' +CONF_WINDOW_SIZE = 'window_size' +CONF_ZERO = 'zero' + +ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical' +ICON_BATTERY = 'mdi:battery' +ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' +ICON_BRIGHTNESS_5 = 'mdi:brightness-5' +ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' +ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' +ICON_EMPTY = '' +ICON_FLASH = 'mdi:flash' +ICON_FLOWER = 'mdi:flower' +ICON_GAS_CYLINDER = 'mdi:gas-cylinder' +ICON_GAUGE = 'mdi:gauge' +ICON_LIGHTBULB = 'mdi:lightbulb' +ICON_MAGNET = 'mdi:magnet' +ICON_NEW_BOX = 'mdi:new-box' +ICON_PERCENT = 'mdi:percent' +ICON_PERIODIC_TABLE_CO2 = 'mdi:periodic-table-co2' +ICON_POWER = 'mdi:power' +ICON_PULSE = 'mdi:pulse' +ICON_RADIATOR = 'mdi:radiator' +ICON_RESTART = 'mdi:restart' +ICON_ROTATE_RIGHT = 'mdi:rotate-right' +ICON_SCALE = 'mdi:scale' +ICON_SCREEN_ROTATION = 'mdi:screen-rotation' +ICON_SIGNAL = 'mdi:signal' +ICON_WEATHER_SUNSET = 'mdi:weather-sunset' +ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' +ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' +ICON_THERMOMETER = 'mdi:thermometer' +ICON_TIMER = 'mdi:timer' +ICON_WATER_PERCENT = 'mdi:water-percent' +ICON_WIFI = 'mdi:wifi' + +UNIT_AMPERE = 'A' +UNIT_CELSIUS = u'°C' +UNIT_DECIBEL = 'dB' +UNIT_DEGREES = u'°' +UNIT_DEGREE_PER_SECOND = u'°/s' +UNIT_EMPTY = '' +UNIT_HECTOPASCAL = 'hPa' +UNIT_KELVIN = 'K' +UNIT_LUX = 'lx' +UNIT_METER = 'm' +UNIT_METER_PER_SECOND_SQUARED = u'm/s²' +UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' +UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' +UNIT_MICROTESLA = u'µT' +UNIT_OHM = u'Ω' +UNIT_PARTS_PER_MILLION = 'ppm' +UNIT_PARTS_PER_BILLION = 'ppb' +UNIT_PERCENT = '%' +UNIT_PULSES_PER_MINUTE = 'pulses/min' +UNIT_SECOND = 's' +UNIT_STEPS = 'steps' +UNIT_VOLT = 'V' +UNIT_WATT = 'W' + +DEVICE_CLASS_CONNECTIVITY = 'connectivity' +DEVICE_CLASS_MOVING = 'moving' diff --git a/esphome/core.py b/esphome/core.py index 0b89e82ec0..7aaf6b2c70 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -1,7 +1,8 @@ -import collections -from collections import OrderedDict +import functools +import heapq import inspect import logging + import math import os import re @@ -9,11 +10,11 @@ import re # pylint: disable=unused-import, wrong-import-order from typing import Any, Dict, List # noqa -from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_ESPHOME_CORE_VERSION, \ - CONF_LOCAL, CONF_USE_ADDRESS, CONF_WIFI, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, \ - CONF_REPOSITORY, CONF_BRANCH +from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI, \ + SOURCE_FILE_EXTENSIONS from esphome.helpers import ensure_unique_string, is_hassio -from esphome.py_compat import IS_PY2, integer_types +from esphome.py_compat import IS_PY2, integer_types, text_type, string_types +from esphome.util import OrderedDict _LOGGER = logging.getLogger(__name__) @@ -54,6 +55,7 @@ class MACAddress(object): def __str__(self): return ':'.join('{:02X}'.format(part) for part in self.parts) + @property def as_hex(self): from esphome.cpp_generator import RawExpression @@ -135,18 +137,18 @@ class TimePeriod(object): def __str__(self): if self.microseconds is not None: - return '{} us'.format(self.total_microseconds) + return '{}us'.format(self.total_microseconds) if self.milliseconds is not None: - return '{} ms'.format(self.total_milliseconds) + return '{}ms'.format(self.total_milliseconds) if self.seconds is not None: - return '{} s'.format(self.total_seconds) + return '{}s'.format(self.total_seconds) if self.minutes is not None: - return '{} min'.format(self.total_minutes) + return '{}min'.format(self.total_minutes) if self.hours is not None: - return '{} h'.format(self.total_hours) + return '{}h'.format(self.total_hours) if self.days is not None: - return '{} d'.format(self.total_days) - return '0' + return '{}d'.format(self.total_days) + return '0s' @property def total_microseconds(self): @@ -224,7 +226,11 @@ LAMBDA_PROG = re.compile(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)') class Lambda(object): def __init__(self, value): - self._value = value + # pylint: disable=protected-access + if isinstance(value, Lambda): + self._value = value._value + else: + self._value = value self._parts = None self._requires_ids = None @@ -258,11 +264,14 @@ class Lambda(object): class ID(object): - def __init__(self, id, is_declaration=False, type=None): + def __init__(self, id, is_declaration=False, type=None, is_manual=None): self.id = id - self.is_manual = id is not None + if is_manual is None: + self.is_manual = id is not None + else: + self.is_manual = is_manual self.is_declaration = is_declaration - self.type = type + self.type = type # type: Optional[MockObjClass] def resolve(self, registered_ids): from esphome.config_validation import RESERVED_IDS @@ -285,18 +294,180 @@ class ID(object): def __eq__(self, other): if not isinstance(other, ID): - raise ValueError("other must be ID") + raise ValueError("other must be ID {} {}".format(type(other), other)) return self.id == other.id def __hash__(self): return hash(self.id) + def copy(self): + return ID(self.id, is_declaration=self.is_declaration, type=self.type, + is_manual=self.is_manual) + + +class DocumentLocation(object): + def __init__(self, document, line, column): + # type: (basestring, int, int) -> None + self.document = document # type: basestring + self.line = line # type: int + self.column = column # type: int + + @classmethod + def from_mark(cls, mark): + return cls( + mark.name, + mark.line, + mark.column + ) + + def __str__(self): + return u'{} {}:{}'.format(self.document, self.line, self.column) + + +class DocumentRange(object): + def __init__(self, start_mark, end_mark): + # type: (DocumentLocation, DocumentLocation) -> None + self.start_mark = start_mark # type: DocumentLocation + self.end_mark = end_mark # type: DocumentLocation + + @classmethod + def from_marks(cls, start_mark, end_mark): + return cls( + DocumentLocation.from_mark(start_mark), + DocumentLocation.from_mark(end_mark) + ) + + def __str__(self): + return u'[{} - {}]'.format(self.start_mark, self.end_mark) + + +class Define(object): + def __init__(self, name, value=None): + self.name = name + self.value = value + + @property + def as_build_flag(self): + if self.value is None: + return u'-D{}'.format(self.name) + return u'-D{}={}'.format(self.name, self.value) + + @property + def as_macro(self): + if self.value is None: + return u'#define {}'.format(self.name) + return u'#define {} {}'.format(self.name, self.value) + + @property + def as_tuple(self): + return self.name, self.value + + def __hash__(self): + return hash(self.as_tuple) + + def __eq__(self, other): + return isinstance(self, type(other)) and self.as_tuple == other.as_tuple + + +class Library(object): + def __init__(self, name, version): + self.name = name + self.version = version + + @property + def as_lib_dep(self): + if self.version is None: + return self.name + return u'{}@{}'.format(self.name, self.version) + + @property + def as_tuple(self): + return self.name, self.version + + def __hash__(self): + return hash(self.as_tuple) + + def __eq__(self, other): + return isinstance(self, type(other)) and self.as_tuple == other.as_tuple + + +def coroutine(func): + return coroutine_with_priority(0.0)(func) + + +def coroutine_with_priority(priority): + def decorator(func): + if getattr(func, '_esphome_coroutine', False): + # If func is already a coroutine, do not re-wrap it (performance) + return func + + @functools.wraps(func) + def _wrapper_generator(*args, **kwargs): + instance_id = kwargs.pop('__esphome_coroutine_instance__') + if not inspect.isgeneratorfunction(func): + # If func is not a generator, return result immediately + yield func(*args, **kwargs) + # pylint: disable=protected-access + CORE._remove_coroutine(instance_id) + return + gen = func(*args, **kwargs) + var = None + try: + while True: + var = gen.send(var) + if inspect.isgenerator(var): + # Yielded generator, equivalent to 'yield from' + x = None + for x in var: + yield None + # Last yield value is the result + var = x + else: + yield var + except StopIteration: + # Stopping iteration + yield var + # pylint: disable=protected-access + CORE._remove_coroutine(instance_id) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + import random + instance_id = random.randint(0, 2**32) + kwargs['__esphome_coroutine_instance__'] = instance_id + gen = _wrapper_generator(*args, **kwargs) + # pylint: disable=protected-access + CORE._add_active_coroutine(instance_id, gen) + return gen + + # pylint: disable=protected-access + wrapper._esphome_coroutine = True + wrapper.priority = priority + return wrapper + return decorator + + +def find_source_files(file): + files = set() + directory = os.path.abspath(os.path.dirname(file)) + for f in os.listdir(directory): + if not os.path.isfile(os.path.join(directory, f)): + continue + _, ext = os.path.splitext(f) + if ext.lower() not in SOURCE_FILE_EXTENSIONS: + continue + files.add(f) + return files + # pylint: disable=too-many-instance-attributes,too-many-public-methods class EsphomeCore(object): def __init__(self): # True if command is run from dashboard self.dashboard = False + # True if command is run from vscode api + self.vscode = False + self.ace = False # The name of the node self.name = None # type: str # The relative path to the configuration YAML @@ -312,11 +483,51 @@ class EsphomeCore(object): # The validated configuration, this is None until the config has been validated self.config = {} # type: ConfigType # The pending tasks in the task queue (mostly for C++ generation) - self.pending_tasks = collections.deque() + # This is a priority queue (with heapq) + # Each item is a tuple of form: (-priority, unique number, task) + self.pending_tasks = [] + # Task counter for pending tasks + self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj self.variables = {} # type: Dict[str, MockObj] - # The list of expressions for the C++ generation - self.expressions = [] # type: List[Expression] + # A list of statements that go in the main setup() block + self.main_statements = [] # type: List[Statement] + # A list of statements to insert in the global block (includes and global variables) + self.global_statements = [] # type: List[Statement] + # A set of platformio libraries to add to the project + self.libraries = set() # type: Set[Library] + # A set of build flags to set in the platformio project + self.build_flags = set() # type: Set[str] + # A set of defines to set for the compile process in esphome/core/defines.h + self.defines = set() # type: Set[Define] + # A dictionary of started coroutines, used to warn when a coroutine was not + # awaited. + self.active_coroutines = {} # type: Dict[int, Any] + # A set of strings of names of loaded integrations, used to find namespace ID conflicts + self.loaded_integrations = set() + # A set of component IDs to track what Component subclasses are declared + self.component_ids = set() + + def reset(self): + self.dashboard = False + self.name = None + self.config_path = None + self.build_path = None + self.esp_platform = None + self.board = None + self.raw_config = None + self.config = None + self.pending_tasks = [] + self.task_counter = 0 + self.variables = {} + self.main_statements = [] + self.global_statements = [] + self.libraries = set() + self.build_flags = set() + self.defines = set() + self.active_coroutines = {} + self.loaded_integrations = set() + self.component_ids = set() @property def address(self): # type: () -> str @@ -328,19 +539,11 @@ class EsphomeCore(object): return None - @property - def esphome_core_version(self): # type: () -> Dict[str, str] - return self.config[CONF_ESPHOME][CONF_ESPHOME_CORE_VERSION] + def _add_active_coroutine(self, instance_id, obj): + self.active_coroutines[instance_id] = obj - @property - def is_dev_esphome_core_version(self): - if CONF_REPOSITORY not in self.esphome_core_version: - return False - return self.esphome_core_version.get(CONF_BRANCH) == 'dev' - - @property - def is_local_esphome_core_copy(self): - return CONF_LOCAL in self.esphome_core_version + def _remove_coroutine(self, instance_id): + self.active_coroutines.pop(instance_id) @property def arduino_version(self): # type: () -> str @@ -354,7 +557,7 @@ class EsphomeCore(object): def config_filename(self): return os.path.basename(self.config_path) - def relative_path(self, *path): + def relative_config_path(self, *path): path_ = os.path.expanduser(os.path.join(*path)) return os.path.join(self.config_dir, path_) @@ -362,6 +565,9 @@ class EsphomeCore(object): path_ = os.path.expanduser(os.path.join(*path)) return os.path.join(self.build_path, path_) + def relative_src_path(self, *path): + return self.relative_build_path('src', *path) + def relative_pioenvs_path(self, *path): if is_hassio(): return os.path.join('/data', self.name, '.pioenvs', *path) @@ -380,28 +586,21 @@ class EsphomeCore(object): def is_esp8266(self): if self.esp_platform is None: raise ValueError - return self.esp_platform == ESP_PLATFORM_ESP8266 + return self.esp_platform == 'ESP8266' @property def is_esp32(self): if self.esp_platform is None: raise ValueError - return self.esp_platform == ESP_PLATFORM_ESP32 + return self.esp_platform == 'ESP32' def add_job(self, func, *args, **kwargs): - domain = kwargs.get('domain') - if inspect.isgeneratorfunction(func): - def func_(): - yield - for _ in func(*args): - yield - else: - def func_(): - yield - func(*args) - gen = func_() - self.pending_tasks.append((gen, domain)) - return gen + coro = coroutine(func) + task = coro(*args, **kwargs) + item = (-coro.priority, self.task_counter, task) + self.task_counter += 1 + heapq.heappush(self.pending_tasks, item) + return task def flush_tasks(self): i = 0 @@ -410,29 +609,92 @@ class EsphomeCore(object): if i > 1000000: raise EsphomeError("Circular dependency detected!") - task, domain = self.pending_tasks.popleft() - _LOGGER.debug("Executing task for domain=%s", domain) + inv_priority, num, task = heapq.heappop(self.pending_tasks) + priority = -inv_priority + _LOGGER.debug("Running %s (num %s)", task, num) try: next(task) - self.pending_tasks.append((task, domain)) + # Decrease priority over time, so that if this task is blocked + # due to a dependency others will clear the dependency + # This could be improved with a less naive approach + priority -= 1 + item = (-priority, num, task) + heapq.heappush(self.pending_tasks, item) except StopIteration: - _LOGGER.debug(" -> %s finished", domain) + _LOGGER.debug(" -> finished") - def add(self, expression, require=True): - from esphome.cpp_generator import Expression + # Print not-awaited coroutines + for obj in self.active_coroutines.values(): + _LOGGER.warning(u"Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj) + _LOGGER.warning(u"Please file a bug report with your configuration.") + if self.active_coroutines: + raise EsphomeError() + if self.component_ids: + comps = u', '.join(u"'{}'".format(x) for x in self.component_ids) + _LOGGER.warning(u"Components %s were never registered. Please create a bug report", + comps) + _LOGGER.warning(u"with your configuration.") + raise EsphomeError() + self.active_coroutines.clear() - if require and isinstance(expression, Expression): - expression.require() - self.expressions.append(expression) + def add(self, expression): + from esphome.cpp_generator import Expression, Statement, statement + + if isinstance(expression, Expression): + expression = statement(expression) + if not isinstance(expression, Statement): + raise ValueError(u"Add '{}' must be expression or statement, not {}" + u"".format(expression, type(expression))) + + self.main_statements.append(expression) _LOGGER.debug("Adding: %s", expression) return expression + def add_global(self, expression): + from esphome.cpp_generator import Expression, Statement, statement + + if isinstance(expression, Expression): + expression = statement(expression) + if not isinstance(expression, Statement): + raise ValueError(u"Add '{}' must be expression or statement, not {}" + u"".format(expression, type(expression))) + self.global_statements.append(expression) + _LOGGER.debug("Adding global: %s", expression) + return expression + + def add_library(self, library): + if not isinstance(library, Library): + raise ValueError(u"Library {} must be instance of Library, not {}" + u"".format(library, type(library))) + self.libraries.add(library) + _LOGGER.debug("Adding library: %s", library) + return library + + def add_build_flag(self, build_flag): + self.build_flags.add(build_flag) + _LOGGER.debug("Adding build flag: %s", build_flag) + return build_flag + + def add_define(self, define): + if isinstance(define, string_types): + define = Define(define) + elif isinstance(define, Define): + pass + else: + raise ValueError(u"Define {} must be string or Define, not {}" + u"".format(define, type(define))) + self.defines.add(define) + _LOGGER.debug("Adding define: %s", define) + return define + def get_variable(self, id): + if not isinstance(id, ID): + raise ValueError("ID {!r} must be of type ID!".format(id)) while True: if id in self.variables: yield self.variables[id] return - _LOGGER.debug("Waiting for variable %s", id) + _LOGGER.debug("Waiting for variable %s (%r)", id, id) yield None def get_variable_with_full_id(self, id): @@ -454,6 +716,43 @@ class EsphomeCore(object): def has_id(self, id): return id in self.variables + @property + def cpp_main_section(self): + from esphome.cpp_generator import statement + + main_code = [] + for exp in self.main_statements: + text = text_type(statement(exp)) + text = text.rstrip() + main_code.append(text) + return u'\n'.join(main_code) + u'\n\n' + + @property + def cpp_global_section(self): + from esphome.cpp_generator import statement + + global_code = [] + for exp in self.global_statements: + text = text_type(statement(exp)) + text = text.rstrip() + global_code.append(text) + return u'\n'.join(global_code) + u'\n' + + +class AutoLoad(OrderedDict): + pass + + +class EnumValue(object): + """Special type used by ESPHome to mark enum values for cv.enum.""" + @property + def enum_value(self): + return getattr(self, '_enum_value', None) + + @enum_value.setter + def enum_value(self, value): + setattr(self, '_enum_value', value) + CORE = EsphomeCore() diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp new file mode 100644 index 0000000000..6bebd3b927 --- /dev/null +++ b/esphome/core/application.cpp @@ -0,0 +1,149 @@ +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +#ifdef USE_STATUS_LED +#include "esphome/components/status_led/status_led.h" +#endif + +namespace esphome { + +static const char *TAG = "app"; + +void Application::register_component_(Component *comp) { + if (comp == nullptr) { + ESP_LOGW(TAG, "Tried to register null component!"); + return; + } + + for (auto *c : this->components_) { + if (comp == c) { + ESP_LOGW(TAG, "Component already registered! (%p)", c); + return; + } + } + this->components_.push_back(comp); +} +void Application::setup() { + ESP_LOGI(TAG, "Running through setup()..."); + ESP_LOGV(TAG, "Sorting components by setup priority..."); + std::stable_sort(this->components_.begin(), this->components_.end(), [](const Component *a, const Component *b) { + return a->get_actual_setup_priority() > b->get_actual_setup_priority(); + }); + + for (uint32_t i = 0; i < this->components_.size(); i++) { + Component *component = this->components_[i]; + if (component->is_failed()) + continue; + + component->call_setup(); + if (component->can_proceed()) + continue; + + std::stable_sort(this->components_.begin(), this->components_.begin() + i + 1, + [](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); }); + + do { + uint32_t new_app_state = STATUS_LED_WARNING; + for (uint32_t j = 0; j <= i; j++) { + if (!this->components_[j]->is_failed()) { + this->components_[j]->call_loop(); + } + new_app_state |= this->components_[j]->get_component_state(); + this->app_state_ |= new_app_state; + } + this->app_state_ = new_app_state; + yield(); + } while (!component->can_proceed()); + } + + ESP_LOGI(TAG, "setup() finished successfully!"); + this->dump_config(); +} +void Application::dump_config() { + ESP_LOGI(TAG, "esphome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_.c_str()); + + for (auto component : this->components_) { + component->dump_config(); + } +} +void Application::loop() { + uint32_t new_app_state = 0; + const uint32_t start = millis(); + for (Component *component : this->components_) { + if (!component->is_failed()) { + component->call_loop(); + } + new_app_state |= component->get_component_state(); + this->app_state_ |= new_app_state; + this->feed_wdt(); + } + this->app_state_ = new_app_state; + const uint32_t end = millis(); + if (end - start > 200) { + ESP_LOGV(TAG, "A component took a long time in a loop() cycle (%.1f s).", (end - start) / 1e3f); + ESP_LOGV(TAG, "Components should block for at most 20-30ms in loop()."); + ESP_LOGV(TAG, "This will become a warning soon."); + } + + const uint32_t now = millis(); + + if (HighFrequencyLoopRequester::is_high_frequency()) { + yield(); + } else { + uint32_t delay_time = this->loop_interval_; + if (now - this->last_loop_ < this->loop_interval_) + delay_time = this->loop_interval_ - (now - this->last_loop_); + delay(delay_time); + } + this->last_loop_ = now; + + if (this->dump_config_scheduled_) { + this->dump_config(); + this->dump_config_scheduled_ = false; + } +} + +void ICACHE_RAM_ATTR HOT Application::feed_wdt() { + static uint32_t LAST_FEED = 0; + uint32_t now = millis(); + if (now - LAST_FEED > 3) { +#ifdef ARDUINO_ARCH_ESP8266 + ESP.wdtFeed(); +#endif +#ifdef ARDUINO_ARCH_ESP32 + yield(); +#endif + LAST_FEED = now; +#ifdef USE_STATUS_LED + if (status_led::global_status_led != nullptr) { + status_led::global_status_led->call_loop(); + } +#endif + } +} +void Application::reboot() { + ESP_LOGI(TAG, "Forcing a reboot..."); + for (auto *comp : this->components_) + comp->on_shutdown(); + ESP.restart(); + // restart() doesn't always end execution + while (true) { + yield(); + } +} +void Application::safe_reboot() { + ESP_LOGI(TAG, "Rebooting safely..."); + for (auto *comp : this->components_) + comp->on_safe_shutdown(); + for (auto *comp : this->components_) + comp->on_shutdown(); + ESP.restart(); + // restart() doesn't always end execution + while (true) { + yield(); + } +} + +Application App; + +} // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h new file mode 100644 index 0000000000..2ee8404a9f --- /dev/null +++ b/esphome/core/application.h @@ -0,0 +1,244 @@ +#pragma once + +#include +#include +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_FAN +#include "esphome/components/fan/fan_state.h" +#endif +#ifdef USE_CLIMATE +#include "esphome/components/climate/climate.h" +#endif +#ifdef USE_LIGHT +#include "esphome/components/light/light_state.h" +#endif +#ifdef USE_COVER +#include "esphome/components/cover/cover.h" +#endif + +namespace esphome { + +class Application { + public: + void pre_setup(const std::string &name, const char *compilation_time) { + this->name_ = name; + this->compilation_time_ = compilation_time; + global_preferences.begin(this->name_); + } + +#ifdef USE_BINARY_SENSOR + void register_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { + this->binary_sensors_.push_back(binary_sensor); + } +#endif + +#ifdef USE_SENSOR + void register_sensor(sensor::Sensor *sensor) { this->sensors_.push_back(sensor); } +#endif + +#ifdef USE_SWITCH + void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); } +#endif + +#ifdef USE_TEXT_SENSOR + void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); } +#endif + +#ifdef USE_FAN + void register_fan(fan::FanState *state) { this->fans_.push_back(state); } +#endif + +#ifdef USE_COVER + void register_cover(cover::Cover *cover) { this->covers_.push_back(cover); } +#endif + +#ifdef USE_CLIMATE + void register_climate(climate::Climate *climate) { this->climates_.push_back(climate); } +#endif + +#ifdef USE_LIGHT + void register_light(light::LightState *light) { this->lights_.push_back(light); } +#endif + + /// Register the component in this Application instance. + template C *register_component(C *c) { + static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); + this->register_component_((Component *) c); + return c; + } + + /// Set up all the registered components. Call this at the end of your setup() function. + void setup(); + + /// Make a loop iteration. Call this in your loop() function. + void loop(); + + /// Get the name of this Application set by set_name(). + const std::string &get_name() const { return this->name_; } + + const std::string &get_compilation_time() const { return this->compilation_time_; } + + /** Set the target interval with which to run the loop() calls. + * If the loop() method takes longer than the target interval, ESPHome won't + * sleep in loop(), but if the time spent in loop() is small than the target, ESPHome + * will delay at the end of the App.loop() method. + * + * This is done to conserve power: In most use-cases, high-speed loop() calls are not required + * and degrade power consumption. + * + * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester + * helper in helpers.h + * + * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. + */ + void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; } + + void dump_config(); + void schedule_dump_config() { this->dump_config_scheduled_ = true; } + + void feed_wdt(); + + void reboot(); + + void safe_reboot(); + + void run_safe_shutdown_hooks() { + for (auto *comp : this->components_) + comp->on_safe_shutdown(); + } + + uint32_t get_app_state() const { return this->app_state_; } + +#ifdef USE_BINARY_SENSOR + const std::vector &get_binary_sensors() { return this->binary_sensors_; } + binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->binary_sensors_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_SWITCH + const std::vector &get_switches() { return this->switches_; } + switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->switches_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_SENSOR + const std::vector &get_sensors() { return this->sensors_; } + sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->sensors_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_TEXT_SENSOR + const std::vector &get_text_sensors() { return this->text_sensors_; } + text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->text_sensors_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_FAN + const std::vector &get_fans() { return this->fans_; } + fan::FanState *get_fan_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->fans_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_COVER + const std::vector &get_covers() { return this->covers_; } + cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->covers_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_LIGHT + const std::vector &get_lights() { return this->lights_; } + light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->lights_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif +#ifdef USE_CLIMATE + const std::vector &get_climates() { return this->climates_; } + climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->climates_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif + + protected: + friend Component; + + void register_component_(Component *comp); + + std::vector components_{}; + +#ifdef USE_BINARY_SENSOR + std::vector binary_sensors_{}; +#endif +#ifdef USE_SWITCH + std::vector switches_{}; +#endif +#ifdef USE_SENSOR + std::vector sensors_{}; +#endif +#ifdef USE_TEXT_SENSOR + std::vector text_sensors_{}; +#endif +#ifdef USE_FAN + std::vector fans_{}; +#endif +#ifdef USE_COVER + std::vector covers_{}; +#endif +#ifdef USE_CLIMATE + std::vector climates_{}; +#endif +#ifdef USE_LIGHT + std::vector lights_{}; +#endif + + std::string name_; + std::string compilation_time_; + uint32_t last_loop_{0}; + uint32_t loop_interval_{16}; + bool dump_config_scheduled_{false}; + uint32_t app_state_{0}; +}; + +/// Global storage of Application pointer - only one Application can exist. +extern Application App; + +} // namespace esphome diff --git a/esphome/core/automation.h b/esphome/core/automation.h new file mode 100644 index 0000000000..ceed28e5b8 --- /dev/null +++ b/esphome/core/automation.h @@ -0,0 +1,166 @@ +#pragma once + +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" + +namespace esphome { + +#define TEMPLATABLE_VALUE_(type, name) \ + protected: \ + TemplatableValue name##_{}; \ +\ + public: \ + template void set_##name(V name) { this->name##_ = name; } + +#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) + +/** Base class for all automation conditions. + * + * @tparam Ts The template parameters to pass when executing. + */ +template class Condition { + public: + /// Check whether this condition passes. This condition check must be instant, and not cause any delays. + virtual bool check(Ts... x) = 0; + + /// Call check with a tuple of values as parameter. + bool check_tuple(const std::tuple &tuple) { + return this->check_tuple_(tuple, typename gens::type()); + } + + protected: + template bool check_tuple_(const std::tuple &tuple, seq) { + return this->check(std::get(tuple)...); + } +}; + +template class Automation; + +template class Trigger { + public: + void trigger(Ts... x) { + if (this->automation_parent_ == nullptr) + return; + this->automation_parent_->trigger(x...); + } + void set_automation_parent(Automation *automation_parent) { this->automation_parent_ = automation_parent; } + void stop() { + if (this->automation_parent_ == nullptr) + return; + this->automation_parent_->stop(); + } + bool is_running() { + if (this->automation_parent_ == nullptr) + return false; + return this->automation_parent_->is_running(); + } + + protected: + Automation *automation_parent_{nullptr}; +}; + +template class ActionList; + +template class Action { + public: + virtual void play(Ts... x) = 0; + virtual void play_complex(Ts... x) { + this->play(x...); + this->play_next(x...); + } + void play_next(Ts... x) { + if (this->next_ != nullptr) { + this->next_->play_complex(x...); + } + } + virtual void stop() {} + virtual void stop_complex() { + this->stop(); + this->stop_next(); + } + void stop_next() { + if (this->next_ != nullptr) { + this->next_->stop_complex(); + } + } + virtual bool is_running() { return this->is_running_next(); } + bool is_running_next() { + if (this->next_ == nullptr) + return false; + return this->next_->is_running(); + } + + void play_next_tuple(const std::tuple &tuple) { + this->play_next_tuple_(tuple, typename gens::type()); + } + + protected: + friend ActionList; + + template void play_next_tuple_(const std::tuple &tuple, seq) { + this->play_next(std::get(tuple)...); + } + + Action *next_ = nullptr; +}; + +template class ActionList { + public: + void add_action(Action *action) { + if (this->actions_end_ == nullptr) { + this->actions_begin_ = action; + } else { + this->actions_end_->next_ = action; + } + this->actions_end_ = action; + } + void add_actions(const std::vector *> &actions) { + for (auto *action : actions) { + this->add_action(action); + } + } + void play(Ts... x) { + if (this->actions_begin_ != nullptr) + this->actions_begin_->play_complex(x...); + } + void play_tuple(const std::tuple &tuple) { this->play_tuple_(tuple, typename gens::type()); } + void stop() { + if (this->actions_begin_ != nullptr) + this->actions_begin_->stop_complex(); + } + bool empty() const { return this->actions_begin_ == nullptr; } + bool is_running() { + if (this->actions_begin_ == nullptr) + return false; + return this->actions_begin_->is_running(); + } + + protected: + template void play_tuple_(const std::tuple &tuple, seq) { this->play(std::get(tuple)...); } + + Action *actions_begin_{nullptr}; + Action *actions_end_{nullptr}; +}; + +template class Automation { + public: + explicit Automation(Trigger *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); } + + Action *add_action(Action *action) { this->actions_.add_action(action); } + void add_actions(const std::vector *> &actions) { this->actions_.add_actions(actions); } + + void stop() { this->actions_.stop(); } + + void trigger(Ts... x) { this->actions_.play(x...); } + + bool is_running() { return this->actions_.is_running(); } + + protected: + Trigger *trigger_; + ActionList actions_; +}; + +} // namespace esphome diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h new file mode 100644 index 0000000000..ad50a3921f --- /dev/null +++ b/esphome/core/base_automation.h @@ -0,0 +1,288 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { + +template class AndCondition : public Condition { + public: + explicit AndCondition(const std::vector *> &conditions) : conditions_(conditions) {} + bool check(Ts... x) override { + for (auto *condition : this->conditions_) { + if (!condition->check(x...)) + return false; + } + + return true; + } + + protected: + std::vector *> conditions_; +}; + +template class OrCondition : public Condition { + public: + explicit OrCondition(const std::vector *> &conditions) : conditions_(conditions) {} + bool check(Ts... x) override { + for (auto *condition : this->conditions_) { + if (condition->check(x...)) + return true; + } + + return false; + } + + protected: + std::vector *> conditions_; +}; + +template class NotCondition : public Condition { + public: + explicit NotCondition(Condition *condition) : condition_(condition) {} + bool check(Ts... x) override { return !this->condition_->check(x...); } + + protected: + Condition *condition_; +}; + +template class LambdaCondition : public Condition { + public: + explicit LambdaCondition(std::function &&f) : f_(std::move(f)) {} + bool check(Ts... x) override { return this->f_(x...); } + + protected: + std::function f_; +}; + +template class ForCondition : public Condition, public Component { + public: + explicit ForCondition(Condition<> *condition) : condition_(condition) {} + + TEMPLATABLE_VALUE(uint32_t, time); + + void loop() override { this->check_internal(); } + float get_setup_priority() const override { return setup_priority::DATA; } + bool check_internal() { + bool cond = this->condition_->check(); + if (!cond) + this->last_inactive_ = millis(); + return cond; + } + + bool check(Ts... x) override { + if (!this->check_internal()) + return false; + return millis() - this->last_inactive_ < this->time_.value(x...); + } + + protected: + Condition<> *condition_; + uint32_t last_inactive_{0}; +}; + +class StartupTrigger : public Trigger<>, public Component { + public: + explicit StartupTrigger(float setup_priority) : setup_priority_(setup_priority) {} + void setup() override { this->trigger(); } + float get_setup_priority() const override { return this->setup_priority_; } + + protected: + float setup_priority_; +}; + +class ShutdownTrigger : public Trigger<>, public Component { + public: + void on_shutdown() override { this->trigger(); } +}; + +class LoopTrigger : public Trigger<>, public Component { + public: + void loop() override { this->trigger(); } + float get_setup_priority() const override { return setup_priority::DATA; } +}; + +template class DelayAction : public Action, public Component { + public: + explicit DelayAction() = default; + + TEMPLATABLE_VALUE(uint32_t, delay) + + void stop() override { + this->cancel_timeout(""); + this->num_running_ = 0; + } + + void play(Ts... x) override { /* ignore - see play_complex */ + } + + void play_complex(Ts... x) override { + auto f = std::bind(&DelayAction::delay_end_, this, x...); + this->num_running_++; + this->set_timeout(this->delay_.value(x...), f); + } + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + bool is_running() override { return this->num_running_ > 0 || this->is_running_next(); } + + protected: + void delay_end_(Ts... x) { + this->num_running_--; + this->play_next(x...); + } + int num_running_{0}; +}; + +template class LambdaAction : public Action { + public: + explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} + void play(Ts... x) override { this->f_(x...); } + + protected: + std::function f_; +}; + +template class IfAction : public Action { + public: + explicit IfAction(Condition *condition) : condition_(condition) {} + + void add_then(const std::vector *> &actions) { + this->then_.add_actions(actions); + this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); + } + + void add_else(const std::vector *> &actions) { + this->else_.add_actions(actions); + this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); + } + + void play(Ts... x) override { /* ignore - see play_complex */ + } + + void play_complex(Ts... x) override { + bool res = this->condition_->check(x...); + if (res) { + if (this->then_.empty()) { + this->play_next(x...); + } else { + this->then_.play(x...); + } + } else { + if (this->else_.empty()) { + this->play_next(x...); + } else { + this->else_.play(x...); + } + } + } + + void stop() override { + this->then_.stop(); + this->else_.stop(); + } + + bool is_running() override { return this->then_.is_running() || this->else_.is_running() || this->is_running_next(); } + + protected: + Condition *condition_; + ActionList then_; + ActionList else_; +}; + +template class WhileAction : public Action { + public: + WhileAction(Condition *condition) : condition_(condition) {} + + void add_then(const std::vector *> &actions) { + this->then_.add_actions(actions); + this->then_.add_action(new LambdaAction([this](Ts... x) { + if (this->condition_->check_tuple(this->var_)) { + // play again + this->then_.play_tuple(this->var_); + } else { + // condition false, play next + this->play_next_tuple(this->var_); + } + })); + } + + void play(Ts... x) override { /* ignore - see play_complex */ + } + + void play_complex(Ts... x) override { + // Store loop parameters + this->var_ = std::make_tuple(x...); + // Initial condition check + if (!this->condition_->check_tuple(this->var_)) { + // If new condition check failed, stop loop if running + this->then_.stop(); + this->play_next_tuple(this->var_); + return; + } + + this->then_.play_tuple(this->var_); + } + + void stop() override { this->then_.stop(); } + + bool is_running() override { return this->then_.is_running() || this->is_running_next(); } + + protected: + Condition *condition_; + ActionList then_; + std::tuple var_{}; +}; + +template class WaitUntilAction : public Action, public Component { + public: + WaitUntilAction(Condition *condition) : condition_(condition) {} + + void play(Ts... x) { /* ignore - see play_complex */ + } + + void play_complex(Ts... x) override { + // Check if we can continue immediately. + if (this->condition_->check(x...)) { + this->triggered_ = false; + this->play_next(x...); + return; + } + this->var_ = std::make_tuple(x...); + this->triggered_ = true; + this->loop(); + } + + void stop() override { this->triggered_ = false; } + + void loop() override { + if (!this->triggered_) + return; + + if (!this->condition_->check_tuple(this->var_)) { + return; + } + + this->triggered_ = false; + this->play_next_tuple(this->var_); + } + + float get_setup_priority() const override { return setup_priority::DATA; } + + bool is_running() override { return this->triggered_ || this->is_running_next(); } + + protected: + Condition *condition_; + bool triggered_{false}; + std::tuple var_{}; +}; + +template class UpdateComponentAction : public Action { + public: + UpdateComponentAction(PollingComponent *component) : component_(component) {} + void play(Ts... x) override { this->component_->update(); } + + protected: + PollingComponent *component_; +}; + +} // namespace esphome diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp new file mode 100644 index 0000000000..fbd7439d70 --- /dev/null +++ b/esphome/core/component.cpp @@ -0,0 +1,251 @@ +#include + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/esphal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { + +static const char *TAG = "component"; + +namespace setup_priority { + +const float BUS = 1000.0f; +const float IO = 900.0f; +const float HARDWARE = 800.0f; +const float DATA = 600.0f; +const float PROCESSOR = 400.0; +const float WIFI = 250.0f; +const float AFTER_WIFI = 200.0f; +const float AFTER_CONNECTION = 100.0f; +const float LATE = -100.0f; + +} // namespace setup_priority + +const uint32_t COMPONENT_STATE_MASK = 0xFF; +const uint32_t COMPONENT_STATE_CONSTRUCTION = 0x00; +const uint32_t COMPONENT_STATE_SETUP = 0x01; +const uint32_t COMPONENT_STATE_LOOP = 0x02; +const uint32_t COMPONENT_STATE_FAILED = 0x03; +const uint32_t STATUS_LED_MASK = 0xFF00; +const uint32_t STATUS_LED_OK = 0x0000; +const uint32_t STATUS_LED_WARNING = 0x0100; +const uint32_t STATUS_LED_ERROR = 0x0200; + +uint32_t global_state = 0; + +float Component::get_loop_priority() const { return 0.0f; } + +float Component::get_setup_priority() const { return setup_priority::DATA; } + +void Component::setup() {} + +void Component::loop() {} + +void Component::set_interval(const std::string &name, uint32_t interval, std::function &&f) { // NOLINT + const uint32_t now = millis(); + // only put offset in lower half + uint32_t offset = 0; + if (interval != 0) + offset = (random_uint32() % interval) / 2; + ESP_LOGVV(TAG, "set_interval(name='%s', interval=%u, offset=%u)", name.c_str(), interval, offset); + + if (!name.empty()) { + this->cancel_interval(name); + } + struct TimeFunction function = { + .name = name, + .type = TimeFunction::INTERVAL, + .interval = interval, + .last_execution = now - interval - offset, + .f = std::move(f), + .remove = false, + }; + this->time_functions_.push_back(function); +} + +bool Component::cancel_interval(const std::string &name) { // NOLINT + return this->cancel_time_function_(name, TimeFunction::INTERVAL); +} + +void Component::set_timeout(const std::string &name, uint32_t timeout, std::function &&f) { // NOLINT + const uint32_t now = millis(); + ESP_LOGVV(TAG, "set_timeout(name='%s', timeout=%u)", name.c_str(), timeout); + + if (!name.empty()) { + this->cancel_timeout(name); + } + struct TimeFunction function = { + .name = name, + .type = TimeFunction::TIMEOUT, + .interval = timeout, + .last_execution = now, + .f = std::move(f), + .remove = false, + }; + this->time_functions_.push_back(function); +} + +bool Component::cancel_timeout(const std::string &name) { // NOLINT + return this->cancel_time_function_(name, TimeFunction::TIMEOUT); +} + +void Component::call_loop() { + this->loop_internal_(); + this->loop(); +} + +bool Component::cancel_time_function_(const std::string &name, TimeFunction::Type type) { + // NOLINTNEXTLINE + for (auto iter = this->time_functions_.begin(); iter != this->time_functions_.end(); iter++) { + if (!iter->remove && iter->name == name && iter->type == type) { + ESP_LOGVV(TAG, "Removing old time function %s.", iter->name.c_str()); + iter->remove = true; + return true; + } + } + return false; +} +void Component::call_setup() { + this->setup_internal_(); + this->setup(); +} +uint32_t Component::get_component_state() const { return this->component_state_; } +void Component::loop_internal_() { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_LOOP; + + for (unsigned int i = 0; i < this->time_functions_.size(); i++) { // NOLINT + const uint32_t now = millis(); + TimeFunction *tf = &this->time_functions_[i]; + if (tf->should_run(now)) { +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + const char *type = + tf->type == TimeFunction::INTERVAL ? "interval" : (tf->type == TimeFunction::TIMEOUT ? "timeout" : "defer"); + ESP_LOGVV(TAG, "Running %s '%s':%u with interval=%u last_execution=%u (now=%u)", type, tf->name.c_str(), i, + tf->interval, tf->last_execution, now); +#endif + + tf->f(); + // The vector might have reallocated due to new items + tf = &this->time_functions_[i]; + + if (tf->type == TimeFunction::INTERVAL && tf->interval != 0) { + const uint32_t amount = (now - tf->last_execution) / tf->interval; + tf->last_execution += (amount * tf->interval); + } else if (tf->type == TimeFunction::DEFER || tf->type == TimeFunction::TIMEOUT) { + tf->remove = true; + } + } + } + + this->time_functions_.erase(std::remove_if(this->time_functions_.begin(), this->time_functions_.end(), + [](const TimeFunction &tf) -> bool { return tf.remove; }), + this->time_functions_.end()); +} +void Component::setup_internal_() { + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_SETUP; +} +void Component::mark_failed() { + ESP_LOGE(TAG, "Component was marked as failed."); + this->component_state_ &= ~COMPONENT_STATE_MASK; + this->component_state_ |= COMPONENT_STATE_FAILED; + this->status_set_error(); +} +void Component::defer(std::function &&f) { this->defer("", std::move(f)); } // NOLINT +bool Component::cancel_defer(const std::string &name) { // NOLINT + return this->cancel_time_function_(name, TimeFunction::DEFER); +} +void Component::defer(const std::string &name, std::function &&f) { // NOLINT + if (!name.empty()) { + this->cancel_defer(name); + } + struct TimeFunction function = { + .name = name, + .type = TimeFunction::DEFER, + .interval = 0, + .last_execution = 0, + .f = std::move(f), + .remove = false, + }; + this->time_functions_.push_back(function); +} +void Component::set_timeout(uint32_t timeout, std::function &&f) { // NOLINT + this->set_timeout("", timeout, std::move(f)); +} +void Component::set_interval(uint32_t interval, std::function &&f) { // NOLINT + this->set_interval("", interval, std::move(f)); +} +bool Component::is_failed() { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } +bool Component::can_proceed() { return true; } +bool Component::status_has_warning() { return this->component_state_ & STATUS_LED_WARNING; } +bool Component::status_has_error() { return this->component_state_ & STATUS_LED_ERROR; } +void Component::status_set_warning() { + this->component_state_ |= STATUS_LED_WARNING; + App.app_state_ |= STATUS_LED_WARNING; +} +void Component::status_set_error() { + this->component_state_ |= STATUS_LED_ERROR; + App.app_state_ |= STATUS_LED_ERROR; +} +void Component::status_clear_warning() { this->component_state_ &= ~STATUS_LED_WARNING; } +void Component::status_clear_error() { this->component_state_ &= ~STATUS_LED_ERROR; } +void Component::status_momentary_warning(const std::string &name, uint32_t length) { + this->status_set_warning(); + this->set_timeout(name, length, [this]() { this->status_clear_warning(); }); +} +void Component::status_momentary_error(const std::string &name, uint32_t length) { + this->status_set_error(); + this->set_timeout(name, length, [this]() { this->status_clear_error(); }); +} +void Component::dump_config() {} +float Component::get_actual_setup_priority() const { + return this->setup_priority_override_.value_or(this->get_setup_priority()); +} +void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } + +PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {} + +void PollingComponent::call_setup() { + // Call component internal setup. + this->setup_internal_(); + + // Let the polling component subclass setup their HW. + this->setup(); + + // Register interval. + this->set_interval("update", this->get_update_interval(), [this]() { this->update(); }); +} + +uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; } +void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } + +const std::string &Nameable::get_name() const { return this->name_; } +void Nameable::set_name(const std::string &name) { + this->name_ = name; + this->calc_object_id_(); +} +Nameable::Nameable(const std::string &name) : name_(name) { this->calc_object_id_(); } + +const std::string &Nameable::get_object_id() { return this->object_id_; } +bool Nameable::is_internal() const { return this->internal_; } +void Nameable::set_internal(bool internal) { this->internal_ = internal; } +void Nameable::calc_object_id_() { + this->object_id_ = sanitize_string_whitelist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_WHITELIST); + // FNV-1 hash + this->object_id_hash_ = fnv1_hash(this->object_id_); +} +uint32_t Nameable::get_object_id_hash() { return this->object_id_hash_; } + +bool Component::TimeFunction::should_run(uint32_t now) const { + if (this->remove) + return false; + if (this->type == DEFER) + return true; + return this->interval != 4294967295UL && now - this->last_execution > this->interval; +} + +} // namespace esphome diff --git a/esphome/core/component.h b/esphome/core/component.h new file mode 100644 index 0000000000..60f306ede4 --- /dev/null +++ b/esphome/core/component.h @@ -0,0 +1,301 @@ +#pragma once + +#include +#include +#include + +#include "esphome/core/optional.h" + +namespace esphome { + +/** Default setup priorities for components of different types. + * + * Components should return one of these setup priorities in get_setup_priority. + */ +namespace setup_priority { + +/// For communication buses like i2c/spi +extern const float BUS; +/// For components that represent GPIO pins like PCF8573 +extern const float IO; +/// For components that deal with hardware and are very important like GPIO switch +extern const float HARDWARE; +/// For components that import data from directly connected sensors like DHT. +extern const float DATA; +/// Alias for DATA (here for compatability reasons) +extern const float HARDWARE_LATE; +/// For components that use data from sensors like displays +extern const float PROCESSOR; +extern const float WIFI; +/// For components that should be initialized after WiFi is connected. +extern const float AFTER_WIFI; +/// For components that should be initialized after a data connection (API/MQTT) is connected. +extern const float AFTER_CONNECTION; +/// For components that should be initialized at the very end of the setup process. +extern const float LATE; + +} // namespace setup_priority + +#define LOG_UPDATE_INTERVAL(this) \ + if (this->get_update_interval() < 100) { \ + ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \ + } else { \ + ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ + } + +extern const uint32_t COMPONENT_STATE_MASK; +extern const uint32_t COMPONENT_STATE_CONSTRUCTION; +extern const uint32_t COMPONENT_STATE_SETUP; +extern const uint32_t COMPONENT_STATE_LOOP; +extern const uint32_t COMPONENT_STATE_FAILED; +extern const uint32_t STATUS_LED_MASK; +extern const uint32_t STATUS_LED_OK; +extern const uint32_t STATUS_LED_WARNING; +extern const uint32_t STATUS_LED_ERROR; + +class Component { + public: + /** Where the component's initialization should happen. + * + * Analogous to Arduino's setup(). This method is guaranteed to only be called once. + * Defaults to doing nothing. + */ + virtual void setup(); + + /** This method will be called repeatedly. + * + * Analogous to Arduino's loop(). setup() is guaranteed to be called before this. + * Defaults to doing nothing. + */ + virtual void loop(); + + virtual void dump_config(); + + /** priority of setup(). higher -> executed earlier + * + * Defaults to 0. + * + * @return The setup priority of this component + */ + virtual float get_setup_priority() const; + + float get_actual_setup_priority() const; + + void set_setup_priority(float priority); + + /** priority of loop(). higher -> executed earlier + * + * Defaults to 0. + * + * @return The loop priority of this component + */ + virtual float get_loop_priority() const; + + /** Public loop() functions. These will be called by the Application instance. + * + * Note: This should normally not be overriden, unless you know what you're doing. + * They're basically to make creating custom components easier. For example the + * SensorComponent can override these methods to not have the user call some super + * methods within their custom sensors. These methods should ALWAYS call the loop_internal() + * and setup_internal() methods. + * + * Basically, it handles stuff like interval/timeout functions and eventually calls loop(). + */ + virtual void call_loop(); + virtual void call_setup(); + + virtual void on_shutdown() {} + virtual void on_safe_shutdown() {} + + uint32_t get_component_state() const; + + /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called. + * + * This might be useful if a component wants to indicate that a connection to its peripheral failed. + * For example, i2c based components can check if the remote device is responding and otherwise + * mark the component as failed. Eventually this will also enable smart status LEDs. + */ + virtual void mark_failed(); + + bool is_failed(); + + virtual bool can_proceed(); + + bool status_has_warning(); + + bool status_has_error(); + + void status_set_warning(); + + void status_set_error(); + + void status_clear_warning(); + + void status_clear_error(); + + void status_momentary_warning(const std::string &name, uint32_t length = 5000); + + void status_momentary_error(const std::string &name, uint32_t length = 5000); + + protected: + /** Set an interval function with a unique name. Empty name means no cancelling possible. + * + * This will call f every interval ms. Can be cancelled via CancelInterval(). + * Similar to javascript's setInterval(). + * + * IMPORTANT: Do not rely on this having correct timing. This is only called from + * loop() and therefore can be significantly delay. If you need exact timing please + * use hardware timers. + * + * @param name The identifier for this interval function. + * @param interval The interval in ms. + * @param f The function (or lambda) that should be called + * + * @see cancel_interval() + */ + void set_interval(const std::string &name, uint32_t interval, std::function &&f); // NOLINT + + void set_interval(uint32_t interval, std::function &&f); // NOLINT + + /** Cancel an interval function. + * + * @param name The identifier for this interval function. + * @return Whether an interval functions was deleted. + */ + bool cancel_interval(const std::string &name); // NOLINT + + void set_timeout(uint32_t timeout, std::function &&f); // NOLINT + + /** Set a timeout function with a unique name. + * + * Similar to javascript's setTimeout(). Empty name means no cancelling possible. + * + * IMPORTANT: Do not rely on this having correct timing. This is only called from + * loop() and therefore can be significantly delay. If you need exact timing please + * use hardware timers. + * + * @param name The identifier for this timeout function. + * @param timeout The timeout in ms. + * @param f The function (or lambda) that should be called + * + * @see cancel_timeout() + */ + void set_timeout(const std::string &name, uint32_t timeout, std::function &&f); // NOLINT + + /** Cancel a timeout function. + * + * @param name The identifier for this timeout function. + * @return Whether a timeout functions was deleted. + */ + bool cancel_timeout(const std::string &name); // NOLINT + + /** Defer a callback to the next loop() call. + * + * If name is specified and a defer() object with the same name exists, the old one is first removed. + * + * @param name The name of the defer function. + * @param f The callback. + */ + void defer(const std::string &name, std::function &&f); // NOLINT + + /// Defer a callback to the next loop() call. + void defer(std::function &&f); // NOLINT + + /// Cancel a defer callback using the specified name, name must not be empty. + bool cancel_defer(const std::string &name); // NOLINT + + void loop_internal_(); + void setup_internal_(); + + /// Internal struct for storing timeout/interval functions. + struct TimeFunction { + std::string name; ///< The name/id of this TimeFunction. + enum Type { TIMEOUT, INTERVAL, DEFER } type; ///< The type of this TimeFunction. Either TIMEOUT, INTERVAL or DEFER. + uint32_t interval; ///< The interval/timeout of this function. + /// The last execution for interval functions and the time, SetInterval was called, for timeout functions. + uint32_t last_execution; + std::function f; ///< The function (or callback) itself. + bool remove; + + bool should_run(uint32_t now) const; + }; + + /// Cancel an only time function. If name is empty, won't do anything. + bool cancel_time_function_(const std::string &name, TimeFunction::Type type); + + /** Storage for interval/timeout functions. + * + * Intentionally a vector despite its map-like nature, because of the + * memory overhead. + */ + std::vector time_functions_; + + uint32_t component_state_{0x0000}; ///< State of this component. + optional setup_priority_override_; +}; + +/** This class simplifies creating components that periodically check a state. + * + * You basically just need to implement the update() function, it will be called every update_interval ms + * after startup. Note that this class cannot guarantee a correct timing, as it's not using timers, just + * a software polling feature with set_interval() from Component. + */ +class PollingComponent : public Component { + public: + PollingComponent() : PollingComponent(0) {} + + /** Initialize this polling component with the given update interval in ms. + * + * @param update_interval The update interval in ms. + */ + explicit PollingComponent(uint32_t update_interval); + + /** Manually set the update interval in ms for this polling object. + * + * Override this if you want to do some validation for the update interval. + * + * @param update_interval The update interval in ms. + */ + virtual void set_update_interval(uint32_t update_interval); + + // ========== OVERRIDE METHODS ========== + // (You'll only need this when creating your own custom sensor) + virtual void update() = 0; + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void call_setup() override; + + /// Get the update interval in ms of this sensor + virtual uint32_t get_update_interval() const; + + protected: + uint32_t update_interval_; +}; + +/// Helper class that enables naming of objects so that it doesn't have to be re-implement every time. +class Nameable { + public: + Nameable() : Nameable("") {} + explicit Nameable(const std::string &name); + const std::string &get_name() const; + void set_name(const std::string &name); + /// Get the sanitized name of this nameable as an ID. Caching it internally. + const std::string &get_object_id(); + uint32_t get_object_id_hash(); + + bool is_internal() const; + void set_internal(bool internal); + + protected: + virtual uint32_t hash_base() = 0; + + void calc_object_id_(); + + std::string name_; + std::string object_id_; + uint32_t object_id_hash_; + bool internal_{false}; +}; + +} // namespace esphome diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp new file mode 100644 index 0000000000..bd68d777ff --- /dev/null +++ b/esphome/core/controller.cpp @@ -0,0 +1,58 @@ +#include "controller.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { + +void Controller::setup_controller() { +#ifdef USE_BINARY_SENSOR + for (auto *obj : App.get_binary_sensors()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); }); + } +#endif +#ifdef USE_FAN + for (auto *obj : App.get_fans()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); }); + } +#endif +#ifdef USE_LIGHT + for (auto *obj : App.get_lights()) { + if (!obj->is_internal()) + obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); }); + } +#endif +#ifdef USE_SENSOR + for (auto *obj : App.get_sensors()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); }); + } +#endif +#ifdef USE_SWITCH + for (auto *obj : App.get_switches()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); }); + } +#endif +#ifdef USE_COVER + for (auto *obj : App.get_covers()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); }); + } +#endif +#ifdef USE_TEXT_SENSOR + for (auto *obj : App.get_text_sensors()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](std::string state) { this->on_text_sensor_update(obj, state); }); + } +#endif +#ifdef USE_CLIMATE + for (auto *obj : App.get_climates()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_climate_update(obj); }); + } +#endif +} + +} // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h new file mode 100644 index 0000000000..fa7d1f2ef0 --- /dev/null +++ b/esphome/core/controller.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_FAN +#include "esphome/components/fan/fan_state.h" +#endif +#ifdef USE_LIGHT +#include "esphome/components/light/light_state.h" +#endif +#ifdef USE_COVER +#include "esphome/components/cover/cover.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif +#ifdef USE_CLIMATE +#include "esphome/components/climate/climate.h" +#endif + +namespace esphome { + +class Controller { + public: + void setup_controller(); +#ifdef USE_BINARY_SENSOR + virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){}; +#endif +#ifdef USE_FAN + virtual void on_fan_update(fan::FanState *obj){}; +#endif +#ifdef USE_LIGHT + virtual void on_light_update(light::LightState *obj){}; +#endif +#ifdef USE_SENSOR + virtual void on_sensor_update(sensor::Sensor *obj, float state){}; +#endif +#ifdef USE_SWITCH + virtual void on_switch_update(switch_::Switch *obj, bool state){}; +#endif +#ifdef USE_COVER + virtual void on_cover_update(cover::Cover *obj){}; +#endif +#ifdef USE_TEXT_SENSOR + virtual void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state){}; +#endif +#ifdef USE_CLIMATE + virtual void on_climate_update(climate::Climate *obj){}; +#endif +}; + +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h new file mode 100644 index 0000000000..af9de1b421 --- /dev/null +++ b/esphome/core/defines.h @@ -0,0 +1,26 @@ +#pragma once +// This file is auto-generated! Do not edit! + +#define ESPHOME_VERSION "dev" + +#define USE_API +#define USE_LOGGER +#define USE_BINARY_SENSOR +#define USE_SENSOR +#define USE_SWITCH +#define USE_WIFI +#define USE_STATUS_LED +#define USE_TEXT_SENSOR +#define USE_FAN +#define USE_COVER +#define USE_LIGHT +#define USE_CLIMATE +#define USE_MQTT +#define USE_POWER_SUPPLY +#define USE_HOMEASSISTANT_TIME +#define USE_JSON +#ifdef ARDUINO_ARCH_ESP32 +#define USE_ESP32_CAMERA +#endif +#define USE_TIME +#define USE_DEEP_SLEEP diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp new file mode 100644 index 0000000000..f0749894c0 --- /dev/null +++ b/esphome/core/esphal.cpp @@ -0,0 +1,280 @@ +#include "esphome/core/esphal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP8266 +extern "C" { +typedef struct { // NOLINT + void *interruptInfo; // NOLINT + void *functionInfo; // NOLINT +} ArgStructure; + +void ICACHE_RAM_ATTR __attachInterruptArg(uint8_t pin, void (*)(void *), void *fp, // NOLINT + int mode); +}; +#endif + +namespace esphome { + +static const char *TAG = "esphal"; + +GPIOPin::GPIOPin(uint8_t pin, uint8_t mode, bool inverted) + : pin_(pin), + mode_(mode), + inverted_(inverted), +#ifdef ARDUINO_ARCH_ESP8266 + gpio_read_(pin < 16 ? &GPI : &GP16I), + gpio_mask_(pin < 16 ? (1UL << pin) : 1) +#endif +#ifdef ARDUINO_ARCH_ESP32 + gpio_set_(pin < 32 ? &GPIO.out_w1ts : &GPIO.out1_w1ts.val), + gpio_clear_(pin < 32 ? &GPIO.out_w1tc : &GPIO.out1_w1tc.val), + gpio_read_(pin < 32 ? &GPIO.in : &GPIO.in1.val), + gpio_mask_(pin < 32 ? (1UL << pin) : (1UL << (pin - 32))) +#endif +{ +} + +const char *GPIOPin::get_pin_mode_name() const { + const char *mode_s; + switch (this->mode_) { + case INPUT: + mode_s = "INPUT"; + break; + case OUTPUT: + mode_s = "OUTPUT"; + break; + case INPUT_PULLUP: + mode_s = "INPUT_PULLUP"; + break; + case OUTPUT_OPEN_DRAIN: + mode_s = "OUTPUT_OPEN_DRAIN"; + break; + case SPECIAL: + mode_s = "SPECIAL"; + break; + case FUNCTION_1: + mode_s = "FUNCTION_1"; + break; + case FUNCTION_2: + mode_s = "FUNCTION_2"; + break; + case FUNCTION_3: + mode_s = "FUNCTION_3"; + break; + case FUNCTION_4: + mode_s = "FUNCTION_4"; + break; + +#ifdef ARDUINO_ARCH_ESP32 + case PULLUP: + mode_s = "PULLUP"; + break; + case PULLDOWN: + mode_s = "PULLDOWN"; + break; + case INPUT_PULLDOWN: + mode_s = "INPUT_PULLDOWN"; + break; + case OPEN_DRAIN: + mode_s = "OPEN_DRAIN"; + break; + case FUNCTION_5: + mode_s = "FUNCTION_5"; + break; + case FUNCTION_6: + mode_s = "FUNCTION_6"; + break; + case ANALOG: + mode_s = "ANALOG"; + break; +#endif +#ifdef ARDUINO_ARCH_ESP8266 + case FUNCTION_0: + mode_s = "FUNCTION_0"; + break; + case WAKEUP_PULLUP: + mode_s = "WAKEUP_PULLUP"; + break; + case WAKEUP_PULLDOWN: + mode_s = "WAKEUP_PULLDOWN"; + break; + case INPUT_PULLDOWN_16: + mode_s = "INPUT_PULLDOWN_16"; + break; +#endif + + default: + mode_s = "UNKNOWN"; + break; + } + + return mode_s; +} + +unsigned char GPIOPin::get_pin() const { return this->pin_; } +unsigned char GPIOPin::get_mode() const { return this->mode_; } + +bool GPIOPin::is_inverted() const { return this->inverted_; } +void GPIOPin::setup() { this->pin_mode(this->mode_); } +bool ICACHE_RAM_ATTR HOT GPIOPin::digital_read() { + return bool((*this->gpio_read_) & this->gpio_mask_) != this->inverted_; +} +bool ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_read() { + return bool((*this->gpio_read_) & this->gpio_mask_) != this->inverted_; +} +void ICACHE_RAM_ATTR HOT GPIOPin::digital_write(bool value) { +#ifdef ARDUINO_ARCH_ESP8266 + if (this->pin_ != 16) { + if (value != this->inverted_) { + GPOS = this->gpio_mask_; + } else { + GPOC = this->gpio_mask_; + } + } else { + if (value != this->inverted_) { + GP16O |= 1; + } else { + GP16O &= ~1; + } + } +#endif +#ifdef ARDUINO_ARCH_ESP32 + if (value != this->inverted_) { + (*this->gpio_set_) = this->gpio_mask_; + } else { + (*this->gpio_clear_) = this->gpio_mask_; + } +#endif +} +void ISRInternalGPIOPin::digital_write(bool value) { +#ifdef ARDUINO_ARCH_ESP8266 + if (this->pin_ != 16) { + if (value != this->inverted_) { + GPOS = this->gpio_mask_; + } else { + GPOC = this->gpio_mask_; + } + } else { + if (value != this->inverted_) { + GP16O |= 1; + } else { + GP16O &= ~1; + } + } +#endif +#ifdef ARDUINO_ARCH_ESP32 + if (value != this->inverted_) { + (*this->gpio_set_) = this->gpio_mask_; + } else { + (*this->gpio_clear_) = this->gpio_mask_; + } +#endif +} +ISRInternalGPIOPin::ISRInternalGPIOPin(uint8_t pin, +#ifdef ARDUINO_ARCH_ESP32 + volatile uint32_t *gpio_clear, volatile uint32_t *gpio_set, +#endif + volatile uint32_t *gpio_read, uint32_t gpio_mask, bool inverted) + : pin_(pin), + inverted_(inverted), + gpio_read_(gpio_read), + gpio_mask_(gpio_mask) +#ifdef ARDUINO_ARCH_ESP32 + , + gpio_clear_(gpio_clear), + gpio_set_(gpio_set) +#endif +{ +} +void ICACHE_RAM_ATTR ISRInternalGPIOPin::clear_interrupt() { +#ifdef ARDUINO_ARCH_ESP8266 + GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, this->gpio_mask_); +#endif +#ifdef ARDUINO_ARCH_ESP32 + if (this->pin_ < 32) { + GPIO.status_w1tc = this->gpio_mask_; + } else { + GPIO.status1_w1tc.intr_st = this->gpio_mask_; + } +#endif +} + +void ICACHE_RAM_ATTR HOT GPIOPin::pin_mode(uint8_t mode) { +#ifdef ARDUINO_ARCH_ESP8266 + if (this->pin_ == 16 && mode == INPUT_PULLUP) { + // pullups are not available on GPIO16, manually override with + // input mode. + pinMode(16, INPUT); + return; + } +#endif + pinMode(this->pin_, mode); +} + +#ifdef ARDUINO_ARCH_ESP8266 +struct ESPHomeInterruptFuncInfo { + void (*func)(void *); + void *arg; +}; + +void ICACHE_RAM_ATTR interrupt_handler(void *arg) { + ArgStructure *as = static_cast(arg); + auto *info = static_cast(as->functionInfo); + info->func(info->arg); +} +#endif + +void GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, int mode) const { + if (this->inverted_) { + if (mode == RISING) { + mode = FALLING; + } else if (mode == FALLING) { + mode = RISING; + } + } +#ifdef ARDUINO_ARCH_ESP8266 + ArgStructure *as = new ArgStructure; + as->interruptInfo = nullptr; + + as->functionInfo = new ESPHomeInterruptFuncInfo{ + .func = func, + .arg = arg, + }; + + __attachInterruptArg(this->pin_, interrupt_handler, as, mode); +#endif +#ifdef ARDUINO_ARCH_ESP32 + // work around issue https://github.com/espressif/arduino-esp32/pull/1776 in arduino core + // yet again proves how horrible code is there :( - how could that have been accepted... + auto *attach = reinterpret_cast(attachInterruptArg); + attach(this->pin_, func, arg, mode); +#endif +} + +ISRInternalGPIOPin *GPIOPin::to_isr() const { + return new ISRInternalGPIOPin(this->pin_, +#ifdef ARDUINO_ARCH_ESP32 + this->gpio_clear_, this->gpio_set_, +#endif + this->gpio_read_, this->gpio_mask_, this->inverted_); +} + +} // namespace esphome + +#ifdef ARDUINO_ESP8266_RELEASE_2_3_0 +// Fix 2.3.0 std missing memchr +extern "C" { +void *memchr(const void *s, int c, size_t n) { + if (n == 0) + return nullptr; + const uint8_t *p = reinterpret_cast(s); + do { + if (*p++ == c) + return const_cast(reinterpret_cast(p - 1)); + } while (--n != 0); + return nullptr; +} +}; +#endif diff --git a/esphome/core/esphal.h b/esphome/core/esphal.h new file mode 100644 index 0000000000..493f7f5e37 --- /dev/null +++ b/esphome/core/esphal.h @@ -0,0 +1,118 @@ +#pragma once + +#include "Arduino.h" +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +// Fix some arduino defs +#ifdef round +#undef round +#endif +#ifdef bool +#undef bool +#endif +#ifdef true +#undef true +#endif +#ifdef false +#undef false +#endif +#ifdef min +#undef min +#endif +#ifdef max +#undef max +#endif +#ifdef abs +#undef abs +#endif + +namespace esphome { + +#define LOG_PIN(prefix, pin) \ + if ((pin) != nullptr) { \ + ESP_LOGCONFIG(TAG, prefix LOG_PIN_PATTERN, LOG_PIN_ARGS(pin)); \ + } +#define LOG_PIN_PATTERN "GPIO%u (Mode: %s%s)" +#define LOG_PIN_ARGS(pin) (pin)->get_pin(), (pin)->get_pin_mode_name(), ((pin)->is_inverted() ? ", INVERTED" : "") + +/// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions) +class ISRInternalGPIOPin { + public: + ISRInternalGPIOPin(uint8_t pin, +#ifdef ARDUINO_ARCH_ESP32 + volatile uint32_t *gpio_clear, volatile uint32_t *gpio_set, +#endif + volatile uint32_t *gpio_read, uint32_t gpio_mask, bool inverted); + bool digital_read(); + void digital_write(bool value); + void clear_interrupt(); + + protected: + const uint8_t pin_; + const bool inverted_; + volatile uint32_t *const gpio_read_; + const uint32_t gpio_mask_; +#ifdef ARDUINO_ARCH_ESP32 + volatile uint32_t *const gpio_clear_; + volatile uint32_t *const gpio_set_; +#endif +}; + +/** A high-level abstraction class that can expose a pin together with useful options like pinMode. + * + * Set the parameters for this at construction time and use setup() to apply them. The inverted parameter will + * automatically invert the input/output for you. + * + * Use read_value() and write_value() to use digitalRead() and digitalWrite(), respectively. + */ +class GPIOPin { + public: + /** Construct the GPIOPin instance. + * + * @param pin The GPIO pin number of this instance. + * @param mode The Arduino pinMode that this pin should be put into at setup(). + * @param inverted Whether all digitalRead/digitalWrite calls should be inverted. + */ + GPIOPin(uint8_t pin, uint8_t mode, bool inverted = false); + + /// Setup the pin mode. + virtual void setup(); + /// Read the binary value from this pin using digitalRead (and inverts automatically). + virtual bool digital_read(); + /// Write the binary value to this pin using digitalWrite (and inverts automatically). + virtual void digital_write(bool value); + /// Set the pin mode + virtual void pin_mode(uint8_t mode); + + /// Get the GPIO pin number. + uint8_t get_pin() const; + const char *get_pin_mode_name() const; + /// Get the pinMode of this pin. + uint8_t get_mode() const; + /// Return whether this pin shall be treated as inverted. (for example active-low) + bool is_inverted() const; + + template void attach_interrupt(void (*func)(T *), T *arg, int mode) const; + + ISRInternalGPIOPin *to_isr() const; + + protected: + void attach_interrupt_(void (*func)(void *), void *arg, int mode) const; + + const uint8_t pin_; + const uint8_t mode_; + const bool inverted_; +#ifdef ARDUINO_ARCH_ESP32 + volatile uint32_t *const gpio_set_; + volatile uint32_t *const gpio_clear_; +#endif + volatile uint32_t *const gpio_read_; + const uint32_t gpio_mask_; +}; + +template void GPIOPin::attach_interrupt(void (*func)(T *), T *arg, int mode) const { + this->attach_interrupt_(reinterpret_cast(func), arg, mode); +} + +} // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp new file mode 100644 index 0000000000..c65ca919ba --- /dev/null +++ b/esphome/core/helpers.cpp @@ -0,0 +1,317 @@ +#include "esphome/core/helpers.h" +#include +#include + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#else +#include +#endif + +#include "esphome/core/log.h" +#include "esphome/core/esphal.h" + +namespace esphome { + +static const char *TAG = "helpers"; + +std::string get_mac_address() { + char tmp[20]; + uint8_t mac[6]; +#ifdef ARDUINO_ARCH_ESP32 + esp_efuse_mac_get_default(mac); +#endif +#ifdef ARDUINO_ARCH_ESP8266 + WiFi.macAddress(mac); +#endif + sprintf(tmp, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return std::string(tmp); +} + +std::string get_mac_address_pretty() { + char tmp[20]; + uint8_t mac[6]; +#ifdef ARDUINO_ARCH_ESP32 + esp_efuse_mac_get_default(mac); +#endif +#ifdef ARDUINO_ARCH_ESP8266 + WiFi.macAddress(mac); +#endif + sprintf(tmp, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return std::string(tmp); +} + +std::string generate_hostname(const std::string &base) { return base + std::string("-") + get_mac_address(); } + +uint32_t random_uint32() { +#ifdef ARDUINO_ARCH_ESP32 + return esp_random(); +#else + return os_random(); +#endif +} + +double random_double() { return random_uint32() / double(UINT32_MAX); } + +float random_float() { return float(random_double()); } + +static uint32_t fast_random_seed = 0; + +void fast_random_set_seed(uint32_t seed) { fast_random_seed = seed; } +uint32_t fast_random_32() { + fast_random_seed = (fast_random_seed * 2654435769ULL) + 40503ULL; + return fast_random_seed; +} +uint16_t fast_random_16() { + uint32_t rand32 = fast_random_32(); + return (rand32 & 0xFFFF) + (rand32 >> 16); +} +uint8_t fast_random_8() { + uint8_t rand32 = fast_random_32(); + return (rand32 & 0xFF) + ((rand32 >> 8) & 0xFF); +} + +float gamma_correct(float value, float gamma) { + if (value <= 0.0f) + return 0.0f; + if (gamma <= 0.0f) + return value; + + return powf(value, gamma); +} +std::string to_lowercase_underscore(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + std::replace(s.begin(), s.end(), ' ', '_'); + return s; +} + +std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist) { + std::string out(s); + out.erase(std::remove_if(out.begin(), out.end(), + [&whitelist](const char &c) { return whitelist.find(c) == std::string::npos; }), + out.end()); + return out; +} + +std::string sanitize_hostname(const std::string &hostname) { + std::string s = sanitize_string_whitelist(hostname, HOSTNAME_CHARACTER_WHITELIST); + return truncate_string(s, 63); +} + +std::string truncate_string(const std::string &s, size_t length) { + if (s.length() > length) + return s.substr(0, length); + return s; +} + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + auto multiplier = float(pow10(accuracy_decimals)); + float value_rounded = roundf(value * multiplier) / multiplier; + char tmp[32]; // should be enough, but we should maybe improve this at some point. + dtostrf(value_rounded, 0, uint8_t(std::max(0, int(accuracy_decimals))), tmp); + return std::string(tmp); +} +std::string uint64_to_string(uint64_t num) { + char buffer[17]; + auto *address16 = reinterpret_cast(&num); + snprintf(buffer, sizeof(buffer), "%04X%04X%04X%04X", address16[3], address16[2], address16[1], address16[0]); + return std::string(buffer); +} +std::string uint32_to_string(uint32_t num) { + char buffer[9]; + auto *address16 = reinterpret_cast(&num); + snprintf(buffer, sizeof(buffer), "%04X%04X", address16[1], address16[0]); + return std::string(buffer); +} +static char *global_json_build_buffer = nullptr; +static size_t global_json_build_buffer_size = 0; + +void reserve_global_json_build_buffer(size_t required_size) { + if (global_json_build_buffer_size == 0 || global_json_build_buffer_size < required_size) { + delete[] global_json_build_buffer; + global_json_build_buffer_size = std::max(required_size, global_json_build_buffer_size * 2); + + size_t remainder = global_json_build_buffer_size % 16U; + if (remainder != 0) + global_json_build_buffer_size += 16 - remainder; + + global_json_build_buffer = new char[global_json_build_buffer_size]; + } +} + +ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { + if (on == nullptr && strcasecmp(str, "on") == 0) + return PARSE_ON; + if (on != nullptr && strcasecmp(str, on) == 0) + return PARSE_ON; + if (off == nullptr && strcasecmp(str, "off") == 0) + return PARSE_OFF; + if (off != nullptr && strcasecmp(str, off) == 0) + return PARSE_OFF; + if (strcasecmp(str, "toggle") == 0) + return PARSE_TOGGLE; + + return PARSE_NONE; +} + +const char *HOSTNAME_CHARACTER_WHITELIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + +void disable_interrupts() { +#ifdef ARDUINO_ARCH_ESP32 + portDISABLE_INTERRUPTS(); +#else + noInterrupts(); +#endif +} +void enable_interrupts() { +#ifdef ARDUINO_ARCH_ESP32 + portENABLE_INTERRUPTS(); +#else + interrupts(); +#endif +} + +uint8_t crc8(uint8_t *data, uint8_t len) { + uint8_t crc = 0; + + while ((len--) != 0u) { + uint8_t inbyte = *data++; + for (uint8_t i = 8; i != 0u; i--) { + bool mix = (crc ^ inbyte) & 0x01; + crc >>= 1; + if (mix) + crc ^= 0x8C; + inbyte >>= 1; + } + } + return crc; +} +void delay_microseconds_accurate(uint32_t usec) { + if (usec == 0) + return; + + if (usec <= 16383UL) { + delayMicroseconds(usec); + } else { + delay(usec / 1000UL); + delayMicroseconds(usec % 1000UL); + } +} + +uint8_t reverse_bits_8(uint8_t x) { + x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1); + x = ((x & 0xCC) >> 2) | ((x & 0x33) << 2); + x = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4); + return x; +} + +uint16_t reverse_bits_16(uint16_t x) { + return uint16_t(reverse_bits_8(x & 0xFF) << 8) | uint16_t(reverse_bits_8(x >> 8)); +} +std::string to_string(const std::string &val) { return val; } +std::string to_string(int val) { + char buf[64]; + sprintf(buf, "%d", val); + return buf; +} +std::string to_string(long val) { + char buf[64]; + sprintf(buf, "%ld", val); + return buf; +} +std::string to_string(long long val) { + char buf[64]; + sprintf(buf, "%lld", val); + return buf; +} +std::string to_string(unsigned val) { + char buf[64]; + sprintf(buf, "%u", val); + return buf; +} +std::string to_string(unsigned long val) { + char buf[64]; + sprintf(buf, "%lu", val); + return buf; +} +std::string to_string(unsigned long long val) { + char buf[64]; + sprintf(buf, "%llu", val); + return buf; +} +std::string to_string(float val) { + char buf[64]; + sprintf(buf, "%f", val); + return buf; +} +std::string to_string(double val) { + char buf[64]; + sprintf(buf, "%f", val); + return buf; +} +std::string to_string(long double val) { + char buf[64]; + sprintf(buf, "%Lf", val); + return buf; +} +optional parse_float(const std::string &str) { + char *end; + float value = ::strtof(str.c_str(), &end); + if (end == nullptr || end != str.end().base()) + return {}; + return value; +} +uint32_t fnv1_hash(const std::string &str) { + uint32_t hash = 2166136261UL; + for (char c : str) { + hash *= 16777619UL; + hash ^= c; + } + return hash; +} +bool str_equals_case_insensitive(const std::string &a, const std::string &b) { + return strcasecmp(a.c_str(), b.c_str()) == 0; +} + +template uint32_t reverse_bits(uint32_t x) { + return uint32_t(reverse_bits_16(x & 0xFFFF) << 16) | uint32_t(reverse_bits_16(x >> 16)); +} + +static int high_freq_num_requests = 0; + +void HighFrequencyLoopRequester::start() { + if (this->started_) + return; + high_freq_num_requests++; + this->started_ = true; +} +void HighFrequencyLoopRequester::stop() { + if (!this->started_) + return; + high_freq_num_requests--; + this->started_ = false; +} +bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; } + +float clamp(float val, float min, float max) { + if (val < min) + return min; + if (val > max) + return max; + return val; +} +float lerp(float completion, float start, float end) { return start + (end - start) * completion; } + +bool str_startswith(const std::string &full, const std::string &start) { return full.rfind(start, 0) == 0; } +bool str_endswith(const std::string &full, const std::string &ending) { + return full.rfind(ending) == (full.size() - ending.size()); +} + +uint16_t encode_uint16(uint8_t msb, uint8_t lsb) { return (uint16_t(msb) << 8) | uint16_t(lsb); } +std::array decode_uint16(uint16_t value) { + uint8_t msb = (value >> 8) & 0xFF; + uint8_t lsb = (value >> 0) & 0xFF; + return {msb, lsb}; +} + +} // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h new file mode 100644 index 0000000000..d21cb85b7d --- /dev/null +++ b/esphome/core/helpers.h @@ -0,0 +1,276 @@ +#pragma once + +#include +#include +#include +#include + +#include "esphome/core/optional.h" +#include "esphome/core/esphal.h" + +#ifdef CLANG_TIDY +#undef ICACHE_RAM_ATTR +#define ICACHE_RAM_ATTR +#undef ICACHE_RODATA_ATTR +#define ICACHE_RODATA_ATTR +#endif + +#define HOT __attribute__((hot)) +#define ESPDEPRECATED(msg) __attribute__((deprecated(msg))) +#define ALWAYS_INLINE __attribute__((always_inline)) +#define PACKED __attribute__((packed)) + +namespace esphome { + +/// The characters that are allowed in a hostname. +extern const char *HOSTNAME_CHARACTER_WHITELIST; + +/// Gets the MAC address as a string, this can be used as way to identify this ESP. +std::string get_mac_address(); + +std::string get_mac_address_pretty(); + +std::string to_string(const std::string &val); +std::string to_string(int val); +std::string to_string(long val); +std::string to_string(long long val); +std::string to_string(unsigned val); +std::string to_string(unsigned long val); +std::string to_string(unsigned long long val); +std::string to_string(float val); +std::string to_string(double val); +std::string to_string(long double val); +optional parse_float(const std::string &str); + +/// Sanitize the hostname by removing characters that are not in the whitelist and truncating it to 63 chars. +std::string sanitize_hostname(const std::string &hostname); + +/// Truncate a string to a specific length +std::string truncate_string(const std::string &s, size_t length); + +/// Convert the string to lowercase_underscore. +std::string to_lowercase_underscore(std::string s); + +/// Compare string a to string b (ignoring case) and return whether they are equal. +bool str_equals_case_insensitive(const std::string &a, const std::string &b); +bool str_startswith(const std::string &full, const std::string &start); +bool str_endswith(const std::string &full, const std::string &ending); + +class HighFrequencyLoopRequester { + public: + void start(); + void stop(); + + static bool is_high_frequency(); + + protected: + bool started_{false}; +}; + +/** Clamp the value between min and max. + * + * @param val The value. + * @param min The minimum value. + * @param max The maximum value. + * @return val clamped in between min and max. + */ +float clamp(float val, float min, float max); + +/** Linearly interpolate between end start and end by completion. + * + * @tparam T The input/output typename. + * @param start The start value. + * @param end The end value. + * @param completion The completion. 0 is start value, 1 is end value. + * @return The linearly interpolated value. + */ +float lerp(float completion, float start, float end); + +/// std::make_unique +template std::unique_ptr make_unique(Args &&... args) { + return std::unique_ptr(new T(std::forward(args)...)); +} + +/// Return a random 32 bit unsigned integer. +uint32_t random_uint32(); + +/** Returns a random double between 0 and 1. + * + * Note: This function probably doesn't provide a truly uniform distribution. + */ +double random_double(); + +/// Returns a random float between 0 and 1. Essentially just casts random_double() to a float. +float random_float(); + +void fast_random_set_seed(uint32_t seed); +uint32_t fast_random_32(); +uint16_t fast_random_16(); +uint8_t fast_random_8(); + +/// Applies gamma correction with the provided gamma to value. +float gamma_correct(float value, float gamma); + +/// Create a string from a value and an accuracy in decimals. +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); + +/// Convert a uint64_t to a hex string +std::string uint64_to_string(uint64_t num); + +/// Convert a uint32_t to a hex string +std::string uint32_to_string(uint32_t num); + +/// Sanitizes the input string with the whitelist. +std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist); + +uint8_t reverse_bits_8(uint8_t x); +uint16_t reverse_bits_16(uint16_t x); +uint32_t reverse_bits_32(uint32_t x); + +/// Encode a 16-bit unsigned integer given a most and least-significant byte. +uint16_t encode_uint16(uint8_t msb, uint8_t lsb); +/// Decode a 16-bit unsigned integer into an array of two values: most significant byte, least significant byte. +std::array decode_uint16(uint16_t value); + +/** Cross-platform method to disable interrupts. + * + * Useful when you need to do some timing-dependent communication. + * + * @see Do not forget to call `enable_interrupts()` again or otherwise things will go very wrong. + */ +void disable_interrupts(); + +/// Cross-platform method to enable interrupts after they have been disabled. +void enable_interrupts(); + +/// Calculate a crc8 of data with the provided data length. +uint8_t crc8(uint8_t *data, uint8_t len); + +enum ParseOnOffState { + PARSE_NONE = 0, + PARSE_ON, + PARSE_OFF, + PARSE_TOGGLE, +}; + +ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); + +// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 +template struct seq {}; // NOLINT +template struct gens : gens {}; // NOLINT +template struct gens<0, S...> { using type = seq; }; // NOLINT + +template class CallbackManager; + +/** Simple helper class to allow having multiple subscribers to a signal. + * + * @tparam Ts The arguments for the callback, wrapped in void(). + */ +template class CallbackManager { + public: + /// Add a callback to the internal callback list. + void add(std::function &&callback) { this->callbacks_.push_back(std::move(callback)); } + + /// Call all callbacks in this manager. + void call(Ts... args) { + for (auto &cb : this->callbacks_) + cb(args...); + } + + protected: + std::vector> callbacks_; +}; + +// https://stackoverflow.com/a/37161919/8924614 +template +struct is_callable // NOLINT +{ + template static auto test(U *p) -> decltype((*p)(std::declval()...), void(), std::true_type()); + + template static auto test(...) -> decltype(std::false_type()); + + static constexpr auto value = decltype(test(nullptr))::value; // NOLINT +}; + +template using enable_if_t = typename std::enable_if::type; + +template class TemplatableValue { + public: + TemplatableValue() : type_(EMPTY) {} + + template::value, int> = 0> + TemplatableValue(F value) : type_(VALUE), value_(value) {} + + template::value, int> = 0> + TemplatableValue(F f) : type_(LAMBDA), f_(f) {} + + bool has_value() { return this->type_ != EMPTY; } + + T value(X... x) { + if (this->type_ == LAMBDA) { + return this->f_(x...); + } + // return value also when empty + return this->value_; + } + + optional optional_value(X... x) { + if (!this->has_value()) { + return {}; + } + return this->value(x...); + } + + T value_or(X... x, T default_value) { + if (!this->has_value()) { + return default_value; + } + return this->value(x...); + } + + protected: + enum { + EMPTY, + VALUE, + LAMBDA, + } type_; + + T value_; + std::function f_; +}; + +void delay_microseconds_accurate(uint32_t usec); + +template class Deduplicator { + public: + bool next(T value) { + if (this->has_value_) { + if (this->last_value_ == value) + return false; + } + this->has_value_ = true; + this->last_value_ = value; + return true; + } + bool has_value() const { return this->has_value_; } + + protected: + bool has_value_{false}; + T last_value_{}; +}; + +template class Parented { + public: + Parented() {} + Parented(T *parent) : parent_(parent) {} + + T *get_parent() const { return parent_; } + void set_parent(T *parent) { parent_ = parent; } + + protected: + T *parent_{nullptr}; +}; + +uint32_t fnv1_hash(const std::string &str); + +} // namespace esphome diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp new file mode 100644 index 0000000000..8adaebe5b5 --- /dev/null +++ b/esphome/core/log.cpp @@ -0,0 +1,67 @@ +#include "esphome/core/log.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +namespace esphome { + +int HOT esp_log_printf_(int level, const char *tag, const char *format, ...) { // NOLINT + va_list arg; + va_start(arg, format); + int ret = esp_log_vprintf_(level, tag, format, arg); + va_end(arg); + return ret; +} +#ifdef USE_STORE_LOG_STR_IN_FLASH +int HOT esp_log_printf_(int level, const char *tag, const __FlashStringHelper *format, ...) { + va_list arg; + va_start(arg, format); + int ret = esp_log_vprintf_(level, tag, format, arg); + va_end(arg); + return ret; + return 0; +} +#endif + +int HOT esp_log_vprintf_(int level, const char *tag, const char *format, va_list args) { // NOLINT +#ifdef USE_LOGGER + auto *log = logger::global_logger; + if (log == nullptr) + return 0; + + return log->log_vprintf_(level, tag, format, args); +#else + return 0; +#endif +} + +#ifdef USE_STORE_LOG_STR_IN_FLASH +int HOT esp_log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args) { // NOLINT +#ifdef USE_LOGGER + auto *log = logger::global_logger; + if (log == nullptr) + return 0; + + return log->log_vprintf_(level, tag, format, args); +#else + return 0; +#endif +} +#endif + +int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT +#ifdef USE_LOGGER + auto *log = logger::global_logger; + if (log == nullptr) + return 0; + + return log->log_vprintf_(log->get_global_log_level(), "", format, args); +#else + return 0; +#endif +} + +} // namespace esphome diff --git a/esphome/core/log.h b/esphome/core/log.h new file mode 100644 index 0000000000..4e4d178b96 --- /dev/null +++ b/esphome/core/log.h @@ -0,0 +1,173 @@ +#pragma once + +#include +#include +#include +#ifdef USE_STORE_LOG_STR_IN_FLASH +#include "WString.h" +#endif + +// avoid esp-idf redefining our macros +#include "esphome/core/esphal.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include "esp_err.h" +#endif + +namespace esphome { + +#define ESPHOME_LOG_LEVEL_NONE 0 +#define ESPHOME_LOG_LEVEL_ERROR 1 +#define ESPHOME_LOG_LEVEL_WARN 2 +#define ESPHOME_LOG_LEVEL_INFO 3 +#define ESPHOME_LOG_LEVEL_DEBUG 4 +#define ESPHOME_LOG_LEVEL_VERBOSE 5 +#define ESPHOME_LOG_LEVEL_VERY_VERBOSE 6 + +#ifndef ESPHOME_LOG_LEVEL +#define ESPHOME_LOG_LEVEL ESPHOME_LOG_LEVEL_DEBUG +#endif + +#define ESPHOME_LOG_COLOR_BLACK "30" +#define ESPHOME_LOG_COLOR_RED "31" // ERROR +#define ESPHOME_LOG_COLOR_GREEN "32" // INFO +#define ESPHOME_LOG_COLOR_YELLOW "33" // WARNING +#define ESPHOME_LOG_COLOR_BLUE "34" +#define ESPHOME_LOG_COLOR_MAGENTA "35" // CONFIG +#define ESPHOME_LOG_COLOR_CYAN "36" // DEBUG +#define ESPHOME_LOG_COLOR_GRAY "37" // VERBOSE +#define ESPHOME_LOG_COLOR_WHITE "38" +#define ESPHOME_LOG_SECRET_BEGIN "\033[5m" +#define ESPHOME_LOG_SECRET_END "\033[6m" +#define LOG_SECRET(x) ESPHOME_LOG_SECRET_BEGIN x ESPHOME_LOG_SECRET_END + +#define ESPHOME_LOG_COLOR(COLOR) "\033[0;" COLOR "m" +#define ESPHOME_LOG_BOLD(COLOR) "\033[1;" COLOR "m" + +#define ESPHOME_LOG_COLOR_E ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED) +#define ESPHOME_LOG_COLOR_W ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW) +#define ESPHOME_LOG_COLOR_I ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN) +#define ESPHOME_LOG_COLOR_C ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA) +#define ESPHOME_LOG_COLOR_D ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN) +#define ESPHOME_LOG_COLOR_V ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY) +#define ESPHOME_LOG_COLOR_VV ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE) +#define ESPHOME_LOG_RESET_COLOR "\033[0m" + +int esp_log_printf_(int level, const char *tag, const char *format, ...) // NOLINT + __attribute__((format(printf, 3, 4))); +#ifdef USE_STORE_LOG_STR_IN_FLASH +int esp_log_printf_(int level, const char *tag, const __FlashStringHelper *format, ...); +#endif +int esp_log_vprintf_(int level, const char *tag, const char *format, va_list args); // NOLINT +#ifdef USE_STORE_LOG_STR_IN_FLASH +int esp_log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args); +#endif +int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT + +#ifdef USE_STORE_LOG_STR_IN_FLASH +#define ESPHOME_LOG_FORMAT(tag, letter, format) \ + F(ESPHOME_LOG_COLOR_##letter "[" #letter "][%s:%03u]: " format ESPHOME_LOG_RESET_COLOR), tag, __LINE__ +#else +#define ESPHOME_LOG_FORMAT(tag, letter, format) \ + ESPHOME_LOG_COLOR_##letter "[" #letter "][%s:%03u]: " format ESPHOME_LOG_RESET_COLOR, tag, __LINE__ +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE +#define esph_log_vv(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, ESPHOME_LOG_FORMAT(tag, VV, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_VERY_VERBOSE +#else +#define esph_log_vv(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE +#define esph_log_v(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, ESPHOME_LOG_FORMAT(tag, V, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_VERBOSE +#else +#define esph_log_v(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +#define esph_log_d(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, ESPHOME_LOG_FORMAT(tag, D, format), ##__VA_ARGS__) + +#define esph_log_config(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, ESPHOME_LOG_FORMAT(tag, C, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_DEBUG +#define ESPHOME_LOG_HAS_CONFIG +#else +#define esph_log_d(tag, format, ...) + +#define esph_log_config(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO +#define esph_log_i(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, ESPHOME_LOG_FORMAT(tag, I, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_INFO +#else +#define esph_log_i(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN +#define esph_log_w(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, ESPHOME_LOG_FORMAT(tag, W, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_WARN +#else +#define esph_log_w(tag, format, ...) +#endif + +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR +#define esph_log_e(tag, format, ...) \ + esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, ESPHOME_LOG_FORMAT(tag, E, format), ##__VA_ARGS__) + +#define ESPHOME_LOG_HAS_ERROR +#else +#define esph_log_e(tag, format, ...) +#endif + +#ifdef ESP_LOGE +#undef ESP_LOGE +#endif +#ifdef ESP_LOGW +#undef ESP_LOGW +#endif +#ifdef ESP_LOGI +#undef ESP_LOGI +#endif +#ifdef ESP_LOGD +#undef ESP_LOGD +#endif +#ifdef ESP_LOGV +#undef ESP_LOGV +#endif + +#define ESP_LOGE(tag, ...) esph_log_e(tag, __VA_ARGS__) +#define LOG_E(tag, ...) ESP_LOGE(tag, __VA__ARGS__) +#define ESP_LOGW(tag, ...) esph_log_w(tag, __VA_ARGS__) +#define LOG_W(tag, ...) ESP_LOGW(tag, __VA__ARGS__) +#define ESP_LOGI(tag, ...) esph_log_i(tag, __VA_ARGS__) +#define LOG_I(tag, ...) ESP_LOGI(tag, __VA__ARGS__) +#define ESP_LOGD(tag, ...) esph_log_d(tag, __VA_ARGS__) +#define LOG_D(tag, ...) ESP_LOGD(tag, __VA__ARGS__) +#define ESP_LOGCONFIG(tag, ...) esph_log_config(tag, __VA_ARGS__) +#define LOG_CONFIG(tag, ...) ESP_LOGCONFIG(tag, __VA__ARGS__) +#define ESP_LOGV(tag, ...) esph_log_v(tag, __VA_ARGS__) +#define LOG_V(tag, ...) ESP_LOGV(tag, __VA__ARGS__) +#define ESP_LOGVV(tag, ...) esph_log_vv(tag, __VA_ARGS__) +#define LOG_VV(tag, ...) ESP_LOGVV(tag, __VA__ARGS__) + +#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" +#define BYTE_TO_BINARY(byte) \ + ((byte) &0x80 ? '1' : '0'), ((byte) &0x40 ? '1' : '0'), ((byte) &0x20 ? '1' : '0'), ((byte) &0x10 ? '1' : '0'), \ + ((byte) &0x08 ? '1' : '0'), ((byte) &0x04 ? '1' : '0'), ((byte) &0x02 ? '1' : '0'), ((byte) &0x01 ? '1' : '0') +#define YESNO(b) ((b) ? "YES" : "NO") +#define ONOFF(b) ((b) ? "ON" : "OFF") + +} // namespace esphome diff --git a/esphome/core/optional.h b/esphome/core/optional.h new file mode 100644 index 0000000000..f6b050f5f8 --- /dev/null +++ b/esphome/core/optional.h @@ -0,0 +1,214 @@ +#pragma once +// +// Copyright (c) 2017 Martin Moene +// +// https://github.com/martinmoene/optional-bare +// +// This code is licensed under the MIT License (MIT). +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// +// Modified by Otto Winter on 18.05.18 + +namespace esphome { + +// type for nullopt + +struct nullopt_t { // NOLINT + struct init {}; // NOLINT + nullopt_t(init) {} +}; + +// extra parenthesis to prevent the most vexing parse: + +const nullopt_t nullopt((nullopt_t::init())); // NOLINT + +// Simplistic optional: requires T to be default constructible, copyable. + +template class optional { // NOLINT + private: + using safe_bool = void (optional::*)() const; + + public: + using value_type = T; + + optional() {} + + optional(nullopt_t) {} + + optional(T const &arg) : has_value_(true), value_(arg) {} + + template optional(optional const &other) : has_value_(other.has_value()), value_(other.value()) {} + + optional &operator=(nullopt_t) { + reset(); + return *this; + } + + template optional &operator=(optional const &other) { + has_value_ = other.has_value(); + value_ = other.value(); + return *this; + } + + void swap(optional &rhs) { + using std::swap; + if (has_value() && rhs.has_value()) { + swap(**this, *rhs); + } else if (!has_value() && rhs.has_value()) { + initialize(*rhs); + rhs.reset(); + } else if (has_value() && !rhs.has_value()) { + rhs.initialize(**this); + reset(); + } + } + + // observers + + value_type const *operator->() const { return &value_; } + + value_type *operator->() { return &value_; } + + value_type const &operator*() const { return value_; } + + value_type &operator*() { return value_; } + + operator safe_bool() const { return has_value() ? &optional::this_type_does_not_support_comparisons : nullptr; } + + bool has_value() const { return has_value_; } + + value_type const &value() const { return value_; } + + value_type &value() { return value_; } + + template value_type value_or(U const &v) const { return has_value() ? value() : static_cast(v); } + + // modifiers + + void reset() { has_value_ = false; } + + private: + void this_type_does_not_support_comparisons() const {} // NOLINT + + template void initialize(V const &value) { // NOLINT + value_ = value; + has_value_ = true; + } + + private: + bool has_value_{false}; // NOLINT + value_type value_; // NOLINT +}; + +// Relational operators + +template inline bool operator==(optional const &x, optional const &y) { + return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y; +} + +template inline bool operator!=(optional const &x, optional const &y) { + return !(x == y); +} + +template inline bool operator<(optional const &x, optional const &y) { + return (!y) ? false : (!x) ? true : *x < *y; +} + +template inline bool operator>(optional const &x, optional const &y) { return (y < x); } + +template inline bool operator<=(optional const &x, optional const &y) { return !(y < x); } + +template inline bool operator>=(optional const &x, optional const &y) { return !(x < y); } + +// Comparison with nullopt + +template inline bool operator==(optional const &x, nullopt_t) { return (!x); } + +template inline bool operator==(nullopt_t, optional const &x) { return (!x); } + +template inline bool operator!=(optional const &x, nullopt_t) { return bool(x); } + +template inline bool operator!=(nullopt_t, optional const &x) { return bool(x); } + +template inline bool operator<(optional const &, nullopt_t) { return false; } + +template inline bool operator<(nullopt_t, optional const &x) { return bool(x); } + +template inline bool operator<=(optional const &x, nullopt_t) { return (!x); } + +template inline bool operator<=(nullopt_t, optional const &) { return true; } + +template inline bool operator>(optional const &x, nullopt_t) { return bool(x); } + +template inline bool operator>(nullopt_t, optional const &) { return false; } + +template inline bool operator>=(optional const &, nullopt_t) { return true; } + +template inline bool operator>=(nullopt_t, optional const &x) { return (!x); } + +// Comparison with T + +template inline bool operator==(optional const &x, U const &v) { + return bool(x) ? *x == v : false; +} + +template inline bool operator==(U const &v, optional const &x) { + return bool(x) ? v == *x : false; +} + +template inline bool operator!=(optional const &x, U const &v) { + return bool(x) ? *x != v : true; +} + +template inline bool operator!=(U const &v, optional const &x) { + return bool(x) ? v != *x : true; +} + +template inline bool operator<(optional const &x, U const &v) { + return bool(x) ? *x < v : true; +} + +template inline bool operator<(U const &v, optional const &x) { + return bool(x) ? v < *x : false; +} + +template inline bool operator<=(optional const &x, U const &v) { + return bool(x) ? *x <= v : true; +} + +template inline bool operator<=(U const &v, optional const &x) { + return bool(x) ? v <= *x : false; +} + +template inline bool operator>(optional const &x, U const &v) { + return bool(x) ? *x > v : false; +} + +template inline bool operator>(U const &v, optional const &x) { + return bool(x) ? v > *x : true; +} + +template inline bool operator>=(optional const &x, U const &v) { + return bool(x) ? *x >= v : false; +} + +template inline bool operator>=(U const &v, optional const &x) { + return bool(x) ? v >= *x : true; +} + +// Specialized algorithms + +template void swap(optional &x, optional &y) { x.swap(y); } + +// Convenience function to create an optional. + +template inline optional make_optional(T const &v) { return optional(v); } + +} // namespace esphome diff --git a/esphome/core/preferences.cpp b/esphome/core/preferences.cpp new file mode 100644 index 0000000000..65140bbdc8 --- /dev/null +++ b/esphome/core/preferences.cpp @@ -0,0 +1,233 @@ +#include "esphome/core/preferences.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef USE_ESP8266_PREFERENCES_FLASH +extern "C" { +#include "spi_flash.h" +} +#endif + +namespace esphome { + +static const char *TAG = "preferences"; + +ESPPreferenceObject::ESPPreferenceObject() : rtc_offset_(0), length_words_(0), type_(0), data_(nullptr) {} +ESPPreferenceObject::ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type) + : rtc_offset_(rtc_offset), length_words_(length), type_(type) { + this->data_ = new uint32_t[this->length_words_ + 1]; + for (uint32_t i = 0; i < this->length_words_ + 1; i++) + this->data_[i] = 0; +} +bool ESPPreferenceObject::load_() { + if (!this->is_initialized()) { + ESP_LOGV(TAG, "Load Pref Not initialized!"); + return false; + } + if (!this->load_internal_()) + return false; + + bool valid = this->data_[this->length_words_] == this->calculate_crc_(); + + ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_, // NOLINT + YESNO(valid), this->data_[0], this->data_[1], this->type_, this->calculate_crc_()); + return valid; +} +bool ESPPreferenceObject::save_() { + if (!this->is_initialized()) { + ESP_LOGV(TAG, "Save Pref Not initialized!"); + return false; + } + + this->data_[this->length_words_] = this->calculate_crc_(); + if (!this->save_internal_()) + return false; + ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_, // NOLINT + this->data_[0], this->data_[1], this->type_, this->calculate_crc_()); + return true; +} + +#ifdef ARDUINO_ARCH_ESP8266 + +#define ESP_RTC_USER_MEM_START 0x60001200 +#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) +#define ESP_RTC_USER_MEM_SIZE_WORDS 128 +#define ESP_RTC_USER_MEM_SIZE_BYTES ESP_RTC_USER_MEM_SIZE_WORDS * 4 + +static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { + if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { + return false; + } + *dest = ESP_RTC_USER_MEM[index]; + return true; +} + +#ifdef USE_ESP8266_PREFERENCES_FLASH +static bool esp8266_preferences_modified = false; +#endif + +static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) { + if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { + return false; + } + if (index < 32 && global_preferences.is_prevent_write()) { + return false; + } + + auto *ptr = &ESP_RTC_USER_MEM[index]; +#ifdef USE_ESP8266_PREFERENCES_FLASH + if (*ptr != value) { + esp8266_preferences_modified = true; + } +#endif + *ptr = value; + return true; +} + +#ifdef USE_ESP8266_PREFERENCES_FLASH +extern "C" uint32_t _SPIFFS_end; + +static const uint32_t get_esp8266_flash_sector() { return (uint32_t(&_SPIFFS_end) - 0x40200000) / SPI_FLASH_SEC_SIZE; } +static const uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; } + +static void load_esp8266_flash() { + ESP_LOGVV(TAG, "Loading preferences from flash..."); + disable_interrupts(); + spi_flash_read(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES); + enable_interrupts(); +} +static void save_esp8266_flash() { + if (!esp8266_preferences_modified) + return; + + ESP_LOGVV(TAG, "Saving preferences to flash..."); + disable_interrupts(); + auto erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); + if (erase_res != SPI_FLASH_RESULT_OK) { + enable_interrupts(); + ESP_LOGV(TAG, "Erase ESP8266 flash failed!"); + return; + } + + auto write_res = spi_flash_write(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES); + enable_interrupts(); + if (write_res != SPI_FLASH_RESULT_OK) { + ESP_LOGV(TAG, "Write ESP8266 flash failed!"); + return; + } + + esp8266_preferences_modified = false; +} +#endif + +bool ESPPreferenceObject::save_internal_() { + for (uint32_t i = 0; i <= this->length_words_; i++) { + if (!esp_rtc_user_mem_write(this->rtc_offset_ + i, this->data_[i])) + return false; + } + +#ifdef USE_ESP8266_PREFERENCES_FLASH + save_esp8266_flash(); +#endif + return true; +} +bool ESPPreferenceObject::load_internal_() { + for (uint32_t i = 0; i <= this->length_words_; i++) { + if (!esp_rtc_user_mem_read(this->rtc_offset_ + i, &this->data_[i])) + return false; + } + return true; +} +ESPPreferences::ESPPreferences() + // offset starts from start of user RTC mem (64 words before that are reserved for system), + // an additional 32 words at the start of user RTC are for eboot (OTA, see eboot_command.h), + // which will be reset each time OTA occurs + : current_offset_(0) {} + +void ESPPreferences::begin(const std::string &name) { +#ifdef USE_ESP8266_PREFERENCES_FLASH + load_esp8266_flash(); +#endif +} + +ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) { + uint32_t start = this->current_offset_; + uint32_t end = start + length + 1; + bool in_normal = start < 96; + // Normal: offset 0-95 maps to RTC offset 32 - 127, + // Eboot: offset 96-127 maps to RTC offset 0 - 31 words + if (in_normal && end > 96) { + // start is in normal but end is not -> switch to Eboot + this->current_offset_ = start = 96; + end = start + length + 1; + in_normal = false; + } + + if (end > 128) { + // Doesn't fit in data, return uninitialized preference obj. + return ESPPreferenceObject(); + } + + uint32_t rtc_offset; + if (in_normal) { + rtc_offset = start + 32; + } else { + rtc_offset = start - 96; + } + + auto pref = ESPPreferenceObject(rtc_offset, length, type); + this->current_offset_ += length + 1; + return pref; +} +void ESPPreferences::prevent_write(bool prevent) { this->prevent_write_ = prevent; } +bool ESPPreferences::is_prevent_write() { return this->prevent_write_; } +#endif + +#ifdef ARDUINO_ARCH_ESP32 +bool ESPPreferenceObject::save_internal_() { + char key[32]; + sprintf(key, "%u", this->rtc_offset_); + uint32_t len = (this->length_words_ + 1) * 4; + size_t ret = global_preferences.preferences_.putBytes(key, this->data_, len); + if (ret != len) { + ESP_LOGV(TAG, "putBytes failed!"); + return false; + } + return true; +} +bool ESPPreferenceObject::load_internal_() { + char key[32]; + sprintf(key, "%u", this->rtc_offset_); + uint32_t len = (this->length_words_ + 1) * 4; + size_t ret = global_preferences.preferences_.getBytes(key, this->data_, len); + if (ret != len) { + ESP_LOGV(TAG, "getBytes failed!"); + return false; + } + return true; +} +ESPPreferences::ESPPreferences() : current_offset_(0) {} +void ESPPreferences::begin(const std::string &name) { + const std::string key = truncate_string(name, 15); + ESP_LOGV(TAG, "Opening preferences with key '%s'", key.c_str()); + this->preferences_.begin(key.c_str()); +} + +ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) { + auto pref = ESPPreferenceObject(this->current_offset_, length, type); + this->current_offset_++; + return pref; +} +#endif +uint32_t ESPPreferenceObject::calculate_crc_() const { + uint32_t crc = this->type_; + for (size_t i = 0; i < this->length_words_; i++) { + crc ^= (this->data_[i] * 2654435769UL) >> 1; + } + return crc; +} +bool ESPPreferenceObject::is_initialized() const { return this->data_ != nullptr; } + +ESPPreferences global_preferences; + +} // namespace esphome diff --git a/esphome/core/preferences.h b/esphome/core/preferences.h new file mode 100644 index 0000000000..32f5256492 --- /dev/null +++ b/esphome/core/preferences.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#include "esphome/core/esphal.h" + +namespace esphome { + +class ESPPreferenceObject { + public: + ESPPreferenceObject(); + ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type); + + template bool save(T *src); + + template bool load(T *dest); + + bool is_initialized() const; + + protected: + bool save_(); + bool load_(); + bool save_internal_(); + bool load_internal_(); + + uint32_t calculate_crc_() const; + + size_t rtc_offset_; + size_t length_words_; + uint32_t type_; + uint32_t *data_; +}; + +class ESPPreferences { + public: + ESPPreferences(); + void begin(const std::string &name); + ESPPreferenceObject make_preference(size_t length, uint32_t type); + template ESPPreferenceObject make_preference(uint32_t type); + +#ifdef ARDUINO_ARCH_ESP8266 + /** On the ESP8266, we can't override the first 128 bytes during OTA uploads + * as the eboot parameters are stored there. Writing there during an OTA upload + * would invalidate applying the new firmware. During normal operation, we use + * this part of the RTC user memory, but stop writing to it during OTA uploads. + * + * @param prevent Whether to prevent writing to the first 32 words of RTC user memory. + */ + void prevent_write(bool prevent); + bool is_prevent_write(); +#endif + + protected: + friend ESPPreferenceObject; + + uint32_t current_offset_; +#ifdef ARDUINO_ARCH_ESP32 + Preferences preferences_; +#endif +#ifdef ARDUINO_ARCH_ESP8266 + bool prevent_write_{false}; +#endif +}; + +extern ESPPreferences global_preferences; + +template ESPPreferenceObject ESPPreferences::make_preference(uint32_t type) { + return this->make_preference((sizeof(T) + 3) / 4, type); +} + +template bool ESPPreferenceObject::save(T *src) { + if (!this->is_initialized()) + return false; + memset(this->data_, 0, this->length_words_ * 4); + memcpy(this->data_, src, sizeof(T)); + return this->save_(); +} + +template bool ESPPreferenceObject::load(T *dest) { + memset(this->data_, 0, this->length_words_ * 4); + if (!this->load_()) + return false; + + memcpy(dest, this->data_, sizeof(T)); + return true; +} + +} // namespace esphome diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp new file mode 100644 index 0000000000..9e70f8b6e4 --- /dev/null +++ b/esphome/core/util.cpp @@ -0,0 +1,120 @@ +#include "esphome/core/util.h" +#include "esphome/core/defines.h" +#include "esphome/core/application.h" + +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif + +#ifdef USE_ETHERNET +#include "esphome/components/ethernet/ethernet_component.h" +#endif + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include +#endif + +namespace esphome { + +bool network_is_connected() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected()) + return true; +#endif + +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->is_connected(); +#endif + + return false; +} + +void network_setup() { + bool ready = true; +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) { + ethernet::global_eth_component->call_setup(); + ready = false; + } +#endif + +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) { + wifi::global_wifi_component->call_setup(); + ready = false; + } +#endif + + while (!ready) { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) { + ethernet::global_eth_component->call_loop(); + ready = ready || ethernet::global_eth_component->can_proceed(); + } +#endif +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) { + wifi::global_wifi_component->call_loop(); + ready = ready || wifi::global_wifi_component->can_proceed(); + } +#endif + + App.feed_wdt(); + } +} +void network_tick() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) + ethernet::global_eth_component->call_loop(); +#endif +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + wifi::global_wifi_component->call_loop(); +#endif +} + +void network_setup_mdns() { + MDNS.begin(App.get_name().c_str()); +#ifdef USE_API + if (api::global_api_server != nullptr) { + MDNS.addService("esphomelib", "tcp", api::global_api_server->get_port()); + // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version + MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION); + MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str()); + } else { +#endif + // Publish "http" service if not using native API. + // This is just to have *some* mDNS service so that .local resolution works + MDNS.addService("http", "tcp", 80); + MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION); +#ifdef USE_API + } +#endif +} +void network_tick_mdns() { +#ifdef ARDUINO_ARCH_ESP8266 + MDNS.update(); +#endif +} + +std::string network_get_address() { +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) + return ethernet::global_eth_component->get_use_address(); +#endif +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr) + return wifi::global_wifi_component->get_use_address(); +#endif + return ""; +} + +} // namespace esphome diff --git a/esphome/core/util.h b/esphome/core/util.h new file mode 100644 index 0000000000..f47eeb8439 --- /dev/null +++ b/esphome/core/util.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace esphome { + +/// Return whether the node is connected to the network (through wifi, eth, ...) +bool network_is_connected(); +/// Get the active network hostname +std::string network_get_address(); + +/// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode) +void network_setup(); +void network_tick(); +void network_setup_mdns(); +void network_tick_mdns(); + +} // namespace esphome diff --git a/esphome/core_config.py b/esphome/core_config.py index 86cc66f3cc..28f2254c65 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -2,31 +2,28 @@ import logging import os import re -import voluptuous as vol - -from esphome import automation, pins +import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation, pins from esphome.const import ARDUINO_VERSION_ESP32_DEV, ARDUINO_VERSION_ESP8266_DEV, \ - CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BRANCH, CONF_BUILD_PATH, \ - CONF_COMMIT, CONF_ESPHOME, CONF_ESPHOME_CORE_VERSION, CONF_INCLUDES, CONF_LIBRARIES, \ - CONF_LOCAL, CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ - CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_REPOSITORY, CONF_TAG, CONF_TRIGGER_ID, \ - CONF_USE_CUSTOM_CODE, ESPHOME_CORE_VERSION, ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266, \ - CONF_ESP8266_RESTORE_FROM_FLASH -from esphome.core import CORE, EsphomeError -from esphome.cpp_generator import Pvariable, RawExpression, add -from esphome.cpp_types import App, const_char_ptr, esphome_ns -from esphome.py_compat import text_type + CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, \ + CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ + CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ + CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_TRIGGER_ID, \ + CONF_ESP8266_RESTORE_FROM_FLASH, __version__, ARDUINO_VERSION_ESP8266_2_3_0, \ + ARDUINO_VERSION_ESP8266_2_5_0, ARDUINO_VERSION_ESP8266_2_5_1, ARDUINO_VERSION_ESP8266_2_5_2 +from esphome.core import CORE, coroutine_with_priority +from esphome.helpers import copy_file_if_changed, walk_files +from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS _LOGGER = logging.getLogger(__name__) -LIBRARY_URI_REPO = u'https://github.com/esphome/esphome-core.git' -GITHUB_ARCHIVE_ZIP = u'https://github.com/esphome/esphome-core/archive/{}.zip' - BUILD_FLASH_MODES = ['qio', 'qout', 'dio', 'dout'] -StartupTrigger = esphome_ns.StartupTrigger -ShutdownTrigger = esphome_ns.ShutdownTrigger -LoopTrigger = esphome_ns.LoopTrigger +StartupTrigger = cg.esphome_ns.class_('StartupTrigger', cg.Component, automation.Trigger.template()) +ShutdownTrigger = cg.esphome_ns.class_('ShutdownTrigger', cg.Component, + automation.Trigger.template()) +LoopTrigger = cg.esphome_ns.class_('LoopTrigger', cg.Component, + automation.Trigger.template()) VERSION_REGEX = re.compile(r'^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$') @@ -40,79 +37,16 @@ def validate_board(value): raise NotImplementedError if value not in board_pins: - raise vol.Invalid(u"Could not find board '{}'. Valid boards are {}".format( + raise cv.Invalid(u"Could not find board '{}'. Valid boards are {}".format( value, u', '.join(pins.ESP8266_BOARD_PINS.keys()))) return value -def validate_simple_esphome_core_version(value): - value = cv.string_strict(value) - if value.upper() == 'LATEST': - if ESPHOME_CORE_VERSION == 'dev': - return validate_simple_esphome_core_version('dev') - return { - CONF_REPOSITORY: LIBRARY_URI_REPO, - CONF_TAG: 'v' + ESPHOME_CORE_VERSION, - } - if value.upper() == 'DEV': - return { - CONF_REPOSITORY: LIBRARY_URI_REPO, - CONF_BRANCH: 'dev' - } - if VERSION_REGEX.match(value) is not None: - return { - CONF_REPOSITORY: LIBRARY_URI_REPO, - CONF_TAG: 'v' + value, - } - raise vol.Invalid("Only simple esphome core versions!") - - -def validate_local_esphome_core_version(value): - value = cv.directory(value) - path = CORE.relative_path(value) - library_json = os.path.join(path, 'library.json') - if not os.path.exists(library_json): - raise vol.Invalid(u"Could not find '{}' file. '{}' does not seem to point to an " - u"esphome-core copy.".format(library_json, value)) - return value - - -def validate_commit(value): - value = cv.string(value) - if re.match(r"^[0-9a-f]{7,}$", value) is None: - raise vol.Invalid("Commit option only accepts commit hashes in hex format.") - return value - - -ESPHOME_CORE_VERSION_SCHEMA = vol.Any( - validate_simple_esphome_core_version, - cv.Schema({ - vol.Required(CONF_LOCAL): validate_local_esphome_core_version, - }), - vol.All( - cv.Schema({ - vol.Optional(CONF_REPOSITORY, default=LIBRARY_URI_REPO): cv.string, - vol.Optional(CONF_COMMIT): validate_commit, - vol.Optional(CONF_BRANCH): cv.string, - vol.Optional(CONF_TAG): cv.string, - }), - cv.has_at_most_one_key(CONF_COMMIT, CONF_BRANCH, CONF_TAG) - ), -) - - -def validate_platform(value): - value = cv.string(value) - if value.upper() in ('ESP8266', 'ESPRESSIF8266'): - return ESP_PLATFORM_ESP8266 - if value.upper() in ('ESP32', 'ESPRESSIF32'): - return ESP_PLATFORM_ESP32 - raise vol.Invalid(u"Invalid platform '{}'. Only options are ESP8266 and ESP32. Please note " - u"the old way to use the latest arduino framework version has been split up " - u"into the arduino_version configuration option.".format(value)) - +validate_platform = cv.one_of('ESP32', 'ESP8266', upper=True) PLATFORMIO_ESP8266_LUT = { + '2.5.2': 'espressif8266@2.2.0', + '2.5.1': 'espressif8266@2.1.0', '2.5.0': 'espressif8266@2.0.1', '2.4.2': 'espressif8266@1.8.0', '2.4.1': 'espressif8266@1.7.3', @@ -126,6 +60,7 @@ PLATFORMIO_ESP8266_LUT = { PLATFORMIO_ESP32_LUT = { '1.0.0': 'espressif32@1.4.0', '1.0.1': 'espressif32@1.6.0', + '1.0.2': 'espressif32@1.8.0', 'RECOMMENDED': 'espressif32@1.6.0', 'LATEST': 'espressif32', 'DEV': ARDUINO_VERSION_ESP32_DEV, @@ -137,17 +72,17 @@ def validate_arduino_version(value): value_ = value.upper() if CORE.is_esp8266: if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP8266_LUT: - raise vol.Invalid("Unfortunately the arduino framework version '{}' is unsupported " - "at this time. You can override this by manually using " - "espressif8266@") + raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported " + "at this time. You can override this by manually using " + "espressif8266@") if value_ in PLATFORMIO_ESP8266_LUT: return PLATFORMIO_ESP8266_LUT[value_] return value if CORE.is_esp32: if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP32_LUT: - raise vol.Invalid("Unfortunately the arduino framework version '{}' is unsupported " - "at this time. You can override this by manually using " - "espressif32@") + raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported " + "at this time. You can override this by manually using " + "espressif32@") if value_ in PLATFORMIO_ESP32_LUT: return PLATFORMIO_ESP32_LUT[value_] return value @@ -158,98 +93,171 @@ def default_build_path(): return CORE.name +VALID_INCLUDE_EXTS = {'.h', '.hpp', '.tcc', '.ino', '.cpp', '.c'} + + +def valid_include(value): + try: + return cv.directory(value) + except cv.Invalid: + pass + value = cv.file_(value) + _, ext = os.path.splitext(value) + if ext not in VALID_INCLUDE_EXTS: + raise cv.Invalid(u"Include has invalid file extension {} - valid extensions are {}" + u"".format(ext, ', '.join(VALID_INCLUDE_EXTS))) + return value + + CONFIG_SCHEMA = cv.Schema({ - vol.Required(CONF_NAME): cv.valid_name, - vol.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESPRESSIF8266', 'ESP32', 'ESPRESSIF32', - upper=True), - vol.Required(CONF_BOARD): validate_board, - vol.Optional(CONF_ESPHOME_CORE_VERSION, default='latest'): ESPHOME_CORE_VERSION_SCHEMA, - vol.Optional(CONF_ARDUINO_VERSION, default='recommended'): validate_arduino_version, - vol.Optional(CONF_USE_CUSTOM_CODE, default=False): cv.boolean, - vol.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, - vol.Optional(CONF_PLATFORMIO_OPTIONS): cv.Schema({ - cv.string_strict: vol.Any([cv.string], cv.string), + cv.Required(CONF_NAME): cv.valid_name, + cv.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESP32', upper=True), + cv.Required(CONF_BOARD): validate_board, + cv.Optional(CONF_ARDUINO_VERSION, default='recommended'): validate_arduino_version, + cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, + cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema({ + cv.string_strict: cv.Any([cv.string], cv.string), }), - vol.Optional(CONF_ESP8266_RESTORE_FROM_FLASH): vol.All(cv.only_on_esp8266, cv.boolean), + cv.SplitDefault(CONF_ESP8266_RESTORE_FROM_FLASH, esp8266=False): cv.All(cv.only_on_esp8266, + cv.boolean), - vol.Optional(CONF_BOARD_FLASH_MODE, default='dout'): cv.one_of(*BUILD_FLASH_MODES, lower=True), - vol.Optional(CONF_ON_BOOT): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(StartupTrigger), - vol.Optional(CONF_PRIORITY): cv.float_, + cv.SplitDefault(CONF_BOARD_FLASH_MODE, esp8266='dout'): cv.one_of(*BUILD_FLASH_MODES, + lower=True), + cv.Optional(CONF_ON_BOOT): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger), + cv.Optional(CONF_PRIORITY, default=600.0): cv.float_, }), - vol.Optional(CONF_ON_SHUTDOWN): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(ShutdownTrigger), + cv.Optional(CONF_ON_SHUTDOWN): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ShutdownTrigger), }), - vol.Optional(CONF_ON_LOOP): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(LoopTrigger), + cv.Optional(CONF_ON_LOOP): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoopTrigger), }), - vol.Optional(CONF_INCLUDES): cv.ensure_list(cv.file_), - vol.Optional(CONF_LIBRARIES): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include), + cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict), - vol.Optional('esphomelib_version'): cv.invalid("The esphomelib_version has been renamed to " - "esphome_core_version in 1.11.0"), + cv.Optional('esphome_core_version'): cv.invalid("The esphome_core_version option has been " + "removed in 1.13 - the esphome core source " + "files are now bundled with ESPHome.") +}) + +PRELOAD_CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_NAME): cv.valid_name, + cv.Required(CONF_PLATFORM): validate_platform, +}, extra=cv.ALLOW_EXTRA) + +PRELOAD_CONFIG_SCHEMA2 = PRELOAD_CONFIG_SCHEMA.extend({ + cv.Required(CONF_BOARD): validate_board, + cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, }) def preload_core_config(config): + core_key = 'esphome' if 'esphomeyaml' in config: _LOGGER.warning("The esphomeyaml section has been renamed to esphome in 1.11.0. " "Please replace 'esphomeyaml:' in your configuration with 'esphome:'.") config[CONF_ESPHOME] = config.pop('esphomeyaml') + core_key = 'esphomeyaml' if CONF_ESPHOME not in config: - raise EsphomeError(u"No esphome section in config") - core_conf = config[CONF_ESPHOME] - if CONF_PLATFORM not in core_conf: - raise EsphomeError("esphome.platform not specified.") - if CONF_BOARD not in core_conf: - raise EsphomeError("esphome.board not specified.") - if CONF_NAME not in core_conf: - raise EsphomeError("esphome.name not specified.") - - try: - CORE.esp_platform = validate_platform(core_conf[CONF_PLATFORM]) - CORE.board = validate_board(core_conf[CONF_BOARD]) - CORE.name = cv.valid_name(core_conf[CONF_NAME]) - CORE.build_path = CORE.relative_path( - cv.string(core_conf.get(CONF_BUILD_PATH, default_build_path()))) - except vol.Invalid as e: - raise EsphomeError(text_type(e)) + raise cv.RequiredFieldInvalid("required key not provided", CONF_ESPHOME) + with cv.prepend_path(core_key): + out = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) + CORE.name = out[CONF_NAME] + CORE.esp_platform = out[CONF_PLATFORM] + with cv.prepend_path(core_key): + out2 = PRELOAD_CONFIG_SCHEMA2(config[CONF_ESPHOME]) + CORE.board = out2[CONF_BOARD] + CORE.build_path = CORE.relative_config_path(out2[CONF_BUILD_PATH]) +def include_file(path, basename): + parts = basename.split(os.path.sep) + dst = CORE.relative_src_path(*parts) + copy_file_if_changed(path, dst) + + _, ext = os.path.splitext(path) + if ext in ['.h', '.hpp', '.tcc']: + # Header, add include statement + cg.add_global(cg.RawStatement(u'#include "{}"'.format(basename))) + + +@coroutine_with_priority(-1000.0) +def add_includes(includes): + # Add includes at the very end, so that the included files can access global variables + for include in includes: + path = CORE.relative_config_path(include) + if os.path.isdir(path): + # Directory, copy tree + for p in walk_files(path): + basename = os.path.relpath(p, os.path.dirname(path)) + include_file(p, basename) + else: + # Copy file + basename = os.path.basename(path) + include_file(path, basename) + + +@coroutine_with_priority(100.0) def to_code(config): - add(App.set_name(config[CONF_NAME])) + cg.add_global(cg.global_ns.namespace('esphome').using) + cg.add_define('ESPHOME_VERSION', __version__) + cg.add(cg.App.pre_setup(config[CONF_NAME], cg.RawExpression('__DATE__ ", " __TIME__'))) for conf in config.get(CONF_ON_BOOT, []): - rhs = App.register_component(StartupTrigger.new(conf.get(CONF_PRIORITY))) - trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) - automation.build_automations(trigger, [], conf) + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY)) + yield cg.register_component(trigger, conf) + yield automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_SHUTDOWN, []): - trigger = Pvariable(conf[CONF_TRIGGER_ID], ShutdownTrigger.new()) - automation.build_automations(trigger, [(const_char_ptr, 'x')], conf) + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield cg.register_component(trigger, conf) + yield automation.build_automation(trigger, [], conf) for conf in config.get(CONF_ON_LOOP, []): - rhs = App.register_component(LoopTrigger.new()) - trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) - automation.build_automations(trigger, [], conf) + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield cg.register_component(trigger, conf) + yield automation.build_automation(trigger, [], conf) - add(App.set_compilation_datetime(RawExpression('__DATE__ ", " __TIME__'))) + # Build flags + if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES and \ + CORE.arduino_version != ARDUINO_VERSION_ESP8266_2_3_0: + flash_size = ESP8266_FLASH_SIZES[CORE.board] + ld_scripts = ESP8266_LD_SCRIPTS[flash_size] + ld_script = None + if CORE.arduino_version in ('espressif8266@1.8.0', 'espressif8266@1.7.3', + 'espressif8266@1.6.0'): + ld_script = ld_scripts[0] + elif CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, ARDUINO_VERSION_ESP8266_2_5_0, + ARDUINO_VERSION_ESP8266_2_5_1, ARDUINO_VERSION_ESP8266_2_5_2): + ld_script = ld_scripts[1] -def lib_deps(config): - return set(config.get(CONF_LIBRARIES, [])) + if ld_script is not None: + cg.add_build_flag('-Wl,-T{}'.format(ld_script)) + cg.add_build_flag('-fno-exceptions') -def includes(config): - ret = [] - for include in config.get(CONF_INCLUDES, []): - path = CORE.relative_path(include) - res = os.path.relpath(path, CORE.relative_build_path('src')) - ret.append(u'#include "{}"'.format(res)) - return ret + # Libraries + if CORE.is_esp32: + cg.add_library('Preferences', None) + cg.add_library('ESPmDNS', None) + elif CORE.is_esp8266: + cg.add_library('ESP8266WiFi', None) + cg.add_library('ESP8266mDNS', None) + for lib in config[CONF_LIBRARIES]: + if '@' in lib: + name, vers = lib.split('@', 1) + cg.add_library(name, vers) + else: + cg.add_library(lib, None) -def required_build_flags(config): + cg.add_build_flag('-Wno-unused-variable') + cg.add_build_flag('-Wno-unused-but-set-variable') + cg.add_build_flag('-Wno-sign-compare') if config.get(CONF_ESP8266_RESTORE_FROM_FLASH, False): - return ['-DUSE_ESP8266_PREFERENCES_FLASH'] - return [] + cg.add_define('USE_ESP8266_PREFERENCES_FLASH') + + if config[CONF_INCLUDES]: + CORE.add_job(add_includes, config[CONF_INCLUDES]) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index f2f2f058ff..c1e4a87179 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,36 +1,26 @@ -from collections import OrderedDict +import inspect + import math -from esphome.core import CORE, HexInt, Lambda, TimePeriod, TimePeriodMicroseconds, \ - TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes -from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last - # pylint: disable=unused-import, wrong-import-order -from typing import Any, Generator, List, Optional, Tuple, Union # noqa -from esphome.core import ID # noqa -from esphome.py_compat import text_type, string_types, integer_types +from typing import Any, Generator, List, Optional, Tuple, Type, Union, Dict, Callable # noqa + +from esphome.core import ( # noqa + CORE, HexInt, ID, Lambda, TimePeriod, TimePeriodMicroseconds, + TimePeriodMilliseconds, TimePeriodMinutes, TimePeriodSeconds, coroutine, Library, Define, + EnumValue) +from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last +from esphome.py_compat import integer_types, string_types, text_type +from esphome.util import OrderedDict class Expression(object): - def __init__(self): - self.requires = [] - self.required = False - def __str__(self): raise NotImplementedError - def require(self): - self.required = True - for require in self.requires: - if require.required: - continue - require.require() - def has_side_effects(self): - return self.required - - -SafeExpType = Union[Expression, bool, str, text_type, int, float, TimePeriod] +SafeExpType = Union[Expression, bool, str, text_type, int, float, TimePeriod, + Type[bool], Type[int], Type[float], List[Any]] class RawExpression(Expression): @@ -50,15 +40,23 @@ class AssignmentExpression(Expression): self.modifier = modifier self.name = name self.rhs = safe_exp(rhs) - self.requires.append(self.rhs) self.obj = obj def __str__(self): - type_ = self.type - return u"{} {}{} = {}".format(type_, self.modifier, self.name, self.rhs) + if self.type is None: + return u"{} = {}".format(self.name, self.rhs) + return u"{} {}{} = {}".format(self.type, self.modifier, self.name, self.rhs) - def has_side_effects(self): - return self.rhs.has_side_effects() + +class VariableDeclarationExpression(Expression): + def __init__(self, type, modifier, name): + super(VariableDeclarationExpression, self).__init__() + self.type = type + self.modifier = modifier + self.name = name + + def __str__(self): + return u"{} {}{}".format(self.type, self.modifier, self.name) class ExpressionList(Expression): @@ -68,26 +66,27 @@ class ExpressionList(Expression): args = list(args) while args and args[-1] is None: args.pop() - self.args = [] - for arg in args: - exp = safe_exp(arg) - self.requires.append(exp) - self.args.append(exp) + self.args = [safe_exp(arg) for arg in args] def __str__(self): text = u", ".join(text_type(x) for x in self.args) return indent_all_but_first_and_last(text) + def __iter__(self): + return iter(self.args) + class TemplateArguments(Expression): def __init__(self, *args): # type: (*SafeExpType) -> None super(TemplateArguments, self).__init__() self.args = ExpressionList(*args) - self.requires.append(self.args) def __str__(self): return u'<{}>'.format(self.args) + def __iter__(self): + return iter(self.args) + class CallExpression(Expression): def __init__(self, base, *args): # type: (Expression, *SafeExpType) -> None @@ -95,12 +94,10 @@ class CallExpression(Expression): self.base = base if args and isinstance(args[0], TemplateArguments): self.template_args = args[0] - self.requires.append(self.template_args) args = args[1:] else: self.template_args = None self.args = ExpressionList(*args) - self.requires.append(self.args) def __str__(self): if self.template_args is not None: @@ -112,8 +109,6 @@ class StructInitializer(Expression): def __init__(self, base, *args): # type: (Expression, *Tuple[str, SafeExpType]) -> None super(StructInitializer, self).__init__() self.base = base - if isinstance(base, Expression): - self.requires.append(base) if not isinstance(args, OrderedDict): args = OrderedDict(args) self.args = OrderedDict() @@ -122,7 +117,6 @@ class StructInitializer(Expression): continue exp = safe_exp(value) self.args[key] = exp - self.requires.append(exp) def __str__(self): cpp = u'{}{{\n'.format(self.base) @@ -142,7 +136,6 @@ class ArrayInitializer(Expression): continue exp = safe_exp(arg) self.args.append(exp) - self.requires.append(exp) def __str__(self): if not self.args: @@ -160,7 +153,7 @@ class ArrayInitializer(Expression): class ParameterExpression(Expression): def __init__(self, type, id): super(ParameterExpression, self).__init__() - self.type = type + self.type = safe_exp(type) self.id = id def __str__(self): @@ -175,7 +168,6 @@ class ParameterListExpression(Expression): if not isinstance(parameter, ParameterExpression): parameter = ParameterExpression(*parameter) self.parameters.append(parameter) - self.requires.append(parameter) def __str__(self): return u", ".join(text_type(x) for x in self.parameters) @@ -188,13 +180,8 @@ class LambdaExpression(Expression): if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) self.parameters = parameters - self.requires.append(self.parameters) self.capture = capture - self.return_type = return_type - if return_type is not None: - self.requires.append(return_type) - for i in range(1, len(parts), 3): - self.requires.append(parts[i]) + self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): cpp = u'[{}]({})'.format(self.capture, self.parameters) @@ -271,8 +258,15 @@ def safe_exp( obj # type: Union[Expression, bool, str, unicode, int, long, float, TimePeriod, list] ): # type: (...) -> Expression + """Try to convert obj to an expression by automatically converting native python types to + expressions/literals. + """ + from esphome.cpp_types import bool_, float_, int32 + if isinstance(obj, Expression): return obj + if isinstance(obj, EnumValue): + return safe_exp(obj.enum_value) if isinstance(obj, bool): return BoolLiteral(obj) if isinstance(obj, string_types): @@ -293,6 +287,18 @@ def safe_exp( return IntLiteral(int(obj.total_minutes)) if isinstance(obj, (tuple, list)): return ArrayInitializer(*[safe_exp(o) for o in obj]) + if obj is bool: + return bool_ + if obj is int: + return int32 + if obj is float: + return float_ + if isinstance(obj, ID): + raise ValueError(u"Object {} is an ID. Did you forget to register the variable?" + u"".format(obj)) + if inspect.isgenerator(obj): + raise ValueError(u"Object {} is a coroutine. Did you forget to await the expression with " + u"'yield'?".format(obj)) raise ValueError(u"Object is not an expression", obj) @@ -322,6 +328,17 @@ class ExpressionStatement(Statement): return u"{};".format(self.expression) +class LineComment(Statement): + def __init__(self, value): # type: (unicode) -> None + super(LineComment, self).__init__() + self._value = value + + def __str__(self): + parts = self._value.split(u'\n') + parts = [u'// {}'.format(x) for x in parts] + return u'\n'.join(parts) + + class ProgmemAssignmentExpression(AssignmentExpression): def __init__(self, type, name, rhs, obj): super(ProgmemAssignmentExpression, self).__init__( @@ -339,7 +356,6 @@ def progmem_array(id, rhs): assignment = ProgmemAssignmentExpression(id.type, id, rhs, obj) CORE.add(assignment) CORE.register_variable(id, obj) - obj.requires.append(assignment) return obj @@ -350,69 +366,184 @@ def statement(expression): # type: (Union[Expression, Statement]) -> Statement def variable(id, # type: ID - rhs, # type: Expression + rhs, # type: SafeExpType type=None # type: MockObj ): # type: (...) -> MockObj + """Declare a new variable (not pointer type) in the code generation. + + :param id: The ID used to declare the variable. + :param rhs: The expression to place on the right hand side of the assignment. + :param type: Manually define a type for the variable, only use this when it's not possible + to do so during config validation phase (for example because of template arguments). + + :returns The new variable as a MockObj. + """ + assert isinstance(id, ID) rhs = safe_exp(rhs) obj = MockObj(id, u'.') - id.type = type or id.type + if type is not None: + id.type = type assignment = AssignmentExpression(id.type, '', id, rhs, obj) CORE.add(assignment) CORE.register_variable(id, obj) - obj.requires.append(assignment) return obj def Pvariable(id, # type: ID - rhs, # type: Expression - has_side_effects=True, # type: bool + rhs, # type: SafeExpType type=None # type: MockObj ): # type: (...) -> MockObj + """Declare a new pointer variable in the code generation. + + :param id: The ID used to declare the variable. + :param rhs: The expression to place on the right hand side of the assignment. + :param type: Manually define a type for the variable, only use this when it's not possible + to do so during config validation phase (for example because of template arguments). + + :returns The new variable as a MockObj. + """ rhs = safe_exp(rhs) - if not has_side_effects and hasattr(rhs, '_has_side_effects'): - # pylint: disable=attribute-defined-outside-init, protected-access - rhs._has_side_effects = False - obj = MockObj(id, u'->', has_side_effects=has_side_effects) - id.type = type or id.type - assignment = AssignmentExpression(id.type, '*', id, rhs, obj) + obj = MockObj(id, u'->') + if type is not None: + id.type = type + decl = VariableDeclarationExpression(id.type, '*', id) + CORE.add_global(decl) + assignment = AssignmentExpression(None, None, id, rhs, obj) CORE.add(assignment) CORE.register_variable(id, obj) - obj.requires.append(assignment) return obj +def new_Pvariable(id, # type: ID + *args # type: *SafeExpType + ): + """Declare a new pointer variable in the code generation by calling it's constructor + with the given arguments. + + :param id: The ID used to declare the variable (also specifies the type). + :param args: The values to pass to the constructor. + + :returns The new variable as a MockObj. + """ + if args and isinstance(args[0], TemplateArguments): + id = id.copy() + id.type = id.type.template(args[0]) + args = args[1:] + rhs = id.type.new(*args) + return Pvariable(id, rhs) + + def add(expression, # type: Union[Expression, Statement] - require=True # type: bool ): # type: (...) -> None - CORE.add(expression, require=require) + """Add an expression to the codegen section. + + After this is called, the given given expression will + show up in the setup() function after this has been called. + """ + CORE.add(expression) +def add_global(expression, # type: Union[SafeExpType, Statement] + ): + # type: (...) -> None + """Add an expression to the codegen global storage (above setup()).""" + CORE.add_global(expression) + + +def add_library(name, # type: str + version # type: Optional[str] + ): + # type: (...) -> None + """Add a library to the codegen library storage. + + :param name: The name of the library (for example 'AsyncTCP') + :param version: The version of the library, may be None. + """ + CORE.add_library(Library(name, version)) + + +def add_build_flag(build_flag, # type: str + ): + # type: (...) -> None + """Add a global build flag to the compiler flags.""" + CORE.add_build_flag(build_flag) + + +def add_define(name, # type: str + value=None, # type: Optional[SafeExpType] + ): + # type: (...) -> None + """Add a global define to the auto-generated defines.h file. + + Optionally define a value to set this define to. + """ + if value is None: + CORE.add_define(Define(name)) + else: + CORE.add_define(Define(name, safe_exp(value))) + + +@coroutine def get_variable(id): # type: (ID) -> Generator[MockObj] - for var in CORE.get_variable(id): - yield None + """ + Wait for the given ID to be defined in the code generation and + return it as a MockObj. + + This is a coroutine, you need to await it with a 'yield' expression! + + :param id: The ID to retrieve + :return: The variable as a MockObj. + """ + var = yield CORE.get_variable(id) yield var +@coroutine +def get_variable_with_full_id(id): # type: (ID) -> Generator[ID, MockObj] + """ + Wait for the given ID to be defined in the code generation and + return it as a MockObj. + + This is a coroutine, you need to await it with a 'yield' expression! + + :param id: The ID to retrieve + :return: The variable as a MockObj. + """ + full_id, var = yield CORE.get_variable_with_full_id(id) + yield full_id, var + + +@coroutine def process_lambda(value, # type: Lambda - parameters, # type: List[Tuple[Expression, str]] + parameters, # type: List[Tuple[SafeExpType, str]] capture='=', # type: str - return_type=None # type: Optional[Expression] + return_type=None # type: Optional[SafeExpType] ): # type: (...) -> Generator[LambdaExpression] - from esphome.components.globals import GlobalVariableComponent + """Process the given lambda value into a LambdaExpression. + + This is a coroutine because lambdas can depend on other IDs, + you need to await it with 'yield'! + + :param value: The lambda to process. + :param parameters: The parameters to pass to the Lambda, list of tuples + :param capture: The capture expression for the lambda, usually ''. + :param return_type: The return type of the lambda. + :return: The generated lambda expression. + """ + from esphome.components.globals import GlobalsComponent if value is None: yield return parts = value.parts[:] for i, id in enumerate(value.requires_ids): - for full_id, var in CORE.get_variable_with_full_id(id): - yield + full_id, var = yield CORE.get_variable_with_full_id(id) if full_id is not None and isinstance(full_id.type, MockObjClass) and \ - full_id.type.inherits_from(GlobalVariableComponent): + full_id.type.inherits_from(GlobalsComponent): parts[i * 3 + 1] = var.value() continue @@ -424,119 +555,137 @@ def process_lambda(value, # type: Lambda yield LambdaExpression(parts, parameters, capture, return_type) +def is_template(value): + """Return if value is a lambda expression.""" + return isinstance(value, Lambda) + + +@coroutine def templatable(value, # type: Any - args, # type: List[Tuple[Expression, str]] - output_type # type: Optional[Expression] + args, # type: List[Tuple[SafeExpType, str]] + output_type, # type: Optional[SafeExpType], + to_exp=None # type: Optional[Any] ): - if isinstance(value, Lambda): - for lambda_ in process_lambda(value, args, return_type=output_type): - yield None + """Generate code for a templatable config option. + + If `value` is a templated value, the lambda expression is returned. + Otherwise the value is returned as-is (optionally process with to_exp). + + :param value: The value to process. + :param args: The arguments for the lambda expression. + :param output_type: The output type of the lambda expression. + :param to_exp: An optional callable to use for converting non-templated values. + :return: The potentially templated value. + """ + if is_template(value): + lambda_ = yield process_lambda(value, args, return_type=output_type) yield lambda_ else: - yield value + if to_exp is None: + yield value + elif isinstance(to_exp, dict): + yield to_exp[value] + else: + yield to_exp(value) class MockObj(Expression): - def __init__(self, base, op=u'.', has_side_effects=True): + """A general expression that can be used to represent any value. + + Mostly consists of magic methods that allow ESPHome's codegen syntax. + """ + def __init__(self, base, op=u'.'): self.base = base self.op = op - self._has_side_effects = has_side_effects super(MockObj, self).__init__() def __getattr__(self, attr): # type: (str) -> MockObj - if attr == u'_': - obj = MockObj(u'{}{}'.format(self.base, self.op)) - obj.requires.append(self) - return obj - if attr == u'new': - obj = MockObj(u'new {}'.format(self.base), u'->') - obj.requires.append(self) - return obj next_op = u'.' if attr.startswith(u'P') and self.op not in ['::', '']: attr = attr[1:] next_op = u'->' if attr.startswith(u'_'): attr = attr[1:] - obj = MockObj(u'{}{}{}'.format(self.base, self.op, attr), next_op) - obj.requires.append(self) - return obj + return MockObj(u'{}{}{}'.format(self.base, self.op, attr), next_op) - def __call__(self, *args, **kwargs): # type: (*Any, **Any) -> MockObj + def __call__(self, *args): # type: (SafeExpType) -> MockObj call = CallExpression(self.base, *args) - obj = MockObj(call, self.op) - obj.requires.append(self) - obj.requires.append(call) - return obj + return MockObj(call, self.op) def __str__(self): # type: () -> unicode return text_type(self.base) - def require(self): # type: () -> None - self.required = True - for require in self.requires: - if require.required: - continue - require.require() + def __repr__(self): + return u'MockObj<{}>'.format(text_type(self.base)) - def template(self, *args): # type: (Tuple[Union[TemplateArguments, Expression]]) -> MockObj + @property + def _(self): # type: () -> MockObj + return MockObj(u'{}{}'.format(self.base, self.op)) + + @property + def new(self): # type: () -> MockObj + return MockObj(u'new {}'.format(self.base), u'->') + + def template(self, *args): # type: (*SafeExpType) -> MockObj if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: args = args[0] - obj = MockObj(u'{}{}'.format(self.base, args)) - obj.requires.append(self) - obj.requires.append(args) - return obj + return MockObj(u'{}{}'.format(self.base, args)) def namespace(self, name): # type: (str) -> MockObj - obj = MockObj(u'{}{}{}'.format(self.base, self.op, name), u'::') - obj.requires.append(self) - return obj + return MockObj(u'{}{}'.format(self._, name), u'::') def class_(self, name, *parents): # type: (str, *MockObjClass) -> MockObjClass op = '' if self.op == '' else '::' - obj = MockObjClass(u'{}{}{}'.format(self.base, op, name), u'.', parents=parents) - obj.requires.append(self) - return obj + return MockObjClass(u'{}{}{}'.format(self.base, op, name), u'.', parents=parents) def struct(self, name): # type: (str) -> MockObjClass return self.class_(name) def enum(self, name, is_class=False): # type: (str, bool) -> MockObj - if is_class: - return self.namespace(name) - - return self + return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op) def operator(self, name): # type: (str) -> MockObj if name == 'ref': - obj = MockObj(u'{} &'.format(self.base), u'') - obj.requires.append(self) - return obj + return MockObj(u'{} &'.format(self.base), u'') if name == 'ptr': - obj = MockObj(u'{} *'.format(self.base), u'') - obj.requires.append(self) - return obj + return MockObj(u'{} *'.format(self.base), u'') if name == "const": - obj = MockObj(u'const {}'.format(self.base), u'') - obj.requires.append(self) - return obj + return MockObj(u'const {}'.format(self.base), u'') raise NotImplementedError - def has_side_effects(self): # type: () -> bool - return self._has_side_effects + @property + def using(self): # type: () -> MockObj + assert self.op == '::' + return MockObj(u'using namespace {}'.format(self.base)) def __getitem__(self, item): # type: (Union[str, Expression]) -> MockObj next_op = u'.' if isinstance(item, str) and item.startswith(u'P'): item = item[1:] next_op = u'->' - obj = MockObj(u'{}[{}]'.format(self.base, item), next_op) - obj.requires.append(self) - if isinstance(item, Expression): - obj.requires.append(item) - return obj + return MockObj(u'{}[{}]'.format(self.base, item), next_op) + + +class MockObjEnum(MockObj): + def __init__(self, *args, **kwargs): + self._enum = kwargs.pop('enum') + self._is_class = kwargs.pop('is_class') + base = kwargs.pop('base') + if self._is_class: + base = base + '::' + self._enum + kwargs['op'] = '::' + kwargs['base'] = base + MockObj.__init__(self, *args, **kwargs) + + def __str__(self): # type: () -> unicode + if self._is_class: + return super(MockObjEnum, self).__str__() + return u'{}{}{}'.format(self.base, self.op, self._enum) + + def __repr__(self): + return u'MockObj<{}>'.format(text_type(self.base)) class MockObjClass(MockObj): @@ -559,17 +708,15 @@ class MockObjClass(MockObj): return True return False - def template(self, - *args # type: Tuple[Union[TemplateArguments, Expression]] - ): - # type: (...) -> MockObjClass + def template(self, *args): + # type: (*SafeExpType) -> MockObjClass if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: args = args[0] new_parents = self._parents[:] new_parents.append(self) - obj = MockObjClass(u'{}{}'.format(self.base, args), parents=new_parents) - obj.requires.append(self) - obj.requires.append(args) - return obj + return MockObjClass(u'{}{}'.format(self.base, args), parents=new_parents) + + def __repr__(self): + return u'MockObjClass<{}, parents={}>'.format(text_type(self.base), self._parents) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 1d7e1b7036..fd79feec1c 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,64 +1,81 @@ -from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_PCF8574, \ - CONF_SETUP_PRIORITY, CONF_MCP23017 -from esphome.core import CORE, EsphomeError -from esphome.cpp_generator import IntLiteral, RawExpression -from esphome.cpp_types import GPIOInputPin, GPIOOutputPin +from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \ + CONF_UPDATE_INTERVAL, CONF_TYPE_ID +from esphome.core import coroutine, ID, CORE +from esphome.cpp_generator import RawExpression, add, get_variable +from esphome.cpp_types import App, GPIOPin +from esphome.py_compat import text_type -def generic_gpio_pin_expression_(conf, mock_obj, default_mode): +@coroutine +def gpio_pin_expression(conf): + """Generate an expression for the given pin option. + + This is a coroutine, you must await it with a 'yield' expression! + """ if conf is None: return + from esphome import pins + for key, (func, _) in pins.PIN_SCHEMA_REGISTRY.items(): + if key in conf: + yield coroutine(func)(conf) + return + number = conf[CONF_NUMBER] + mode = conf[CONF_MODE] inverted = conf.get(CONF_INVERTED) - if CONF_PCF8574 in conf: - from esphome.components import pcf8574 - - for hub in CORE.get_variable(conf[CONF_PCF8574]): - yield None - - if default_mode == u'INPUT': - mode = pcf8574.PCF8675_GPIO_MODES[conf.get(CONF_MODE, u'INPUT')] - yield hub.make_input_pin(number, mode, inverted) - return - if default_mode == u'OUTPUT': - yield hub.make_output_pin(number, inverted) - return - - raise EsphomeError(u"Unknown default mode {}".format(default_mode)) - if CONF_MCP23017 in conf: - from esphome.components import mcp23017 - - for hub in CORE.get_variable(conf[CONF_MCP23017]): - yield None - - if default_mode == u'INPUT': - mode = mcp23017.MCP23017_GPIO_MODES[conf.get(CONF_MODE, u'INPUT')] - yield hub.make_input_pin(number, mode, inverted) - return - if default_mode == u'OUTPUT': - yield hub.make_output_pin(number, inverted) - return - - raise EsphomeError(u"Unknown default mode {}".format(default_mode)) - if len(conf) == 1: - yield IntLiteral(number) - return - mode = RawExpression(conf.get(CONF_MODE, default_mode)) - yield mock_obj(number, mode, inverted) + yield GPIOPin.new(number, RawExpression(mode), inverted) -def gpio_output_pin_expression(conf): - for exp in generic_gpio_pin_expression_(conf, GPIOOutputPin, 'OUTPUT'): - yield None - yield exp +@coroutine +def register_component(var, config): + """Register the given obj as a component. + This is a coroutine, you must await it with a 'yield' expression! -def gpio_input_pin_expression(conf): - for exp in generic_gpio_pin_expression_(conf, GPIOInputPin, 'INPUT'): - yield None - yield exp - - -def setup_component(obj, config): + :param var: The variable representing the component. + :param config: The configuration for the component. + """ + id_ = text_type(var.base) + if id_ not in CORE.component_ids: + raise ValueError(u"Component ID {} was not declared to inherit from Component, " + u"or was registered twice. Please create a bug report with your " + u"configuration.".format(id_)) + CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: - CORE.add(obj.set_setup_priority(config[CONF_SETUP_PRIORITY])) + add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) + if CONF_UPDATE_INTERVAL in config: + add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + add(App.register_component(var)) + yield var + + +@coroutine +def register_parented(var, value): + if isinstance(value, ID): + paren = yield get_variable(value) + else: + paren = value + add(var.set_parent(paren)) + + +def extract_registry_entry_config(registry, full_config): + # type: (Registry, ConfigType) -> RegistryEntry + key, config = next((k, v) for k, v in full_config.items() if k in registry) + return registry[key], config + + +@coroutine +def build_registry_entry(registry, full_config): + registry_entry, config = extract_registry_entry_config(registry, full_config) + type_id = full_config[CONF_TYPE_ID] + builder = registry_entry.coroutine_fun + yield builder(config, type_id) + + +@coroutine +def build_registry_list(registry, config): + actions = [] + for conf in config: + action = yield build_registry_entry(registry, conf) + actions.append(action) + yield actions diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 01518fe806..d3e5b2d561 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -2,7 +2,9 @@ from esphome.cpp_generator import MockObj global_ns = MockObj('', '') void = global_ns.namespace('void') +nullptr = global_ns.namespace('nullptr') float_ = global_ns.namespace('float') +double = global_ns.namespace('double') bool_ = global_ns.namespace('bool') std_ns = global_ns.namespace('std') std_string = std_ns.class_('string') @@ -15,10 +17,7 @@ const_char_ptr = global_ns.namespace('const char *') NAN = global_ns.namespace('NAN') esphome_ns = global_ns # using namespace esphome; App = esphome_ns.App -io_ns = esphome_ns.namespace('io') Nameable = esphome_ns.class_('Nameable') -Trigger = esphome_ns.class_('Trigger') -Action = esphome_ns.class_('Action') Component = esphome_ns.class_('Component') ComponentPtr = Component.operator('ptr') PollingComponent = esphome_ns.class_('PollingComponent', Component) @@ -29,8 +28,5 @@ JsonObject = arduino_json_ns.class_('JsonObject') JsonObjectRef = JsonObject.operator('ref') JsonObjectConstRef = JsonObjectRef.operator('const') Controller = esphome_ns.class_('Controller') -StoringController = esphome_ns.class_('StoringController', Controller) GPIOPin = esphome_ns.class_('GPIOPin') -GPIOOutputPin = esphome_ns.class_('GPIOOutputPin', GPIOPin) -GPIOInputPin = esphome_ns.class_('GPIOInputPin', GPIOPin) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 6c1d9f2616..14b06a3c82 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -3,6 +3,7 @@ from __future__ import print_function import codecs import collections +import functools import hashlib import hmac import json @@ -39,15 +40,73 @@ from typing import Optional # noqa from esphome.zeroconf import DashboardStatus, Zeroconf _LOGGER = logging.getLogger(__name__) -CONFIG_DIR = '' -PASSWORD_DIGEST = '' -COOKIE_SECRET = None -USING_PASSWORD = False -ON_HASSIO = False -USING_HASSIO_AUTH = True -HASSIO_MQTT_CONFIG = None -RELATIVE_URL = os.getenv('ESPHOME_DASHBOARD_RELATIVE_URL', '/') -STATUS_USE_PING = get_bool_env('ESPHOME_DASHBOARD_USE_PING') + + +class DashboardSettings(object): + def __init__(self): + self.config_dir = '' + self.password_digest = '' + self.using_password = False + self.on_hassio = False + self.cookie_secret = None + + def parse_args(self, args): + self.on_hassio = args.hassio + if not self.on_hassio: + self.using_password = bool(args.password) + if self.using_password: + if IS_PY2: + self.password_digest = hmac.new(args.password).digest() + else: + self.password_digest = hmac.new(args.password.encode()).digest() + self.config_dir = args.configuration + + @property + def relative_url(self): + return os.getenv('ESPHOME_DASHBOARD_RELATIVE_URL', '/') + + @property + def status_use_ping(self): + return get_bool_env('ESPHOME_DASHBOARD_USE_PING') + + @property + def using_hassio_auth(self): + if not self.on_hassio: + return False + return not get_bool_env('DISABLE_HA_AUTHENTICATION') + + @property + def using_auth(self): + return self.using_password or self.using_hassio_auth + + def check_password(self, password): + if not self.using_auth: + return True + + if IS_PY2: + password = hmac.new(password).digest() + else: + password = hmac.new(password.encode()).digest() + return hmac.compare_digest(self.password_digest, password) + + def rel_path(self, *args): + return os.path.join(self.config_dir, *args) + + def list_yaml_files(self): + files = [] + for file in os.listdir(self.config_dir): + if not file.endswith('.yaml'): + continue + if file.startswith('.'): + continue + if file == 'secrets.yaml': + continue + files.append(file) + files.sort() + return files + + +settings = DashboardSettings() if IS_PY2: cookie_authenticated_yes = 'yes' @@ -61,20 +120,33 @@ def template_args(): 'version': version, 'docs_link': 'https://beta.esphome.io/' if 'b' in version else 'https://esphome.io/', 'get_static_file_url': get_static_file_url, - 'relative_url': RELATIVE_URL, + 'relative_url': settings.relative_url, 'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'), } def authenticated(func): + @functools.wraps(func) def decorator(self, *args, **kwargs): - if not self.is_authenticated(): - self.redirect(RELATIVE_URL + 'login') + if not is_authenticated(self): + self.redirect('./login') return None return func(self, *args, **kwargs) return decorator +def is_authenticated(request_handler): + if settings.on_hassio: + # Handle ingress - disable auth on ingress port + # X-Hassio-Ingress is automatically stripped on the non-ingress server in nginx + header = request_handler.request.headers.get('X-Hassio-Ingress', 'NO') + if str(header) == 'YES': + return True + if settings.using_auth: + return request_handler.get_secure_cookie('authenticated') == cookie_authenticated_yes + return True + + def bind_config(func): def decorator(self, *args, **kwargs): configuration = self.get_argument('configuration') @@ -89,115 +161,158 @@ def bind_config(func): # pylint: disable=abstract-method class BaseHandler(tornado.web.RequestHandler): - def is_authenticated(self): - if USING_HASSIO_AUTH or USING_PASSWORD: - return self.get_secure_cookie('authenticated') == cookie_authenticated_yes + pass - return True + +def websocket_class(cls): + # pylint: disable=protected-access + if not hasattr(cls, '_message_handlers'): + cls._message_handlers = {} + + for _, method in cls.__dict__.iteritems(): + if hasattr(method, "_message_handler"): + cls._message_handlers[method._message_handler] = method + + return cls + + +def websocket_method(name): + def wrap(fn): + # pylint: disable=protected-access + fn._message_handler = name + return fn + return wrap # pylint: disable=abstract-method, arguments-differ +@websocket_class class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def __init__(self, application, request, **kwargs): super(EsphomeCommandWebSocket, self).__init__(application, request, **kwargs) - self.proc = None - self.closed = False + self._proc = None + self._is_closed = False + @authenticated def on_message(self, message): - if USING_HASSIO_AUTH or USING_PASSWORD: - if self.get_secure_cookie('authenticated') != cookie_authenticated_yes: - return - if self.proc is not None: + # Messages are always JSON, 500 when not + json_message = json.loads(message) + type_ = json_message['type'] + # pylint: disable=no-member + handlers = type(self)._message_handlers + if type_ not in handlers: + _LOGGER.warning("Requested unknown message type %s", type_) return - command = self.build_command(message) + + handlers[type_](self, json_message) + + @websocket_method('spawn') + def handle_spawn(self, json_message): + if self._proc is not None: + # spawn can only be called once + return + command = self.build_command(json_message) _LOGGER.info(u"Running command '%s'", ' '.join(shlex_quote(x) for x in command)) - self.proc = tornado.process.Subprocess(command, - stdout=tornado.process.Subprocess.STREAM, - stderr=subprocess.STDOUT) - self.proc.set_exit_callback(self.proc_on_exit) - tornado.ioloop.IOLoop.current().spawn_callback(self.redirect_stream) + self._proc = tornado.process.Subprocess(command, + stdout=tornado.process.Subprocess.STREAM, + stderr=subprocess.STDOUT, + stdin=tornado.process.Subprocess.STREAM) + self._proc.set_exit_callback(self._proc_on_exit) + tornado.ioloop.IOLoop.current().spawn_callback(self._redirect_stdout) + + @property + def is_process_active(self): + return self._proc is not None and self._proc.returncode is None + + @websocket_method('stdin') + def handle_stdin(self, json_message): + if not self.is_process_active: + return + data = json_message['data'] + data = codecs.encode(data, 'utf8', 'replace') + _LOGGER.debug("< stdin: %s", data) + self._proc.stdin.write(data) @tornado.gen.coroutine - def redirect_stream(self): + def _redirect_stdout(self): + if IS_PY2: + reg = '[\n\r]' + else: + reg = b'[\n\r]' + while True: try: - if IS_PY2: - reg = '[\n\r]' - else: - reg = b'[\n\r]' - data = yield self.proc.stdout.read_until_regex(reg) - if not IS_PY2: - data = data.decode('utf-8', 'backslashreplace') + data = yield self._proc.stdout.read_until_regex(reg) except tornado.iostream.StreamClosedError: break - try: - self.write_message({'event': 'line', 'data': data}) - except UnicodeDecodeError: - data = codecs.decode(data, 'utf8', 'replace') - self.write_message({'event': 'line', 'data': data}) + data = codecs.decode(data, 'utf8', 'replace') - def proc_on_exit(self, returncode): - if not self.closed: - _LOGGER.debug("Process exited with return code %s", returncode) + _LOGGER.debug("> stdout: %s", data) + self.write_message({'event': 'line', 'data': data}) + + def _proc_on_exit(self, returncode): + if not self._is_closed: + # Check if the proc was not forcibly closed + _LOGGER.info("Process exited with return code %s", returncode) self.write_message({'event': 'exit', 'code': returncode}) def on_close(self): - self.closed = True - if self.proc is not None and self.proc.returncode is None: + # Check if proc exists (if 'start' has been run) + if self.is_process_active: _LOGGER.debug("Terminating process") - self.proc.proc.terminate() + self._proc.proc.terminate() + # Shutdown proc on WS close + self._is_closed = True - def build_command(self, message): + def build_command(self, json_message): raise NotImplementedError class EsphomeLogsHandler(EsphomeCommandWebSocket): - def build_command(self, message): - js = json.loads(message) - config_file = CONFIG_DIR + '/' + js['configuration'] - return ["esphome", "--dashboard", config_file, "logs", '--serial-port', js["port"]] + def build_command(self, json_message): + config_file = settings.rel_path(json_message['configuration']) + return ["esphome", "--dashboard", config_file, "logs", '--serial-port', + json_message["port"]] -class EsphomeRunHandler(EsphomeCommandWebSocket): - def build_command(self, message): - js = json.loads(message) - config_file = os.path.join(CONFIG_DIR, js['configuration']) - return ["esphome", "--dashboard", config_file, "run", '--upload-port', js["port"]] +class EsphomeUploadHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + config_file = settings.rel_path(json_message['configuration']) + return ["esphome", "--dashboard", config_file, "run", '--upload-port', + json_message["port"]] class EsphomeCompileHandler(EsphomeCommandWebSocket): - def build_command(self, message): - js = json.loads(message) - config_file = os.path.join(CONFIG_DIR, js['configuration']) + def build_command(self, json_message): + config_file = settings.rel_path(json_message['configuration']) return ["esphome", "--dashboard", config_file, "compile"] class EsphomeValidateHandler(EsphomeCommandWebSocket): - def build_command(self, message): - js = json.loads(message) - config_file = os.path.join(CONFIG_DIR, js['configuration']) + def build_command(self, json_message): + config_file = settings.rel_path(json_message['configuration']) return ["esphome", "--dashboard", config_file, "config"] class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): - def build_command(self, message): - js = json.loads(message) - config_file = os.path.join(CONFIG_DIR, js['configuration']) + def build_command(self, json_message): + config_file = settings.rel_path(json_message['configuration']) return ["esphome", "--dashboard", config_file, "clean-mqtt"] class EsphomeCleanHandler(EsphomeCommandWebSocket): - def build_command(self, message): - js = json.loads(message) - config_file = os.path.join(CONFIG_DIR, js['configuration']) + def build_command(self, json_message): + config_file = settings.rel_path(json_message['configuration']) return ["esphome", "--dashboard", config_file, "clean"] -class EsphomeHassConfigHandler(EsphomeCommandWebSocket): - def build_command(self, message): - js = json.loads(message) - config_file = os.path.join(CONFIG_DIR, js['configuration']) - return ["esphome", "--dashboard", config_file, "hass-config"] +class EsphomeVscodeHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + return ["esphome", "--dashboard", "-q", 'dummy', "vscode"] + + +class EsphomeAceEditorHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + return ["esphome", "--dashboard", "-q", settings.config_dir, "vscode", "--ace"] class SerialPortRequestHandler(BaseHandler): @@ -224,9 +339,9 @@ class WizardRequestHandler(BaseHandler): from esphome import wizard kwargs = {k: u''.join(decode_text(x) for x in v) for k, v in self.request.arguments.items()} - destination = os.path.join(CONFIG_DIR, kwargs['name'] + u'.yaml') + destination = settings.rel_path(kwargs['name'] + u'.yaml') wizard.wizard_write(path=destination, **kwargs) - self.redirect('/?begin=True') + self.redirect('./?begin=True') class DownloadBinaryRequestHandler(BaseHandler): @@ -234,7 +349,7 @@ class DownloadBinaryRequestHandler(BaseHandler): @bind_config def get(self, configuration=None): # pylint: disable=no-value-for-parameter - storage_path = ext_storage_path(CONFIG_DIR, configuration) + storage_path = ext_storage_path(settings.config_dir, configuration) storage_json = StorageJSON.load(storage_path) if storage_json is None: self.send_error() @@ -253,22 +368,8 @@ class DownloadBinaryRequestHandler(BaseHandler): self.finish() -def _list_yaml_files(): - files = [] - for file in os.listdir(CONFIG_DIR): - if not file.endswith('.yaml'): - continue - if file.startswith('.'): - continue - if file == 'secrets.yaml': - continue - files.append(file) - files.sort() - return files - - def _list_dashboard_entries(): - files = _list_yaml_files() + files = settings.list_yaml_files() return [DashboardEntry(file) for file in files] @@ -280,12 +381,12 @@ class DashboardEntry(object): @property def full_path(self): # type: () -> str - return os.path.join(CONFIG_DIR, self.filename) + return os.path.join(settings.config_dir, self.filename) @property def storage(self): # type: () -> Optional[StorageJSON] if not self._loaded_storage: - self._storage = StorageJSON.load(ext_storage_path(CONFIG_DIR, self.filename)) + self._storage = StorageJSON.load(ext_storage_path(settings.config_dir, self.filename)) self._loaded_storage = True return self._storage @@ -329,6 +430,12 @@ class DashboardEntry(object): def update_new(self): return const.__version__ + @property + def loaded_integrations(self): + if self.storage is None: + return [] + return self.storage.loaded_integrations + class MainRequestHandler(BaseHandler): @authenticated @@ -428,7 +535,7 @@ class EditRequestHandler(BaseHandler): @bind_config def get(self, configuration=None): # pylint: disable=no-value-for-parameter - with open(os.path.join(CONFIG_DIR, configuration), 'r') as f: + with open(settings.rel_path(configuration), 'r') as f: content = f.read() self.write(content) @@ -436,7 +543,7 @@ class EditRequestHandler(BaseHandler): @bind_config def post(self, configuration=None): # pylint: disable=no-value-for-parameter - with open(os.path.join(CONFIG_DIR, configuration), 'wb') as f: + with open(settings.rel_path(configuration), 'wb') as f: f.write(self.request.body) self.set_status(200) @@ -445,20 +552,20 @@ class DeleteRequestHandler(BaseHandler): @authenticated @bind_config def post(self, configuration=None): - config_file = os.path.join(CONFIG_DIR, configuration) - storage_path = ext_storage_path(CONFIG_DIR, configuration) + config_file = settings.rel_path(configuration) + storage_path = ext_storage_path(settings.config_dir, configuration) storage_json = StorageJSON.load(storage_path) if storage_json is None: self.set_status(500) return name = storage_json.name - trash_path = trash_storage_path(CONFIG_DIR) + trash_path = trash_storage_path(settings.config_dir) mkdir_p(trash_path) shutil.move(config_file, os.path.join(trash_path, configuration)) # Delete build folder (if exists) - build_folder = os.path.join(CONFIG_DIR, name) + build_folder = os.path.join(settings.config_dir, name) if build_folder is not None: shutil.rmtree(build_folder, os.path.join(trash_path, name)) @@ -467,8 +574,8 @@ class UndoDeleteRequestHandler(BaseHandler): @authenticated @bind_config def post(self, configuration=None): - config_file = os.path.join(CONFIG_DIR, configuration) - trash_path = trash_storage_path(CONFIG_DIR) + config_file = settings.rel_path(configuration) + trash_path = trash_storage_path(settings.config_dir) shutil.move(os.path.join(trash_path, configuration), config_file) @@ -479,10 +586,10 @@ PING_REQUEST = threading.Event() class LoginHandler(BaseHandler): def get(self): - if USING_HASSIO_AUTH: + if settings.using_hassio_auth: self.render_hassio_login() return - self.write('
' + self.write('' 'Password: ' '' '
') @@ -515,16 +622,12 @@ class LoginHandler(BaseHandler): self.render_hassio_login(error="Invalid username or password") def post(self): - if USING_HASSIO_AUTH: + if settings.using_hassio_auth: self.post_hassio_login() return password = str(self.get_argument("password", '')) - if IS_PY2: - password = hmac.new(password).digest() - else: - password = hmac.new(password.encode()).digest() - if hmac.compare_digest(PASSWORD_DIGEST, password): + if settings.check_password(password): self.set_secure_cookie("authenticated", cookie_authenticated_yes) self.redirect("/") @@ -541,7 +644,7 @@ def get_static_file_url(name): with open(path, 'rb') as f_handle: hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] _STATIC_FILE_HASHES[name] = hash_ - return RELATIVE_URL + u'static/{}?hash={}'.format(name, hash_) + return u'./static/{}?hash={}'.format(name, hash_) def make_app(debug=False): @@ -569,31 +672,33 @@ def make_app(debug=False): self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') static_path = os.path.join(os.path.dirname(__file__), 'static') - settings = { + app_settings = { 'debug': debug, - 'cookie_secret': COOKIE_SECRET, + 'cookie_secret': settings.cookie_secret, 'log_function': log_function, 'websocket_ping_interval': 30.0, } + rel = settings.relative_url app = tornado.web.Application([ - (RELATIVE_URL + "", MainRequestHandler), - (RELATIVE_URL + "login", LoginHandler), - (RELATIVE_URL + "logs", EsphomeLogsHandler), - (RELATIVE_URL + "run", EsphomeRunHandler), - (RELATIVE_URL + "compile", EsphomeCompileHandler), - (RELATIVE_URL + "validate", EsphomeValidateHandler), - (RELATIVE_URL + "clean-mqtt", EsphomeCleanMqttHandler), - (RELATIVE_URL + "clean", EsphomeCleanHandler), - (RELATIVE_URL + "hass-config", EsphomeHassConfigHandler), - (RELATIVE_URL + "edit", EditRequestHandler), - (RELATIVE_URL + "download.bin", DownloadBinaryRequestHandler), - (RELATIVE_URL + "serial-ports", SerialPortRequestHandler), - (RELATIVE_URL + "ping", PingRequestHandler), - (RELATIVE_URL + "delete", DeleteRequestHandler), - (RELATIVE_URL + "undo-delete", UndoDeleteRequestHandler), - (RELATIVE_URL + "wizard.html", WizardRequestHandler), - (RELATIVE_URL + r"static/(.*)", StaticFileHandler, {'path': static_path}), - ], **settings) + (rel + "", MainRequestHandler), + (rel + "login", LoginHandler), + (rel + "logs", EsphomeLogsHandler), + (rel + "upload", EsphomeUploadHandler), + (rel + "compile", EsphomeCompileHandler), + (rel + "validate", EsphomeValidateHandler), + (rel + "clean-mqtt", EsphomeCleanMqttHandler), + (rel + "clean", EsphomeCleanHandler), + (rel + "vscode", EsphomeVscodeHandler), + (rel + "ace", EsphomeAceEditorHandler), + (rel + "edit", EditRequestHandler), + (rel + "download.bin", DownloadBinaryRequestHandler), + (rel + "serial-ports", SerialPortRequestHandler), + (rel + "ping", PingRequestHandler), + (rel + "delete", DeleteRequestHandler), + (rel + "undo-delete", UndoDeleteRequestHandler), + (rel + "wizard.html", WizardRequestHandler), + (rel + r"static/(.*)", StaticFileHandler, {'path': static_path}), + ], **app_settings) if debug: _STATIC_FILE_HASHES.clear() @@ -602,49 +707,27 @@ def make_app(debug=False): def start_web_server(args): - global CONFIG_DIR - global PASSWORD_DIGEST - global USING_PASSWORD - global ON_HASSIO - global USING_HASSIO_AUTH - global COOKIE_SECRET + settings.parse_args(args) + mkdir_p(settings.rel_path(".esphome")) - CONFIG_DIR = args.configuration - mkdir_p(CONFIG_DIR) - mkdir_p(os.path.join(CONFIG_DIR, ".esphome")) - - ON_HASSIO = args.hassio - if ON_HASSIO: - USING_HASSIO_AUTH = not get_bool_env('DISABLE_HA_AUTHENTICATION') - USING_PASSWORD = False - else: - USING_HASSIO_AUTH = False - USING_PASSWORD = args.password - - if USING_PASSWORD: - if IS_PY2: - PASSWORD_DIGEST = hmac.new(args.password).digest() - else: - PASSWORD_DIGEST = hmac.new(args.password.encode()).digest() - - if USING_HASSIO_AUTH or USING_PASSWORD: - path = esphome_storage_path(CONFIG_DIR) + if settings.using_auth: + path = esphome_storage_path(settings.config_dir) storage = EsphomeStorageJSON.load(path) if storage is None: storage = EsphomeStorageJSON.get_default() storage.save(path) - COOKIE_SECRET = storage.cookie_secret + settings.cookie_secret = storage.cookie_secret app = make_app(args.verbose) if args.socket is not None: _LOGGER.info("Starting dashboard web server on unix socket %s and configuration dir %s...", - args.socket, CONFIG_DIR) + args.socket, settings.config_dir) server = tornado.httpserver.HTTPServer(app) socket = tornado.netutil.bind_unix_socket(args.socket, mode=0o666) server.add_socket(socket) else: _LOGGER.info("Starting dashboard web server on port %s and configuration dir %s...", - args.port, CONFIG_DIR) + args.port, settings.config_dir) app.listen(args.port) if args.open_ui: @@ -652,7 +735,7 @@ def start_web_server(args): webbrowser.open('localhost:{}'.format(args.port)) - if STATUS_USE_PING: + if settings.status_use_ping: status_thread = PingStatusThread() else: status_thread = MDNSStatusThread() diff --git a/esphome/dashboard/static/ace.js b/esphome/dashboard/static/ace.js index 58119de349..f5fca22af2 100644 --- a/esphome/dashboard/static/ace.js +++ b/esphome/dashboard/static/ace.js @@ -14,4 +14,3 @@ } }); })(); - \ No newline at end of file diff --git a/esphome/dashboard/static/esphome.js b/esphome/dashboard/static/esphome.js index bd3d006080..e7c96ec4dd 100644 --- a/esphome/dashboard/static/esphome.js +++ b/esphome/dashboard/static/esphome.js @@ -1,9 +1,18 @@ // Disclaimer: This file was written in a hurry and by someone // who does not know JS at all. This file desperately needs cleanup. + +// ============================= Global Vars ============================= document.addEventListener('DOMContentLoaded', () => { M.AutoInit(document.body); }); +let wsProtocol = "ws:"; +if (window.location.protocol === "https:") { + wsProtocol = 'wss:'; +} +const wsUrl = `${wsProtocol}//${window.location.host}${window.location.pathname}`; + +// ============================= Color Log Parsing ============================= const initializeColorState = () => { return { bold: false, @@ -170,30 +179,20 @@ const colorReplace = (pre, state, text) => { } } addSpan(text.substring(i)); - scrollToBottomOfElement(pre); + if (pre.scrollTop + 56 >= (pre.scrollHeight - pre.offsetHeight)) { + // at bottom + pre.scrollTop = pre.scrollHeight; + } }; -const removeUpdateAvailable = (filename) => { - const p = document.querySelector(`.update-available[data-node="${filename}"]`); - if (p === undefined) - return; - p.remove(); -}; - -let configuration = ""; -let wsProtocol = "ws:"; -if (window.location.protocol === "https:") { - wsProtocol = 'wss:'; -} -const wsUrl = `${wsProtocol}//${window.location.hostname}:${window.location.port}${relative_url}`; - +// ============================= Online/Offline Status Indicators ============================= let isFetchingPing = false; const fetchPing = () => { if (isFetchingPing) return; isFetchingPing = true; - fetch(`${relative_url}ping`, {credentials: "same-origin"}).then(res => res.json()) + fetch(`./ping`, {credentials: "same-origin"}).then(res => res.json()) .then(response => { for (let filename in response) { let node = document.querySelector(`.status-indicator[data-node="${filename}"]`); @@ -231,11 +230,12 @@ const fetchPing = () => { setInterval(fetchPing, 2000); fetchPing(); +// ============================= Serial Port Selector ============================= const portSelect = document.querySelector('.nav-wrapper select'); let ports = []; const fetchSerialPorts = (begin=false) => { - fetch(`${relative_url}serial-ports`, {credentials: "same-origin"}).then(res => res.json()) + fetch(`./serial-ports`, {credentials: "same-origin"}).then(res => res.json()) .then(response => { if (ports.length === response.length) { let allEqual = true; @@ -286,315 +286,263 @@ const getUploadPort = () => { setInterval(fetchSerialPorts, 5000); fetchSerialPorts(true); -const logsModalElem = document.getElementById("modal-logs"); -document.querySelectorAll(".action-show-logs").forEach((showLogs) => { - showLogs.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); - const modalInstance = M.Modal.getInstance(logsModalElem); - const log = logsModalElem.querySelector(".log"); - log.innerHTML = ""; - const colorState = initializeColorState(); - const stopLogsButton = logsModalElem.querySelector(".stop-logs"); +// ============================= Logs Button ============================= + +class LogModalElem { + constructor({ + name, + onPrepare = (modalElem, config) => {}, + onProcessExit = (modalElem, code) => {}, + onSocketClose = (modalElem) => {}, + dismissible = true, + }) { + this.modalId = `modal-${name}`; + this.actionClass = `action-${name}`; + this.wsUrl = `${wsUrl}${name}`; + this.dismissible = dismissible; + this.activeConfig = null; + + this.modalElem = document.getElementById(this.modalId); + this.logElem = this.modalElem.querySelector('.log'); + this.onPrepare = onPrepare; + this.onProcessExit = onProcessExit; + this.onSocketClose = onSocketClose; + } + + setup() { + const boundOnPress = this._onPress.bind(this); + document.querySelectorAll(`.${this.actionClass}`).forEach((btn) => { + btn.addEventListener('click', boundOnPress); + }); + } + + _setupModalInstance() { + this.modalInstance = M.Modal.getInstance(this.modalElem); + this.modalInstance.options.dismissible = this.dismissible; + this._boundKeydown = this._onKeydown.bind(this); + this.modalInstance.options.onOpenStart = () => { + document.addEventListener('keydown', this._boundKeydown); + }; + this.modalInstance.options.onCloseStart = this._onCloseStart.bind(this); + } + + _onCloseStart() { + document.removeEventListener('keydown', this._boundKeydown); + this.activeSocket.close(); + } + + _onPress(event) { + this.activeConfig = event.target.getAttribute('data-node'); + this._setupModalInstance(); + // clear log + this.logElem.innerHTML = ""; + const colorlogState = initializeColorState(); + // prepare modal + this.modalElem.querySelectorAll('.filename').forEach((field) => { + field.innerHTML = this.activeConfig; + }); + this.onPrepare(this.modalElem, this.activeConfig); + document.addEventListener('keydown', this._onKeydown); + let stopped = false; - stopLogsButton.innerHTML = "Stop"; - modalInstance.open(); - const filenameField = logsModalElem.querySelector('.filename'); - filenameField.innerHTML = configuration; + // open modal + this.modalInstance.open(); - const logSocket = new WebSocket(wsUrl + "logs"); - logSocket.addEventListener('message', (event) => { + const socket = new WebSocket(this.wsUrl); + this.activeSocket = socket; + socket.addEventListener('message', (event) => { const data = JSON.parse(event.data); if (data.event === "line") { - colorReplace(log, colorState, data.data); + colorReplace(this.logElem, colorlogState, data.data); } else if (data.event === "exit") { - if (data.code === 0) { - M.toast({html: "Program exited successfully."}); - } else { - M.toast({html: `Program failed with code ${data.code}`}); - } - - stopLogsButton.innerHTML = "Close"; + this.onProcessExit(this.modalElem, data.code); stopped = true; } }); - logSocket.addEventListener('open', () => { - const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); - logSocket.send(msg); + socket.addEventListener('open', () => { + const msg = JSON.stringify(this.encodeSpawnMessage(this.activeConfig)); + socket.send(msg); }); - logSocket.addEventListener('close', () => { + socket.addEventListener('close', () => { if (!stopped) { - M.toast({html: 'Terminated process.'}); + this.onSocketClose(this.modalElem); } }); - modalInstance.options.onCloseStart = () => { - logSocket.close(); + } + + _onKeydown(event) { + if (event.keyCode === 27) { + this.modalInstance.close(); + } + } + + encodeSpawnMessage(config) { + return { + type: 'spawn', + configuration: config, + port: getUploadPort(), }; - }); + } +} + +const logsModal = new LogModalElem({ + name: "logs", + onPrepare: (modalElem, config) => { + modalElem.querySelector(".stop-logs").innerHTML = "Stop"; + }, + onProcessExit: (modalElem, code) => { + if (code === 0) { + M.toast({html: "Program exited successfully."}); + } else { + M.toast({html: `Program failed with code ${code}`}); + } + modalElem.querySelector(".stop-logs").innerHTML = "Close"; + }, + onSocketClose: (modalElem) => { + M.toast({html: 'Terminated process.'}); + }, }); +logsModal.setup(); -const uploadModalElem = document.getElementById("modal-upload"); - -document.querySelectorAll(".action-upload").forEach((upload) => { - upload.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); - const modalInstance = M.Modal.getInstance(uploadModalElem); - modalInstance.options['dismissible'] = false; - const log = uploadModalElem.querySelector(".log"); - log.innerHTML = ""; - const colorState = initializeColorState(); - const stopLogsButton = uploadModalElem.querySelector(".stop-logs"); - let stopped = false; - stopLogsButton.innerHTML = "Stop"; - modalInstance.open(); - - const filenameField = uploadModalElem.querySelector('.filename'); - filenameField.innerHTML = configuration; - - const logSocket = new WebSocket(wsUrl + "run"); - logSocket.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - if (data.event === "line") { - colorReplace(log, colorState, data.data); - } else if (data.event === "exit") { - if (data.code === 0) { - M.toast({html: "Program exited successfully."}); - removeUpdateAvailable(configuration); - } else { - M.toast({html: `Program failed with code ${data.code}`}); - } - - stopLogsButton.innerHTML = "Close"; - stopped = true; - } - }); - logSocket.addEventListener('open', () => { - const msg = JSON.stringify({configuration: configuration, port: getUploadPort()}); - logSocket.send(msg); - }); - logSocket.addEventListener('close', () => { - if (!stopped) { - M.toast({html: 'Terminated process.'}); - } - }); - modalInstance.options.onCloseStart = () => { - logSocket.close(); - }; - }); +const retryUploadButton = document.querySelector('.retry-upload'); +const editAfterUploadButton = document.querySelector('.edit-after-upload'); +const downloadAfterUploadButton = document.querySelector('.download-after-upload'); +const uploadModal = new LogModalElem({ + name: 'upload', + onPrepare: (modalElem, config) => { + downloadAfterUploadButton.classList.add('disabled'); + retryUploadButton.setAttribute('data-node', uploadModal.activeConfig); + retryUploadButton.classList.add('disabled'); + editAfterUploadButton.setAttribute('data-node', uploadModal.activeConfig); + modalElem.querySelector(".stop-logs").innerHTML = "Stop"; + }, + onProcessExit: (modalElem, code) => { + if (code === 0) { + M.toast({html: "Program exited successfully."}); + // if compilation succeeds but OTA fails, you can still download the binary and upload manually + downloadAfterUploadButton.classList.remove('disabled'); + } else { + M.toast({html: `Program failed with code ${code}`}); + downloadAfterUploadButton.classList.add('disabled'); + retryUploadButton.classList.remove('disabled'); + } + modalElem.querySelector(".stop-logs").innerHTML = "Close"; + }, + onSocketClose: (modalElem) => { + M.toast({html: 'Terminated process.'}); + }, + dismissible: false, }); - -const validateModalElem = document.getElementById("modal-validate"); - -document.querySelectorAll(".action-validate").forEach((upload) => { - upload.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); - const modalInstance = M.Modal.getInstance(validateModalElem); - const log = validateModalElem.querySelector(".log"); - log.innerHTML = ""; - const colorState = initializeColorState(); - const stopLogsButton = validateModalElem.querySelector(".stop-logs"); - let stopped = false; - stopLogsButton.innerHTML = "Stop"; - modalInstance.open(); - - const filenameField = validateModalElem.querySelector('.filename'); - filenameField.innerHTML = configuration; - - const logSocket = new WebSocket(wsUrl + "validate"); - logSocket.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - if (data.event === "line") { - colorReplace(log, colorState, data.data); - } else if (data.event === "exit") { - if (data.code === 0) { - M.toast({ - html: `${configuration} is valid 👍`, - displayLength: 5000, - }); - } else { - M.toast({ - html: `${configuration} is invalid 😕`, - displayLength: 5000, - }); - } - - stopLogsButton.innerHTML = "Close"; - stopped = true; - } - }); - logSocket.addEventListener('open', () => { - const msg = JSON.stringify({configuration: configuration}); - logSocket.send(msg); - }); - logSocket.addEventListener('close', () => { - if (!stopped) { - M.toast({html: 'Terminated process.'}); - } - }); - modalInstance.options.onCloseStart = () => { - logSocket.close(); - }; - }); -}); - -const compileModalElem = document.getElementById("modal-compile"); -const downloadButton = compileModalElem.querySelector('.download-binary'); - -document.querySelectorAll(".action-compile").forEach((upload) => { - upload.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); - const modalInstance = M.Modal.getInstance(compileModalElem); - modalInstance.options['dismissible'] = false; - const log = compileModalElem.querySelector(".log"); - log.innerHTML = ""; - const colorState = initializeColorState(); - const stopLogsButton = compileModalElem.querySelector(".stop-logs"); - let stopped = false; - stopLogsButton.innerHTML = "Stop"; - downloadButton.classList.add('disabled'); - - modalInstance.open(); - - const filenameField = compileModalElem.querySelector('.filename'); - filenameField.innerHTML = configuration; - - const logSocket = new WebSocket(wsUrl + "compile"); - logSocket.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - if (data.event === "line") { - colorReplace(log, colorState, data.data); - } else if (data.event === "exit") { - if (data.code === 0) { - M.toast({html: "Program exited successfully."}); - downloadButton.classList.remove('disabled'); - } else { - M.toast({html: `Program failed with code ${data.code}`}); - } - - stopLogsButton.innerHTML = "Close"; - stopped = true; - } - }); - logSocket.addEventListener('open', () => { - const msg = JSON.stringify({configuration: configuration}); - logSocket.send(msg); - }); - logSocket.addEventListener('close', () => { - if (!stopped) { - M.toast({html: 'Terminated process.'}); - } - }); - modalInstance.options.onCloseStart = () => { - logSocket.close(); - }; - }); -}); - -downloadButton.addEventListener('click', () => { +uploadModal.setup(); +downloadAfterUploadButton.addEventListener('click', () => { const link = document.createElement("a"); link.download = name; - link.href = `${relative_url}download.bin?configuration=${encodeURIComponent(configuration)}`; + link.href = `./download.bin?configuration=${encodeURIComponent(uploadModal.activeConfig)}`; document.body.appendChild(link); link.click(); link.remove(); }); -const cleanMqttModalElem = document.getElementById("modal-clean-mqtt"); +const validateModal = new LogModalElem({ + name: 'validate', + onPrepare: (modalElem, config) => { + modalElem.querySelector(".stop-logs").innerHTML = "Stop"; + modalElem.querySelector(".action-edit").setAttribute('data-node', validateModal.activeConfig); + modalElem.querySelector(".action-upload").setAttribute('data-node', validateModal.activeConfig); + modalElem.querySelector(".action-upload").classList.add('disabled'); + }, + onProcessExit: (modalElem, code) => { + if (code === 0) { + M.toast({ + html: `${validateModal.activeConfig} is valid 👍`, + displayLength: 5000, + }); + modalElem.querySelector(".action-upload").classList.remove('disabled'); + } else { + M.toast({ + html: `${validateModal.activeConfig} is invalid 😕`, + displayLength: 5000, + }); + } + modalElem.querySelector(".stop-logs").innerHTML = "Close"; + }, + onSocketClose: (modalElem) => { + M.toast({html: 'Terminated process.'}); + }, +}); +validateModal.setup(); -document.querySelectorAll(".action-clean-mqtt").forEach((btn) => { - btn.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); - const modalInstance = M.Modal.getInstance(cleanMqttModalElem); - const log = cleanMqttModalElem.querySelector(".log"); - log.innerHTML = ""; - const colorState = initializeColorState(); - const stopLogsButton = cleanMqttModalElem.querySelector(".stop-logs"); - let stopped = false; - stopLogsButton.innerHTML = "Stop"; - modalInstance.open(); - - const filenameField = cleanMqttModalElem.querySelector('.filename'); - filenameField.innerHTML = configuration; - - const logSocket = new WebSocket(wsUrl + "clean-mqtt"); - logSocket.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - if (data.event === "line") { - colorReplace(log, colorState, data.data); - } else if (data.event === "exit") { - stopLogsButton.innerHTML = "Close"; - stopped = true; - } - }); - logSocket.addEventListener('open', () => { - const msg = JSON.stringify({configuration: configuration}); - logSocket.send(msg); - }); - logSocket.addEventListener('close', () => { - if (!stopped) { - M.toast({html: 'Terminated process.'}); - } - }); - modalInstance.options.onCloseStart = () => { - logSocket.close(); - }; - }); +const downloadButton = document.querySelector('.download-binary'); +const compileModal = new LogModalElem({ + name: 'compile', + onPrepare: (modalElem, config) => { + modalElem.querySelector('.stop-logs').innerHTML = "Stop"; + downloadButton.classList.add('disabled'); + }, + onProcessExit: (modalElem, code) => { + if (code === 0) { + M.toast({html: "Program exited successfully."}); + downloadButton.classList.remove('disabled'); + } else { + M.toast({html: `Program failed with code ${data.code}`}); + } + modalElem.querySelector(".stop-logs").innerHTML = "Close"; + }, + onSocketClose: (modalElem) => { + M.toast({html: 'Terminated process.'}); + }, + dismissible: false, +}); +compileModal.setup(); +downloadButton.addEventListener('click', () => { + const link = document.createElement("a"); + link.download = name; + link.href = `./download.bin?configuration=${encodeURIComponent(compileModal.activeConfig)}`; + document.body.appendChild(link); + link.click(); + link.remove(); }); -const cleanModalElem = document.getElementById("modal-clean"); - -document.querySelectorAll(".action-clean").forEach((btn) => { - btn.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); - const modalInstance = M.Modal.getInstance(cleanModalElem); - const log = cleanModalElem.querySelector(".log"); - log.innerHTML = ""; - const colorState = initializeColorState(); - const stopLogsButton = cleanModalElem.querySelector(".stop-logs"); - let stopped = false; - stopLogsButton.innerHTML = "Stop"; - modalInstance.open(); - - const filenameField = cleanModalElem.querySelector('.filename'); - filenameField.innerHTML = configuration; - - const logSocket = new WebSocket(wsUrl + "clean"); - logSocket.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - if (data.event === "line") { - colorReplace(log, colorState, data.data); - } else if (data.event === "exit") { - if (data.code === 0) { - M.toast({html: "Program exited successfully."}); - downloadButton.classList.remove('disabled'); - } else { - M.toast({html: `Program failed with code ${data.code}`}); - } - stopLogsButton.innerHTML = "Close"; - stopped = true; - } - }); - logSocket.addEventListener('open', () => { - const msg = JSON.stringify({configuration: configuration}); - logSocket.send(msg); - }); - logSocket.addEventListener('close', () => { - if (!stopped) { - M.toast({html: 'Terminated process.'}); - } - }); - modalInstance.options.onCloseStart = () => { - logSocket.close(); - }; - }); +const cleanMqttModal = new LogModalElem({ + name: 'clean-mqtt', + onPrepare: (modalElem, config) => { + modalElem.querySelector('.stop-logs').innerHTML = "Stop"; + }, + onProcessExit: (modalElem, code) => { + modalElem.querySelector(".stop-logs").innerHTML = "Close"; + }, + onSocketClose: (modalElem) => { + M.toast({html: 'Terminated process.'}); + }, }); +cleanMqttModal.setup(); + +const cleanModal = new LogModalElem({ + name: 'clean', + onPrepare: (modalElem, config) => { + modalElem.querySelector(".stop-logs").innerHTML = "Stop"; + }, + onProcessExit: (modalElem, code) => { + if (code === 0) { + M.toast({html: "Program exited successfully."}); + } else { + M.toast({html: `Program failed with code ${code}`}); + } + modalElem.querySelector(".stop-logs").innerHTML = "Close"; + }, + onSocketClose: (modalElem) => { + M.toast({html: 'Terminated process.'}); + }, +}); +cleanModal.setup(); document.querySelectorAll(".action-delete").forEach((btn) => { btn.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); + let configuration = e.target.getAttribute('data-node'); - fetch(`${relative_url}delete?configuration=${configuration}`, { + fetch(`./delete?configuration=${configuration}`, { credentials: "same-origin", method: "POST", }).then(res => res.text()).then(() => { @@ -606,7 +554,7 @@ document.querySelectorAll(".action-delete").forEach((btn) => { document.querySelector(`.entry-row[data-node="${configuration}"]`).remove(); undoButton.addEventListener('click', () => { - fetch(`${relative_url}undo-delete?configuration=${configuration}`, { + fetch(`./undo-delete?configuration=${configuration}`, { credentials: "same-origin", method: "POST", }).then(res => res.text()).then(() => { @@ -620,24 +568,110 @@ document.querySelectorAll(".action-delete").forEach((btn) => { const editModalElem = document.getElementById("modal-editor"); const editorElem = editModalElem.querySelector("#editor"); const editor = ace.edit(editorElem); +let activeEditorConfig = null; +let aceWs = null; +let aceValidationScheduled = false; +let aceValidationRunning = false; +const startAceWebsocket = () => { + aceWs = new WebSocket(`${wsUrl}ace`); + aceWs.addEventListener('message', (event) => { + const raw = JSON.parse(event.data); + if (raw.event === "line") { + const msg = JSON.parse(raw.data); + if (msg.type === "result") { + console.log(msg); + const arr = []; + + for (const v of msg.validation_errors) { + let o = { + text: v.message, + type: 'error', + row: 0, + column: 0 + }; + if (v.range != null) { + o.row = v.range.start_line; + o.column = v.range.start_col; + } + arr.push(o); + } + for (const v of msg.yaml_errors) { + arr.push({ + text: v.message, + type: 'error', + row: 0, + column: 0 + }); + } + + editor.session.setAnnotations(arr); + + if(arr.length) { + editorUploadButton.classList.add('disabled'); + } else { + editorUploadButton.classList.remove('disabled'); + } + + aceValidationRunning = false; + } else if (msg.type === "read_file") { + sendAceStdin({ + type: 'file_response', + content: editor.getValue() + }); + } + } + }); + aceWs.addEventListener('open', () => { + const msg = JSON.stringify({type: 'spawn'}); + aceWs.send(msg); + }); + aceWs.addEventListener('close', () => { + aceWs = null; + setTimeout(startAceWebsocket, 5000) + }); +}; +const sendAceStdin = (data) => { + let send = JSON.stringify({ + type: 'stdin', + data: JSON.stringify(data)+'\n', + }); + aceWs.send(send); +}; +startAceWebsocket(); + editor.setTheme("ace/theme/dreamweaver"); editor.session.setMode("ace/mode/yaml"); editor.session.setOption('useSoftTabs', true); editor.session.setOption('tabSize', 2); +editor.session.setOption('useWorker', false); const saveButton = editModalElem.querySelector(".save-button"); +const editorUploadButton = editModalElem.querySelector(".editor-upload-button"); const saveEditor = () => { - fetch(`${relative_url}edit?configuration=${configuration}`, { + fetch(`./edit?configuration=${activeEditorConfig}`, { credentials: "same-origin", method: "POST", body: editor.getValue() }).then(res => res.text()).then(() => { M.toast({ - html: `Saved ${configuration}` + html: `Saved ${activeEditorConfig}` }); }); }; +const debounce = (func, wait) => { + let timeout; + return function() { + let context = this, args = arguments; + let later = function() { + timeout = null; + func.apply(context, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +}; + editor.commands.addCommand({ name: 'saveCommand', bindKey: {win: 'Ctrl-S', mac: 'Command-S'}, @@ -645,16 +679,36 @@ editor.commands.addCommand({ readOnly: false }); +editor.session.on('change', debounce(() => { + aceValidationScheduled = true; +}, 250)); + +setInterval(() => { + if (!aceValidationScheduled || aceValidationRunning) + return; + if (aceWs == null) + return; + + sendAceStdin({ + type: 'validate', + file: activeEditorConfig + }); + aceValidationRunning = true; + aceValidationScheduled = false; +}, 100); + saveButton.addEventListener('click', saveEditor); +editorUploadButton.addEventListener('click', saveEditor); document.querySelectorAll(".action-edit").forEach((btn) => { btn.addEventListener('click', (e) => { - configuration = e.target.getAttribute('data-node'); + activeEditorConfig = e.target.getAttribute('data-node'); const modalInstance = M.Modal.getInstance(editModalElem); const filenameField = editModalElem.querySelector('.filename'); - filenameField.innerHTML = configuration; + editorUploadButton.setAttribute('data-node', activeEditorConfig); + filenameField.innerHTML = activeEditorConfig; - fetch(`${relative_url}edit?configuration=${configuration}`, {credentials: "same-origin"}) + fetch(`./edit?configuration=${activeEditorConfig}`, {credentials: "same-origin"}) .then(res => res.text()).then(response => { editor.setValue(response, -1); }); @@ -669,10 +723,6 @@ const startWizard = () => { const modalInstance = M.Modal.getInstance(modalSetupElem); modalInstance.open(); - modalInstance.options.onCloseStart = () => { - - }; - $('.stepper').activateStepper({ linearStepsNavigation: false, autoFocusInput: true, @@ -682,15 +732,12 @@ const startWizard = () => { }); }; -const scrollToBottomOfElement = (element) => { - var atBottom = false; - if (element.scrollTop + 30 >= (element.scrollHeight - element.offsetHeight)) { - atBottom = true; - } +setupWizardStart.addEventListener('click', startWizard); - if (atBottom) { - element.scrollTop = element.scrollHeight; - } -} +jQuery.validator.addMethod("nospaces", (value, element) => { + return value.indexOf(' ') < 0; +}, "Name must not contain spaces."); -setupWizardStart.addEventListener('click', startWizard); \ No newline at end of file +jQuery.validator.addMethod("lowercase", (value, element) => { + return value === value.toLowerCase(); +}, "Name must be lowercase."); diff --git a/esphome/dashboard/static/ext-searchbox.js b/esphome/dashboard/static/ext-searchbox.js index 13241257d3..c6379623a7 100644 --- a/esphome/dashboard/static/ext-searchbox.js +++ b/esphome/dashboard/static/ext-searchbox.js @@ -5,4 +5,3 @@ ace.define("ace/ext/searchbox",["require","exports","module","ace/lib/dom","ace/ } }); })(); - \ No newline at end of file diff --git a/esphome/dashboard/static/jquery.validate.min.js b/esphome/dashboard/static/jquery.validate.min.js index 32ba047d75..36d155fe1a 100644 --- a/esphome/dashboard/static/jquery.validate.min.js +++ b/esphome/dashboard/static/jquery.validate.min.js @@ -1,4 +1,4 @@ /*! jQuery Validation Plugin - v1.15.0 - 2/24/2016 * http://jqueryvalidation.org/ * Copyright (c) 2016 Jörn Zaefferer; Licensed MIT */ -!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){a.extend(a.fn,{validate:function(b){if(!this.length)return void(b&&b.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."));var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.on("click.validate",":submit",function(b){c.settings.submitHandler&&(c.submitButton=b.target),a(this).hasClass("cancel")&&(c.cancelSubmit=!0),void 0!==a(this).attr("formnovalidate")&&(c.cancelSubmit=!0)}),this.on("submit.validate",function(b){function d(){var d,e;return c.settings.submitHandler?(c.submitButton&&(d=a("").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),e=c.settings.submitHandler.call(c,c.currentForm,b),c.submitButton&&d.remove(),void 0!==e?e:!1):!0}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,b||(d=d.concat(c.errorList))}),c.errorList=d),b},rules:function(b,c){if(this.length){var d,e,f,g,h,i,j=this[0];if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(b,c){i[c]=f[c],delete f[c],"required"===c&&a(j).removeAttr("aria-required")}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g),a(j).attr("aria-required","true")),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}}),a.extend(a.expr[":"],{blank:function(b){return!a.trim(""+a(b).val())},filled:function(b){var c=a(b).val();return null!==c&&!!a.trim(""+c)},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:void 0===c?b:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",pendingClass:"pending",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||-1!==a.inArray(c.keyCode,d)||(b.name in this.submitted||b.name in this.invalid)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date ( ISO ).",number:"Please enter a valid number.",digits:"Please enter only digits.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}."),step:a.validator.format("Please enter a multiple of {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){var c=a.data(this.form,"validator"),d="on"+b.type.replace(/^validate/,""),e=c.settings;e[d]&&!a(this).is(e.ignore)&&e[d].call(c,this,b)}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){d[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox'], [contenteditable]",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler),a(this.currentForm).find("[required], [data-rule-required], .required").attr("aria-required","true")},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c,d,e=this.clean(b),f=this.validationTargetFor(e),g=this,h=!0;return void 0===f?delete this.invalid[e.name]:(this.prepareElement(f),this.currentElements=a(f),d=this.groups[f.name],d&&a.each(this.groups,function(a,b){b===d&&a!==f.name&&(e=g.validationTargetFor(g.clean(g.findByName(a))),e&&e.name in g.invalid&&(g.currentElements.push(e),h=h&&g.check(e)))}),c=this.check(f)!==!1,h=h&&c,c?this.invalid[f.name]=!1:this.invalid[f.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),a(b).attr("aria-invalid",!c)),h},showErrors:function(b){if(b){var c=this;a.extend(this.errorMap,b),this.errorList=a.map(this.errorMap,function(a,b){return{message:a,element:c.findByName(b)[0]}}),this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.invalid={},this.submitted={},this.prepareForm(),this.hideErrors();var b=this.elements().removeData("previousValue").removeAttr("aria-invalid");this.resetElements(b)},resetElements:function(a){var b;if(this.settings.unhighlight)for(b=0;a[b];b++)this.settings.unhighlight.call(this,a[b],this.settings.errorClass,""),this.findByName(a[b].name).removeClass(this.settings.validClass);else a.removeClass(this.settings.errorClass).removeClass(this.settings.validClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)a[b]&&c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea, [contenteditable]").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){var d=this.name||a(this).attr("name");return!d&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.hasAttribute("contenteditable")&&(this.form=a(this).closest("form")[0]),d in c||!b.objectLength(a(this).rules())?!1:(c[d]=!0,!0)})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},resetInternals:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([])},reset:function(){this.resetInternals(),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d,e=a(b),f=b.type;return"radio"===f||"checkbox"===f?this.findByName(b.name).filter(":checked").val():"number"===f&&"undefined"!=typeof b.validity?b.validity.badInput?"NaN":e.val():(c=b.hasAttribute("contenteditable")?e.text():e.val(),"file"===f?"C:\\fakepath\\"===c.substr(0,12)?c.substr(12):(d=c.lastIndexOf("/"),d>=0?c.substr(d+1):(d=c.lastIndexOf("\\"),d>=0?c.substr(d+1):c)):"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f=a(b).rules(),g=a.map(f,function(a,b){return b}).length,h=!1,i=this.elementValue(b);if("function"==typeof f.normalizer){if(i=f.normalizer.call(b,i),"string"!=typeof i)throw new TypeError("The normalizer should return a string value.");delete f.normalizer}for(d in f){e={method:d,parameters:f[d]};try{if(c=a.validator.methods[d].call(this,i,b,e.parameters),"dependency-mismatch"===c&&1===g){h=!0;continue}if(h=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(j){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",j),j instanceof TypeError&&(j.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),j}}if(!h)return this.objectLength(f)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+""),e=/\$?\{(\d+)\}/g;return"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),d},formatAndAdd:function(a,b){var c=this.defaultMessage(a,b);this.errorList.push({message:c,element:a,method:b.method}),this.errorMap[a.name]=c,this.submitted[a.name]=c},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g,h=this.errorsFor(b),i=this.idOrName(b),j=a(b).attr("aria-describedby");h.length?(h.removeClass(this.settings.validClass).addClass(this.settings.errorClass),h.html(c)):(h=a("<"+this.settings.errorElement+">").attr("id",i+"-error").addClass(this.settings.errorClass).html(c||""),d=h,this.settings.wrapper&&(d=h.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement(d,a(b)):d.insertAfter(b),h.is("label")?h.attr("for",i):0===h.parents("label[for='"+this.escapeCssMeta(i)+"']").length&&(f=h.attr("id"),j?j.match(new RegExp("\\b"+this.escapeCssMeta(f)+"\\b"))||(j+=" "+f):j=f,a(b).attr("aria-describedby",j),e=this.groups[b.name],e&&(g=this,a.each(g.groups,function(b,c){c===e&&a("[name='"+g.escapeCssMeta(b)+"']",g.currentForm).attr("aria-describedby",h.attr("id"))})))),!c&&this.settings.success&&(h.text(""),"string"==typeof this.settings.success?h.addClass(this.settings.success):this.settings.success(h,b)),this.toShow=this.toShow.add(h)},errorsFor:function(b){var c=this.escapeCssMeta(this.idOrName(b)),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+this.escapeCssMeta(d).replace(/\s+/g,", #")),this.errors().filter(e)},escapeCssMeta:function(a){return a.replace(/([\\!"#$%&'()*+,./:;<=>?@\[\]^`{|}~])/g,"\\$1")},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+this.escapeCssMeta(b)+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return this.dependTypes[typeof a]?this.dependTypes[typeof a](a,b):!0},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(b){this.pending[b.name]||(this.pendingRequest++,a(b).addClass(this.settings.pendingClass),this.pending[b.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],a(b).removeClass(this.settings.pendingClass),c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b,c){return a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,{method:c})})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator").find(".validate-equalTo-blur").off(".validate-equalTo").removeClass("validate-equalTo-blur")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max|step/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0!==e.param?e.param:!0:(a.data(c.form,"validator").resetElements(a(c)),delete b[d])}}),a.each(b,function(d,e){b[d]=a.isFunction(e)&&"normalizer"!==d?e(c):e}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var c;b[this]&&(a.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(c=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(c[0]),Number(c[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(a)},date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a).toString())},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},minlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d},maxlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||d>=e},rangelength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d[0]&&e<=d[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||c>=a},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},step:function(b,c,d){var e=a(c).attr("type"),f="Step attribute on input type "+e+" is not supported.",g=["text","number","range"],h=new RegExp("\\b"+e+"\\b"),i=e&&!h.test(g.join());if(i)throw new Error(f);return this.optional(c)||b%d===0},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.not(".validate-equalTo-blur").length&&e.addClass("validate-equalTo-blur").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d,e){if(this.optional(c))return"dependency-mismatch";e="string"==typeof e&&e||"remote";var f,g,h,i=this.previousValue(c,e);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),i.originalMessage=i.originalMessage||this.settings.messages[c.name][e],this.settings.messages[c.name][e]=i.message,d="string"==typeof d&&{url:d}||d,h=a.param(a.extend({data:b},d.data)),i.old===h?i.valid:(i.old=h,f=this,this.startRequest(c),g={},g[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:g,context:f.currentForm,success:function(a){var d,g,h,j=a===!0||"true"===a;f.settings.messages[c.name][e]=i.originalMessage,j?(h=f.formSubmitted,f.resetInternals(),f.toHide=f.errorsFor(c),f.formSubmitted=h,f.successList.push(c),f.invalid[c.name]=!1,f.showErrors()):(d={},g=a||f.defaultMessage(c,{method:e,parameters:b}),d[c.name]=i.message=g,f.invalid[c.name]=!0,f.showErrors(d)),i.valid=j,f.stopRequest(c,j)}},d)),"pending")}}});var b,c={};a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,d){var e=a.port;"abort"===a.mode&&(c[e]&&c[e].abort(),c[e]=d)}):(b=a.ajax,a.ajax=function(d){var e=("mode"in d?d:a.ajaxSettings).mode,f=("port"in d?d:a.ajaxSettings).port;return"abort"===e?(c[f]&&c[f].abort(),c[f]=b.apply(this,arguments),c[f]):b.apply(this,arguments)})}); \ No newline at end of file +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){a.extend(a.fn,{validate:function(b){if(!this.length)return void(b&&b.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."));var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.on("click.validate",":submit",function(b){c.settings.submitHandler&&(c.submitButton=b.target),a(this).hasClass("cancel")&&(c.cancelSubmit=!0),void 0!==a(this).attr("formnovalidate")&&(c.cancelSubmit=!0)}),this.on("submit.validate",function(b){function d(){var d,e;return c.settings.submitHandler?(c.submitButton&&(d=a("").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),e=c.settings.submitHandler.call(c,c.currentForm,b),c.submitButton&&d.remove(),void 0!==e?e:!1):!0}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,b||(d=d.concat(c.errorList))}),c.errorList=d),b},rules:function(b,c){if(this.length){var d,e,f,g,h,i,j=this[0];if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(b,c){i[c]=f[c],delete f[c],"required"===c&&a(j).removeAttr("aria-required")}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g),a(j).attr("aria-required","true")),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}}),a.extend(a.expr[":"],{blank:function(b){return!a.trim(""+a(b).val())},filled:function(b){var c=a(b).val();return null!==c&&!!a.trim(""+c)},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:void 0===c?b:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",pendingClass:"pending",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||-1!==a.inArray(c.keyCode,d)||(b.name in this.submitted||b.name in this.invalid)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date ( ISO ).",number:"Please enter a valid number.",digits:"Please enter only digits.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}."),step:a.validator.format("Please enter a multiple of {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){var c=a.data(this.form,"validator"),d="on"+b.type.replace(/^validate/,""),e=c.settings;e[d]&&!a(this).is(e.ignore)&&e[d].call(c,this,b)}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){d[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox'], [contenteditable]",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler),a(this.currentForm).find("[required], [data-rule-required], .required").attr("aria-required","true")},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c,d,e=this.clean(b),f=this.validationTargetFor(e),g=this,h=!0;return void 0===f?delete this.invalid[e.name]:(this.prepareElement(f),this.currentElements=a(f),d=this.groups[f.name],d&&a.each(this.groups,function(a,b){b===d&&a!==f.name&&(e=g.validationTargetFor(g.clean(g.findByName(a))),e&&e.name in g.invalid&&(g.currentElements.push(e),h=h&&g.check(e)))}),c=this.check(f)!==!1,h=h&&c,c?this.invalid[f.name]=!1:this.invalid[f.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),a(b).attr("aria-invalid",!c)),h},showErrors:function(b){if(b){var c=this;a.extend(this.errorMap,b),this.errorList=a.map(this.errorMap,function(a,b){return{message:a,element:c.findByName(b)[0]}}),this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.invalid={},this.submitted={},this.prepareForm(),this.hideErrors();var b=this.elements().removeData("previousValue").removeAttr("aria-invalid");this.resetElements(b)},resetElements:function(a){var b;if(this.settings.unhighlight)for(b=0;a[b];b++)this.settings.unhighlight.call(this,a[b],this.settings.errorClass,""),this.findByName(a[b].name).removeClass(this.settings.validClass);else a.removeClass(this.settings.errorClass).removeClass(this.settings.validClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)a[b]&&c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea, [contenteditable]").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){var d=this.name||a(this).attr("name");return!d&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.hasAttribute("contenteditable")&&(this.form=a(this).closest("form")[0]),d in c||!b.objectLength(a(this).rules())?!1:(c[d]=!0,!0)})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},resetInternals:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([])},reset:function(){this.resetInternals(),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d,e=a(b),f=b.type;return"radio"===f||"checkbox"===f?this.findByName(b.name).filter(":checked").val():"number"===f&&"undefined"!=typeof b.validity?b.validity.badInput?"NaN":e.val():(c=b.hasAttribute("contenteditable")?e.text():e.val(),"file"===f?"C:\\fakepath\\"===c.substr(0,12)?c.substr(12):(d=c.lastIndexOf("/"),d>=0?c.substr(d+1):(d=c.lastIndexOf("\\"),d>=0?c.substr(d+1):c)):"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f=a(b).rules(),g=a.map(f,function(a,b){return b}).length,h=!1,i=this.elementValue(b);if("function"==typeof f.normalizer){if(i=f.normalizer.call(b,i),"string"!=typeof i)throw new TypeError("The normalizer should return a string value.");delete f.normalizer}for(d in f){e={method:d,parameters:f[d]};try{if(c=a.validator.methods[d].call(this,i,b,e.parameters),"dependency-mismatch"===c&&1===g){h=!0;continue}if(h=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(j){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",j),j instanceof TypeError&&(j.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),j}}if(!h)return this.objectLength(f)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+""),e=/\$?\{(\d+)\}/g;return"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),d},formatAndAdd:function(a,b){var c=this.defaultMessage(a,b);this.errorList.push({message:c,element:a,method:b.method}),this.errorMap[a.name]=c,this.submitted[a.name]=c},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g,h=this.errorsFor(b),i=this.idOrName(b),j=a(b).attr("aria-describedby");h.length?(h.removeClass(this.settings.validClass).addClass(this.settings.errorClass),h.html(c)):(h=a("<"+this.settings.errorElement+">").attr("id",i+"-error").addClass(this.settings.errorClass).html(c||""),d=h,this.settings.wrapper&&(d=h.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement(d,a(b)):d.insertAfter(b),h.is("label")?h.attr("for",i):0===h.parents("label[for='"+this.escapeCssMeta(i)+"']").length&&(f=h.attr("id"),j?j.match(new RegExp("\\b"+this.escapeCssMeta(f)+"\\b"))||(j+=" "+f):j=f,a(b).attr("aria-describedby",j),e=this.groups[b.name],e&&(g=this,a.each(g.groups,function(b,c){c===e&&a("[name='"+g.escapeCssMeta(b)+"']",g.currentForm).attr("aria-describedby",h.attr("id"))})))),!c&&this.settings.success&&(h.text(""),"string"==typeof this.settings.success?h.addClass(this.settings.success):this.settings.success(h,b)),this.toShow=this.toShow.add(h)},errorsFor:function(b){var c=this.escapeCssMeta(this.idOrName(b)),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+this.escapeCssMeta(d).replace(/\s+/g,", #")),this.errors().filter(e)},escapeCssMeta:function(a){return a.replace(/([\\!"#$%&'()*+,./:;<=>?@\[\]^`{|}~])/g,"\\$1")},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+this.escapeCssMeta(b)+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return this.dependTypes[typeof a]?this.dependTypes[typeof a](a,b):!0},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(b){this.pending[b.name]||(this.pendingRequest++,a(b).addClass(this.settings.pendingClass),this.pending[b.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],a(b).removeClass(this.settings.pendingClass),c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b,c){return a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,{method:c})})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator").find(".validate-equalTo-blur").off(".validate-equalTo").removeClass("validate-equalTo-blur")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max|step/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0!==e.param?e.param:!0:(a.data(c.form,"validator").resetElements(a(c)),delete b[d])}}),a.each(b,function(d,e){b[d]=a.isFunction(e)&&"normalizer"!==d?e(c):e}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var c;b[this]&&(a.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(c=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(c[0]),Number(c[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(a)},date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a).toString())},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},minlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d},maxlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||d>=e},rangelength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d[0]&&e<=d[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||c>=a},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},step:function(b,c,d){var e=a(c).attr("type"),f="Step attribute on input type "+e+" is not supported.",g=["text","number","range"],h=new RegExp("\\b"+e+"\\b"),i=e&&!h.test(g.join());if(i)throw new Error(f);return this.optional(c)||b%d===0},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.not(".validate-equalTo-blur").length&&e.addClass("validate-equalTo-blur").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d,e){if(this.optional(c))return"dependency-mismatch";e="string"==typeof e&&e||"remote";var f,g,h,i=this.previousValue(c,e);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),i.originalMessage=i.originalMessage||this.settings.messages[c.name][e],this.settings.messages[c.name][e]=i.message,d="string"==typeof d&&{url:d}||d,h=a.param(a.extend({data:b},d.data)),i.old===h?i.valid:(i.old=h,f=this,this.startRequest(c),g={},g[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:g,context:f.currentForm,success:function(a){var d,g,h,j=a===!0||"true"===a;f.settings.messages[c.name][e]=i.originalMessage,j?(h=f.formSubmitted,f.resetInternals(),f.toHide=f.errorsFor(c),f.formSubmitted=h,f.successList.push(c),f.invalid[c.name]=!1,f.showErrors()):(d={},g=a||f.defaultMessage(c,{method:e,parameters:b}),d[c.name]=i.message=g,f.invalid[c.name]=!0,f.showErrors(d)),i.valid=j,f.stopRequest(c,j)}},d)),"pending")}}});var b,c={};a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,d){var e=a.port;"abort"===a.mode&&(c[e]&&c[e].abort(),c[e]=d)}):(b=a.ajax,a.ajax=function(d){var e=("mode"in d?d:a.ajaxSettings).mode,f=("port"in d?d:a.ajaxSettings).port;return"abort"===e?(c[f]&&c[f].abort(),c[f]=b.apply(this,arguments),c[f]):b.apply(this,arguments)})}); diff --git a/esphome/dashboard/static/materialize-stepper.min.css b/esphome/dashboard/static/materialize-stepper.min.css old mode 100755 new mode 100644 diff --git a/esphome/dashboard/static/materialize-stepper.min.js b/esphome/dashboard/static/materialize-stepper.min.js old mode 100755 new mode 100644 diff --git a/esphome/dashboard/static/materialize.min.js b/esphome/dashboard/static/materialize.min.js index 7d80c9375b..db081d2233 100644 --- a/esphome/dashboard/static/materialize.min.js +++ b/esphome/dashboard/static/materialize.min.js @@ -3,4 +3,4 @@ * Copyright 2014-2017 Materialize * MIT License (https://raw.githubusercontent.com/Dogfalo/materialize/master/LICENSE) */ -var _get=function t(e,i,n){null===e&&(e=Function.prototype);var s=Object.getOwnPropertyDescriptor(e,i);if(void 0===s){var o=Object.getPrototypeOf(e);return null===o?void 0:t(o,i,n)}if("value"in s)return s.value;var a=s.get;return void 0!==a?a.call(n):void 0},_createClass=function(){function n(t,e){for(var i=0;i/,p=/^\w+$/;function v(t,e){e=e||o;var i=u.test(t)?e.getElementsByClassName(t.slice(1)):p.test(t)?e.getElementsByTagName(t):e.querySelectorAll(t);return i}function f(t){if(!i){var e=(i=o.implementation.createHTMLDocument(null)).createElement("base");e.href=o.location.href,i.head.appendChild(e)}return i.body.innerHTML=t,i.body.childNodes}function m(t){"loading"!==o.readyState?t():o.addEventListener("DOMContentLoaded",t)}function g(t,e){if(!t)return this;if(t.cash&&t!==a)return t;var i,n=t,s=0;if(d(t))n=l.test(t)?o.getElementById(t.slice(1)):c.test(t)?f(t):v(t,e);else if(h(t))return m(t),this;if(!n)return this;if(n.nodeType||n===a)this[0]=n,this.length=1;else for(i=this.length=n.length;ss.right-i||l+e.width>window.innerWidth-i)&&(n.right=!0),(ho-i||h+e.height>window.innerHeight-i)&&(n.bottom=!0),n},M.checkPossibleAlignments=function(t,e,i,n){var s={top:!0,right:!0,bottom:!0,left:!0,spaceOnTop:null,spaceOnRight:null,spaceOnBottom:null,spaceOnLeft:null},o="visible"===getComputedStyle(e).overflow,a=e.getBoundingClientRect(),r=Math.min(a.height,window.innerHeight),l=Math.min(a.width,window.innerWidth),h=t.getBoundingClientRect(),d=e.scrollLeft,u=e.scrollTop,c=i.left-d,p=i.top-u,v=i.top+h.height-u;return s.spaceOnRight=o?window.innerWidth-(h.left+i.width):l-(c+i.width),s.spaceOnRight<0&&(s.left=!1),s.spaceOnLeft=o?h.right-i.width:c-i.width+h.width,s.spaceOnLeft<0&&(s.right=!1),s.spaceOnBottom=o?window.innerHeight-(h.top+i.height+n):r-(p+i.height+n),s.spaceOnBottom<0&&(s.top=!1),s.spaceOnTop=o?h.bottom-(i.height+n):v-(i.height-n),s.spaceOnTop<0&&(s.bottom=!1),s},M.getOverflowParent=function(t){return null==t?null:t===document.body||"visible"!==getComputedStyle(t).overflow?t:M.getOverflowParent(t.parentElement)},M.getIdFromTrigger=function(t){var e=t.getAttribute("data-target");return e||(e=(e=t.getAttribute("href"))?e.slice(1):""),e},M.getDocumentScrollTop=function(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0},M.getDocumentScrollLeft=function(){return window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0};var getTime=Date.now||function(){return(new Date).getTime()};M.throttle=function(i,n,s){var o=void 0,a=void 0,r=void 0,l=null,h=0;s||(s={});var d=function(){h=!1===s.leading?0:getTime(),l=null,r=i.apply(o,a),o=a=null};return function(){var t=getTime();h||!1!==s.leading||(h=t);var e=n-(t-h);return o=this,a=arguments,e<=0?(clearTimeout(l),l=null,h=t,r=i.apply(o,a),o=a=null):l||!1===s.trailing||(l=setTimeout(d,e)),r}};var $jscomp={scope:{}};$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(t,e,i){if(i.get||i.set)throw new TypeError("ES3 does not support getters and setters.");t!=Array.prototype&&t!=Object.prototype&&(t[e]=i.value)},$jscomp.getGlobal=function(t){return"undefined"!=typeof window&&window===t?t:"undefined"!=typeof global&&null!=global?global:t},$jscomp.global=$jscomp.getGlobal(this),$jscomp.SYMBOL_PREFIX="jscomp_symbol_",$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){},$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)},$jscomp.symbolCounter_=0,$jscomp.Symbol=function(t){return $jscomp.SYMBOL_PREFIX+(t||"")+$jscomp.symbolCounter_++},$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var t=$jscomp.global.Symbol.iterator;t||(t=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator")),"function"!=typeof Array.prototype[t]&&$jscomp.defineProperty(Array.prototype,t,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}}),$jscomp.initSymbolIterator=function(){}},$jscomp.arrayIterator=function(t){var e=0;return $jscomp.iteratorPrototype(function(){return e=k.currentTime)for(var h=0;ht&&(s.duration=e.duration),s.children.push(e)}),s.seek(0),s.reset(),s.autoplay&&s.restart(),s},s},O.random=function(t,e){return Math.floor(Math.random()*(e-t+1))+t},O}(),function(r,l){"use strict";var e={accordion:!0,onOpenStart:void 0,onOpenEnd:void 0,onCloseStart:void 0,onCloseEnd:void 0,inDuration:300,outDuration:300},t=function(t){function s(t,e){_classCallCheck(this,s);var i=_possibleConstructorReturn(this,(s.__proto__||Object.getPrototypeOf(s)).call(this,s,t,e));(i.el.M_Collapsible=i).options=r.extend({},s.defaults,e),i.$headers=i.$el.children("li").children(".collapsible-header"),i.$headers.attr("tabindex",0),i._setupEventHandlers();var n=i.$el.children("li.active").children(".collapsible-body");return i.options.accordion?n.first().css("display","block"):n.css("display","block"),i}return _inherits(s,Component),_createClass(s,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Collapsible=void 0}},{key:"_setupEventHandlers",value:function(){var e=this;this._handleCollapsibleClickBound=this._handleCollapsibleClick.bind(this),this._handleCollapsibleKeydownBound=this._handleCollapsibleKeydown.bind(this),this.el.addEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.addEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_removeEventHandlers",value:function(){var e=this;this.el.removeEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.removeEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_handleCollapsibleClick",value:function(t){var e=r(t.target).closest(".collapsible-header");if(t.target&&e.length){var i=e.closest(".collapsible");if(i[0]===this.el){var n=e.closest("li"),s=i.children("li"),o=n[0].classList.contains("active"),a=s.index(n);o?this.close(a):this.open(a)}}}},{key:"_handleCollapsibleKeydown",value:function(t){13===t.keyCode&&this._handleCollapsibleClickBound(t)}},{key:"_animateIn",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css({display:"block",overflow:"hidden",height:0,paddingTop:"",paddingBottom:""});var s=n.css("padding-top"),o=n.css("padding-bottom"),a=n[0].scrollHeight;n.css({paddingTop:0,paddingBottom:0}),l({targets:n[0],height:a,paddingTop:s,paddingBottom:o,duration:this.options.inDuration,easing:"easeInOutCubic",complete:function(t){n.css({overflow:"",paddingTop:"",paddingBottom:"",height:""}),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,i[0])}})}}},{key:"_animateOut",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css("overflow","hidden"),l({targets:n[0],height:0,paddingTop:0,paddingBottom:0,duration:this.options.outDuration,easing:"easeInOutCubic",complete:function(){n.css({height:"",overflow:"",padding:"",display:""}),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,i[0])}})}}},{key:"open",value:function(t){var i=this,e=this.$el.children("li").eq(t);if(e.length&&!e[0].classList.contains("active")){if("function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,e[0]),this.options.accordion){var n=this.$el.children("li");this.$el.children("li.active").each(function(t){var e=n.index(r(t));i.close(e)})}e[0].classList.add("active"),this._animateIn(t)}}},{key:"close",value:function(t){var e=this.$el.children("li").eq(t);e.length&&e[0].classList.contains("active")&&("function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,e[0]),e[0].classList.remove("active"),this._animateOut(t))}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Collapsible}},{key:"defaults",get:function(){return e}}]),s}();M.Collapsible=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"collapsible","M_Collapsible")}(cash,M.anime),function(h,i){"use strict";var e={alignment:"left",autoFocus:!0,constrainWidth:!0,container:null,coverTrigger:!0,closeOnClick:!0,hover:!1,inDuration:150,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onItemClick:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return i.el.M_Dropdown=i,n._dropdowns.push(i),i.id=M.getIdFromTrigger(t),i.dropdownEl=document.getElementById(i.id),i.$dropdownEl=h(i.dropdownEl),i.options=h.extend({},n.defaults,e),i.isOpen=!1,i.isScrollable=!1,i.isTouchMoving=!1,i.focusedIndex=-1,i.filterQuery=[],i.options.container?h(i.options.container).append(i.dropdownEl):i.$el.after(i.dropdownEl),i._makeDropdownFocusable(),i._resetFilterQueryBound=i._resetFilterQuery.bind(i),i._handleDocumentClickBound=i._handleDocumentClick.bind(i),i._handleDocumentTouchmoveBound=i._handleDocumentTouchmove.bind(i),i._handleDropdownClickBound=i._handleDropdownClick.bind(i),i._handleDropdownKeydownBound=i._handleDropdownKeydown.bind(i),i._handleTriggerKeydownBound=i._handleTriggerKeydown.bind(i),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._resetDropdownStyles(),this._removeEventHandlers(),n._dropdowns.splice(n._dropdowns.indexOf(this),1),this.el.M_Dropdown=void 0}},{key:"_setupEventHandlers",value:function(){this.el.addEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.addEventListener("click",this._handleDropdownClickBound),this.options.hover?(this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.addEventListener("mouseleave",this._handleMouseLeaveBound)):(this._handleClickBound=this._handleClick.bind(this),this.el.addEventListener("click",this._handleClickBound))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.removeEventListener("click",this._handleDropdownClickBound),this.options.hover?(this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.removeEventListener("mouseleave",this._handleMouseLeaveBound)):this.el.removeEventListener("click",this._handleClickBound)}},{key:"_setupTemporaryEventHandlers",value:function(){document.body.addEventListener("click",this._handleDocumentClickBound,!0),document.body.addEventListener("touchend",this._handleDocumentClickBound),document.body.addEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.addEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_removeTemporaryEventHandlers",value:function(){document.body.removeEventListener("click",this._handleDocumentClickBound,!0),document.body.removeEventListener("touchend",this._handleDocumentClickBound),document.body.removeEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.removeEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_handleClick",value:function(t){t.preventDefault(),this.open()}},{key:"_handleMouseEnter",value:function(){this.open()}},{key:"_handleMouseLeave",value:function(t){var e=t.toElement||t.relatedTarget,i=!!h(e).closest(".dropdown-content").length,n=!1,s=h(e).closest(".dropdown-trigger");s.length&&s[0].M_Dropdown&&s[0].M_Dropdown.isOpen&&(n=!0),n||i||this.close()}},{key:"_handleDocumentClick",value:function(t){var e=this,i=h(t.target);this.options.closeOnClick&&i.closest(".dropdown-content").length&&!this.isTouchMoving?setTimeout(function(){e.close()},0):!i.closest(".dropdown-trigger").length&&i.closest(".dropdown-content").length||setTimeout(function(){e.close()},0),this.isTouchMoving=!1}},{key:"_handleTriggerKeydown",value:function(t){t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ENTER||this.isOpen||(t.preventDefault(),this.open())}},{key:"_handleDocumentTouchmove",value:function(t){h(t.target).closest(".dropdown-content").length&&(this.isTouchMoving=!0)}},{key:"_handleDropdownClick",value:function(t){if("function"==typeof this.options.onItemClick){var e=h(t.target).closest("li")[0];this.options.onItemClick.call(this,e)}}},{key:"_handleDropdownKeydown",value:function(t){if(t.which===M.keys.TAB)t.preventDefault(),this.close();else if(t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ARROW_UP||!this.isOpen)if(t.which===M.keys.ENTER&&this.isOpen){var e=this.dropdownEl.children[this.focusedIndex],i=h(e).find("a, button").first();i.length?i[0].click():e&&e.click()}else t.which===M.keys.ESC&&this.isOpen&&(t.preventDefault(),this.close());else{t.preventDefault();var n=t.which===M.keys.ARROW_DOWN?1:-1,s=this.focusedIndex,o=!1;do{if(s+=n,this.dropdownEl.children[s]&&-1!==this.dropdownEl.children[s].tabIndex){o=!0;break}}while(sl.spaceOnBottom?(h="bottom",i+=l.spaceOnTop,o-=l.spaceOnTop):i+=l.spaceOnBottom)),!l[d]){var u="left"===d?"right":"left";l[u]?d=u:l.spaceOnLeft>l.spaceOnRight?(d="right",n+=l.spaceOnLeft,s-=l.spaceOnLeft):(d="left",n+=l.spaceOnRight)}return"bottom"===h&&(o=o-e.height+(this.options.coverTrigger?t.height:0)),"right"===d&&(s=s-e.width+t.width),{x:s,y:o,verticalAlignment:h,horizontalAlignment:d,height:i,width:n}}},{key:"_animateIn",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:[0,1],easing:"easeOutQuad"},scaleX:[.3,1],scaleY:[.3,1],duration:this.options.inDuration,easing:"easeOutQuint",complete:function(t){e.options.autoFocus&&e.dropdownEl.focus(),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,e.el)}})}},{key:"_animateOut",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:0,easing:"easeOutQuint"},scaleX:.3,scaleY:.3,duration:this.options.outDuration,easing:"easeOutQuint",complete:function(t){e._resetDropdownStyles(),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,e.el)}})}},{key:"_placeDropdown",value:function(){var t=this.options.constrainWidth?this.el.getBoundingClientRect().width:this.dropdownEl.getBoundingClientRect().width;this.dropdownEl.style.width=t+"px";var e=this._getDropdownPosition();this.dropdownEl.style.left=e.x+"px",this.dropdownEl.style.top=e.y+"px",this.dropdownEl.style.height=e.height+"px",this.dropdownEl.style.width=e.width+"px",this.dropdownEl.style.transformOrigin=("left"===e.horizontalAlignment?"0":"100%")+" "+("top"===e.verticalAlignment?"0":"100%")}},{key:"open",value:function(){this.isOpen||(this.isOpen=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this._resetDropdownStyles(),this.dropdownEl.style.display="block",this._placeDropdown(),this._animateIn(),this._setupTemporaryEventHandlers())}},{key:"close",value:function(){this.isOpen&&(this.isOpen=!1,this.focusedIndex=-1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this._animateOut(),this._removeTemporaryEventHandlers(),this.options.autoFocus&&this.el.focus())}},{key:"recalculateDimensions",value:function(){this.isOpen&&(this.$dropdownEl.css({width:"",height:"",left:"",top:"","transform-origin":""}),this._placeDropdown())}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Dropdown}},{key:"defaults",get:function(){return e}}]),n}();t._dropdowns=[],M.Dropdown=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"dropdown","M_Dropdown")}(cash,M.anime),function(s,i){"use strict";var e={opacity:.5,inDuration:250,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,preventScrolling:!0,dismissible:!0,startingTop:"4%",endingTop:"10%"},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Modal=i).options=s.extend({},n.defaults,e),i.isOpen=!1,i.id=i.$el.attr("id"),i._openingTrigger=void 0,i.$overlay=s(''),i.el.tabIndex=0,i._nthModalOpened=0,n._count++,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._count--,this._removeEventHandlers(),this.el.removeAttribute("style"),this.$overlay.remove(),this.el.M_Modal=void 0}},{key:"_setupEventHandlers",value:function(){this._handleOverlayClickBound=this._handleOverlayClick.bind(this),this._handleModalCloseClickBound=this._handleModalCloseClick.bind(this),1===n._count&&document.body.addEventListener("click",this._handleTriggerClick),this.$overlay[0].addEventListener("click",this._handleOverlayClickBound),this.el.addEventListener("click",this._handleModalCloseClickBound)}},{key:"_removeEventHandlers",value:function(){0===n._count&&document.body.removeEventListener("click",this._handleTriggerClick),this.$overlay[0].removeEventListener("click",this._handleOverlayClickBound),this.el.removeEventListener("click",this._handleModalCloseClickBound)}},{key:"_handleTriggerClick",value:function(t){var e=s(t.target).closest(".modal-trigger");if(e.length){var i=M.getIdFromTrigger(e[0]),n=document.getElementById(i).M_Modal;n&&n.open(e),t.preventDefault()}}},{key:"_handleOverlayClick",value:function(){this.options.dismissible&&this.close()}},{key:"_handleModalCloseClick",value:function(t){s(t.target).closest(".modal-close").length&&this.close()}},{key:"_handleKeydown",value:function(t){27===t.keyCode&&this.options.dismissible&&this.close()}},{key:"_handleFocus",value:function(t){this.el.contains(t.target)||this._nthModalOpened!==n._modalsOpen||this.el.focus()}},{key:"_animateIn",value:function(){var t=this;s.extend(this.el.style,{display:"block",opacity:0}),s.extend(this.$overlay[0].style,{display:"block",opacity:0}),i({targets:this.$overlay[0],opacity:this.options.opacity,duration:this.options.inDuration,easing:"easeOutQuad"});var e={targets:this.el,duration:this.options.inDuration,easing:"easeOutCubic",complete:function(){"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el,t._openingTrigger)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:0,opacity:1}):s.extend(e,{top:[this.options.startingTop,this.options.endingTop],opacity:1,scaleX:[.8,1],scaleY:[.8,1]}),i(e)}},{key:"_animateOut",value:function(){var t=this;i({targets:this.$overlay[0],opacity:0,duration:this.options.outDuration,easing:"easeOutQuart"});var e={targets:this.el,duration:this.options.outDuration,easing:"easeOutCubic",complete:function(){t.el.style.display="none",t.$overlay.remove(),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:"-100%",opacity:0}):s.extend(e,{top:[this.options.endingTop,this.options.startingTop],opacity:0,scaleX:.8,scaleY:.8}),i(e)}},{key:"open",value:function(t){if(!this.isOpen)return this.isOpen=!0,n._modalsOpen++,this._nthModalOpened=n._modalsOpen,this.$overlay[0].style.zIndex=1e3+2*n._modalsOpen,this.el.style.zIndex=1e3+2*n._modalsOpen+1,this._openingTrigger=t?t[0]:void 0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el,this._openingTrigger),this.options.preventScrolling&&(document.body.style.overflow="hidden"),this.el.classList.add("open"),this.el.insertAdjacentElement("afterend",this.$overlay[0]),this.options.dismissible&&(this._handleKeydownBound=this._handleKeydown.bind(this),this._handleFocusBound=this._handleFocus.bind(this),document.addEventListener("keydown",this._handleKeydownBound),document.addEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateIn(),this.el.focus(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,n._modalsOpen--,this._nthModalOpened=0,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this.el.classList.remove("open"),0===n._modalsOpen&&(document.body.style.overflow=""),this.options.dismissible&&(document.removeEventListener("keydown",this._handleKeydownBound),document.removeEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateOut(),this}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Modal}},{key:"defaults",get:function(){return e}}]),n}();t._modalsOpen=0,t._count=0,M.Modal=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"modal","M_Modal")}(cash,M.anime),function(o,a){"use strict";var e={inDuration:275,outDuration:200,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Materialbox=i).options=o.extend({},n.defaults,e),i.overlayActive=!1,i.doneAnimating=!0,i.placeholder=o("
").addClass("material-placeholder"),i.originalWidth=0,i.originalHeight=0,i.originInlineStyles=i.$el.attr("style"),i.caption=i.el.getAttribute("data-caption")||"",i.$el.before(i.placeholder),i.placeholder.append(i.$el),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Materialbox=void 0,o(this.placeholder).after(this.el).remove(),this.$el.removeAttr("style")}},{key:"_setupEventHandlers",value:function(){this._handleMaterialboxClickBound=this._handleMaterialboxClick.bind(this),this.el.addEventListener("click",this._handleMaterialboxClickBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleMaterialboxClickBound)}},{key:"_handleMaterialboxClick",value:function(t){!1===this.doneAnimating||this.overlayActive&&this.doneAnimating?this.close():this.open()}},{key:"_handleWindowScroll",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowResize",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowEscape",value:function(t){27===t.keyCode&&this.doneAnimating&&this.overlayActive&&this.close()}},{key:"_makeAncestorsOverflowVisible",value:function(){this.ancestorsChanged=o();for(var t=this.placeholder[0].parentNode;null!==t&&!o(t).is(document);){var e=o(t);"visible"!==e.css("overflow")&&(e.css("overflow","visible"),void 0===this.ancestorsChanged?this.ancestorsChanged=e:this.ancestorsChanged=this.ancestorsChanged.add(e)),t=t.parentNode}}},{key:"_animateImageIn",value:function(){var t=this,e={targets:this.el,height:[this.originalHeight,this.newHeight],width:[this.originalWidth,this.newWidth],left:M.getDocumentScrollLeft()+this.windowWidth/2-this.placeholder.offset().left-this.newWidth/2,top:M.getDocumentScrollTop()+this.windowHeight/2-this.placeholder.offset().top-this.newHeight/2,duration:this.options.inDuration,easing:"easeOutQuad",complete:function(){t.doneAnimating=!0,"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el)}};this.maxWidth=this.$el.css("max-width"),this.maxHeight=this.$el.css("max-height"),"none"!==this.maxWidth&&(e.maxWidth=this.newWidth),"none"!==this.maxHeight&&(e.maxHeight=this.newHeight),a(e)}},{key:"_animateImageOut",value:function(){var t=this,e={targets:this.el,width:this.originalWidth,height:this.originalHeight,left:0,top:0,duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){t.placeholder.css({height:"",width:"",position:"",top:"",left:""}),t.attrWidth&&t.$el.attr("width",t.attrWidth),t.attrHeight&&t.$el.attr("height",t.attrHeight),t.$el.removeAttr("style"),t.originInlineStyles&&t.$el.attr("style",t.originInlineStyles),t.$el.removeClass("active"),t.doneAnimating=!0,t.ancestorsChanged.length&&t.ancestorsChanged.css("overflow",""),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};a(e)}},{key:"_updateVars",value:function(){this.windowWidth=window.innerWidth,this.windowHeight=window.innerHeight,this.caption=this.el.getAttribute("data-caption")||""}},{key:"open",value:function(){var t=this;this._updateVars(),this.originalWidth=this.el.getBoundingClientRect().width,this.originalHeight=this.el.getBoundingClientRect().height,this.doneAnimating=!1,this.$el.addClass("active"),this.overlayActive=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this.placeholder.css({width:this.placeholder[0].getBoundingClientRect().width+"px",height:this.placeholder[0].getBoundingClientRect().height+"px",position:"relative",top:0,left:0}),this._makeAncestorsOverflowVisible(),this.$el.css({position:"absolute","z-index":1e3,"will-change":"left, top, width, height"}),this.attrWidth=this.$el.attr("width"),this.attrHeight=this.$el.attr("height"),this.attrWidth&&(this.$el.css("width",this.attrWidth+"px"),this.$el.removeAttr("width")),this.attrHeight&&(this.$el.css("width",this.attrHeight+"px"),this.$el.removeAttr("height")),this.$overlay=o('
').css({opacity:0}).one("click",function(){t.doneAnimating&&t.close()}),this.$el.before(this.$overlay);var e=this.$overlay[0].getBoundingClientRect();this.$overlay.css({width:this.windowWidth+"px",height:this.windowHeight+"px",left:-1*e.left+"px",top:-1*e.top+"px"}),a.remove(this.el),a.remove(this.$overlay[0]),a({targets:this.$overlay[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}),""!==this.caption&&(this.$photocaption&&a.remove(this.$photoCaption[0]),this.$photoCaption=o('
'),this.$photoCaption.text(this.caption),o("body").append(this.$photoCaption),this.$photoCaption.css({display:"inline"}),a({targets:this.$photoCaption[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}));var i=0,n=this.originalWidth/this.windowWidth,s=this.originalHeight/this.windowHeight;this.newWidth=0,this.newHeight=0,si.options.responsiveThreshold,i.$img=i.$el.find("img").first(),i.$img.each(function(){this.complete&&s(this).trigger("load")}),i._updateParallax(),i._setupEventHandlers(),i._setupStyles(),n._parallaxes.push(i),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._parallaxes.splice(n._parallaxes.indexOf(this),1),this.$img[0].style.transform="",this._removeEventHandlers(),this.$el[0].M_Parallax=void 0}},{key:"_setupEventHandlers",value:function(){this._handleImageLoadBound=this._handleImageLoad.bind(this),this.$img[0].addEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(n._handleScrollThrottled=M.throttle(n._handleScroll,5),window.addEventListener("scroll",n._handleScrollThrottled),n._handleWindowResizeThrottled=M.throttle(n._handleWindowResize,5),window.addEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_removeEventHandlers",value:function(){this.$img[0].removeEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(window.removeEventListener("scroll",n._handleScrollThrottled),window.removeEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_setupStyles",value:function(){this.$img[0].style.opacity=1}},{key:"_handleImageLoad",value:function(){this._updateParallax()}},{key:"_updateParallax",value:function(){var t=0e.options.responsiveThreshold}}},{key:"defaults",get:function(){return e}}]),n}();t._parallaxes=[],M.Parallax=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"parallax","M_Parallax")}(cash),function(a,s){"use strict";var e={duration:300,onShow:null,swipeable:!1,responsiveThreshold:1/0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tabs=i).options=a.extend({},n.defaults,e),i.$tabLinks=i.$el.children("li.tab").children("a"),i.index=0,i._setupActiveTabLink(),i.options.swipeable?i._setupSwipeableTabs():i._setupNormalTabs(),i._setTabsAndTabWidth(),i._createIndicator(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._indicator.parentNode.removeChild(this._indicator),this.options.swipeable?this._teardownSwipeableTabs():this._teardownNormalTabs(),this.$el[0].M_Tabs=void 0}},{key:"_setupEventHandlers",value:function(){this._handleWindowResizeBound=this._handleWindowResize.bind(this),window.addEventListener("resize",this._handleWindowResizeBound),this._handleTabClickBound=this._handleTabClick.bind(this),this.el.addEventListener("click",this._handleTabClickBound)}},{key:"_removeEventHandlers",value:function(){window.removeEventListener("resize",this._handleWindowResizeBound),this.el.removeEventListener("click",this._handleTabClickBound)}},{key:"_handleWindowResize",value:function(){this._setTabsAndTabWidth(),0!==this.tabWidth&&0!==this.tabsWidth&&(this._indicator.style.left=this._calcLeftPos(this.$activeTabLink)+"px",this._indicator.style.right=this._calcRightPos(this.$activeTabLink)+"px")}},{key:"_handleTabClick",value:function(t){var e=this,i=a(t.target).closest("li.tab"),n=a(t.target).closest("a");if(n.length&&n.parent().hasClass("tab"))if(i.hasClass("disabled"))t.preventDefault();else if(!n.attr("target")){this.$activeTabLink.removeClass("active");var s=this.$content;this.$activeTabLink=n,this.$content=a(M.escapeHash(n[0].hash)),this.$tabLinks=this.$el.children("li.tab").children("a"),this.$activeTabLink.addClass("active");var o=this.index;this.index=Math.max(this.$tabLinks.index(n),0),this.options.swipeable?this._tabsCarousel&&this._tabsCarousel.set(this.index,function(){"function"==typeof e.options.onShow&&e.options.onShow.call(e,e.$content[0])}):this.$content.length&&(this.$content[0].style.display="block",this.$content.addClass("active"),"function"==typeof this.options.onShow&&this.options.onShow.call(this,this.$content[0]),s.length&&!s.is(this.$content)&&(s[0].style.display="none",s.removeClass("active"))),this._setTabsAndTabWidth(),this._animateIndicator(o),t.preventDefault()}}},{key:"_createIndicator",value:function(){var t=this,e=document.createElement("li");e.classList.add("indicator"),this.el.appendChild(e),this._indicator=e,setTimeout(function(){t._indicator.style.left=t._calcLeftPos(t.$activeTabLink)+"px",t._indicator.style.right=t._calcRightPos(t.$activeTabLink)+"px"},0)}},{key:"_setupActiveTabLink",value:function(){this.$activeTabLink=a(this.$tabLinks.filter('[href="'+location.hash+'"]')),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a.active").first()),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a").first()),this.$tabLinks.removeClass("active"),this.$activeTabLink[0].classList.add("active"),this.index=Math.max(this.$tabLinks.index(this.$activeTabLink),0),this.$activeTabLink.length&&(this.$content=a(M.escapeHash(this.$activeTabLink[0].hash)),this.$content.addClass("active"))}},{key:"_setupSwipeableTabs",value:function(){var i=this;window.innerWidth>this.options.responsiveThreshold&&(this.options.swipeable=!1);var n=a();this.$tabLinks.each(function(t){var e=a(M.escapeHash(t.hash));e.addClass("carousel-item"),n=n.add(e)});var t=a('');n.first().before(t),t.append(n),n[0].style.display="";var e=this.$activeTabLink.closest(".tab").index();this._tabsCarousel=M.Carousel.init(t[0],{fullWidth:!0,noWrap:!0,onCycleTo:function(t){var e=i.index;i.index=a(t).index(),i.$activeTabLink.removeClass("active"),i.$activeTabLink=i.$tabLinks.eq(i.index),i.$activeTabLink.addClass("active"),i._animateIndicator(e),"function"==typeof i.options.onShow&&i.options.onShow.call(i,i.$content[0])}}),this._tabsCarousel.set(e)}},{key:"_teardownSwipeableTabs",value:function(){var t=this._tabsCarousel.$el;this._tabsCarousel.destroy(),t.after(t.children()),t.remove()}},{key:"_setupNormalTabs",value:function(){this.$tabLinks.not(this.$activeTabLink).each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="none")}})}},{key:"_teardownNormalTabs",value:function(){this.$tabLinks.each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="")}})}},{key:"_setTabsAndTabWidth",value:function(){this.tabsWidth=this.$el.width(),this.tabWidth=Math.max(this.tabsWidth,this.el.scrollWidth)/this.$tabLinks.length}},{key:"_calcRightPos",value:function(t){return Math.ceil(this.tabsWidth-t.position().left-t[0].getBoundingClientRect().width)}},{key:"_calcLeftPos",value:function(t){return Math.floor(t.position().left)}},{key:"updateTabIndicator",value:function(){this._setTabsAndTabWidth(),this._animateIndicator(this.index)}},{key:"_animateIndicator",value:function(t){var e=0,i=0;0<=this.index-t?e=90:i=90;var n={targets:this._indicator,left:{value:this._calcLeftPos(this.$activeTabLink),delay:e},right:{value:this._calcRightPos(this.$activeTabLink),delay:i},duration:this.options.duration,easing:"easeOutQuad"};s.remove(this._indicator),s(n)}},{key:"select",value:function(t){var e=this.$tabLinks.filter('[href="#'+t+'"]');e.length&&e.trigger("click")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tabs}},{key:"defaults",get:function(){return e}}]),n}();M.Tabs=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tabs","M_Tabs")}(cash,M.anime),function(d,e){"use strict";var i={exitDelay:200,enterDelay:0,html:null,margin:5,inDuration:250,outDuration:200,position:"bottom",transitionMovement:10},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tooltip=i).options=d.extend({},n.defaults,e),i.isOpen=!1,i.isHovered=!1,i.isFocused=!1,i._appendTooltipEl(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){d(this.tooltipEl).remove(),this._removeEventHandlers(),this.el.M_Tooltip=void 0}},{key:"_appendTooltipEl",value:function(){var t=document.createElement("div");t.classList.add("material-tooltip"),this.tooltipEl=t;var e=document.createElement("div");e.classList.add("tooltip-content"),e.innerHTML=this.options.html,t.appendChild(e),document.body.appendChild(t)}},{key:"_updateTooltipContent",value:function(){this.tooltipEl.querySelector(".tooltip-content").innerHTML=this.options.html}},{key:"_setupEventHandlers",value:function(){this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this._handleFocusBound=this._handleFocus.bind(this),this._handleBlurBound=this._handleBlur.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.el.addEventListener("focus",this._handleFocusBound,!0),this.el.addEventListener("blur",this._handleBlurBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.el.removeEventListener("focus",this._handleFocusBound,!0),this.el.removeEventListener("blur",this._handleBlurBound,!0)}},{key:"open",value:function(t){this.isOpen||(t=void 0===t||void 0,this.isOpen=!0,this.options=d.extend({},this.options,this._getAttributeOptions()),this._updateTooltipContent(),this._setEnterDelayTimeout(t))}},{key:"close",value:function(){this.isOpen&&(this.isHovered=!1,this.isFocused=!1,this.isOpen=!1,this._setExitDelayTimeout())}},{key:"_setExitDelayTimeout",value:function(){var t=this;clearTimeout(this._exitDelayTimeout),this._exitDelayTimeout=setTimeout(function(){t.isHovered||t.isFocused||t._animateOut()},this.options.exitDelay)}},{key:"_setEnterDelayTimeout",value:function(t){var e=this;clearTimeout(this._enterDelayTimeout),this._enterDelayTimeout=setTimeout(function(){(e.isHovered||e.isFocused||t)&&e._animateIn()},this.options.enterDelay)}},{key:"_positionTooltip",value:function(){var t,e=this.el,i=this.tooltipEl,n=e.offsetHeight,s=e.offsetWidth,o=i.offsetHeight,a=i.offsetWidth,r=this.options.margin,l=void 0,h=void 0;this.xMovement=0,this.yMovement=0,l=e.getBoundingClientRect().top+M.getDocumentScrollTop(),h=e.getBoundingClientRect().left+M.getDocumentScrollLeft(),"top"===this.options.position?(l+=-o-r,h+=s/2-a/2,this.yMovement=-this.options.transitionMovement):"right"===this.options.position?(l+=n/2-o/2,h+=s+r,this.xMovement=this.options.transitionMovement):"left"===this.options.position?(l+=n/2-o/2,h+=-a-r,this.xMovement=-this.options.transitionMovement):(l+=n+r,h+=s/2-a/2,this.yMovement=this.options.transitionMovement),t=this._repositionWithinScreen(h,l,a,o),d(i).css({top:t.y+"px",left:t.x+"px"})}},{key:"_repositionWithinScreen",value:function(t,e,i,n){var s=M.getDocumentScrollLeft(),o=M.getDocumentScrollTop(),a=t-s,r=e-o,l={left:a,top:r,width:i,height:n},h=this.options.margin+this.options.transitionMovement,d=M.checkWithinContainer(document.body,l,h);return d.left?a=h:d.right&&(a-=a+i-window.innerWidth),d.top?r=h:d.bottom&&(r-=r+n-window.innerHeight),{x:a+s,y:r+o}}},{key:"_animateIn",value:function(){this._positionTooltip(),this.tooltipEl.style.visibility="visible",e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:1,translateX:this.xMovement,translateY:this.yMovement,duration:this.options.inDuration,easing:"easeOutCubic"})}},{key:"_animateOut",value:function(){e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:0,translateX:0,translateY:0,duration:this.options.outDuration,easing:"easeOutCubic"})}},{key:"_handleMouseEnter",value:function(){this.isHovered=!0,this.isFocused=!1,this.open(!1)}},{key:"_handleMouseLeave",value:function(){this.isHovered=!1,this.isFocused=!1,this.close()}},{key:"_handleFocus",value:function(){M.tabPressed&&(this.isFocused=!0,this.open(!1))}},{key:"_handleBlur",value:function(){this.isFocused=!1,this.close()}},{key:"_getAttributeOptions",value:function(){var t={},e=this.el.getAttribute("data-tooltip"),i=this.el.getAttribute("data-position");return e&&(t.html=e),i&&(t.position=i),t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tooltip}},{key:"defaults",get:function(){return i}}]),n}();M.Tooltip=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tooltip","M_Tooltip")}(cash,M.anime),function(i){"use strict";var t=t||{},e=document.querySelectorAll.bind(document);function m(t){var e="";for(var i in t)t.hasOwnProperty(i)&&(e+=i+":"+t[i]+";");return e}var g={duration:750,show:function(t,e){if(2===t.button)return!1;var i=e||this,n=document.createElement("div");n.className="waves-ripple",i.appendChild(n);var s,o,a,r,l,h,d,u=(h={top:0,left:0},d=(s=i)&&s.ownerDocument,o=d.documentElement,void 0!==s.getBoundingClientRect&&(h=s.getBoundingClientRect()),a=null!==(l=r=d)&&l===l.window?r:9===r.nodeType&&r.defaultView,{top:h.top+a.pageYOffset-o.clientTop,left:h.left+a.pageXOffset-o.clientLeft}),c=t.pageY-u.top,p=t.pageX-u.left,v="scale("+i.clientWidth/100*10+")";"touches"in t&&(c=t.touches[0].pageY-u.top,p=t.touches[0].pageX-u.left),n.setAttribute("data-hold",Date.now()),n.setAttribute("data-scale",v),n.setAttribute("data-x",p),n.setAttribute("data-y",c);var f={top:c+"px",left:p+"px"};n.className=n.className+" waves-notransition",n.setAttribute("style",m(f)),n.className=n.className.replace("waves-notransition",""),f["-webkit-transform"]=v,f["-moz-transform"]=v,f["-ms-transform"]=v,f["-o-transform"]=v,f.transform=v,f.opacity="1",f["-webkit-transition-duration"]=g.duration+"ms",f["-moz-transition-duration"]=g.duration+"ms",f["-o-transition-duration"]=g.duration+"ms",f["transition-duration"]=g.duration+"ms",f["-webkit-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-moz-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-o-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",n.setAttribute("style",m(f))},hide:function(t){l.touchup(t);var e=this,i=(e.clientWidth,null),n=e.getElementsByClassName("waves-ripple");if(!(0i||1"+o+""+a+""+r+""),i.length&&e.prepend(i)}},{key:"_resetCurrentElement",value:function(){this.activeIndex=-1,this.$active.removeClass("active")}},{key:"_resetAutocomplete",value:function(){h(this.container).empty(),this._resetCurrentElement(),this.oldVal=null,this.isOpen=!1,this._mousedown=!1}},{key:"selectOption",value:function(t){var e=t.text().trim();this.el.value=e,this.$el.trigger("change"),this._resetAutocomplete(),this.close(),"function"==typeof this.options.onAutocomplete&&this.options.onAutocomplete.call(this,e)}},{key:"_renderDropdown",value:function(t,i){var n=this;this._resetAutocomplete();var e=[];for(var s in t)if(t.hasOwnProperty(s)&&-1!==s.toLowerCase().indexOf(i)){if(this.count>=this.options.limit)break;var o={data:t[s],key:s};e.push(o),this.count++}if(this.options.sortFunction){e.sort(function(t,e){return n.options.sortFunction(t.key.toLowerCase(),e.key.toLowerCase(),i.toLowerCase())})}for(var a=0;a");r.data?l.append(''+r.key+""):l.append(""+r.key+""),h(this.container).append(l),this._highlight(i,l)}}},{key:"open",value:function(){var t=this.el.value.toLowerCase();this._resetAutocomplete(),t.length>=this.options.minLength&&(this.isOpen=!0,this._renderDropdown(this.options.data,t)),this.dropdown.isOpen?this.dropdown.recalculateDimensions():this.dropdown.open()}},{key:"close",value:function(){this.dropdown.close()}},{key:"updateData",value:function(t){var e=this.el.value.toLowerCase();this.options.data=t,this.isOpen&&this._renderDropdown(t,e)}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Autocomplete}},{key:"defaults",get:function(){return e}}]),s}();t._keydown=!1,M.Autocomplete=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"autocomplete","M_Autocomplete")}(cash),function(d){M.updateTextFields=function(){d("input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], input[type=date], input[type=time], textarea").each(function(t,e){var i=d(this);0'),d("body").append(e));var i=t.css("font-family"),n=t.css("font-size"),s=t.css("line-height"),o=t.css("padding-top"),a=t.css("padding-right"),r=t.css("padding-bottom"),l=t.css("padding-left");n&&e.css("font-size",n),i&&e.css("font-family",i),s&&e.css("line-height",s),o&&e.css("padding-top",o),a&&e.css("padding-right",a),r&&e.css("padding-bottom",r),l&&e.css("padding-left",l),t.data("original-height")||t.data("original-height",t.height()),"off"===t.attr("wrap")&&e.css("overflow-wrap","normal").css("white-space","pre"),e.text(t[0].value+"\n");var h=e.html().replace(/\n/g,"
");e.html(h),0'),this.$slides.each(function(t,e){var i=s('
  • ');n.$indicators.append(i[0])}),this.$el.append(this.$indicators[0]),this.$indicators=this.$indicators.children("li.indicator-item"))}},{key:"_removeIndicators",value:function(){this.$el.find("ul.indicators").remove()}},{key:"set",value:function(t){var e=this;if(t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.activeIndex!=t){this.$active=this.$slides.eq(this.activeIndex);var i=this.$active.find(".caption");this.$active.removeClass("active"),o({targets:this.$active[0],opacity:0,duration:this.options.duration,easing:"easeOutQuad",complete:function(){e.$slides.not(".active").each(function(t){o({targets:t,opacity:0,translateX:0,translateY:0,duration:0,easing:"easeOutQuad"})})}}),this._animateCaptionIn(i[0],this.options.duration),this.options.indicators&&(this.$indicators.eq(this.activeIndex).removeClass("active"),this.$indicators.eq(t).addClass("active")),o({targets:this.$slides.eq(t)[0],opacity:1,duration:this.options.duration,easing:"easeOutQuad"}),o({targets:this.$slides.eq(t).find(".caption")[0],opacity:1,translateX:0,translateY:0,duration:this.options.duration,delay:this.options.duration,easing:"easeOutQuad"}),this.$slides.eq(t).addClass("active"),this.activeIndex=t,this.start()}}},{key:"pause",value:function(){clearInterval(this.interval)}},{key:"start",value:function(){clearInterval(this.interval),this.interval=setInterval(this._handleIntervalBound,this.options.duration+this.options.interval)}},{key:"next",value:function(){var t=this.activeIndex+1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}},{key:"prev",value:function(){var t=this.activeIndex-1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Slider}},{key:"defaults",get:function(){return e}}]),n}();M.Slider=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"slider","M_Slider")}(cash,M.anime),function(n,s){n(document).on("click",".card",function(t){if(n(this).children(".card-reveal").length){var i=n(t.target).closest(".card");void 0===i.data("initialOverflow")&&i.data("initialOverflow",void 0===i.css("overflow")?"":i.css("overflow"));var e=n(this).find(".card-reveal");n(t.target).is(n(".card-reveal .card-title"))||n(t.target).is(n(".card-reveal .card-title i"))?s({targets:e[0],translateY:0,duration:225,easing:"easeInOutQuad",complete:function(t){var e=t.animatables[0].target;n(e).css({display:"none"}),i.css("overflow",i.data("initialOverflow"))}}):(n(t.target).is(n(".card .activator"))||n(t.target).is(n(".card .activator i")))&&(i.css("overflow","hidden"),e.css({display:"block"}),s({targets:e[0],translateY:"-100%",duration:300,easing:"easeInOutQuad"}))}})}(cash,M.anime),function(h){"use strict";var e={data:[],placeholder:"",secondaryPlaceholder:"",autocompleteOptions:{},limit:1/0,onChipAdd:null,onChipSelect:null,onChipDelete:null},t=function(t){function l(t,e){_classCallCheck(this,l);var i=_possibleConstructorReturn(this,(l.__proto__||Object.getPrototypeOf(l)).call(this,l,t,e));return(i.el.M_Chips=i).options=h.extend({},l.defaults,e),i.$el.addClass("chips input-field"),i.chipsData=[],i.$chips=h(),i._setupInput(),i.hasAutocomplete=0"),this.$el.append(this.$input)),this.$input.addClass("input")}},{key:"_setupLabel",value:function(){this.$label=this.$el.find("label"),this.$label.length&&this.$label.setAttribute("for",this.$input.attr("id"))}},{key:"_setPlaceholder",value:function(){void 0!==this.chipsData&&!this.chipsData.length&&this.options.placeholder?h(this.$input).prop("placeholder",this.options.placeholder):(void 0===this.chipsData||this.chipsData.length)&&this.options.secondaryPlaceholder&&h(this.$input).prop("placeholder",this.options.secondaryPlaceholder)}},{key:"_isValid",value:function(t){if(t.hasOwnProperty("tag")&&""!==t.tag){for(var e=!1,i=0;i=this.options.limit)){var e=this._renderChip(t);this.$chips.add(e),this.chipsData.push(t),h(this.$input).before(e),this._setPlaceholder(),"function"==typeof this.options.onChipAdd&&this.options.onChipAdd.call(this,this.$el,e)}}},{key:"deleteChip",value:function(t){var e=this.$chips.eq(t);this.$chips.eq(t).remove(),this.$chips=this.$chips.filter(function(t){return 0<=h(t).index()}),this.chipsData.splice(t,1),this._setPlaceholder(),"function"==typeof this.options.onChipDelete&&this.options.onChipDelete.call(this,this.$el,e[0])}},{key:"selectChip",value:function(t){var e=this.$chips.eq(t);(this._selectedChip=e)[0].focus(),"function"==typeof this.options.onChipSelect&&this.options.onChipSelect.call(this,this.$el,e[0])}}],[{key:"init",value:function(t,e){return _get(l.__proto__||Object.getPrototypeOf(l),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Chips}},{key:"_handleChipsKeydown",value:function(t){l._keydown=!0;var e=h(t.target).closest(".chips"),i=t.target&&e.length;if(!h(t.target).is("input, textarea")&&i){var n=e[0].M_Chips;if(8===t.keyCode||46===t.keyCode){t.preventDefault();var s=n.chipsData.length;if(n._selectedChip){var o=n._selectedChip.index();n.deleteChip(o),n._selectedChip=null,s=Math.max(o-1,0)}n.chipsData.length&&n.selectChip(s)}else if(37===t.keyCode){if(n._selectedChip){var a=n._selectedChip.index()-1;if(a<0)return;n.selectChip(a)}}else if(39===t.keyCode&&n._selectedChip){var r=n._selectedChip.index()+1;r>=n.chipsData.length?n.$input[0].focus():n.selectChip(r)}}}},{key:"_handleChipsKeyup",value:function(t){l._keydown=!1}},{key:"_handleChipsBlur",value:function(t){l._keydown||(h(t.target).closest(".chips")[0].M_Chips._selectedChip=null)}},{key:"defaults",get:function(){return e}}]),l}();t._keydown=!1,M.Chips=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"chips","M_Chips"),h(document).ready(function(){h(document.body).on("click",".chip .close",function(){var t=h(this).closest(".chips");t.length&&t[0].M_Chips||h(this).closest(".chip").remove()})})}(cash),function(s){"use strict";var e={top:0,bottom:1/0,offset:0,onPositionChange:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Pushpin=i).options=s.extend({},n.defaults,e),i.originalOffset=i.el.offsetTop,n._pushpins.push(i),i._setupEventHandlers(),i._updatePosition(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this.el.style.top=null,this._removePinClasses(),this._removeEventHandlers();var t=n._pushpins.indexOf(this);n._pushpins.splice(t,1)}},{key:"_setupEventHandlers",value:function(){document.addEventListener("scroll",n._updateElements)}},{key:"_removeEventHandlers",value:function(){document.removeEventListener("scroll",n._updateElements)}},{key:"_updatePosition",value:function(){var t=M.getDocumentScrollTop()+this.options.offset;this.options.top<=t&&this.options.bottom>=t&&!this.el.classList.contains("pinned")&&(this._removePinClasses(),this.el.style.top=this.options.offset+"px",this.el.classList.add("pinned"),"function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pinned")),tthis.options.bottom&&!this.el.classList.contains("pin-bottom")&&(this._removePinClasses(),this.el.classList.add("pin-bottom"),this.el.style.top=this.options.bottom-this.originalOffset+"px","function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pin-bottom"))}},{key:"_removePinClasses",value:function(){this.el.classList.remove("pin-top"),this.el.classList.remove("pinned"),this.el.classList.remove("pin-bottom")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Pushpin}},{key:"_updateElements",value:function(){for(var t in n._pushpins){n._pushpins[t]._updatePosition()}}},{key:"defaults",get:function(){return e}}]),n}();t._pushpins=[],M.Pushpin=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"pushpin","M_Pushpin")}(cash),function(r,s){"use strict";var e={direction:"top",hoverEnabled:!0,toolbarEnabled:!1};r.fn.reverse=[].reverse;var t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_FloatingActionButton=i).options=r.extend({},n.defaults,e),i.isOpen=!1,i.$anchor=i.$el.children("a").first(),i.$menu=i.$el.children("ul").first(),i.$floatingBtns=i.$el.find("ul .btn-floating"),i.$floatingBtnsReverse=i.$el.find("ul .btn-floating").reverse(),i.offsetY=0,i.offsetX=0,i.$el.addClass("direction-"+i.options.direction),"top"===i.options.direction?i.offsetY=40:"right"===i.options.direction?i.offsetX=-40:"bottom"===i.options.direction?i.offsetY=-40:i.offsetX=40,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_FloatingActionButton=void 0}},{key:"_setupEventHandlers",value:function(){this._handleFABClickBound=this._handleFABClick.bind(this),this._handleOpenBound=this.open.bind(this),this._handleCloseBound=this.close.bind(this),this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.addEventListener("mouseenter",this._handleOpenBound),this.el.addEventListener("mouseleave",this._handleCloseBound)):this.el.addEventListener("click",this._handleFABClickBound)}},{key:"_removeEventHandlers",value:function(){this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.removeEventListener("mouseenter",this._handleOpenBound),this.el.removeEventListener("mouseleave",this._handleCloseBound)):this.el.removeEventListener("click",this._handleFABClickBound)}},{key:"_handleFABClick",value:function(){this.isOpen?this.close():this.open()}},{key:"_handleDocumentClick",value:function(t){r(t.target).closest(this.$menu).length||this.close()}},{key:"open",value:function(){this.isOpen||(this.options.toolbarEnabled?this._animateInToolbar():this._animateInFAB(),this.isOpen=!0)}},{key:"close",value:function(){this.isOpen&&(this.options.toolbarEnabled?(window.removeEventListener("scroll",this._handleCloseBound,!0),document.body.removeEventListener("click",this._handleDocumentClickBound,!0),this._animateOutToolbar()):this._animateOutFAB(),this.isOpen=!1)}},{key:"_animateInFAB",value:function(){var e=this;this.$el.addClass("active");var i=0;this.$floatingBtnsReverse.each(function(t){s({targets:t,opacity:1,scale:[.4,1],translateY:[e.offsetY,0],translateX:[e.offsetX,0],duration:275,delay:i,easing:"easeInOutQuad"}),i+=40})}},{key:"_animateOutFAB",value:function(){var e=this;this.$floatingBtnsReverse.each(function(t){s.remove(t),s({targets:t,opacity:0,scale:.4,translateY:e.offsetY,translateX:e.offsetX,duration:175,easing:"easeOutQuad",complete:function(){e.$el.removeClass("active")}})})}},{key:"_animateInToolbar",value:function(){var t,e=this,i=window.innerWidth,n=window.innerHeight,s=this.el.getBoundingClientRect(),o=r('
    '),a=this.$anchor.css("background-color");this.$anchor.append(o),this.offsetX=s.left-i/2+s.width/2,this.offsetY=n-s.bottom,t=i/o[0].clientWidth,this.btnBottom=s.bottom,this.btnLeft=s.left,this.btnWidth=s.width,this.$el.addClass("active"),this.$el.css({"text-align":"center",width:"100%",bottom:0,left:0,transform:"translateX("+this.offsetX+"px)",transition:"none"}),this.$anchor.css({transform:"translateY("+-this.offsetY+"px)",transition:"none"}),o.css({"background-color":a}),setTimeout(function(){e.$el.css({transform:"",transition:"transform .2s cubic-bezier(0.550, 0.085, 0.680, 0.530), background-color 0s linear .2s"}),e.$anchor.css({overflow:"visible",transform:"",transition:"transform .2s"}),setTimeout(function(){e.$el.css({overflow:"hidden","background-color":a}),o.css({transform:"scale("+t+")",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"}),e.$menu.children("li").children("a").css({opacity:1}),e._handleDocumentClickBound=e._handleDocumentClick.bind(e),window.addEventListener("scroll",e._handleCloseBound,!0),document.body.addEventListener("click",e._handleDocumentClickBound,!0)},100)},0)}},{key:"_animateOutToolbar",value:function(){var t=this,e=window.innerWidth,i=window.innerHeight,n=this.$el.find(".fab-backdrop"),s=this.$anchor.css("background-color");this.offsetX=this.btnLeft-e/2+this.btnWidth/2,this.offsetY=i-this.btnBottom,this.$el.removeClass("active"),this.$el.css({"background-color":"transparent",transition:"none"}),this.$anchor.css({transition:"none"}),n.css({transform:"scale(0)","background-color":s}),this.$menu.children("li").children("a").css({opacity:""}),setTimeout(function(){n.remove(),t.$el.css({"text-align":"",width:"",bottom:"",left:"",overflow:"","background-color":"",transform:"translate3d("+-t.offsetX+"px,0,0)"}),t.$anchor.css({overflow:"",transform:"translate3d(0,"+t.offsetY+"px,0)"}),setTimeout(function(){t.$el.css({transform:"translate3d(0,0,0)",transition:"transform .2s"}),t.$anchor.css({transform:"translate3d(0,0,0)",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"})},20)},200)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FloatingActionButton}},{key:"defaults",get:function(){return e}}]),n}();M.FloatingActionButton=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"floatingActionButton","M_FloatingActionButton")}(cash,M.anime),function(g){"use strict";var e={autoClose:!1,format:"mmm dd, yyyy",parse:null,defaultDate:null,setDefaultDate:!1,disableWeekends:!1,disableDayFn:null,firstDay:0,minDate:null,maxDate:null,yearRange:10,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,showMonthAfterYear:!1,showDaysInNextAndPreviousMonths:!1,container:null,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok",previousMonth:"‹",nextMonth:"›",months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],weekdaysAbbrev:["S","M","T","W","T","F","S"]},events:[],onSelect:null,onOpen:null,onClose:null,onDraw:null},t=function(t){function B(t,e){_classCallCheck(this,B);var i=_possibleConstructorReturn(this,(B.__proto__||Object.getPrototypeOf(B)).call(this,B,t,e));(i.el.M_Datepicker=i).options=g.extend({},B.defaults,e),e&&e.hasOwnProperty("i18n")&&"object"==typeof e.i18n&&(i.options.i18n=g.extend({},B.defaults.i18n,e.i18n)),i.options.minDate&&i.options.minDate.setHours(0,0,0,0),i.options.maxDate&&i.options.maxDate.setHours(0,0,0,0),i.id=M.guid(),i._setupVariables(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupEventHandlers(),i.options.defaultDate||(i.options.defaultDate=new Date(Date.parse(i.el.value)));var n=i.options.defaultDate;return B._isDate(n)?i.options.setDefaultDate?(i.setDate(n,!0),i.setInputValue()):i.gotoDate(n):i.gotoDate(new Date),i.isOpen=!1,i}return _inherits(B,Component),_createClass(B,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),g(this.modalEl).remove(),this.destroySelects(),this.el.M_Datepicker=void 0}},{key:"destroySelects",value:function(){var t=this.calendarEl.querySelector(".orig-select-year");t&&M.FormSelect.getInstance(t).destroy();var e=this.calendarEl.querySelector(".orig-select-month");e&&M.FormSelect.getInstance(e).destroy()}},{key:"_insertHTMLIntoDOM",value:function(){this.options.showClearBtn&&(g(this.clearBtn).css({visibility:""}),this.clearBtn.innerHTML=this.options.i18n.clear),this.doneBtn.innerHTML=this.options.i18n.done,this.cancelBtn.innerHTML=this.options.i18n.cancel,this.options.container?this.$modalEl.appendTo(this.options.container):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modalEl.id="modal-"+this.id,this.modal=M.Modal.init(this.modalEl,{onCloseEnd:function(){t.isOpen=!1}})}},{key:"toString",value:function(t){var e=this;return t=t||this.options.format,B._isDate(this.date)?t.split(/(d{1,4}|m{1,4}|y{4}|yy|!.)/g).map(function(t){return e.formats[t]?e.formats[t]():t}).join(""):""}},{key:"setDate",value:function(t,e){if(!t)return this.date=null,this._renderDateDisplay(),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),B._isDate(t)){var i=this.options.minDate,n=this.options.maxDate;B._isDate(i)&&tn.maxDate||n.disableWeekends&&B._isWeekend(y)||n.disableDayFn&&n.disableDayFn(y),isEmpty:C,isStartRange:x,isEndRange:L,isInRange:T,showDaysInNextAndPreviousMonths:n.showDaysInNextAndPreviousMonths};l.push(this.renderDay($)),7==++_&&(r.push(this.renderRow(l,n.isRTL,m)),_=0,m=!(l=[]))}return this.renderTable(n,r,i)}},{key:"renderDay",value:function(t){var e=[],i="false";if(t.isEmpty){if(!t.showDaysInNextAndPreviousMonths)return'';e.push("is-outside-current-month"),e.push("is-selection-disabled")}return t.isDisabled&&e.push("is-disabled"),t.isToday&&e.push("is-today"),t.isSelected&&(e.push("is-selected"),i="true"),t.hasEvent&&e.push("has-event"),t.isInRange&&e.push("is-inrange"),t.isStartRange&&e.push("is-startrange"),t.isEndRange&&e.push("is-endrange"),'"}},{key:"renderRow",value:function(t,e,i){return''+(e?t.reverse():t).join("")+""}},{key:"renderTable",value:function(t,e,i){return'
    '+this.renderHead(t)+this.renderBody(e)+"
    "}},{key:"renderHead",value:function(t){var e=void 0,i=[];for(e=0;e<7;e++)i.push(''+this.renderDayName(t,e,!0)+"");return""+(t.isRTL?i.reverse():i).join("")+""}},{key:"renderBody",value:function(t){return""+t.join("")+""}},{key:"renderTitle",value:function(t,e,i,n,s,o){var a,r,l=void 0,h=void 0,d=void 0,u=this.options,c=i===u.minYear,p=i===u.maxYear,v='
    ',f=!0,m=!0;for(d=[],l=0;l<12;l++)d.push('");for(a='",g.isArray(u.yearRange)?(l=u.yearRange[0],h=u.yearRange[1]+1):(l=i-u.yearRange,h=1+i+u.yearRange),d=[];l=u.minYear&&d.push('");r='";v+='',v+='
    ',u.showMonthAfterYear?v+=r+a:v+=a+r,v+="
    ",c&&(0===n||u.minMonth>=n)&&(f=!1),p&&(11===n||u.maxMonth<=n)&&(m=!1);return(v+='')+"
    "}},{key:"draw",value:function(t){if(this.isOpen||t){var e,i=this.options,n=i.minYear,s=i.maxYear,o=i.minMonth,a=i.maxMonth,r="";this._y<=n&&(this._y=n,!isNaN(o)&&this._m=s&&(this._y=s,!isNaN(a)&&this._m>a&&(this._m=a)),e="datepicker-title-"+Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,2);for(var l=0;l<1;l++)this._renderDateDisplay(),r+=this.renderTitle(this,l,this.calendars[l].year,this.calendars[l].month,this.calendars[0].year,e)+this.render(this.calendars[l].year,this.calendars[l].month,e);this.destroySelects(),this.calendarEl.innerHTML=r;var h=this.calendarEl.querySelector(".orig-select-year"),d=this.calendarEl.querySelector(".orig-select-month");M.FormSelect.init(h,{classes:"select-year",dropdownOptions:{container:document.body,constrainWidth:!1}}),M.FormSelect.init(d,{classes:"select-month",dropdownOptions:{container:document.body,constrainWidth:!1}}),h.addEventListener("change",this._handleYearChange.bind(this)),d.addEventListener("change",this._handleMonthChange.bind(this)),"function"==typeof this.options.onDraw&&this.options.onDraw(this)}}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleInputChangeBound=this._handleInputChange.bind(this),this._handleCalendarClickBound=this._handleCalendarClick.bind(this),this._finishSelectionBound=this._finishSelection.bind(this),this._handleMonthChange=this._handleMonthChange.bind(this),this._closeBound=this.close.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.el.addEventListener("change",this._handleInputChangeBound),this.calendarEl.addEventListener("click",this._handleCalendarClickBound),this.doneBtn.addEventListener("click",this._finishSelectionBound),this.cancelBtn.addEventListener("click",this._closeBound),this.options.showClearBtn&&(this._handleClearClickBound=this._handleClearClick.bind(this),this.clearBtn.addEventListener("click",this._handleClearClickBound))}},{key:"_setupVariables",value:function(){var e=this;this.$modalEl=g(B._template),this.modalEl=this.$modalEl[0],this.calendarEl=this.modalEl.querySelector(".datepicker-calendar"),this.yearTextEl=this.modalEl.querySelector(".year-text"),this.dateTextEl=this.modalEl.querySelector(".date-text"),this.options.showClearBtn&&(this.clearBtn=this.modalEl.querySelector(".datepicker-clear")),this.doneBtn=this.modalEl.querySelector(".datepicker-done"),this.cancelBtn=this.modalEl.querySelector(".datepicker-cancel"),this.formats={d:function(){return e.date.getDate()},dd:function(){var t=e.date.getDate();return(t<10?"0":"")+t},ddd:function(){return e.options.i18n.weekdaysShort[e.date.getDay()]},dddd:function(){return e.options.i18n.weekdays[e.date.getDay()]},m:function(){return e.date.getMonth()+1},mm:function(){var t=e.date.getMonth()+1;return(t<10?"0":"")+t},mmm:function(){return e.options.i18n.monthsShort[e.date.getMonth()]},mmmm:function(){return e.options.i18n.months[e.date.getMonth()]},yy:function(){return(""+e.date.getFullYear()).slice(2)},yyyy:function(){return e.date.getFullYear()}}}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound),this.el.removeEventListener("change",this._handleInputChangeBound),this.calendarEl.removeEventListener("click",this._handleCalendarClickBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleCalendarClick",value:function(t){if(this.isOpen){var e=g(t.target);e.hasClass("is-disabled")||(!e.hasClass("datepicker-day-button")||e.hasClass("is-empty")||e.parent().hasClass("is-disabled")?e.closest(".month-prev").length?this.prevMonth():e.closest(".month-next").length&&this.nextMonth():(this.setDate(new Date(t.target.getAttribute("data-year"),t.target.getAttribute("data-month"),t.target.getAttribute("data-day"))),this.options.autoClose&&this._finishSelection()))}}},{key:"_handleClearClick",value:function(){this.date=null,this.setInputValue(),this.close()}},{key:"_handleMonthChange",value:function(t){this.gotoMonth(t.target.value)}},{key:"_handleYearChange",value:function(t){this.gotoYear(t.target.value)}},{key:"gotoMonth",value:function(t){isNaN(t)||(this.calendars[0].month=parseInt(t,10),this.adjustCalendars())}},{key:"gotoYear",value:function(t){isNaN(t)||(this.calendars[0].year=parseInt(t,10),this.adjustCalendars())}},{key:"_handleInputChange",value:function(t){var e=void 0;t.firedBy!==this&&(e=this.options.parse?this.options.parse(this.el.value,this.options.format):new Date(Date.parse(this.el.value)),B._isDate(e)&&this.setDate(e))}},{key:"renderDayName",value:function(t,e,i){for(e+=t.firstDay;7<=e;)e-=7;return i?t.i18n.weekdaysAbbrev[e]:t.i18n.weekdays[e]}},{key:"_finishSelection",value:function(){this.setInputValue(),this.close()}},{key:"open",value:function(){if(!this.isOpen)return this.isOpen=!0,"function"==typeof this.options.onOpen&&this.options.onOpen.call(this),this.draw(),this.modal.open(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,"function"==typeof this.options.onClose&&this.options.onClose.call(this),this.modal.close(),this}}],[{key:"init",value:function(t,e){return _get(B.__proto__||Object.getPrototypeOf(B),"init",this).call(this,this,t,e)}},{key:"_isDate",value:function(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())}},{key:"_isWeekend",value:function(t){var e=t.getDay();return 0===e||6===e}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"_getDaysInMonth",value:function(t,e){return[31,B._isLeapYear(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]}},{key:"_isLeapYear",value:function(t){return t%4==0&&t%100!=0||t%400==0}},{key:"_compareDates",value:function(t,e){return t.getTime()===e.getTime()}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Datepicker}},{key:"defaults",get:function(){return e}}]),B}();t._template=['"].join(""),M.Datepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"datepicker","M_Datepicker")}(cash),function(h){"use strict";var e={dialRadius:135,outerRadius:105,innerRadius:70,tickRadius:20,duration:350,container:null,defaultTime:"now",fromNow:0,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok"},autoClose:!1,twelveHour:!0,vibrate:!0,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onSelect:null},t=function(t){function f(t,e){_classCallCheck(this,f);var i=_possibleConstructorReturn(this,(f.__proto__||Object.getPrototypeOf(f)).call(this,f,t,e));return(i.el.M_Timepicker=i).options=h.extend({},f.defaults,e),i.id=M.guid(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupVariables(),i._setupEventHandlers(),i._clockSetup(),i._pickerSetup(),i}return _inherits(f,Component),_createClass(f,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),h(this.modalEl).remove(),this.el.M_Timepicker=void 0}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleClockClickStartBound=this._handleClockClickStart.bind(this),this._handleDocumentClickMoveBound=this._handleDocumentClickMove.bind(this),this._handleDocumentClickEndBound=this._handleDocumentClickEnd.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.plate.addEventListener("mousedown",this._handleClockClickStartBound),this.plate.addEventListener("touchstart",this._handleClockClickStartBound),h(this.spanHours).on("click",this.showView.bind(this,"hours")),h(this.spanMinutes).on("click",this.showView.bind(this,"minutes"))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleClockClickStart",value:function(t){t.preventDefault();var e=this.plate.getBoundingClientRect(),i=e.left,n=e.top;this.x0=i+this.options.dialRadius,this.y0=n+this.options.dialRadius,this.moved=!1;var s=f._Pos(t);this.dx=s.x-this.x0,this.dy=s.y-this.y0,this.setHand(this.dx,this.dy,!1),document.addEventListener("mousemove",this._handleDocumentClickMoveBound),document.addEventListener("touchmove",this._handleDocumentClickMoveBound),document.addEventListener("mouseup",this._handleDocumentClickEndBound),document.addEventListener("touchend",this._handleDocumentClickEndBound)}},{key:"_handleDocumentClickMove",value:function(t){t.preventDefault();var e=f._Pos(t),i=e.x-this.x0,n=e.y-this.y0;this.moved=!0,this.setHand(i,n,!1,!0)}},{key:"_handleDocumentClickEnd",value:function(t){var e=this;t.preventDefault(),document.removeEventListener("mouseup",this._handleDocumentClickEndBound),document.removeEventListener("touchend",this._handleDocumentClickEndBound);var i=f._Pos(t),n=i.x-this.x0,s=i.y-this.y0;this.moved&&n===this.dx&&s===this.dy&&this.setHand(n,s),"hours"===this.currentView?this.showView("minutes",this.options.duration/2):this.options.autoClose&&(h(this.minutesView).addClass("timepicker-dial-out"),setTimeout(function(){e.done()},this.options.duration/2)),"function"==typeof this.options.onSelect&&this.options.onSelect.call(this,this.hours,this.minutes),document.removeEventListener("mousemove",this._handleDocumentClickMoveBound),document.removeEventListener("touchmove",this._handleDocumentClickMoveBound)}},{key:"_insertHTMLIntoDOM",value:function(){this.$modalEl=h(f._template),this.modalEl=this.$modalEl[0],this.modalEl.id="modal-"+this.id;var t=document.querySelector(this.options.container);this.options.container&&t?this.$modalEl.appendTo(t):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modal=M.Modal.init(this.modalEl,{onOpenStart:this.options.onOpenStart,onOpenEnd:this.options.onOpenEnd,onCloseStart:this.options.onCloseStart,onCloseEnd:function(){"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t),t.isOpen=!1}})}},{key:"_setupVariables",value:function(){this.currentView="hours",this.vibrate=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,this._canvas=this.modalEl.querySelector(".timepicker-canvas"),this.plate=this.modalEl.querySelector(".timepicker-plate"),this.hoursView=this.modalEl.querySelector(".timepicker-hours"),this.minutesView=this.modalEl.querySelector(".timepicker-minutes"),this.spanHours=this.modalEl.querySelector(".timepicker-span-hours"),this.spanMinutes=this.modalEl.querySelector(".timepicker-span-minutes"),this.spanAmPm=this.modalEl.querySelector(".timepicker-span-am-pm"),this.footer=this.modalEl.querySelector(".timepicker-footer"),this.amOrPm="PM"}},{key:"_pickerSetup",value:function(){var t=h('").appendTo(this.footer).on("click",this.clear.bind(this));this.options.showClearBtn&&t.css({visibility:""});var e=h('
    ');h('").appendTo(e).on("click",this.close.bind(this)),h('").appendTo(e).on("click",this.done.bind(this)),e.appendTo(this.footer)}},{key:"_clockSetup",value:function(){this.options.twelveHour&&(this.$amBtn=h('
    AM
    '),this.$pmBtn=h('
    PM
    '),this.$amBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm),this.$pmBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm)),this._buildHoursView(),this._buildMinutesView(),this._buildSVGClock()}},{key:"_buildSVGClock",value:function(){var t=this.options.dialRadius,e=this.options.tickRadius,i=2*t,n=f._createSVGEl("svg");n.setAttribute("class","timepicker-svg"),n.setAttribute("width",i),n.setAttribute("height",i);var s=f._createSVGEl("g");s.setAttribute("transform","translate("+t+","+t+")");var o=f._createSVGEl("circle");o.setAttribute("class","timepicker-canvas-bearing"),o.setAttribute("cx",0),o.setAttribute("cy",0),o.setAttribute("r",4);var a=f._createSVGEl("line");a.setAttribute("x1",0),a.setAttribute("y1",0);var r=f._createSVGEl("circle");r.setAttribute("class","timepicker-canvas-bg"),r.setAttribute("r",e),s.appendChild(a),s.appendChild(r),s.appendChild(o),n.appendChild(s),this._canvas.appendChild(n),this.hand=a,this.bg=r,this.bearing=o,this.g=s}},{key:"_buildHoursView",value:function(){var t=h('
    ');if(this.options.twelveHour)for(var e=1;e<13;e+=1){var i=t.clone(),n=e/6*Math.PI,s=this.options.outerRadius;i.css({left:this.options.dialRadius+Math.sin(n)*s-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*s-this.options.tickRadius+"px"}),i.html(0===e?"00":e),this.hoursView.appendChild(i[0])}else for(var o=0;o<24;o+=1){var a=t.clone(),r=o/6*Math.PI,l=0'),e=0;e<60;e+=5){var i=t.clone(),n=e/30*Math.PI;i.css({left:this.options.dialRadius+Math.sin(n)*this.options.outerRadius-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*this.options.outerRadius-this.options.tickRadius+"px"}),i.html(f._addLeadingZero(e)),this.minutesView.appendChild(i[0])}}},{key:"_handleAmPmClick",value:function(t){var e=h(t.target);this.amOrPm=e.hasClass("am-btn")?"AM":"PM",this._updateAmPmView()}},{key:"_updateAmPmView",value:function(){this.options.twelveHour&&(this.$amBtn.toggleClass("text-primary","AM"===this.amOrPm),this.$pmBtn.toggleClass("text-primary","PM"===this.amOrPm))}},{key:"_updateTimeFromInput",value:function(){var t=((this.el.value||this.options.defaultTime||"")+"").split(":");if(this.options.twelveHour&&void 0!==t[1]&&(0','",""].join(""),M.Timepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"timepicker","M_Timepicker")}(cash),function(s){"use strict";var e={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_CharacterCounter=i).options=s.extend({},n.defaults,e),i.isInvalid=!1,i.isValidLength=!1,i._setupCounter(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.CharacterCounter=void 0,this._removeCounter()}},{key:"_setupEventHandlers",value:function(){this._handleUpdateCounterBound=this.updateCounter.bind(this),this.el.addEventListener("focus",this._handleUpdateCounterBound,!0),this.el.addEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("focus",this._handleUpdateCounterBound,!0),this.el.removeEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_setupCounter",value:function(){this.counterEl=document.createElement("span"),s(this.counterEl).addClass("character-counter").css({float:"right","font-size":"12px",height:1}),this.$el.parent().append(this.counterEl)}},{key:"_removeCounter",value:function(){s(this.counterEl).remove()}},{key:"updateCounter",value:function(){var t=+this.$el.attr("data-length"),e=this.el.value.length;this.isValidLength=e<=t;var i=e;t&&(i+="/"+t,this._validateInput()),s(this.counterEl).html(i)}},{key:"_validateInput",value:function(){this.isValidLength&&this.isInvalid?(this.isInvalid=!1,this.$el.removeClass("invalid")):this.isValidLength||this.isInvalid||(this.isInvalid=!0,this.$el.removeClass("valid"),this.$el.addClass("invalid"))}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_CharacterCounter}},{key:"defaults",get:function(){return e}}]),n}();M.CharacterCounter=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"characterCounter","M_CharacterCounter")}(cash),function(b){"use strict";var e={duration:200,dist:-100,shift:0,padding:0,numVisible:5,fullWidth:!1,indicators:!1,noWrap:!1,onCycleTo:null},t=function(t){function i(t,e){_classCallCheck(this,i);var n=_possibleConstructorReturn(this,(i.__proto__||Object.getPrototypeOf(i)).call(this,i,t,e));return(n.el.M_Carousel=n).options=b.extend({},i.defaults,e),n.hasMultipleSlides=1'),n.$el.find(".carousel-item").each(function(t,e){if(n.images.push(t),n.showIndicators){var i=b('
  • ');0===e&&i[0].classList.add("active"),n.$indicators.append(i)}}),n.showIndicators&&n.$el.append(n.$indicators),n.count=n.images.length,n.options.numVisible=Math.min(n.count,n.options.numVisible),n.xform="transform",["webkit","Moz","O","ms"].every(function(t){var e=t+"Transform";return void 0===document.body.style[e]||(n.xform=e,!1)}),n._setupEventHandlers(),n._scroll(n.offset),n}return _inherits(i,Component),_createClass(i,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Carousel=void 0}},{key:"_setupEventHandlers",value:function(){var i=this;this._handleCarouselTapBound=this._handleCarouselTap.bind(this),this._handleCarouselDragBound=this._handleCarouselDrag.bind(this),this._handleCarouselReleaseBound=this._handleCarouselRelease.bind(this),this._handleCarouselClickBound=this._handleCarouselClick.bind(this),void 0!==window.ontouchstart&&(this.el.addEventListener("touchstart",this._handleCarouselTapBound),this.el.addEventListener("touchmove",this._handleCarouselDragBound),this.el.addEventListener("touchend",this._handleCarouselReleaseBound)),this.el.addEventListener("mousedown",this._handleCarouselTapBound),this.el.addEventListener("mousemove",this._handleCarouselDragBound),this.el.addEventListener("mouseup",this._handleCarouselReleaseBound),this.el.addEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.addEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&(this._handleIndicatorClickBound=this._handleIndicatorClick.bind(this),this.$indicators.find(".indicator-item").each(function(t,e){t.addEventListener("click",i._handleIndicatorClickBound)}));var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){var i=this;void 0!==window.ontouchstart&&(this.el.removeEventListener("touchstart",this._handleCarouselTapBound),this.el.removeEventListener("touchmove",this._handleCarouselDragBound),this.el.removeEventListener("touchend",this._handleCarouselReleaseBound)),this.el.removeEventListener("mousedown",this._handleCarouselTapBound),this.el.removeEventListener("mousemove",this._handleCarouselDragBound),this.el.removeEventListener("mouseup",this._handleCarouselReleaseBound),this.el.removeEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.removeEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&this.$indicators.find(".indicator-item").each(function(t,e){t.removeEventListener("click",i._handleIndicatorClickBound)}),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleCarouselTap",value:function(t){"mousedown"===t.type&&b(t.target).is("img")&&t.preventDefault(),this.pressed=!0,this.dragged=!1,this.verticalDragged=!1,this.reference=this._xpos(t),this.referenceY=this._ypos(t),this.velocity=this.amplitude=0,this.frame=this.offset,this.timestamp=Date.now(),clearInterval(this.ticker),this.ticker=setInterval(this._trackBound,100)}},{key:"_handleCarouselDrag",value:function(t){var e=void 0,i=void 0,n=void 0;if(this.pressed)if(e=this._xpos(t),i=this._ypos(t),n=this.reference-e,Math.abs(this.referenceY-i)<30&&!this.verticalDragged)(2=this.dim*(this.count-1)?this.target=this.dim*(this.count-1):this.target<0&&(this.target=0)),this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScrollBound),this.dragged&&(t.preventDefault(),t.stopPropagation()),!1}},{key:"_handleCarouselClick",value:function(t){if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;if(!this.options.fullWidth){var e=b(t.target).closest(".carousel-item").index();0!==this._wrap(this.center)-e&&(t.preventDefault(),t.stopPropagation()),this._cycleTo(e)}}},{key:"_handleIndicatorClick",value:function(t){t.stopPropagation();var e=b(t.target).closest(".indicator-item");e.length&&this._cycleTo(e.index())}},{key:"_handleResize",value:function(t){this.options.fullWidth?(this.itemWidth=this.$el.find(".carousel-item").first().innerWidth(),this.imageHeight=this.$el.find(".carousel-item.active").height(),this.dim=2*this.itemWidth+this.options.padding,this.offset=2*this.center*this.itemWidth,this.target=this.offset,this._setCarouselHeight(!0)):this._scroll()}},{key:"_setCarouselHeight",value:function(t){var i=this,e=this.$el.find(".carousel-item.active").length?this.$el.find(".carousel-item.active").first():this.$el.find(".carousel-item").first(),n=e.find("img").first();if(n.length)if(n[0].complete){var s=n.height();if(0=this.count?t%this.count:t<0?this._wrap(this.count+t%this.count):t}},{key:"_track",value:function(){var t,e,i,n;e=(t=Date.now())-this.timestamp,this.timestamp=t,i=this.offset-this.frame,this.frame=this.offset,n=1e3*i/(1+e),this.velocity=.8*n+.2*this.velocity}},{key:"_autoScroll",value:function(){var t=void 0,e=void 0;this.amplitude&&(t=Date.now()-this.timestamp,2<(e=this.amplitude*Math.exp(-t/this.options.duration))||e<-2?(this._scroll(this.target-e),requestAnimationFrame(this._autoScrollBound)):this._scroll(this.target))}},{key:"_scroll",value:function(t){var e=this;this.$el.hasClass("scrolling")||this.el.classList.add("scrolling"),null!=this.scrollingTimeout&&window.clearTimeout(this.scrollingTimeout),this.scrollingTimeout=window.setTimeout(function(){e.$el.removeClass("scrolling")},this.options.duration);var i,n,s,o,a=void 0,r=void 0,l=void 0,h=void 0,d=void 0,u=void 0,c=this.center,p=1/this.options.numVisible;if(this.offset="number"==typeof t?t:this.offset,this.center=Math.floor((this.offset+this.dim/2)/this.dim),o=-(s=(n=this.offset-this.center*this.dim)<0?1:-1)*n*2/this.dim,i=this.count>>1,this.options.fullWidth?(l="translateX(0)",u=1):(l="translateX("+(this.el.clientWidth-this.itemWidth)/2+"px) ",l+="translateY("+(this.el.clientHeight-this.itemHeight)/2+"px)",u=1-p*o),this.showIndicators){var v=this.center%this.count,f=this.$indicators.find(".indicator-item.active");f.index()!==v&&(f.removeClass("active"),this.$indicators.find(".indicator-item").eq(v)[0].classList.add("active"))}if(!this.noWrap||0<=this.center&&this.center=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"prev",value:function(t){(void 0===t||isNaN(t))&&(t=1);var e=this.center-t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"set",value:function(t,e){if((void 0===t||isNaN(t))&&(t=0),t>this.count||t<0){if(this.noWrap)return;t=this._wrap(t)}this._cycleTo(t,e)}}],[{key:"init",value:function(t,e){return _get(i.__proto__||Object.getPrototypeOf(i),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Carousel}},{key:"defaults",get:function(){return e}}]),i}();M.Carousel=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"carousel","M_Carousel")}(cash),function(S){"use strict";var e={onOpen:void 0,onClose:void 0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_TapTarget=i).options=S.extend({},n.defaults,e),i.isOpen=!1,i.$origin=S("#"+i.$el.attr("data-target")),i._setup(),i._calculatePositioning(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.TapTarget=void 0}},{key:"_setupEventHandlers",value:function(){this._handleDocumentClickBound=this._handleDocumentClick.bind(this),this._handleTargetClickBound=this._handleTargetClick.bind(this),this._handleOriginClickBound=this._handleOriginClick.bind(this),this.el.addEventListener("click",this._handleTargetClickBound),this.originEl.addEventListener("click",this._handleOriginClickBound);var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleTargetClickBound),this.originEl.removeEventListener("click",this._handleOriginClickBound),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleTargetClick",value:function(t){this.open()}},{key:"_handleOriginClick",value:function(t){this.close()}},{key:"_handleResize",value:function(t){this._calculatePositioning()}},{key:"_handleDocumentClick",value:function(t){S(t.target).closest(".tap-target-wrapper").length||(this.close(),t.preventDefault(),t.stopPropagation())}},{key:"_setup",value:function(){this.wrapper=this.$el.parent()[0],this.waveEl=S(this.wrapper).find(".tap-target-wave")[0],this.originEl=S(this.wrapper).find(".tap-target-origin")[0],this.contentEl=this.$el.find(".tap-target-content")[0],S(this.wrapper).hasClass(".tap-target-wrapper")||(this.wrapper=document.createElement("div"),this.wrapper.classList.add("tap-target-wrapper"),this.$el.before(S(this.wrapper)),this.wrapper.append(this.el)),this.contentEl||(this.contentEl=document.createElement("div"),this.contentEl.classList.add("tap-target-content"),this.$el.append(this.contentEl)),this.waveEl||(this.waveEl=document.createElement("div"),this.waveEl.classList.add("tap-target-wave"),this.originEl||(this.originEl=this.$origin.clone(!0,!0),this.originEl.addClass("tap-target-origin"),this.originEl.removeAttr("id"),this.originEl.removeAttr("style"),this.originEl=this.originEl[0],this.waveEl.append(this.originEl)),this.wrapper.append(this.waveEl))}},{key:"_calculatePositioning",value:function(){var t="fixed"===this.$origin.css("position");if(!t)for(var e=this.$origin.parents(),i=0;i'+t.getAttribute("label")+"")[0]),i.each(function(t){var e=n._appendOptionWithIcon(n.$el,t,"optgroup-option");n._addOptionToValueDict(t,e)})}}),this.$el.after(this.dropdownOptions),this.input=document.createElement("input"),d(this.input).addClass("select-dropdown dropdown-trigger"),this.input.setAttribute("type","text"),this.input.setAttribute("readonly","true"),this.input.setAttribute("data-target",this.dropdownOptions.id),this.el.disabled&&d(this.input).prop("disabled","true"),this.$el.before(this.input),this._setValueToInput();var t=d('');if(this.$el.before(t[0]),!this.el.disabled){var e=d.extend({},this.options.dropdownOptions);e.onOpenEnd=function(t){var e=d(n.dropdownOptions).find(".selected").first();if(e.length&&(M.keyDown=!0,n.dropdown.focusedIndex=e.index(),n.dropdown._focusFocusedItem(),M.keyDown=!1,n.dropdown.isScrollable)){var i=e[0].getBoundingClientRect().top-n.dropdownOptions.getBoundingClientRect().top;i-=n.dropdownOptions.clientHeight/2,n.dropdownOptions.scrollTop=i}},this.isMultiple&&(e.closeOnClick=!1),this.dropdown=M.Dropdown.init(this.input,e)}this._setSelectedStates()}},{key:"_addOptionToValueDict",value:function(t,e){var i=Object.keys(this._valueDict).length,n=this.dropdownOptions.id+i,s={};e.id=n,s.el=t,s.optionEl=e,this._valueDict[n]=s}},{key:"_removeDropdown",value:function(){d(this.wrapper).find(".caret").remove(),d(this.input).remove(),d(this.dropdownOptions).remove(),d(this.wrapper).before(this.$el),d(this.wrapper).remove()}},{key:"_appendOptionWithIcon",value:function(t,e,i){var n=e.disabled?"disabled ":"",s="optgroup-option"===i?"optgroup-option ":"",o=this.isMultiple?'":e.innerHTML,a=d("
  • "),r=d("");r.html(o),a.addClass(n+" "+s),a.append(r);var l=e.getAttribute("data-icon");if(l){var h=d('');a.prepend(h)}return d(this.dropdownOptions).append(a[0]),a[0]}},{key:"_toggleEntryFromArray",value:function(t){var e=!this._keysSelected.hasOwnProperty(t),i=d(this._valueDict[t].optionEl);return e?this._keysSelected[t]=!0:delete this._keysSelected[t],i.toggleClass("selected",e),i.find('input[type="checkbox"]').prop("checked",e),i.prop("selected",e),e}},{key:"_setValueToInput",value:function(){var i=[];if(this.$el.find("option").each(function(t){if(d(t).prop("selected")){var e=d(t).text();i.push(e)}}),!i.length){var t=this.$el.find("option:disabled").eq(0);t.length&&""===t[0].value&&i.push(t.text())}this.input.value=i.join(", ")}},{key:"_setSelectedStates",value:function(){for(var t in this._keysSelected={},this._valueDict){var e=this._valueDict[t],i=d(e.el).prop("selected");d(e.optionEl).find('input[type="checkbox"]').prop("checked",i),i?(this._activateOption(d(this.dropdownOptions),d(e.optionEl)),this._keysSelected[t]=!0):d(e.optionEl).removeClass("selected")}}},{key:"_activateOption",value:function(t,e){e&&(this.isMultiple||t.find("li.selected").removeClass("selected"),d(e).addClass("selected"))}},{key:"getSelectedValues",value:function(){var t=[];for(var e in this._keysSelected)t.push(this._valueDict[e].el.value);return t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FormSelect}},{key:"defaults",get:function(){return e}}]),n}();M.FormSelect=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"formSelect","M_FormSelect")}(cash),function(s,e){"use strict";var i={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Range=i).options=s.extend({},n.defaults,e),i._mousedown=!1,i._setupThumb(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeThumb(),this.el.M_Range=void 0}},{key:"_setupEventHandlers",value:function(){this._handleRangeChangeBound=this._handleRangeChange.bind(this),this._handleRangeMousedownTouchstartBound=this._handleRangeMousedownTouchstart.bind(this),this._handleRangeInputMousemoveTouchmoveBound=this._handleRangeInputMousemoveTouchmove.bind(this),this._handleRangeMouseupTouchendBound=this._handleRangeMouseupTouchend.bind(this),this._handleRangeBlurMouseoutTouchleaveBound=this._handleRangeBlurMouseoutTouchleave.bind(this),this.el.addEventListener("change",this._handleRangeChangeBound),this.el.addEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.addEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.addEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("change",this._handleRangeChangeBound),this.el.removeEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_handleRangeChange",value:function(){s(this.value).html(this.$el.val()),s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px")}},{key:"_handleRangeMousedownTouchstart",value:function(t){if(s(this.value).html(this.$el.val()),this._mousedown=!0,this.$el.addClass("active"),s(this.thumb).hasClass("active")||this._showRangeBubble(),"input"!==t.type){var e=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",e+"px")}}},{key:"_handleRangeInputMousemoveTouchmove",value:function(){if(this._mousedown){s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px"),s(this.value).html(this.$el.val())}}},{key:"_handleRangeMouseupTouchend",value:function(){this._mousedown=!1,this.$el.removeClass("active")}},{key:"_handleRangeBlurMouseoutTouchleave",value:function(){if(!this._mousedown){var t=7+parseInt(this.$el.css("padding-left"))+"px";s(this.thumb).hasClass("active")&&(e.remove(this.thumb),e({targets:this.thumb,height:0,width:0,top:10,easing:"easeOutQuad",marginLeft:t,duration:100})),s(this.thumb).removeClass("active")}}},{key:"_setupThumb",value:function(){this.thumb=document.createElement("span"),this.value=document.createElement("span"),s(this.thumb).addClass("thumb"),s(this.value).addClass("value"),s(this.thumb).append(this.value),this.$el.after(this.thumb)}},{key:"_removeThumb",value:function(){s(this.thumb).remove()}},{key:"_showRangeBubble",value:function(){var t=-7+parseInt(s(this.thumb).parent().css("padding-left"))+"px";e.remove(this.thumb),e({targets:this.thumb,height:30,width:30,top:-30,marginLeft:t,duration:300,easing:"easeOutQuint"})}},{key:"_calcRangeOffset",value:function(){var t=this.$el.width()-15,e=parseFloat(this.$el.attr("max"))||100,i=parseFloat(this.$el.attr("min"))||0;return(parseFloat(this.$el.val())-i)/(e-i)*t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Range}},{key:"defaults",get:function(){return i}}]),n}();M.Range=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"range","M_Range"),t.init(s("input[type=range]"))}(cash,M.anime); \ No newline at end of file +var _get=function t(e,i,n){null===e&&(e=Function.prototype);var s=Object.getOwnPropertyDescriptor(e,i);if(void 0===s){var o=Object.getPrototypeOf(e);return null===o?void 0:t(o,i,n)}if("value"in s)return s.value;var a=s.get;return void 0!==a?a.call(n):void 0},_createClass=function(){function n(t,e){for(var i=0;i/,p=/^\w+$/;function v(t,e){e=e||o;var i=u.test(t)?e.getElementsByClassName(t.slice(1)):p.test(t)?e.getElementsByTagName(t):e.querySelectorAll(t);return i}function f(t){if(!i){var e=(i=o.implementation.createHTMLDocument(null)).createElement("base");e.href=o.location.href,i.head.appendChild(e)}return i.body.innerHTML=t,i.body.childNodes}function m(t){"loading"!==o.readyState?t():o.addEventListener("DOMContentLoaded",t)}function g(t,e){if(!t)return this;if(t.cash&&t!==a)return t;var i,n=t,s=0;if(d(t))n=l.test(t)?o.getElementById(t.slice(1)):c.test(t)?f(t):v(t,e);else if(h(t))return m(t),this;if(!n)return this;if(n.nodeType||n===a)this[0]=n,this.length=1;else for(i=this.length=n.length;ss.right-i||l+e.width>window.innerWidth-i)&&(n.right=!0),(ho-i||h+e.height>window.innerHeight-i)&&(n.bottom=!0),n},M.checkPossibleAlignments=function(t,e,i,n){var s={top:!0,right:!0,bottom:!0,left:!0,spaceOnTop:null,spaceOnRight:null,spaceOnBottom:null,spaceOnLeft:null},o="visible"===getComputedStyle(e).overflow,a=e.getBoundingClientRect(),r=Math.min(a.height,window.innerHeight),l=Math.min(a.width,window.innerWidth),h=t.getBoundingClientRect(),d=e.scrollLeft,u=e.scrollTop,c=i.left-d,p=i.top-u,v=i.top+h.height-u;return s.spaceOnRight=o?window.innerWidth-(h.left+i.width):l-(c+i.width),s.spaceOnRight<0&&(s.left=!1),s.spaceOnLeft=o?h.right-i.width:c-i.width+h.width,s.spaceOnLeft<0&&(s.right=!1),s.spaceOnBottom=o?window.innerHeight-(h.top+i.height+n):r-(p+i.height+n),s.spaceOnBottom<0&&(s.top=!1),s.spaceOnTop=o?h.bottom-(i.height+n):v-(i.height-n),s.spaceOnTop<0&&(s.bottom=!1),s},M.getOverflowParent=function(t){return null==t?null:t===document.body||"visible"!==getComputedStyle(t).overflow?t:M.getOverflowParent(t.parentElement)},M.getIdFromTrigger=function(t){var e=t.getAttribute("data-target");return e||(e=(e=t.getAttribute("href"))?e.slice(1):""),e},M.getDocumentScrollTop=function(){return window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0},M.getDocumentScrollLeft=function(){return window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0};var getTime=Date.now||function(){return(new Date).getTime()};M.throttle=function(i,n,s){var o=void 0,a=void 0,r=void 0,l=null,h=0;s||(s={});var d=function(){h=!1===s.leading?0:getTime(),l=null,r=i.apply(o,a),o=a=null};return function(){var t=getTime();h||!1!==s.leading||(h=t);var e=n-(t-h);return o=this,a=arguments,e<=0?(clearTimeout(l),l=null,h=t,r=i.apply(o,a),o=a=null):l||!1===s.trailing||(l=setTimeout(d,e)),r}};var $jscomp={scope:{}};$jscomp.defineProperty="function"==typeof Object.defineProperties?Object.defineProperty:function(t,e,i){if(i.get||i.set)throw new TypeError("ES3 does not support getters and setters.");t!=Array.prototype&&t!=Object.prototype&&(t[e]=i.value)},$jscomp.getGlobal=function(t){return"undefined"!=typeof window&&window===t?t:"undefined"!=typeof global&&null!=global?global:t},$jscomp.global=$jscomp.getGlobal(this),$jscomp.SYMBOL_PREFIX="jscomp_symbol_",$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){},$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)},$jscomp.symbolCounter_=0,$jscomp.Symbol=function(t){return $jscomp.SYMBOL_PREFIX+(t||"")+$jscomp.symbolCounter_++},$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var t=$jscomp.global.Symbol.iterator;t||(t=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator")),"function"!=typeof Array.prototype[t]&&$jscomp.defineProperty(Array.prototype,t,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}}),$jscomp.initSymbolIterator=function(){}},$jscomp.arrayIterator=function(t){var e=0;return $jscomp.iteratorPrototype(function(){return e=k.currentTime)for(var h=0;ht&&(s.duration=e.duration),s.children.push(e)}),s.seek(0),s.reset(),s.autoplay&&s.restart(),s},s},O.random=function(t,e){return Math.floor(Math.random()*(e-t+1))+t},O}(),function(r,l){"use strict";var e={accordion:!0,onOpenStart:void 0,onOpenEnd:void 0,onCloseStart:void 0,onCloseEnd:void 0,inDuration:300,outDuration:300},t=function(t){function s(t,e){_classCallCheck(this,s);var i=_possibleConstructorReturn(this,(s.__proto__||Object.getPrototypeOf(s)).call(this,s,t,e));(i.el.M_Collapsible=i).options=r.extend({},s.defaults,e),i.$headers=i.$el.children("li").children(".collapsible-header"),i.$headers.attr("tabindex",0),i._setupEventHandlers();var n=i.$el.children("li.active").children(".collapsible-body");return i.options.accordion?n.first().css("display","block"):n.css("display","block"),i}return _inherits(s,Component),_createClass(s,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Collapsible=void 0}},{key:"_setupEventHandlers",value:function(){var e=this;this._handleCollapsibleClickBound=this._handleCollapsibleClick.bind(this),this._handleCollapsibleKeydownBound=this._handleCollapsibleKeydown.bind(this),this.el.addEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.addEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_removeEventHandlers",value:function(){var e=this;this.el.removeEventListener("click",this._handleCollapsibleClickBound),this.$headers.each(function(t){t.removeEventListener("keydown",e._handleCollapsibleKeydownBound)})}},{key:"_handleCollapsibleClick",value:function(t){var e=r(t.target).closest(".collapsible-header");if(t.target&&e.length){var i=e.closest(".collapsible");if(i[0]===this.el){var n=e.closest("li"),s=i.children("li"),o=n[0].classList.contains("active"),a=s.index(n);o?this.close(a):this.open(a)}}}},{key:"_handleCollapsibleKeydown",value:function(t){13===t.keyCode&&this._handleCollapsibleClickBound(t)}},{key:"_animateIn",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css({display:"block",overflow:"hidden",height:0,paddingTop:"",paddingBottom:""});var s=n.css("padding-top"),o=n.css("padding-bottom"),a=n[0].scrollHeight;n.css({paddingTop:0,paddingBottom:0}),l({targets:n[0],height:a,paddingTop:s,paddingBottom:o,duration:this.options.inDuration,easing:"easeInOutCubic",complete:function(t){n.css({overflow:"",paddingTop:"",paddingBottom:"",height:""}),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,i[0])}})}}},{key:"_animateOut",value:function(t){var e=this,i=this.$el.children("li").eq(t);if(i.length){var n=i.children(".collapsible-body");l.remove(n[0]),n.css("overflow","hidden"),l({targets:n[0],height:0,paddingTop:0,paddingBottom:0,duration:this.options.outDuration,easing:"easeInOutCubic",complete:function(){n.css({height:"",overflow:"",padding:"",display:""}),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,i[0])}})}}},{key:"open",value:function(t){var i=this,e=this.$el.children("li").eq(t);if(e.length&&!e[0].classList.contains("active")){if("function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,e[0]),this.options.accordion){var n=this.$el.children("li");this.$el.children("li.active").each(function(t){var e=n.index(r(t));i.close(e)})}e[0].classList.add("active"),this._animateIn(t)}}},{key:"close",value:function(t){var e=this.$el.children("li").eq(t);e.length&&e[0].classList.contains("active")&&("function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,e[0]),e[0].classList.remove("active"),this._animateOut(t))}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Collapsible}},{key:"defaults",get:function(){return e}}]),s}();M.Collapsible=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"collapsible","M_Collapsible")}(cash,M.anime),function(h,i){"use strict";var e={alignment:"left",autoFocus:!0,constrainWidth:!0,container:null,coverTrigger:!0,closeOnClick:!0,hover:!1,inDuration:150,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onItemClick:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return i.el.M_Dropdown=i,n._dropdowns.push(i),i.id=M.getIdFromTrigger(t),i.dropdownEl=document.getElementById(i.id),i.$dropdownEl=h(i.dropdownEl),i.options=h.extend({},n.defaults,e),i.isOpen=!1,i.isScrollable=!1,i.isTouchMoving=!1,i.focusedIndex=-1,i.filterQuery=[],i.options.container?h(i.options.container).append(i.dropdownEl):i.$el.after(i.dropdownEl),i._makeDropdownFocusable(),i._resetFilterQueryBound=i._resetFilterQuery.bind(i),i._handleDocumentClickBound=i._handleDocumentClick.bind(i),i._handleDocumentTouchmoveBound=i._handleDocumentTouchmove.bind(i),i._handleDropdownClickBound=i._handleDropdownClick.bind(i),i._handleDropdownKeydownBound=i._handleDropdownKeydown.bind(i),i._handleTriggerKeydownBound=i._handleTriggerKeydown.bind(i),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._resetDropdownStyles(),this._removeEventHandlers(),n._dropdowns.splice(n._dropdowns.indexOf(this),1),this.el.M_Dropdown=void 0}},{key:"_setupEventHandlers",value:function(){this.el.addEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.addEventListener("click",this._handleDropdownClickBound),this.options.hover?(this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.addEventListener("mouseleave",this._handleMouseLeaveBound)):(this._handleClickBound=this._handleClick.bind(this),this.el.addEventListener("click",this._handleClickBound))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("keydown",this._handleTriggerKeydownBound),this.dropdownEl.removeEventListener("click",this._handleDropdownClickBound),this.options.hover?(this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.dropdownEl.removeEventListener("mouseleave",this._handleMouseLeaveBound)):this.el.removeEventListener("click",this._handleClickBound)}},{key:"_setupTemporaryEventHandlers",value:function(){document.body.addEventListener("click",this._handleDocumentClickBound,!0),document.body.addEventListener("touchend",this._handleDocumentClickBound),document.body.addEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.addEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_removeTemporaryEventHandlers",value:function(){document.body.removeEventListener("click",this._handleDocumentClickBound,!0),document.body.removeEventListener("touchend",this._handleDocumentClickBound),document.body.removeEventListener("touchmove",this._handleDocumentTouchmoveBound),this.dropdownEl.removeEventListener("keydown",this._handleDropdownKeydownBound)}},{key:"_handleClick",value:function(t){t.preventDefault(),this.open()}},{key:"_handleMouseEnter",value:function(){this.open()}},{key:"_handleMouseLeave",value:function(t){var e=t.toElement||t.relatedTarget,i=!!h(e).closest(".dropdown-content").length,n=!1,s=h(e).closest(".dropdown-trigger");s.length&&s[0].M_Dropdown&&s[0].M_Dropdown.isOpen&&(n=!0),n||i||this.close()}},{key:"_handleDocumentClick",value:function(t){var e=this,i=h(t.target);this.options.closeOnClick&&i.closest(".dropdown-content").length&&!this.isTouchMoving?setTimeout(function(){e.close()},0):!i.closest(".dropdown-trigger").length&&i.closest(".dropdown-content").length||setTimeout(function(){e.close()},0),this.isTouchMoving=!1}},{key:"_handleTriggerKeydown",value:function(t){t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ENTER||this.isOpen||(t.preventDefault(),this.open())}},{key:"_handleDocumentTouchmove",value:function(t){h(t.target).closest(".dropdown-content").length&&(this.isTouchMoving=!0)}},{key:"_handleDropdownClick",value:function(t){if("function"==typeof this.options.onItemClick){var e=h(t.target).closest("li")[0];this.options.onItemClick.call(this,e)}}},{key:"_handleDropdownKeydown",value:function(t){if(t.which===M.keys.TAB)t.preventDefault(),this.close();else if(t.which!==M.keys.ARROW_DOWN&&t.which!==M.keys.ARROW_UP||!this.isOpen)if(t.which===M.keys.ENTER&&this.isOpen){var e=this.dropdownEl.children[this.focusedIndex],i=h(e).find("a, button").first();i.length?i[0].click():e&&e.click()}else t.which===M.keys.ESC&&this.isOpen&&(t.preventDefault(),this.close());else{t.preventDefault();var n=t.which===M.keys.ARROW_DOWN?1:-1,s=this.focusedIndex,o=!1;do{if(s+=n,this.dropdownEl.children[s]&&-1!==this.dropdownEl.children[s].tabIndex){o=!0;break}}while(sl.spaceOnBottom?(h="bottom",i+=l.spaceOnTop,o-=l.spaceOnTop):i+=l.spaceOnBottom)),!l[d]){var u="left"===d?"right":"left";l[u]?d=u:l.spaceOnLeft>l.spaceOnRight?(d="right",n+=l.spaceOnLeft,s-=l.spaceOnLeft):(d="left",n+=l.spaceOnRight)}return"bottom"===h&&(o=o-e.height+(this.options.coverTrigger?t.height:0)),"right"===d&&(s=s-e.width+t.width),{x:s,y:o,verticalAlignment:h,horizontalAlignment:d,height:i,width:n}}},{key:"_animateIn",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:[0,1],easing:"easeOutQuad"},scaleX:[.3,1],scaleY:[.3,1],duration:this.options.inDuration,easing:"easeOutQuint",complete:function(t){e.options.autoFocus&&e.dropdownEl.focus(),"function"==typeof e.options.onOpenEnd&&e.options.onOpenEnd.call(e,e.el)}})}},{key:"_animateOut",value:function(){var e=this;i.remove(this.dropdownEl),i({targets:this.dropdownEl,opacity:{value:0,easing:"easeOutQuint"},scaleX:.3,scaleY:.3,duration:this.options.outDuration,easing:"easeOutQuint",complete:function(t){e._resetDropdownStyles(),"function"==typeof e.options.onCloseEnd&&e.options.onCloseEnd.call(e,e.el)}})}},{key:"_placeDropdown",value:function(){var t=this.options.constrainWidth?this.el.getBoundingClientRect().width:this.dropdownEl.getBoundingClientRect().width;this.dropdownEl.style.width=t+"px";var e=this._getDropdownPosition();this.dropdownEl.style.left=e.x+"px",this.dropdownEl.style.top=e.y+"px",this.dropdownEl.style.height=e.height+"px",this.dropdownEl.style.width=e.width+"px",this.dropdownEl.style.transformOrigin=("left"===e.horizontalAlignment?"0":"100%")+" "+("top"===e.verticalAlignment?"0":"100%")}},{key:"open",value:function(){this.isOpen||(this.isOpen=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this._resetDropdownStyles(),this.dropdownEl.style.display="block",this._placeDropdown(),this._animateIn(),this._setupTemporaryEventHandlers())}},{key:"close",value:function(){this.isOpen&&(this.isOpen=!1,this.focusedIndex=-1,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this._animateOut(),this._removeTemporaryEventHandlers(),this.options.autoFocus&&this.el.focus())}},{key:"recalculateDimensions",value:function(){this.isOpen&&(this.$dropdownEl.css({width:"",height:"",left:"",top:"","transform-origin":""}),this._placeDropdown())}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Dropdown}},{key:"defaults",get:function(){return e}}]),n}();t._dropdowns=[],M.Dropdown=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"dropdown","M_Dropdown")}(cash,M.anime),function(s,i){"use strict";var e={opacity:.5,inDuration:250,outDuration:250,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,preventScrolling:!0,dismissible:!0,startingTop:"4%",endingTop:"10%"},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Modal=i).options=s.extend({},n.defaults,e),i.isOpen=!1,i.id=i.$el.attr("id"),i._openingTrigger=void 0,i.$overlay=s(''),i.el.tabIndex=0,i._nthModalOpened=0,n._count++,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._count--,this._removeEventHandlers(),this.el.removeAttribute("style"),this.$overlay.remove(),this.el.M_Modal=void 0}},{key:"_setupEventHandlers",value:function(){this._handleOverlayClickBound=this._handleOverlayClick.bind(this),this._handleModalCloseClickBound=this._handleModalCloseClick.bind(this),1===n._count&&document.body.addEventListener("click",this._handleTriggerClick),this.$overlay[0].addEventListener("click",this._handleOverlayClickBound),this.el.addEventListener("click",this._handleModalCloseClickBound)}},{key:"_removeEventHandlers",value:function(){0===n._count&&document.body.removeEventListener("click",this._handleTriggerClick),this.$overlay[0].removeEventListener("click",this._handleOverlayClickBound),this.el.removeEventListener("click",this._handleModalCloseClickBound)}},{key:"_handleTriggerClick",value:function(t){var e=s(t.target).closest(".modal-trigger");if(e.length){var i=M.getIdFromTrigger(e[0]),n=document.getElementById(i).M_Modal;n&&n.open(e),t.preventDefault()}}},{key:"_handleOverlayClick",value:function(){this.options.dismissible&&this.close()}},{key:"_handleModalCloseClick",value:function(t){s(t.target).closest(".modal-close").length&&this.close()}},{key:"_handleKeydown",value:function(t){27===t.keyCode&&this.options.dismissible&&this.close()}},{key:"_handleFocus",value:function(t){this.el.contains(t.target)||this._nthModalOpened!==n._modalsOpen||this.el.focus()}},{key:"_animateIn",value:function(){var t=this;s.extend(this.el.style,{display:"block",opacity:0}),s.extend(this.$overlay[0].style,{display:"block",opacity:0}),i({targets:this.$overlay[0],opacity:this.options.opacity,duration:this.options.inDuration,easing:"easeOutQuad"});var e={targets:this.el,duration:this.options.inDuration,easing:"easeOutCubic",complete:function(){"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el,t._openingTrigger)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:0,opacity:1}):s.extend(e,{top:[this.options.startingTop,this.options.endingTop],opacity:1,scaleX:[.8,1],scaleY:[.8,1]}),i(e)}},{key:"_animateOut",value:function(){var t=this;i({targets:this.$overlay[0],opacity:0,duration:this.options.outDuration,easing:"easeOutQuart"});var e={targets:this.el,duration:this.options.outDuration,easing:"easeOutCubic",complete:function(){t.el.style.display="none",t.$overlay.remove(),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};this.el.classList.contains("bottom-sheet")?s.extend(e,{bottom:"-100%",opacity:0}):s.extend(e,{top:[this.options.endingTop,this.options.startingTop],opacity:0,scaleX:.8,scaleY:.8}),i(e)}},{key:"open",value:function(t){if(!this.isOpen)return this.isOpen=!0,n._modalsOpen++,this._nthModalOpened=n._modalsOpen,this.$overlay[0].style.zIndex=1e3+2*n._modalsOpen,this.el.style.zIndex=1e3+2*n._modalsOpen+1,this._openingTrigger=t?t[0]:void 0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el,this._openingTrigger),this.options.preventScrolling&&(document.body.style.overflow="hidden"),this.el.classList.add("open"),this.el.insertAdjacentElement("afterend",this.$overlay[0]),this.options.dismissible&&(this._handleKeydownBound=this._handleKeydown.bind(this),this._handleFocusBound=this._handleFocus.bind(this),document.addEventListener("keydown",this._handleKeydownBound),document.addEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateIn(),this.el.focus(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,n._modalsOpen--,this._nthModalOpened=0,"function"==typeof this.options.onCloseStart&&this.options.onCloseStart.call(this,this.el),this.el.classList.remove("open"),0===n._modalsOpen&&(document.body.style.overflow=""),this.options.dismissible&&(document.removeEventListener("keydown",this._handleKeydownBound),document.removeEventListener("focus",this._handleFocusBound,!0)),i.remove(this.el),i.remove(this.$overlay[0]),this._animateOut(),this}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Modal}},{key:"defaults",get:function(){return e}}]),n}();t._modalsOpen=0,t._count=0,M.Modal=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"modal","M_Modal")}(cash,M.anime),function(o,a){"use strict";var e={inDuration:275,outDuration:200,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Materialbox=i).options=o.extend({},n.defaults,e),i.overlayActive=!1,i.doneAnimating=!0,i.placeholder=o("
    ").addClass("material-placeholder"),i.originalWidth=0,i.originalHeight=0,i.originInlineStyles=i.$el.attr("style"),i.caption=i.el.getAttribute("data-caption")||"",i.$el.before(i.placeholder),i.placeholder.append(i.$el),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Materialbox=void 0,o(this.placeholder).after(this.el).remove(),this.$el.removeAttr("style")}},{key:"_setupEventHandlers",value:function(){this._handleMaterialboxClickBound=this._handleMaterialboxClick.bind(this),this.el.addEventListener("click",this._handleMaterialboxClickBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleMaterialboxClickBound)}},{key:"_handleMaterialboxClick",value:function(t){!1===this.doneAnimating||this.overlayActive&&this.doneAnimating?this.close():this.open()}},{key:"_handleWindowScroll",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowResize",value:function(){this.overlayActive&&this.close()}},{key:"_handleWindowEscape",value:function(t){27===t.keyCode&&this.doneAnimating&&this.overlayActive&&this.close()}},{key:"_makeAncestorsOverflowVisible",value:function(){this.ancestorsChanged=o();for(var t=this.placeholder[0].parentNode;null!==t&&!o(t).is(document);){var e=o(t);"visible"!==e.css("overflow")&&(e.css("overflow","visible"),void 0===this.ancestorsChanged?this.ancestorsChanged=e:this.ancestorsChanged=this.ancestorsChanged.add(e)),t=t.parentNode}}},{key:"_animateImageIn",value:function(){var t=this,e={targets:this.el,height:[this.originalHeight,this.newHeight],width:[this.originalWidth,this.newWidth],left:M.getDocumentScrollLeft()+this.windowWidth/2-this.placeholder.offset().left-this.newWidth/2,top:M.getDocumentScrollTop()+this.windowHeight/2-this.placeholder.offset().top-this.newHeight/2,duration:this.options.inDuration,easing:"easeOutQuad",complete:function(){t.doneAnimating=!0,"function"==typeof t.options.onOpenEnd&&t.options.onOpenEnd.call(t,t.el)}};this.maxWidth=this.$el.css("max-width"),this.maxHeight=this.$el.css("max-height"),"none"!==this.maxWidth&&(e.maxWidth=this.newWidth),"none"!==this.maxHeight&&(e.maxHeight=this.newHeight),a(e)}},{key:"_animateImageOut",value:function(){var t=this,e={targets:this.el,width:this.originalWidth,height:this.originalHeight,left:0,top:0,duration:this.options.outDuration,easing:"easeOutQuad",complete:function(){t.placeholder.css({height:"",width:"",position:"",top:"",left:""}),t.attrWidth&&t.$el.attr("width",t.attrWidth),t.attrHeight&&t.$el.attr("height",t.attrHeight),t.$el.removeAttr("style"),t.originInlineStyles&&t.$el.attr("style",t.originInlineStyles),t.$el.removeClass("active"),t.doneAnimating=!0,t.ancestorsChanged.length&&t.ancestorsChanged.css("overflow",""),"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t,t.el)}};a(e)}},{key:"_updateVars",value:function(){this.windowWidth=window.innerWidth,this.windowHeight=window.innerHeight,this.caption=this.el.getAttribute("data-caption")||""}},{key:"open",value:function(){var t=this;this._updateVars(),this.originalWidth=this.el.getBoundingClientRect().width,this.originalHeight=this.el.getBoundingClientRect().height,this.doneAnimating=!1,this.$el.addClass("active"),this.overlayActive=!0,"function"==typeof this.options.onOpenStart&&this.options.onOpenStart.call(this,this.el),this.placeholder.css({width:this.placeholder[0].getBoundingClientRect().width+"px",height:this.placeholder[0].getBoundingClientRect().height+"px",position:"relative",top:0,left:0}),this._makeAncestorsOverflowVisible(),this.$el.css({position:"absolute","z-index":1e3,"will-change":"left, top, width, height"}),this.attrWidth=this.$el.attr("width"),this.attrHeight=this.$el.attr("height"),this.attrWidth&&(this.$el.css("width",this.attrWidth+"px"),this.$el.removeAttr("width")),this.attrHeight&&(this.$el.css("width",this.attrHeight+"px"),this.$el.removeAttr("height")),this.$overlay=o('
    ').css({opacity:0}).one("click",function(){t.doneAnimating&&t.close()}),this.$el.before(this.$overlay);var e=this.$overlay[0].getBoundingClientRect();this.$overlay.css({width:this.windowWidth+"px",height:this.windowHeight+"px",left:-1*e.left+"px",top:-1*e.top+"px"}),a.remove(this.el),a.remove(this.$overlay[0]),a({targets:this.$overlay[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}),""!==this.caption&&(this.$photocaption&&a.remove(this.$photoCaption[0]),this.$photoCaption=o('
    '),this.$photoCaption.text(this.caption),o("body").append(this.$photoCaption),this.$photoCaption.css({display:"inline"}),a({targets:this.$photoCaption[0],opacity:1,duration:this.options.inDuration,easing:"easeOutQuad"}));var i=0,n=this.originalWidth/this.windowWidth,s=this.originalHeight/this.windowHeight;this.newWidth=0,this.newHeight=0,si.options.responsiveThreshold,i.$img=i.$el.find("img").first(),i.$img.each(function(){this.complete&&s(this).trigger("load")}),i._updateParallax(),i._setupEventHandlers(),i._setupStyles(),n._parallaxes.push(i),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){n._parallaxes.splice(n._parallaxes.indexOf(this),1),this.$img[0].style.transform="",this._removeEventHandlers(),this.$el[0].M_Parallax=void 0}},{key:"_setupEventHandlers",value:function(){this._handleImageLoadBound=this._handleImageLoad.bind(this),this.$img[0].addEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(n._handleScrollThrottled=M.throttle(n._handleScroll,5),window.addEventListener("scroll",n._handleScrollThrottled),n._handleWindowResizeThrottled=M.throttle(n._handleWindowResize,5),window.addEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_removeEventHandlers",value:function(){this.$img[0].removeEventListener("load",this._handleImageLoadBound),0===n._parallaxes.length&&(window.removeEventListener("scroll",n._handleScrollThrottled),window.removeEventListener("resize",n._handleWindowResizeThrottled))}},{key:"_setupStyles",value:function(){this.$img[0].style.opacity=1}},{key:"_handleImageLoad",value:function(){this._updateParallax()}},{key:"_updateParallax",value:function(){var t=0e.options.responsiveThreshold}}},{key:"defaults",get:function(){return e}}]),n}();t._parallaxes=[],M.Parallax=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"parallax","M_Parallax")}(cash),function(a,s){"use strict";var e={duration:300,onShow:null,swipeable:!1,responsiveThreshold:1/0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tabs=i).options=a.extend({},n.defaults,e),i.$tabLinks=i.$el.children("li.tab").children("a"),i.index=0,i._setupActiveTabLink(),i.options.swipeable?i._setupSwipeableTabs():i._setupNormalTabs(),i._setTabsAndTabWidth(),i._createIndicator(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._indicator.parentNode.removeChild(this._indicator),this.options.swipeable?this._teardownSwipeableTabs():this._teardownNormalTabs(),this.$el[0].M_Tabs=void 0}},{key:"_setupEventHandlers",value:function(){this._handleWindowResizeBound=this._handleWindowResize.bind(this),window.addEventListener("resize",this._handleWindowResizeBound),this._handleTabClickBound=this._handleTabClick.bind(this),this.el.addEventListener("click",this._handleTabClickBound)}},{key:"_removeEventHandlers",value:function(){window.removeEventListener("resize",this._handleWindowResizeBound),this.el.removeEventListener("click",this._handleTabClickBound)}},{key:"_handleWindowResize",value:function(){this._setTabsAndTabWidth(),0!==this.tabWidth&&0!==this.tabsWidth&&(this._indicator.style.left=this._calcLeftPos(this.$activeTabLink)+"px",this._indicator.style.right=this._calcRightPos(this.$activeTabLink)+"px")}},{key:"_handleTabClick",value:function(t){var e=this,i=a(t.target).closest("li.tab"),n=a(t.target).closest("a");if(n.length&&n.parent().hasClass("tab"))if(i.hasClass("disabled"))t.preventDefault();else if(!n.attr("target")){this.$activeTabLink.removeClass("active");var s=this.$content;this.$activeTabLink=n,this.$content=a(M.escapeHash(n[0].hash)),this.$tabLinks=this.$el.children("li.tab").children("a"),this.$activeTabLink.addClass("active");var o=this.index;this.index=Math.max(this.$tabLinks.index(n),0),this.options.swipeable?this._tabsCarousel&&this._tabsCarousel.set(this.index,function(){"function"==typeof e.options.onShow&&e.options.onShow.call(e,e.$content[0])}):this.$content.length&&(this.$content[0].style.display="block",this.$content.addClass("active"),"function"==typeof this.options.onShow&&this.options.onShow.call(this,this.$content[0]),s.length&&!s.is(this.$content)&&(s[0].style.display="none",s.removeClass("active"))),this._setTabsAndTabWidth(),this._animateIndicator(o),t.preventDefault()}}},{key:"_createIndicator",value:function(){var t=this,e=document.createElement("li");e.classList.add("indicator"),this.el.appendChild(e),this._indicator=e,setTimeout(function(){t._indicator.style.left=t._calcLeftPos(t.$activeTabLink)+"px",t._indicator.style.right=t._calcRightPos(t.$activeTabLink)+"px"},0)}},{key:"_setupActiveTabLink",value:function(){this.$activeTabLink=a(this.$tabLinks.filter('[href="'+location.hash+'"]')),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a.active").first()),0===this.$activeTabLink.length&&(this.$activeTabLink=this.$el.children("li.tab").children("a").first()),this.$tabLinks.removeClass("active"),this.$activeTabLink[0].classList.add("active"),this.index=Math.max(this.$tabLinks.index(this.$activeTabLink),0),this.$activeTabLink.length&&(this.$content=a(M.escapeHash(this.$activeTabLink[0].hash)),this.$content.addClass("active"))}},{key:"_setupSwipeableTabs",value:function(){var i=this;window.innerWidth>this.options.responsiveThreshold&&(this.options.swipeable=!1);var n=a();this.$tabLinks.each(function(t){var e=a(M.escapeHash(t.hash));e.addClass("carousel-item"),n=n.add(e)});var t=a('');n.first().before(t),t.append(n),n[0].style.display="";var e=this.$activeTabLink.closest(".tab").index();this._tabsCarousel=M.Carousel.init(t[0],{fullWidth:!0,noWrap:!0,onCycleTo:function(t){var e=i.index;i.index=a(t).index(),i.$activeTabLink.removeClass("active"),i.$activeTabLink=i.$tabLinks.eq(i.index),i.$activeTabLink.addClass("active"),i._animateIndicator(e),"function"==typeof i.options.onShow&&i.options.onShow.call(i,i.$content[0])}}),this._tabsCarousel.set(e)}},{key:"_teardownSwipeableTabs",value:function(){var t=this._tabsCarousel.$el;this._tabsCarousel.destroy(),t.after(t.children()),t.remove()}},{key:"_setupNormalTabs",value:function(){this.$tabLinks.not(this.$activeTabLink).each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="none")}})}},{key:"_teardownNormalTabs",value:function(){this.$tabLinks.each(function(t){if(t.hash){var e=a(M.escapeHash(t.hash));e.length&&(e[0].style.display="")}})}},{key:"_setTabsAndTabWidth",value:function(){this.tabsWidth=this.$el.width(),this.tabWidth=Math.max(this.tabsWidth,this.el.scrollWidth)/this.$tabLinks.length}},{key:"_calcRightPos",value:function(t){return Math.ceil(this.tabsWidth-t.position().left-t[0].getBoundingClientRect().width)}},{key:"_calcLeftPos",value:function(t){return Math.floor(t.position().left)}},{key:"updateTabIndicator",value:function(){this._setTabsAndTabWidth(),this._animateIndicator(this.index)}},{key:"_animateIndicator",value:function(t){var e=0,i=0;0<=this.index-t?e=90:i=90;var n={targets:this._indicator,left:{value:this._calcLeftPos(this.$activeTabLink),delay:e},right:{value:this._calcRightPos(this.$activeTabLink),delay:i},duration:this.options.duration,easing:"easeOutQuad"};s.remove(this._indicator),s(n)}},{key:"select",value:function(t){var e=this.$tabLinks.filter('[href="#'+t+'"]');e.length&&e.trigger("click")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tabs}},{key:"defaults",get:function(){return e}}]),n}();M.Tabs=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tabs","M_Tabs")}(cash,M.anime),function(d,e){"use strict";var i={exitDelay:200,enterDelay:0,html:null,margin:5,inDuration:250,outDuration:200,position:"bottom",transitionMovement:10},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Tooltip=i).options=d.extend({},n.defaults,e),i.isOpen=!1,i.isHovered=!1,i.isFocused=!1,i._appendTooltipEl(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){d(this.tooltipEl).remove(),this._removeEventHandlers(),this.el.M_Tooltip=void 0}},{key:"_appendTooltipEl",value:function(){var t=document.createElement("div");t.classList.add("material-tooltip"),this.tooltipEl=t;var e=document.createElement("div");e.classList.add("tooltip-content"),e.innerHTML=this.options.html,t.appendChild(e),document.body.appendChild(t)}},{key:"_updateTooltipContent",value:function(){this.tooltipEl.querySelector(".tooltip-content").innerHTML=this.options.html}},{key:"_setupEventHandlers",value:function(){this._handleMouseEnterBound=this._handleMouseEnter.bind(this),this._handleMouseLeaveBound=this._handleMouseLeave.bind(this),this._handleFocusBound=this._handleFocus.bind(this),this._handleBlurBound=this._handleBlur.bind(this),this.el.addEventListener("mouseenter",this._handleMouseEnterBound),this.el.addEventListener("mouseleave",this._handleMouseLeaveBound),this.el.addEventListener("focus",this._handleFocusBound,!0),this.el.addEventListener("blur",this._handleBlurBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("mouseenter",this._handleMouseEnterBound),this.el.removeEventListener("mouseleave",this._handleMouseLeaveBound),this.el.removeEventListener("focus",this._handleFocusBound,!0),this.el.removeEventListener("blur",this._handleBlurBound,!0)}},{key:"open",value:function(t){this.isOpen||(t=void 0===t||void 0,this.isOpen=!0,this.options=d.extend({},this.options,this._getAttributeOptions()),this._updateTooltipContent(),this._setEnterDelayTimeout(t))}},{key:"close",value:function(){this.isOpen&&(this.isHovered=!1,this.isFocused=!1,this.isOpen=!1,this._setExitDelayTimeout())}},{key:"_setExitDelayTimeout",value:function(){var t=this;clearTimeout(this._exitDelayTimeout),this._exitDelayTimeout=setTimeout(function(){t.isHovered||t.isFocused||t._animateOut()},this.options.exitDelay)}},{key:"_setEnterDelayTimeout",value:function(t){var e=this;clearTimeout(this._enterDelayTimeout),this._enterDelayTimeout=setTimeout(function(){(e.isHovered||e.isFocused||t)&&e._animateIn()},this.options.enterDelay)}},{key:"_positionTooltip",value:function(){var t,e=this.el,i=this.tooltipEl,n=e.offsetHeight,s=e.offsetWidth,o=i.offsetHeight,a=i.offsetWidth,r=this.options.margin,l=void 0,h=void 0;this.xMovement=0,this.yMovement=0,l=e.getBoundingClientRect().top+M.getDocumentScrollTop(),h=e.getBoundingClientRect().left+M.getDocumentScrollLeft(),"top"===this.options.position?(l+=-o-r,h+=s/2-a/2,this.yMovement=-this.options.transitionMovement):"right"===this.options.position?(l+=n/2-o/2,h+=s+r,this.xMovement=this.options.transitionMovement):"left"===this.options.position?(l+=n/2-o/2,h+=-a-r,this.xMovement=-this.options.transitionMovement):(l+=n+r,h+=s/2-a/2,this.yMovement=this.options.transitionMovement),t=this._repositionWithinScreen(h,l,a,o),d(i).css({top:t.y+"px",left:t.x+"px"})}},{key:"_repositionWithinScreen",value:function(t,e,i,n){var s=M.getDocumentScrollLeft(),o=M.getDocumentScrollTop(),a=t-s,r=e-o,l={left:a,top:r,width:i,height:n},h=this.options.margin+this.options.transitionMovement,d=M.checkWithinContainer(document.body,l,h);return d.left?a=h:d.right&&(a-=a+i-window.innerWidth),d.top?r=h:d.bottom&&(r-=r+n-window.innerHeight),{x:a+s,y:r+o}}},{key:"_animateIn",value:function(){this._positionTooltip(),this.tooltipEl.style.visibility="visible",e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:1,translateX:this.xMovement,translateY:this.yMovement,duration:this.options.inDuration,easing:"easeOutCubic"})}},{key:"_animateOut",value:function(){e.remove(this.tooltipEl),e({targets:this.tooltipEl,opacity:0,translateX:0,translateY:0,duration:this.options.outDuration,easing:"easeOutCubic"})}},{key:"_handleMouseEnter",value:function(){this.isHovered=!0,this.isFocused=!1,this.open(!1)}},{key:"_handleMouseLeave",value:function(){this.isHovered=!1,this.isFocused=!1,this.close()}},{key:"_handleFocus",value:function(){M.tabPressed&&(this.isFocused=!0,this.open(!1))}},{key:"_handleBlur",value:function(){this.isFocused=!1,this.close()}},{key:"_getAttributeOptions",value:function(){var t={},e=this.el.getAttribute("data-tooltip"),i=this.el.getAttribute("data-position");return e&&(t.html=e),i&&(t.position=i),t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Tooltip}},{key:"defaults",get:function(){return i}}]),n}();M.Tooltip=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"tooltip","M_Tooltip")}(cash,M.anime),function(i){"use strict";var t=t||{},e=document.querySelectorAll.bind(document);function m(t){var e="";for(var i in t)t.hasOwnProperty(i)&&(e+=i+":"+t[i]+";");return e}var g={duration:750,show:function(t,e){if(2===t.button)return!1;var i=e||this,n=document.createElement("div");n.className="waves-ripple",i.appendChild(n);var s,o,a,r,l,h,d,u=(h={top:0,left:0},d=(s=i)&&s.ownerDocument,o=d.documentElement,void 0!==s.getBoundingClientRect&&(h=s.getBoundingClientRect()),a=null!==(l=r=d)&&l===l.window?r:9===r.nodeType&&r.defaultView,{top:h.top+a.pageYOffset-o.clientTop,left:h.left+a.pageXOffset-o.clientLeft}),c=t.pageY-u.top,p=t.pageX-u.left,v="scale("+i.clientWidth/100*10+")";"touches"in t&&(c=t.touches[0].pageY-u.top,p=t.touches[0].pageX-u.left),n.setAttribute("data-hold",Date.now()),n.setAttribute("data-scale",v),n.setAttribute("data-x",p),n.setAttribute("data-y",c);var f={top:c+"px",left:p+"px"};n.className=n.className+" waves-notransition",n.setAttribute("style",m(f)),n.className=n.className.replace("waves-notransition",""),f["-webkit-transform"]=v,f["-moz-transform"]=v,f["-ms-transform"]=v,f["-o-transform"]=v,f.transform=v,f.opacity="1",f["-webkit-transition-duration"]=g.duration+"ms",f["-moz-transition-duration"]=g.duration+"ms",f["-o-transition-duration"]=g.duration+"ms",f["transition-duration"]=g.duration+"ms",f["-webkit-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-moz-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["-o-transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",f["transition-timing-function"]="cubic-bezier(0.250, 0.460, 0.450, 0.940)",n.setAttribute("style",m(f))},hide:function(t){l.touchup(t);var e=this,i=(e.clientWidth,null),n=e.getElementsByClassName("waves-ripple");if(!(0i||1"+o+""+a+""+r+""),i.length&&e.prepend(i)}},{key:"_resetCurrentElement",value:function(){this.activeIndex=-1,this.$active.removeClass("active")}},{key:"_resetAutocomplete",value:function(){h(this.container).empty(),this._resetCurrentElement(),this.oldVal=null,this.isOpen=!1,this._mousedown=!1}},{key:"selectOption",value:function(t){var e=t.text().trim();this.el.value=e,this.$el.trigger("change"),this._resetAutocomplete(),this.close(),"function"==typeof this.options.onAutocomplete&&this.options.onAutocomplete.call(this,e)}},{key:"_renderDropdown",value:function(t,i){var n=this;this._resetAutocomplete();var e=[];for(var s in t)if(t.hasOwnProperty(s)&&-1!==s.toLowerCase().indexOf(i)){if(this.count>=this.options.limit)break;var o={data:t[s],key:s};e.push(o),this.count++}if(this.options.sortFunction){e.sort(function(t,e){return n.options.sortFunction(t.key.toLowerCase(),e.key.toLowerCase(),i.toLowerCase())})}for(var a=0;a");r.data?l.append(''+r.key+""):l.append(""+r.key+""),h(this.container).append(l),this._highlight(i,l)}}},{key:"open",value:function(){var t=this.el.value.toLowerCase();this._resetAutocomplete(),t.length>=this.options.minLength&&(this.isOpen=!0,this._renderDropdown(this.options.data,t)),this.dropdown.isOpen?this.dropdown.recalculateDimensions():this.dropdown.open()}},{key:"close",value:function(){this.dropdown.close()}},{key:"updateData",value:function(t){var e=this.el.value.toLowerCase();this.options.data=t,this.isOpen&&this._renderDropdown(t,e)}}],[{key:"init",value:function(t,e){return _get(s.__proto__||Object.getPrototypeOf(s),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Autocomplete}},{key:"defaults",get:function(){return e}}]),s}();t._keydown=!1,M.Autocomplete=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"autocomplete","M_Autocomplete")}(cash),function(d){M.updateTextFields=function(){d("input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], input[type=number], input[type=search], input[type=date], input[type=time], textarea").each(function(t,e){var i=d(this);0'),d("body").append(e));var i=t.css("font-family"),n=t.css("font-size"),s=t.css("line-height"),o=t.css("padding-top"),a=t.css("padding-right"),r=t.css("padding-bottom"),l=t.css("padding-left");n&&e.css("font-size",n),i&&e.css("font-family",i),s&&e.css("line-height",s),o&&e.css("padding-top",o),a&&e.css("padding-right",a),r&&e.css("padding-bottom",r),l&&e.css("padding-left",l),t.data("original-height")||t.data("original-height",t.height()),"off"===t.attr("wrap")&&e.css("overflow-wrap","normal").css("white-space","pre"),e.text(t[0].value+"\n");var h=e.html().replace(/\n/g,"
    ");e.html(h),0'),this.$slides.each(function(t,e){var i=s('
  • ');n.$indicators.append(i[0])}),this.$el.append(this.$indicators[0]),this.$indicators=this.$indicators.children("li.indicator-item"))}},{key:"_removeIndicators",value:function(){this.$el.find("ul.indicators").remove()}},{key:"set",value:function(t){var e=this;if(t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.activeIndex!=t){this.$active=this.$slides.eq(this.activeIndex);var i=this.$active.find(".caption");this.$active.removeClass("active"),o({targets:this.$active[0],opacity:0,duration:this.options.duration,easing:"easeOutQuad",complete:function(){e.$slides.not(".active").each(function(t){o({targets:t,opacity:0,translateX:0,translateY:0,duration:0,easing:"easeOutQuad"})})}}),this._animateCaptionIn(i[0],this.options.duration),this.options.indicators&&(this.$indicators.eq(this.activeIndex).removeClass("active"),this.$indicators.eq(t).addClass("active")),o({targets:this.$slides.eq(t)[0],opacity:1,duration:this.options.duration,easing:"easeOutQuad"}),o({targets:this.$slides.eq(t).find(".caption")[0],opacity:1,translateX:0,translateY:0,duration:this.options.duration,delay:this.options.duration,easing:"easeOutQuad"}),this.$slides.eq(t).addClass("active"),this.activeIndex=t,this.start()}}},{key:"pause",value:function(){clearInterval(this.interval)}},{key:"start",value:function(){clearInterval(this.interval),this.interval=setInterval(this._handleIntervalBound,this.options.duration+this.options.interval)}},{key:"next",value:function(){var t=this.activeIndex+1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}},{key:"prev",value:function(){var t=this.activeIndex-1;t>=this.$slides.length?t=0:t<0&&(t=this.$slides.length-1),this.set(t)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Slider}},{key:"defaults",get:function(){return e}}]),n}();M.Slider=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"slider","M_Slider")}(cash,M.anime),function(n,s){n(document).on("click",".card",function(t){if(n(this).children(".card-reveal").length){var i=n(t.target).closest(".card");void 0===i.data("initialOverflow")&&i.data("initialOverflow",void 0===i.css("overflow")?"":i.css("overflow"));var e=n(this).find(".card-reveal");n(t.target).is(n(".card-reveal .card-title"))||n(t.target).is(n(".card-reveal .card-title i"))?s({targets:e[0],translateY:0,duration:225,easing:"easeInOutQuad",complete:function(t){var e=t.animatables[0].target;n(e).css({display:"none"}),i.css("overflow",i.data("initialOverflow"))}}):(n(t.target).is(n(".card .activator"))||n(t.target).is(n(".card .activator i")))&&(i.css("overflow","hidden"),e.css({display:"block"}),s({targets:e[0],translateY:"-100%",duration:300,easing:"easeInOutQuad"}))}})}(cash,M.anime),function(h){"use strict";var e={data:[],placeholder:"",secondaryPlaceholder:"",autocompleteOptions:{},limit:1/0,onChipAdd:null,onChipSelect:null,onChipDelete:null},t=function(t){function l(t,e){_classCallCheck(this,l);var i=_possibleConstructorReturn(this,(l.__proto__||Object.getPrototypeOf(l)).call(this,l,t,e));return(i.el.M_Chips=i).options=h.extend({},l.defaults,e),i.$el.addClass("chips input-field"),i.chipsData=[],i.$chips=h(),i._setupInput(),i.hasAutocomplete=0"),this.$el.append(this.$input)),this.$input.addClass("input")}},{key:"_setupLabel",value:function(){this.$label=this.$el.find("label"),this.$label.length&&this.$label.setAttribute("for",this.$input.attr("id"))}},{key:"_setPlaceholder",value:function(){void 0!==this.chipsData&&!this.chipsData.length&&this.options.placeholder?h(this.$input).prop("placeholder",this.options.placeholder):(void 0===this.chipsData||this.chipsData.length)&&this.options.secondaryPlaceholder&&h(this.$input).prop("placeholder",this.options.secondaryPlaceholder)}},{key:"_isValid",value:function(t){if(t.hasOwnProperty("tag")&&""!==t.tag){for(var e=!1,i=0;i=this.options.limit)){var e=this._renderChip(t);this.$chips.add(e),this.chipsData.push(t),h(this.$input).before(e),this._setPlaceholder(),"function"==typeof this.options.onChipAdd&&this.options.onChipAdd.call(this,this.$el,e)}}},{key:"deleteChip",value:function(t){var e=this.$chips.eq(t);this.$chips.eq(t).remove(),this.$chips=this.$chips.filter(function(t){return 0<=h(t).index()}),this.chipsData.splice(t,1),this._setPlaceholder(),"function"==typeof this.options.onChipDelete&&this.options.onChipDelete.call(this,this.$el,e[0])}},{key:"selectChip",value:function(t){var e=this.$chips.eq(t);(this._selectedChip=e)[0].focus(),"function"==typeof this.options.onChipSelect&&this.options.onChipSelect.call(this,this.$el,e[0])}}],[{key:"init",value:function(t,e){return _get(l.__proto__||Object.getPrototypeOf(l),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Chips}},{key:"_handleChipsKeydown",value:function(t){l._keydown=!0;var e=h(t.target).closest(".chips"),i=t.target&&e.length;if(!h(t.target).is("input, textarea")&&i){var n=e[0].M_Chips;if(8===t.keyCode||46===t.keyCode){t.preventDefault();var s=n.chipsData.length;if(n._selectedChip){var o=n._selectedChip.index();n.deleteChip(o),n._selectedChip=null,s=Math.max(o-1,0)}n.chipsData.length&&n.selectChip(s)}else if(37===t.keyCode){if(n._selectedChip){var a=n._selectedChip.index()-1;if(a<0)return;n.selectChip(a)}}else if(39===t.keyCode&&n._selectedChip){var r=n._selectedChip.index()+1;r>=n.chipsData.length?n.$input[0].focus():n.selectChip(r)}}}},{key:"_handleChipsKeyup",value:function(t){l._keydown=!1}},{key:"_handleChipsBlur",value:function(t){l._keydown||(h(t.target).closest(".chips")[0].M_Chips._selectedChip=null)}},{key:"defaults",get:function(){return e}}]),l}();t._keydown=!1,M.Chips=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"chips","M_Chips"),h(document).ready(function(){h(document.body).on("click",".chip .close",function(){var t=h(this).closest(".chips");t.length&&t[0].M_Chips||h(this).closest(".chip").remove()})})}(cash),function(s){"use strict";var e={top:0,bottom:1/0,offset:0,onPositionChange:null},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Pushpin=i).options=s.extend({},n.defaults,e),i.originalOffset=i.el.offsetTop,n._pushpins.push(i),i._setupEventHandlers(),i._updatePosition(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this.el.style.top=null,this._removePinClasses(),this._removeEventHandlers();var t=n._pushpins.indexOf(this);n._pushpins.splice(t,1)}},{key:"_setupEventHandlers",value:function(){document.addEventListener("scroll",n._updateElements)}},{key:"_removeEventHandlers",value:function(){document.removeEventListener("scroll",n._updateElements)}},{key:"_updatePosition",value:function(){var t=M.getDocumentScrollTop()+this.options.offset;this.options.top<=t&&this.options.bottom>=t&&!this.el.classList.contains("pinned")&&(this._removePinClasses(),this.el.style.top=this.options.offset+"px",this.el.classList.add("pinned"),"function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pinned")),tthis.options.bottom&&!this.el.classList.contains("pin-bottom")&&(this._removePinClasses(),this.el.classList.add("pin-bottom"),this.el.style.top=this.options.bottom-this.originalOffset+"px","function"==typeof this.options.onPositionChange&&this.options.onPositionChange.call(this,"pin-bottom"))}},{key:"_removePinClasses",value:function(){this.el.classList.remove("pin-top"),this.el.classList.remove("pinned"),this.el.classList.remove("pin-bottom")}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Pushpin}},{key:"_updateElements",value:function(){for(var t in n._pushpins){n._pushpins[t]._updatePosition()}}},{key:"defaults",get:function(){return e}}]),n}();t._pushpins=[],M.Pushpin=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"pushpin","M_Pushpin")}(cash),function(r,s){"use strict";var e={direction:"top",hoverEnabled:!0,toolbarEnabled:!1};r.fn.reverse=[].reverse;var t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_FloatingActionButton=i).options=r.extend({},n.defaults,e),i.isOpen=!1,i.$anchor=i.$el.children("a").first(),i.$menu=i.$el.children("ul").first(),i.$floatingBtns=i.$el.find("ul .btn-floating"),i.$floatingBtnsReverse=i.$el.find("ul .btn-floating").reverse(),i.offsetY=0,i.offsetX=0,i.$el.addClass("direction-"+i.options.direction),"top"===i.options.direction?i.offsetY=40:"right"===i.options.direction?i.offsetX=-40:"bottom"===i.options.direction?i.offsetY=-40:i.offsetX=40,i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_FloatingActionButton=void 0}},{key:"_setupEventHandlers",value:function(){this._handleFABClickBound=this._handleFABClick.bind(this),this._handleOpenBound=this.open.bind(this),this._handleCloseBound=this.close.bind(this),this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.addEventListener("mouseenter",this._handleOpenBound),this.el.addEventListener("mouseleave",this._handleCloseBound)):this.el.addEventListener("click",this._handleFABClickBound)}},{key:"_removeEventHandlers",value:function(){this.options.hoverEnabled&&!this.options.toolbarEnabled?(this.el.removeEventListener("mouseenter",this._handleOpenBound),this.el.removeEventListener("mouseleave",this._handleCloseBound)):this.el.removeEventListener("click",this._handleFABClickBound)}},{key:"_handleFABClick",value:function(){this.isOpen?this.close():this.open()}},{key:"_handleDocumentClick",value:function(t){r(t.target).closest(this.$menu).length||this.close()}},{key:"open",value:function(){this.isOpen||(this.options.toolbarEnabled?this._animateInToolbar():this._animateInFAB(),this.isOpen=!0)}},{key:"close",value:function(){this.isOpen&&(this.options.toolbarEnabled?(window.removeEventListener("scroll",this._handleCloseBound,!0),document.body.removeEventListener("click",this._handleDocumentClickBound,!0),this._animateOutToolbar()):this._animateOutFAB(),this.isOpen=!1)}},{key:"_animateInFAB",value:function(){var e=this;this.$el.addClass("active");var i=0;this.$floatingBtnsReverse.each(function(t){s({targets:t,opacity:1,scale:[.4,1],translateY:[e.offsetY,0],translateX:[e.offsetX,0],duration:275,delay:i,easing:"easeInOutQuad"}),i+=40})}},{key:"_animateOutFAB",value:function(){var e=this;this.$floatingBtnsReverse.each(function(t){s.remove(t),s({targets:t,opacity:0,scale:.4,translateY:e.offsetY,translateX:e.offsetX,duration:175,easing:"easeOutQuad",complete:function(){e.$el.removeClass("active")}})})}},{key:"_animateInToolbar",value:function(){var t,e=this,i=window.innerWidth,n=window.innerHeight,s=this.el.getBoundingClientRect(),o=r('
    '),a=this.$anchor.css("background-color");this.$anchor.append(o),this.offsetX=s.left-i/2+s.width/2,this.offsetY=n-s.bottom,t=i/o[0].clientWidth,this.btnBottom=s.bottom,this.btnLeft=s.left,this.btnWidth=s.width,this.$el.addClass("active"),this.$el.css({"text-align":"center",width:"100%",bottom:0,left:0,transform:"translateX("+this.offsetX+"px)",transition:"none"}),this.$anchor.css({transform:"translateY("+-this.offsetY+"px)",transition:"none"}),o.css({"background-color":a}),setTimeout(function(){e.$el.css({transform:"",transition:"transform .2s cubic-bezier(0.550, 0.085, 0.680, 0.530), background-color 0s linear .2s"}),e.$anchor.css({overflow:"visible",transform:"",transition:"transform .2s"}),setTimeout(function(){e.$el.css({overflow:"hidden","background-color":a}),o.css({transform:"scale("+t+")",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"}),e.$menu.children("li").children("a").css({opacity:1}),e._handleDocumentClickBound=e._handleDocumentClick.bind(e),window.addEventListener("scroll",e._handleCloseBound,!0),document.body.addEventListener("click",e._handleDocumentClickBound,!0)},100)},0)}},{key:"_animateOutToolbar",value:function(){var t=this,e=window.innerWidth,i=window.innerHeight,n=this.$el.find(".fab-backdrop"),s=this.$anchor.css("background-color");this.offsetX=this.btnLeft-e/2+this.btnWidth/2,this.offsetY=i-this.btnBottom,this.$el.removeClass("active"),this.$el.css({"background-color":"transparent",transition:"none"}),this.$anchor.css({transition:"none"}),n.css({transform:"scale(0)","background-color":s}),this.$menu.children("li").children("a").css({opacity:""}),setTimeout(function(){n.remove(),t.$el.css({"text-align":"",width:"",bottom:"",left:"",overflow:"","background-color":"",transform:"translate3d("+-t.offsetX+"px,0,0)"}),t.$anchor.css({overflow:"",transform:"translate3d(0,"+t.offsetY+"px,0)"}),setTimeout(function(){t.$el.css({transform:"translate3d(0,0,0)",transition:"transform .2s"}),t.$anchor.css({transform:"translate3d(0,0,0)",transition:"transform .2s cubic-bezier(0.550, 0.055, 0.675, 0.190)"})},20)},200)}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FloatingActionButton}},{key:"defaults",get:function(){return e}}]),n}();M.FloatingActionButton=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"floatingActionButton","M_FloatingActionButton")}(cash,M.anime),function(g){"use strict";var e={autoClose:!1,format:"mmm dd, yyyy",parse:null,defaultDate:null,setDefaultDate:!1,disableWeekends:!1,disableDayFn:null,firstDay:0,minDate:null,maxDate:null,yearRange:10,minYear:0,maxYear:9999,minMonth:void 0,maxMonth:void 0,startRange:null,endRange:null,isRTL:!1,showMonthAfterYear:!1,showDaysInNextAndPreviousMonths:!1,container:null,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok",previousMonth:"‹",nextMonth:"›",months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],weekdays:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],weekdaysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],weekdaysAbbrev:["S","M","T","W","T","F","S"]},events:[],onSelect:null,onOpen:null,onClose:null,onDraw:null},t=function(t){function B(t,e){_classCallCheck(this,B);var i=_possibleConstructorReturn(this,(B.__proto__||Object.getPrototypeOf(B)).call(this,B,t,e));(i.el.M_Datepicker=i).options=g.extend({},B.defaults,e),e&&e.hasOwnProperty("i18n")&&"object"==typeof e.i18n&&(i.options.i18n=g.extend({},B.defaults.i18n,e.i18n)),i.options.minDate&&i.options.minDate.setHours(0,0,0,0),i.options.maxDate&&i.options.maxDate.setHours(0,0,0,0),i.id=M.guid(),i._setupVariables(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupEventHandlers(),i.options.defaultDate||(i.options.defaultDate=new Date(Date.parse(i.el.value)));var n=i.options.defaultDate;return B._isDate(n)?i.options.setDefaultDate?(i.setDate(n,!0),i.setInputValue()):i.gotoDate(n):i.gotoDate(new Date),i.isOpen=!1,i}return _inherits(B,Component),_createClass(B,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),g(this.modalEl).remove(),this.destroySelects(),this.el.M_Datepicker=void 0}},{key:"destroySelects",value:function(){var t=this.calendarEl.querySelector(".orig-select-year");t&&M.FormSelect.getInstance(t).destroy();var e=this.calendarEl.querySelector(".orig-select-month");e&&M.FormSelect.getInstance(e).destroy()}},{key:"_insertHTMLIntoDOM",value:function(){this.options.showClearBtn&&(g(this.clearBtn).css({visibility:""}),this.clearBtn.innerHTML=this.options.i18n.clear),this.doneBtn.innerHTML=this.options.i18n.done,this.cancelBtn.innerHTML=this.options.i18n.cancel,this.options.container?this.$modalEl.appendTo(this.options.container):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modalEl.id="modal-"+this.id,this.modal=M.Modal.init(this.modalEl,{onCloseEnd:function(){t.isOpen=!1}})}},{key:"toString",value:function(t){var e=this;return t=t||this.options.format,B._isDate(this.date)?t.split(/(d{1,4}|m{1,4}|y{4}|yy|!.)/g).map(function(t){return e.formats[t]?e.formats[t]():t}).join(""):""}},{key:"setDate",value:function(t,e){if(!t)return this.date=null,this._renderDateDisplay(),this.draw();if("string"==typeof t&&(t=new Date(Date.parse(t))),B._isDate(t)){var i=this.options.minDate,n=this.options.maxDate;B._isDate(i)&&tn.maxDate||n.disableWeekends&&B._isWeekend(y)||n.disableDayFn&&n.disableDayFn(y),isEmpty:C,isStartRange:x,isEndRange:L,isInRange:T,showDaysInNextAndPreviousMonths:n.showDaysInNextAndPreviousMonths};l.push(this.renderDay($)),7==++_&&(r.push(this.renderRow(l,n.isRTL,m)),_=0,m=!(l=[]))}return this.renderTable(n,r,i)}},{key:"renderDay",value:function(t){var e=[],i="false";if(t.isEmpty){if(!t.showDaysInNextAndPreviousMonths)return'';e.push("is-outside-current-month"),e.push("is-selection-disabled")}return t.isDisabled&&e.push("is-disabled"),t.isToday&&e.push("is-today"),t.isSelected&&(e.push("is-selected"),i="true"),t.hasEvent&&e.push("has-event"),t.isInRange&&e.push("is-inrange"),t.isStartRange&&e.push("is-startrange"),t.isEndRange&&e.push("is-endrange"),'"}},{key:"renderRow",value:function(t,e,i){return''+(e?t.reverse():t).join("")+""}},{key:"renderTable",value:function(t,e,i){return'
    '+this.renderHead(t)+this.renderBody(e)+"
    "}},{key:"renderHead",value:function(t){var e=void 0,i=[];for(e=0;e<7;e++)i.push(''+this.renderDayName(t,e,!0)+"");return""+(t.isRTL?i.reverse():i).join("")+""}},{key:"renderBody",value:function(t){return""+t.join("")+""}},{key:"renderTitle",value:function(t,e,i,n,s,o){var a,r,l=void 0,h=void 0,d=void 0,u=this.options,c=i===u.minYear,p=i===u.maxYear,v='
    ',f=!0,m=!0;for(d=[],l=0;l<12;l++)d.push('");for(a='",g.isArray(u.yearRange)?(l=u.yearRange[0],h=u.yearRange[1]+1):(l=i-u.yearRange,h=1+i+u.yearRange),d=[];l=u.minYear&&d.push('");r='";v+='',v+='
    ',u.showMonthAfterYear?v+=r+a:v+=a+r,v+="
    ",c&&(0===n||u.minMonth>=n)&&(f=!1),p&&(11===n||u.maxMonth<=n)&&(m=!1);return(v+='')+"
    "}},{key:"draw",value:function(t){if(this.isOpen||t){var e,i=this.options,n=i.minYear,s=i.maxYear,o=i.minMonth,a=i.maxMonth,r="";this._y<=n&&(this._y=n,!isNaN(o)&&this._m=s&&(this._y=s,!isNaN(a)&&this._m>a&&(this._m=a)),e="datepicker-title-"+Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,2);for(var l=0;l<1;l++)this._renderDateDisplay(),r+=this.renderTitle(this,l,this.calendars[l].year,this.calendars[l].month,this.calendars[0].year,e)+this.render(this.calendars[l].year,this.calendars[l].month,e);this.destroySelects(),this.calendarEl.innerHTML=r;var h=this.calendarEl.querySelector(".orig-select-year"),d=this.calendarEl.querySelector(".orig-select-month");M.FormSelect.init(h,{classes:"select-year",dropdownOptions:{container:document.body,constrainWidth:!1}}),M.FormSelect.init(d,{classes:"select-month",dropdownOptions:{container:document.body,constrainWidth:!1}}),h.addEventListener("change",this._handleYearChange.bind(this)),d.addEventListener("change",this._handleMonthChange.bind(this)),"function"==typeof this.options.onDraw&&this.options.onDraw(this)}}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleInputChangeBound=this._handleInputChange.bind(this),this._handleCalendarClickBound=this._handleCalendarClick.bind(this),this._finishSelectionBound=this._finishSelection.bind(this),this._handleMonthChange=this._handleMonthChange.bind(this),this._closeBound=this.close.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.el.addEventListener("change",this._handleInputChangeBound),this.calendarEl.addEventListener("click",this._handleCalendarClickBound),this.doneBtn.addEventListener("click",this._finishSelectionBound),this.cancelBtn.addEventListener("click",this._closeBound),this.options.showClearBtn&&(this._handleClearClickBound=this._handleClearClick.bind(this),this.clearBtn.addEventListener("click",this._handleClearClickBound))}},{key:"_setupVariables",value:function(){var e=this;this.$modalEl=g(B._template),this.modalEl=this.$modalEl[0],this.calendarEl=this.modalEl.querySelector(".datepicker-calendar"),this.yearTextEl=this.modalEl.querySelector(".year-text"),this.dateTextEl=this.modalEl.querySelector(".date-text"),this.options.showClearBtn&&(this.clearBtn=this.modalEl.querySelector(".datepicker-clear")),this.doneBtn=this.modalEl.querySelector(".datepicker-done"),this.cancelBtn=this.modalEl.querySelector(".datepicker-cancel"),this.formats={d:function(){return e.date.getDate()},dd:function(){var t=e.date.getDate();return(t<10?"0":"")+t},ddd:function(){return e.options.i18n.weekdaysShort[e.date.getDay()]},dddd:function(){return e.options.i18n.weekdays[e.date.getDay()]},m:function(){return e.date.getMonth()+1},mm:function(){var t=e.date.getMonth()+1;return(t<10?"0":"")+t},mmm:function(){return e.options.i18n.monthsShort[e.date.getMonth()]},mmmm:function(){return e.options.i18n.months[e.date.getMonth()]},yy:function(){return(""+e.date.getFullYear()).slice(2)},yyyy:function(){return e.date.getFullYear()}}}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound),this.el.removeEventListener("change",this._handleInputChangeBound),this.calendarEl.removeEventListener("click",this._handleCalendarClickBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleCalendarClick",value:function(t){if(this.isOpen){var e=g(t.target);e.hasClass("is-disabled")||(!e.hasClass("datepicker-day-button")||e.hasClass("is-empty")||e.parent().hasClass("is-disabled")?e.closest(".month-prev").length?this.prevMonth():e.closest(".month-next").length&&this.nextMonth():(this.setDate(new Date(t.target.getAttribute("data-year"),t.target.getAttribute("data-month"),t.target.getAttribute("data-day"))),this.options.autoClose&&this._finishSelection()))}}},{key:"_handleClearClick",value:function(){this.date=null,this.setInputValue(),this.close()}},{key:"_handleMonthChange",value:function(t){this.gotoMonth(t.target.value)}},{key:"_handleYearChange",value:function(t){this.gotoYear(t.target.value)}},{key:"gotoMonth",value:function(t){isNaN(t)||(this.calendars[0].month=parseInt(t,10),this.adjustCalendars())}},{key:"gotoYear",value:function(t){isNaN(t)||(this.calendars[0].year=parseInt(t,10),this.adjustCalendars())}},{key:"_handleInputChange",value:function(t){var e=void 0;t.firedBy!==this&&(e=this.options.parse?this.options.parse(this.el.value,this.options.format):new Date(Date.parse(this.el.value)),B._isDate(e)&&this.setDate(e))}},{key:"renderDayName",value:function(t,e,i){for(e+=t.firstDay;7<=e;)e-=7;return i?t.i18n.weekdaysAbbrev[e]:t.i18n.weekdays[e]}},{key:"_finishSelection",value:function(){this.setInputValue(),this.close()}},{key:"open",value:function(){if(!this.isOpen)return this.isOpen=!0,"function"==typeof this.options.onOpen&&this.options.onOpen.call(this),this.draw(),this.modal.open(),this}},{key:"close",value:function(){if(this.isOpen)return this.isOpen=!1,"function"==typeof this.options.onClose&&this.options.onClose.call(this),this.modal.close(),this}}],[{key:"init",value:function(t,e){return _get(B.__proto__||Object.getPrototypeOf(B),"init",this).call(this,this,t,e)}},{key:"_isDate",value:function(t){return/Date/.test(Object.prototype.toString.call(t))&&!isNaN(t.getTime())}},{key:"_isWeekend",value:function(t){var e=t.getDay();return 0===e||6===e}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"_getDaysInMonth",value:function(t,e){return[31,B._isLeapYear(t)?29:28,31,30,31,30,31,31,30,31,30,31][e]}},{key:"_isLeapYear",value:function(t){return t%4==0&&t%100!=0||t%400==0}},{key:"_compareDates",value:function(t,e){return t.getTime()===e.getTime()}},{key:"_setToStartOfDay",value:function(t){B._isDate(t)&&t.setHours(0,0,0,0)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Datepicker}},{key:"defaults",get:function(){return e}}]),B}();t._template=['"].join(""),M.Datepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"datepicker","M_Datepicker")}(cash),function(h){"use strict";var e={dialRadius:135,outerRadius:105,innerRadius:70,tickRadius:20,duration:350,container:null,defaultTime:"now",fromNow:0,showClearBtn:!1,i18n:{cancel:"Cancel",clear:"Clear",done:"Ok"},autoClose:!1,twelveHour:!0,vibrate:!0,onOpenStart:null,onOpenEnd:null,onCloseStart:null,onCloseEnd:null,onSelect:null},t=function(t){function f(t,e){_classCallCheck(this,f);var i=_possibleConstructorReturn(this,(f.__proto__||Object.getPrototypeOf(f)).call(this,f,t,e));return(i.el.M_Timepicker=i).options=h.extend({},f.defaults,e),i.id=M.guid(),i._insertHTMLIntoDOM(),i._setupModal(),i._setupVariables(),i._setupEventHandlers(),i._clockSetup(),i._pickerSetup(),i}return _inherits(f,Component),_createClass(f,[{key:"destroy",value:function(){this._removeEventHandlers(),this.modal.destroy(),h(this.modalEl).remove(),this.el.M_Timepicker=void 0}},{key:"_setupEventHandlers",value:function(){this._handleInputKeydownBound=this._handleInputKeydown.bind(this),this._handleInputClickBound=this._handleInputClick.bind(this),this._handleClockClickStartBound=this._handleClockClickStart.bind(this),this._handleDocumentClickMoveBound=this._handleDocumentClickMove.bind(this),this._handleDocumentClickEndBound=this._handleDocumentClickEnd.bind(this),this.el.addEventListener("click",this._handleInputClickBound),this.el.addEventListener("keydown",this._handleInputKeydownBound),this.plate.addEventListener("mousedown",this._handleClockClickStartBound),this.plate.addEventListener("touchstart",this._handleClockClickStartBound),h(this.spanHours).on("click",this.showView.bind(this,"hours")),h(this.spanMinutes).on("click",this.showView.bind(this,"minutes"))}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleInputClickBound),this.el.removeEventListener("keydown",this._handleInputKeydownBound)}},{key:"_handleInputClick",value:function(){this.open()}},{key:"_handleInputKeydown",value:function(t){t.which===M.keys.ENTER&&(t.preventDefault(),this.open())}},{key:"_handleClockClickStart",value:function(t){t.preventDefault();var e=this.plate.getBoundingClientRect(),i=e.left,n=e.top;this.x0=i+this.options.dialRadius,this.y0=n+this.options.dialRadius,this.moved=!1;var s=f._Pos(t);this.dx=s.x-this.x0,this.dy=s.y-this.y0,this.setHand(this.dx,this.dy,!1),document.addEventListener("mousemove",this._handleDocumentClickMoveBound),document.addEventListener("touchmove",this._handleDocumentClickMoveBound),document.addEventListener("mouseup",this._handleDocumentClickEndBound),document.addEventListener("touchend",this._handleDocumentClickEndBound)}},{key:"_handleDocumentClickMove",value:function(t){t.preventDefault();var e=f._Pos(t),i=e.x-this.x0,n=e.y-this.y0;this.moved=!0,this.setHand(i,n,!1,!0)}},{key:"_handleDocumentClickEnd",value:function(t){var e=this;t.preventDefault(),document.removeEventListener("mouseup",this._handleDocumentClickEndBound),document.removeEventListener("touchend",this._handleDocumentClickEndBound);var i=f._Pos(t),n=i.x-this.x0,s=i.y-this.y0;this.moved&&n===this.dx&&s===this.dy&&this.setHand(n,s),"hours"===this.currentView?this.showView("minutes",this.options.duration/2):this.options.autoClose&&(h(this.minutesView).addClass("timepicker-dial-out"),setTimeout(function(){e.done()},this.options.duration/2)),"function"==typeof this.options.onSelect&&this.options.onSelect.call(this,this.hours,this.minutes),document.removeEventListener("mousemove",this._handleDocumentClickMoveBound),document.removeEventListener("touchmove",this._handleDocumentClickMoveBound)}},{key:"_insertHTMLIntoDOM",value:function(){this.$modalEl=h(f._template),this.modalEl=this.$modalEl[0],this.modalEl.id="modal-"+this.id;var t=document.querySelector(this.options.container);this.options.container&&t?this.$modalEl.appendTo(t):this.$modalEl.insertBefore(this.el)}},{key:"_setupModal",value:function(){var t=this;this.modal=M.Modal.init(this.modalEl,{onOpenStart:this.options.onOpenStart,onOpenEnd:this.options.onOpenEnd,onCloseStart:this.options.onCloseStart,onCloseEnd:function(){"function"==typeof t.options.onCloseEnd&&t.options.onCloseEnd.call(t),t.isOpen=!1}})}},{key:"_setupVariables",value:function(){this.currentView="hours",this.vibrate=navigator.vibrate?"vibrate":navigator.webkitVibrate?"webkitVibrate":null,this._canvas=this.modalEl.querySelector(".timepicker-canvas"),this.plate=this.modalEl.querySelector(".timepicker-plate"),this.hoursView=this.modalEl.querySelector(".timepicker-hours"),this.minutesView=this.modalEl.querySelector(".timepicker-minutes"),this.spanHours=this.modalEl.querySelector(".timepicker-span-hours"),this.spanMinutes=this.modalEl.querySelector(".timepicker-span-minutes"),this.spanAmPm=this.modalEl.querySelector(".timepicker-span-am-pm"),this.footer=this.modalEl.querySelector(".timepicker-footer"),this.amOrPm="PM"}},{key:"_pickerSetup",value:function(){var t=h('").appendTo(this.footer).on("click",this.clear.bind(this));this.options.showClearBtn&&t.css({visibility:""});var e=h('
    ');h('").appendTo(e).on("click",this.close.bind(this)),h('").appendTo(e).on("click",this.done.bind(this)),e.appendTo(this.footer)}},{key:"_clockSetup",value:function(){this.options.twelveHour&&(this.$amBtn=h('
    AM
    '),this.$pmBtn=h('
    PM
    '),this.$amBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm),this.$pmBtn.on("click",this._handleAmPmClick.bind(this)).appendTo(this.spanAmPm)),this._buildHoursView(),this._buildMinutesView(),this._buildSVGClock()}},{key:"_buildSVGClock",value:function(){var t=this.options.dialRadius,e=this.options.tickRadius,i=2*t,n=f._createSVGEl("svg");n.setAttribute("class","timepicker-svg"),n.setAttribute("width",i),n.setAttribute("height",i);var s=f._createSVGEl("g");s.setAttribute("transform","translate("+t+","+t+")");var o=f._createSVGEl("circle");o.setAttribute("class","timepicker-canvas-bearing"),o.setAttribute("cx",0),o.setAttribute("cy",0),o.setAttribute("r",4);var a=f._createSVGEl("line");a.setAttribute("x1",0),a.setAttribute("y1",0);var r=f._createSVGEl("circle");r.setAttribute("class","timepicker-canvas-bg"),r.setAttribute("r",e),s.appendChild(a),s.appendChild(r),s.appendChild(o),n.appendChild(s),this._canvas.appendChild(n),this.hand=a,this.bg=r,this.bearing=o,this.g=s}},{key:"_buildHoursView",value:function(){var t=h('
    ');if(this.options.twelveHour)for(var e=1;e<13;e+=1){var i=t.clone(),n=e/6*Math.PI,s=this.options.outerRadius;i.css({left:this.options.dialRadius+Math.sin(n)*s-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*s-this.options.tickRadius+"px"}),i.html(0===e?"00":e),this.hoursView.appendChild(i[0])}else for(var o=0;o<24;o+=1){var a=t.clone(),r=o/6*Math.PI,l=0'),e=0;e<60;e+=5){var i=t.clone(),n=e/30*Math.PI;i.css({left:this.options.dialRadius+Math.sin(n)*this.options.outerRadius-this.options.tickRadius+"px",top:this.options.dialRadius-Math.cos(n)*this.options.outerRadius-this.options.tickRadius+"px"}),i.html(f._addLeadingZero(e)),this.minutesView.appendChild(i[0])}}},{key:"_handleAmPmClick",value:function(t){var e=h(t.target);this.amOrPm=e.hasClass("am-btn")?"AM":"PM",this._updateAmPmView()}},{key:"_updateAmPmView",value:function(){this.options.twelveHour&&(this.$amBtn.toggleClass("text-primary","AM"===this.amOrPm),this.$pmBtn.toggleClass("text-primary","PM"===this.amOrPm))}},{key:"_updateTimeFromInput",value:function(){var t=((this.el.value||this.options.defaultTime||"")+"").split(":");if(this.options.twelveHour&&void 0!==t[1]&&(0','",""].join(""),M.Timepicker=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"timepicker","M_Timepicker")}(cash),function(s){"use strict";var e={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_CharacterCounter=i).options=s.extend({},n.defaults,e),i.isInvalid=!1,i.isValidLength=!1,i._setupCounter(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.CharacterCounter=void 0,this._removeCounter()}},{key:"_setupEventHandlers",value:function(){this._handleUpdateCounterBound=this.updateCounter.bind(this),this.el.addEventListener("focus",this._handleUpdateCounterBound,!0),this.el.addEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("focus",this._handleUpdateCounterBound,!0),this.el.removeEventListener("input",this._handleUpdateCounterBound,!0)}},{key:"_setupCounter",value:function(){this.counterEl=document.createElement("span"),s(this.counterEl).addClass("character-counter").css({float:"right","font-size":"12px",height:1}),this.$el.parent().append(this.counterEl)}},{key:"_removeCounter",value:function(){s(this.counterEl).remove()}},{key:"updateCounter",value:function(){var t=+this.$el.attr("data-length"),e=this.el.value.length;this.isValidLength=e<=t;var i=e;t&&(i+="/"+t,this._validateInput()),s(this.counterEl).html(i)}},{key:"_validateInput",value:function(){this.isValidLength&&this.isInvalid?(this.isInvalid=!1,this.$el.removeClass("invalid")):this.isValidLength||this.isInvalid||(this.isInvalid=!0,this.$el.removeClass("valid"),this.$el.addClass("invalid"))}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_CharacterCounter}},{key:"defaults",get:function(){return e}}]),n}();M.CharacterCounter=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"characterCounter","M_CharacterCounter")}(cash),function(b){"use strict";var e={duration:200,dist:-100,shift:0,padding:0,numVisible:5,fullWidth:!1,indicators:!1,noWrap:!1,onCycleTo:null},t=function(t){function i(t,e){_classCallCheck(this,i);var n=_possibleConstructorReturn(this,(i.__proto__||Object.getPrototypeOf(i)).call(this,i,t,e));return(n.el.M_Carousel=n).options=b.extend({},i.defaults,e),n.hasMultipleSlides=1'),n.$el.find(".carousel-item").each(function(t,e){if(n.images.push(t),n.showIndicators){var i=b('
  • ');0===e&&i[0].classList.add("active"),n.$indicators.append(i)}}),n.showIndicators&&n.$el.append(n.$indicators),n.count=n.images.length,n.options.numVisible=Math.min(n.count,n.options.numVisible),n.xform="transform",["webkit","Moz","O","ms"].every(function(t){var e=t+"Transform";return void 0===document.body.style[e]||(n.xform=e,!1)}),n._setupEventHandlers(),n._scroll(n.offset),n}return _inherits(i,Component),_createClass(i,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.M_Carousel=void 0}},{key:"_setupEventHandlers",value:function(){var i=this;this._handleCarouselTapBound=this._handleCarouselTap.bind(this),this._handleCarouselDragBound=this._handleCarouselDrag.bind(this),this._handleCarouselReleaseBound=this._handleCarouselRelease.bind(this),this._handleCarouselClickBound=this._handleCarouselClick.bind(this),void 0!==window.ontouchstart&&(this.el.addEventListener("touchstart",this._handleCarouselTapBound),this.el.addEventListener("touchmove",this._handleCarouselDragBound),this.el.addEventListener("touchend",this._handleCarouselReleaseBound)),this.el.addEventListener("mousedown",this._handleCarouselTapBound),this.el.addEventListener("mousemove",this._handleCarouselDragBound),this.el.addEventListener("mouseup",this._handleCarouselReleaseBound),this.el.addEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.addEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&(this._handleIndicatorClickBound=this._handleIndicatorClick.bind(this),this.$indicators.find(".indicator-item").each(function(t,e){t.addEventListener("click",i._handleIndicatorClickBound)}));var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){var i=this;void 0!==window.ontouchstart&&(this.el.removeEventListener("touchstart",this._handleCarouselTapBound),this.el.removeEventListener("touchmove",this._handleCarouselDragBound),this.el.removeEventListener("touchend",this._handleCarouselReleaseBound)),this.el.removeEventListener("mousedown",this._handleCarouselTapBound),this.el.removeEventListener("mousemove",this._handleCarouselDragBound),this.el.removeEventListener("mouseup",this._handleCarouselReleaseBound),this.el.removeEventListener("mouseleave",this._handleCarouselReleaseBound),this.el.removeEventListener("click",this._handleCarouselClickBound),this.showIndicators&&this.$indicators&&this.$indicators.find(".indicator-item").each(function(t,e){t.removeEventListener("click",i._handleIndicatorClickBound)}),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleCarouselTap",value:function(t){"mousedown"===t.type&&b(t.target).is("img")&&t.preventDefault(),this.pressed=!0,this.dragged=!1,this.verticalDragged=!1,this.reference=this._xpos(t),this.referenceY=this._ypos(t),this.velocity=this.amplitude=0,this.frame=this.offset,this.timestamp=Date.now(),clearInterval(this.ticker),this.ticker=setInterval(this._trackBound,100)}},{key:"_handleCarouselDrag",value:function(t){var e=void 0,i=void 0,n=void 0;if(this.pressed)if(e=this._xpos(t),i=this._ypos(t),n=this.reference-e,Math.abs(this.referenceY-i)<30&&!this.verticalDragged)(2=this.dim*(this.count-1)?this.target=this.dim*(this.count-1):this.target<0&&(this.target=0)),this.amplitude=this.target-this.offset,this.timestamp=Date.now(),requestAnimationFrame(this._autoScrollBound),this.dragged&&(t.preventDefault(),t.stopPropagation()),!1}},{key:"_handleCarouselClick",value:function(t){if(this.dragged)return t.preventDefault(),t.stopPropagation(),!1;if(!this.options.fullWidth){var e=b(t.target).closest(".carousel-item").index();0!==this._wrap(this.center)-e&&(t.preventDefault(),t.stopPropagation()),this._cycleTo(e)}}},{key:"_handleIndicatorClick",value:function(t){t.stopPropagation();var e=b(t.target).closest(".indicator-item");e.length&&this._cycleTo(e.index())}},{key:"_handleResize",value:function(t){this.options.fullWidth?(this.itemWidth=this.$el.find(".carousel-item").first().innerWidth(),this.imageHeight=this.$el.find(".carousel-item.active").height(),this.dim=2*this.itemWidth+this.options.padding,this.offset=2*this.center*this.itemWidth,this.target=this.offset,this._setCarouselHeight(!0)):this._scroll()}},{key:"_setCarouselHeight",value:function(t){var i=this,e=this.$el.find(".carousel-item.active").length?this.$el.find(".carousel-item.active").first():this.$el.find(".carousel-item").first(),n=e.find("img").first();if(n.length)if(n[0].complete){var s=n.height();if(0=this.count?t%this.count:t<0?this._wrap(this.count+t%this.count):t}},{key:"_track",value:function(){var t,e,i,n;e=(t=Date.now())-this.timestamp,this.timestamp=t,i=this.offset-this.frame,this.frame=this.offset,n=1e3*i/(1+e),this.velocity=.8*n+.2*this.velocity}},{key:"_autoScroll",value:function(){var t=void 0,e=void 0;this.amplitude&&(t=Date.now()-this.timestamp,2<(e=this.amplitude*Math.exp(-t/this.options.duration))||e<-2?(this._scroll(this.target-e),requestAnimationFrame(this._autoScrollBound)):this._scroll(this.target))}},{key:"_scroll",value:function(t){var e=this;this.$el.hasClass("scrolling")||this.el.classList.add("scrolling"),null!=this.scrollingTimeout&&window.clearTimeout(this.scrollingTimeout),this.scrollingTimeout=window.setTimeout(function(){e.$el.removeClass("scrolling")},this.options.duration);var i,n,s,o,a=void 0,r=void 0,l=void 0,h=void 0,d=void 0,u=void 0,c=this.center,p=1/this.options.numVisible;if(this.offset="number"==typeof t?t:this.offset,this.center=Math.floor((this.offset+this.dim/2)/this.dim),o=-(s=(n=this.offset-this.center*this.dim)<0?1:-1)*n*2/this.dim,i=this.count>>1,this.options.fullWidth?(l="translateX(0)",u=1):(l="translateX("+(this.el.clientWidth-this.itemWidth)/2+"px) ",l+="translateY("+(this.el.clientHeight-this.itemHeight)/2+"px)",u=1-p*o),this.showIndicators){var v=this.center%this.count,f=this.$indicators.find(".indicator-item.active");f.index()!==v&&(f.removeClass("active"),this.$indicators.find(".indicator-item").eq(v)[0].classList.add("active"))}if(!this.noWrap||0<=this.center&&this.center=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"prev",value:function(t){(void 0===t||isNaN(t))&&(t=1);var e=this.center-t;if(e>=this.count||e<0){if(this.noWrap)return;e=this._wrap(e)}this._cycleTo(e)}},{key:"set",value:function(t,e){if((void 0===t||isNaN(t))&&(t=0),t>this.count||t<0){if(this.noWrap)return;t=this._wrap(t)}this._cycleTo(t,e)}}],[{key:"init",value:function(t,e){return _get(i.__proto__||Object.getPrototypeOf(i),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Carousel}},{key:"defaults",get:function(){return e}}]),i}();M.Carousel=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"carousel","M_Carousel")}(cash),function(S){"use strict";var e={onOpen:void 0,onClose:void 0},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_TapTarget=i).options=S.extend({},n.defaults,e),i.isOpen=!1,i.$origin=S("#"+i.$el.attr("data-target")),i._setup(),i._calculatePositioning(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this.el.TapTarget=void 0}},{key:"_setupEventHandlers",value:function(){this._handleDocumentClickBound=this._handleDocumentClick.bind(this),this._handleTargetClickBound=this._handleTargetClick.bind(this),this._handleOriginClickBound=this._handleOriginClick.bind(this),this.el.addEventListener("click",this._handleTargetClickBound),this.originEl.addEventListener("click",this._handleOriginClickBound);var t=M.throttle(this._handleResize,200);this._handleThrottledResizeBound=t.bind(this),window.addEventListener("resize",this._handleThrottledResizeBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("click",this._handleTargetClickBound),this.originEl.removeEventListener("click",this._handleOriginClickBound),window.removeEventListener("resize",this._handleThrottledResizeBound)}},{key:"_handleTargetClick",value:function(t){this.open()}},{key:"_handleOriginClick",value:function(t){this.close()}},{key:"_handleResize",value:function(t){this._calculatePositioning()}},{key:"_handleDocumentClick",value:function(t){S(t.target).closest(".tap-target-wrapper").length||(this.close(),t.preventDefault(),t.stopPropagation())}},{key:"_setup",value:function(){this.wrapper=this.$el.parent()[0],this.waveEl=S(this.wrapper).find(".tap-target-wave")[0],this.originEl=S(this.wrapper).find(".tap-target-origin")[0],this.contentEl=this.$el.find(".tap-target-content")[0],S(this.wrapper).hasClass(".tap-target-wrapper")||(this.wrapper=document.createElement("div"),this.wrapper.classList.add("tap-target-wrapper"),this.$el.before(S(this.wrapper)),this.wrapper.append(this.el)),this.contentEl||(this.contentEl=document.createElement("div"),this.contentEl.classList.add("tap-target-content"),this.$el.append(this.contentEl)),this.waveEl||(this.waveEl=document.createElement("div"),this.waveEl.classList.add("tap-target-wave"),this.originEl||(this.originEl=this.$origin.clone(!0,!0),this.originEl.addClass("tap-target-origin"),this.originEl.removeAttr("id"),this.originEl.removeAttr("style"),this.originEl=this.originEl[0],this.waveEl.append(this.originEl)),this.wrapper.append(this.waveEl))}},{key:"_calculatePositioning",value:function(){var t="fixed"===this.$origin.css("position");if(!t)for(var e=this.$origin.parents(),i=0;i'+t.getAttribute("label")+"")[0]),i.each(function(t){var e=n._appendOptionWithIcon(n.$el,t,"optgroup-option");n._addOptionToValueDict(t,e)})}}),this.$el.after(this.dropdownOptions),this.input=document.createElement("input"),d(this.input).addClass("select-dropdown dropdown-trigger"),this.input.setAttribute("type","text"),this.input.setAttribute("readonly","true"),this.input.setAttribute("data-target",this.dropdownOptions.id),this.el.disabled&&d(this.input).prop("disabled","true"),this.$el.before(this.input),this._setValueToInput();var t=d('');if(this.$el.before(t[0]),!this.el.disabled){var e=d.extend({},this.options.dropdownOptions);e.onOpenEnd=function(t){var e=d(n.dropdownOptions).find(".selected").first();if(e.length&&(M.keyDown=!0,n.dropdown.focusedIndex=e.index(),n.dropdown._focusFocusedItem(),M.keyDown=!1,n.dropdown.isScrollable)){var i=e[0].getBoundingClientRect().top-n.dropdownOptions.getBoundingClientRect().top;i-=n.dropdownOptions.clientHeight/2,n.dropdownOptions.scrollTop=i}},this.isMultiple&&(e.closeOnClick=!1),this.dropdown=M.Dropdown.init(this.input,e)}this._setSelectedStates()}},{key:"_addOptionToValueDict",value:function(t,e){var i=Object.keys(this._valueDict).length,n=this.dropdownOptions.id+i,s={};e.id=n,s.el=t,s.optionEl=e,this._valueDict[n]=s}},{key:"_removeDropdown",value:function(){d(this.wrapper).find(".caret").remove(),d(this.input).remove(),d(this.dropdownOptions).remove(),d(this.wrapper).before(this.$el),d(this.wrapper).remove()}},{key:"_appendOptionWithIcon",value:function(t,e,i){var n=e.disabled?"disabled ":"",s="optgroup-option"===i?"optgroup-option ":"",o=this.isMultiple?'":e.innerHTML,a=d("
  • "),r=d("");r.html(o),a.addClass(n+" "+s),a.append(r);var l=e.getAttribute("data-icon");if(l){var h=d('');a.prepend(h)}return d(this.dropdownOptions).append(a[0]),a[0]}},{key:"_toggleEntryFromArray",value:function(t){var e=!this._keysSelected.hasOwnProperty(t),i=d(this._valueDict[t].optionEl);return e?this._keysSelected[t]=!0:delete this._keysSelected[t],i.toggleClass("selected",e),i.find('input[type="checkbox"]').prop("checked",e),i.prop("selected",e),e}},{key:"_setValueToInput",value:function(){var i=[];if(this.$el.find("option").each(function(t){if(d(t).prop("selected")){var e=d(t).text();i.push(e)}}),!i.length){var t=this.$el.find("option:disabled").eq(0);t.length&&""===t[0].value&&i.push(t.text())}this.input.value=i.join(", ")}},{key:"_setSelectedStates",value:function(){for(var t in this._keysSelected={},this._valueDict){var e=this._valueDict[t],i=d(e.el).prop("selected");d(e.optionEl).find('input[type="checkbox"]').prop("checked",i),i?(this._activateOption(d(this.dropdownOptions),d(e.optionEl)),this._keysSelected[t]=!0):d(e.optionEl).removeClass("selected")}}},{key:"_activateOption",value:function(t,e){e&&(this.isMultiple||t.find("li.selected").removeClass("selected"),d(e).addClass("selected"))}},{key:"getSelectedValues",value:function(){var t=[];for(var e in this._keysSelected)t.push(this._valueDict[e].el.value);return t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_FormSelect}},{key:"defaults",get:function(){return e}}]),n}();M.FormSelect=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"formSelect","M_FormSelect")}(cash),function(s,e){"use strict";var i={},t=function(t){function n(t,e){_classCallCheck(this,n);var i=_possibleConstructorReturn(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n,t,e));return(i.el.M_Range=i).options=s.extend({},n.defaults,e),i._mousedown=!1,i._setupThumb(),i._setupEventHandlers(),i}return _inherits(n,Component),_createClass(n,[{key:"destroy",value:function(){this._removeEventHandlers(),this._removeThumb(),this.el.M_Range=void 0}},{key:"_setupEventHandlers",value:function(){this._handleRangeChangeBound=this._handleRangeChange.bind(this),this._handleRangeMousedownTouchstartBound=this._handleRangeMousedownTouchstart.bind(this),this._handleRangeInputMousemoveTouchmoveBound=this._handleRangeInputMousemoveTouchmove.bind(this),this._handleRangeMouseupTouchendBound=this._handleRangeMouseupTouchend.bind(this),this._handleRangeBlurMouseoutTouchleaveBound=this._handleRangeBlurMouseoutTouchleave.bind(this),this.el.addEventListener("change",this._handleRangeChangeBound),this.el.addEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.addEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.addEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.addEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.addEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.addEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_removeEventHandlers",value:function(){this.el.removeEventListener("change",this._handleRangeChangeBound),this.el.removeEventListener("mousedown",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("touchstart",this._handleRangeMousedownTouchstartBound),this.el.removeEventListener("input",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mousemove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("touchmove",this._handleRangeInputMousemoveTouchmoveBound),this.el.removeEventListener("mouseup",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("touchend",this._handleRangeMouseupTouchendBound),this.el.removeEventListener("blur",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("mouseout",this._handleRangeBlurMouseoutTouchleaveBound),this.el.removeEventListener("touchleave",this._handleRangeBlurMouseoutTouchleaveBound)}},{key:"_handleRangeChange",value:function(){s(this.value).html(this.$el.val()),s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px")}},{key:"_handleRangeMousedownTouchstart",value:function(t){if(s(this.value).html(this.$el.val()),this._mousedown=!0,this.$el.addClass("active"),s(this.thumb).hasClass("active")||this._showRangeBubble(),"input"!==t.type){var e=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",e+"px")}}},{key:"_handleRangeInputMousemoveTouchmove",value:function(){if(this._mousedown){s(this.thumb).hasClass("active")||this._showRangeBubble();var t=this._calcRangeOffset();s(this.thumb).addClass("active").css("left",t+"px"),s(this.value).html(this.$el.val())}}},{key:"_handleRangeMouseupTouchend",value:function(){this._mousedown=!1,this.$el.removeClass("active")}},{key:"_handleRangeBlurMouseoutTouchleave",value:function(){if(!this._mousedown){var t=7+parseInt(this.$el.css("padding-left"))+"px";s(this.thumb).hasClass("active")&&(e.remove(this.thumb),e({targets:this.thumb,height:0,width:0,top:10,easing:"easeOutQuad",marginLeft:t,duration:100})),s(this.thumb).removeClass("active")}}},{key:"_setupThumb",value:function(){this.thumb=document.createElement("span"),this.value=document.createElement("span"),s(this.thumb).addClass("thumb"),s(this.value).addClass("value"),s(this.thumb).append(this.value),this.$el.after(this.thumb)}},{key:"_removeThumb",value:function(){s(this.thumb).remove()}},{key:"_showRangeBubble",value:function(){var t=-7+parseInt(s(this.thumb).parent().css("padding-left"))+"px";e.remove(this.thumb),e({targets:this.thumb,height:30,width:30,top:-30,marginLeft:t,duration:300,easing:"easeOutQuint"})}},{key:"_calcRangeOffset",value:function(){var t=this.$el.width()-15,e=parseFloat(this.$el.attr("max"))||100,i=parseFloat(this.$el.attr("min"))||0;return(parseFloat(this.$el.val())-i)/(e-i)*t}}],[{key:"init",value:function(t,e){return _get(n.__proto__||Object.getPrototypeOf(n),"init",this).call(this,this,t,e)}},{key:"getInstance",value:function(t){return(t.jquery?t[0]:t).M_Range}},{key:"defaults",get:function(){return i}}]),n}();M.Range=t,M.jQueryLoaded&&M.initializeJqueryWrapper(t,"range","M_Range"),t.init(s("input[type=range]"))}(cash,M.anime); diff --git a/esphome/dashboard/static/mode-yaml.js b/esphome/dashboard/static/mode-yaml.js index e64c0bbb18..998e39c9a4 100644 --- a/esphome/dashboard/static/mode-yaml.js +++ b/esphome/dashboard/static/mode-yaml.js @@ -5,4 +5,3 @@ ace.define("ace/mode/yaml_highlight_rules",["require","exports","module","ace/li } }); })(); - \ No newline at end of file diff --git a/esphome/dashboard/static/theme-dreamweaver.js b/esphome/dashboard/static/theme-dreamweaver.js index fe965171cd..a2fc54e022 100644 --- a/esphome/dashboard/static/theme-dreamweaver.js +++ b/esphome/dashboard/static/theme-dreamweaver.js @@ -5,4 +5,3 @@ ace.define("ace/theme/dreamweaver",["require","exports","module","ace/lib/dom"], } }); })(); - \ No newline at end of file diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index af3dd7a717..60f3a79a02 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -16,7 +16,6 @@ - {% if streamer_mode %}