Merge branch 'beta'

This commit is contained in:
Otto Winter 2019-05-30 14:15:28 +02:00
commit d7bf3c51d9
No known key found for this signature in database
GPG key ID: DB66C0BE6013F97E
925 changed files with 61610 additions and 12642 deletions

137
.clang-format Normal file
View file

@ -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: '^<ext/.*\.h>'
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

127
.clang-tidy Normal file
View file

@ -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: ''

27
.editorconfig Normal file
View file

@ -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

View file

@ -4,11 +4,10 @@
**Related issue (if applicable):** fixes <link to issue> **Related issue (if applicable):** fixes <link to issue>
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here> **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here>
**Pull request in [esphome-core](https://github.com/esphome/esphome-core) with C++ framework changes (if applicable):** esphome/esphome-core#<esphome-core PR number goes here>
## Checklist: ## Checklist:
- [ ] The code change is tested and works locally. - [ ] The code change is tested and works locally.
- [ ] Tests have been added to verify that the new code works (under `tests/` folder). - [ ] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed: 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).

93
.gitignore vendored
View file

@ -6,6 +6,19 @@ __pycache__/
# C extensions # C extensions
*.so *.so
# Hide sublime text stuff
*.sublime-project
*.sublime-workspace
# Hide some OS X stuff
.DS_Store
.AppleDouble
.LSOverride
Icon
# Thumbnails
._*
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
@ -25,12 +38,6 @@ wheels/
*.egg *.egg
MANIFEST 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 # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
@ -51,36 +58,9 @@ coverage.xml
*.mo *.mo
*.pot *.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 # pyenv
.python-version .python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments # Environments
.env .env
.venv .venv
@ -90,19 +70,46 @@ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy # mypy
.mypy_cache/ .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/ config/
tests/build/ tests/build/
tests/.esphome/ tests/.esphome/
/.temp-clang-tidy.cpp

View file

@ -3,6 +3,8 @@
variables: variables:
DOCKER_DRIVER: overlay2 DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://docker:2375/ DOCKER_HOST: tcp://docker:2375/
BASE_VERSION: '1.5.1'
TZ: UTC
stages: stages:
- lint - lint
@ -10,23 +12,20 @@ stages:
- deploy - deploy
.lint: &lint .lint: &lint
image: esphome/esphome-base-amd64 image: esphome/esphome-lint:latest
stage: lint stage: lint
before_script: before_script:
- pip install -e . - script/setup
- pip install flake8==3.6.0 pylint==1.9.4 pillow
tags: tags:
- docker - docker
.test: &test .test: &test
image: esphome/esphome-base-amd64 image: esphome/esphome-lint:latest
stage: test stage: test
before_script: before_script:
- pip install -e . - script/setup
tags: tags:
- docker - docker
variables:
TZ: UTC
.docker-base: &docker-base .docker-base: &docker-base
image: esphome/esphome-base-builder image: esphome/esphome-base-builder
@ -41,11 +40,11 @@ stages:
- | - |
if [[ "${IS_HASSIO}" == "YES" ]]; then 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} BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH}
DOCKERFILE=docker/Dockerfile.hassio DOCKERFILE=docker/Dockerfile.hassio
else 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 if [[ "${BUILD_ARCH}" == "amd64" ]]; then
BUILD_TO=esphome/esphome BUILD_TO=esphome/esphome
else else
@ -94,15 +93,32 @@ stages:
- docker - docker
stage: deploy stage: deploy
flake8: lint-custom:
<<: *lint <<: *lint
script: script:
- flake8 esphome - script/ci-custom.py
pylint: lint-python:
<<: *lint <<: *lint
script: script:
- pylint esphome - script/lint-python
lint-tidy:
<<: *lint
script:
- pio init --ide atom
- |
if ! patch -R -p0 -s -f --dry-run <script/.neopixelbus.patch; then
patch -p0 < script/.neopixelbus.patch
fi
- script/clang-tidy --all-headers --fix
- script/ci-suggest-changes
lint-format:
<<: *lint
script:
- script/clang-format -i
- script/ci-suggest-changes
test1: test1:
<<: *test <<: *test
@ -120,16 +136,12 @@ test3:
- esphome tests/test3.yaml compile - esphome tests/test3.yaml compile
.deploy-pypi: &deploy-pypi .deploy-pypi: &deploy-pypi
<<: *lint
stage: deploy stage: deploy
image: python:2.7
before_script:
- pip install -e .
- pip install twine
script: script:
- pip install twine wheel
- python setup.py sdist bdist_wheel - python setup.py sdist bdist_wheel
- twine upload dist/* - twine upload dist/*
tags:
- docker
deploy-release:pypi: deploy-release:pypi:
<<: *deploy-pypi <<: *deploy-pypi
@ -152,12 +164,6 @@ deploy-beta:pypi:
except: except:
- branches - branches
.latest-vars: &latest-vars
RELEASE: YES
LATEST: YES
# Also push to beta tag
BETA: YES
.beta: &beta .beta: &beta
<<: *docker-base <<: *docker-base
only: only:
@ -165,60 +171,53 @@ deploy-beta:pypi:
except: except:
- branches - branches
.beta-vars: &beta-vars
RELEASE: YES
BETA: YES
.dev: &dev .dev: &dev
<<: *docker-base <<: *docker-base
only: only:
- dev - dev
.dev-vars: &dev-vars aarch64-beta-docker:
DEV: YES <<: *beta
variables:
#aarch64-beta-docker: BETA: "YES"
# <<: *beta BUILD_ARCH: aarch64
# variables: IS_HASSIO: "NO"
# BETA: "YES" RELEASE: "YES"
# BUILD_ARCH: aarch64 aarch64-beta-hassio:
# IS_HASSIO: "NO" <<: *beta
# RELEASE: "YES" variables:
#aarch64-beta-hassio: BETA: "YES"
# <<: *beta BUILD_ARCH: aarch64
# variables: IS_HASSIO: "YES"
# BETA: "YES" RELEASE: "YES"
# BUILD_ARCH: aarch64 aarch64-dev-docker:
# IS_HASSIO: "YES" <<: *dev
# RELEASE: "YES" variables:
#aarch64-dev-docker: BUILD_ARCH: aarch64
# <<: *dev DEV: "YES"
# variables: IS_HASSIO: "NO"
# BUILD_ARCH: aarch64 aarch64-dev-hassio:
# DEV: "YES" <<: *dev
# IS_HASSIO: "NO" variables:
#aarch64-dev-hassio: BUILD_ARCH: aarch64
# <<: *dev DEV: "YES"
# variables: IS_HASSIO: "YES"
# BUILD_ARCH: aarch64 aarch64-latest-docker:
# DEV: "YES" <<: *latest
# IS_HASSIO: "YES" variables:
#aarch64-latest-docker: BETA: "YES"
# <<: *latest BUILD_ARCH: aarch64
# variables: IS_HASSIO: "NO"
# BETA: "YES" LATEST: "YES"
# BUILD_ARCH: aarch64 RELEASE: "YES"
# IS_HASSIO: "NO" aarch64-latest-hassio:
# LATEST: "YES" <<: *latest
# RELEASE: "YES" variables:
#aarch64-latest-hassio: BETA: "YES"
# <<: *latest BUILD_ARCH: aarch64
# variables: IS_HASSIO: "YES"
# BETA: "YES" LATEST: "YES"
# BUILD_ARCH: aarch64 RELEASE: "YES"
# IS_HASSIO: "YES"
# LATEST: "YES"
# RELEASE: "YES"
amd64-beta-docker: amd64-beta-docker:
<<: *beta <<: *beta
variables: variables:
@ -261,45 +260,45 @@ amd64-latest-hassio:
IS_HASSIO: "YES" IS_HASSIO: "YES"
LATEST: "YES" LATEST: "YES"
RELEASE: "YES" RELEASE: "YES"
armhf-beta-docker: armv7-beta-docker:
<<: *beta <<: *beta
variables: variables:
BETA: "YES" BETA: "YES"
BUILD_ARCH: armhf BUILD_ARCH: armv7
IS_HASSIO: "NO" IS_HASSIO: "NO"
RELEASE: "YES" RELEASE: "YES"
armhf-beta-hassio: armv7-beta-hassio:
<<: *beta <<: *beta
variables: variables:
BETA: "YES" BETA: "YES"
BUILD_ARCH: armhf BUILD_ARCH: armv7
IS_HASSIO: "YES" IS_HASSIO: "YES"
RELEASE: "YES" RELEASE: "YES"
armhf-dev-docker: armv7-dev-docker:
<<: *dev <<: *dev
variables: variables:
BUILD_ARCH: armhf BUILD_ARCH: armv7
DEV: "YES" DEV: "YES"
IS_HASSIO: "NO" IS_HASSIO: "NO"
armhf-dev-hassio: armv7-dev-hassio:
<<: *dev <<: *dev
variables: variables:
BUILD_ARCH: armhf BUILD_ARCH: armv7
DEV: "YES" DEV: "YES"
IS_HASSIO: "YES" IS_HASSIO: "YES"
armhf-latest-docker: armv7-latest-docker:
<<: *latest <<: *latest
variables: variables:
BETA: "YES" BETA: "YES"
BUILD_ARCH: armhf BUILD_ARCH: armv7
IS_HASSIO: "NO" IS_HASSIO: "NO"
LATEST: "YES" LATEST: "YES"
RELEASE: "YES" RELEASE: "YES"
armhf-latest-hassio: armv7-latest-hassio:
<<: *latest <<: *latest
variables: variables:
BETA: "YES" BETA: "YES"
BUILD_ARCH: armhf BUILD_ARCH: armv7
IS_HASSIO: "YES" IS_HASSIO: "YES"
LATEST: "YES" LATEST: "YES"
RELEASE: "YES" RELEASE: "YES"

6
.gitpod.yml Normal file
View file

@ -0,0 +1,6 @@
ports:
- port: 6052
onOpen: open-preview
tasks:
- before: script/setup
command: python -m esphome config dashboard

View file

@ -1,9 +1,10 @@
sudo: false sudo: false
language: python language: python
install: script/setup
cache: cache:
directories: directories:
- "~/.platformio" - "~/.platformio"
- "$TRAVIS_BUILD_DIR/.piolibdeps"
- "$TRAVIS_BUILD_DIR/tests/build/test1/.piolibdeps" - "$TRAVIS_BUILD_DIR/tests/build/test1/.piolibdeps"
- "$TRAVIS_BUILD_DIR/tests/build/test2/.piolibdeps" - "$TRAVIS_BUILD_DIR/tests/build/test2/.piolibdeps"
- "$TRAVIS_BUILD_DIR/tests/build/test3/.piolibdeps" - "$TRAVIS_BUILD_DIR/tests/build/test3/.piolibdeps"
@ -13,26 +14,43 @@ matrix:
include: include:
- python: "2.7" - python: "2.7"
env: TARGET=Lint2.7 env: TARGET=Lint2.7
install: pip install -e . && pip install flake8==3.6.0 pylint==1.9.4 pillow
script: script:
- script/ci-custom.py
- flake8 esphome - flake8 esphome
- pylint esphome - pylint esphome
- python: "3.5.3" - python: "3.5.3"
env: TARGET=Lint3.5 env: TARGET=Lint3.5
install: pip install -U https://github.com/platformio/platformio-core/archive/develop.zip && pip install -e . && pip install flake8==3.6.0 pylint==2.3.0 pillow
script: script:
- script/ci-custom.py
- flake8 esphome - flake8 esphome
- pylint esphome - pylint esphome
- python: "2.7" - python: "2.7"
env: TARGET=Test2.7 env: TARGET=Test2.7
install: pip install -e . && pip install flake8==3.6.0 pylint==1.9.4 pillow
script: script:
- esphome tests/test1.yaml compile - esphome tests/test1.yaml compile
- esphome tests/test2.yaml compile - esphome tests/test2.yaml compile
- esphome tests/test3.yaml compile - esphome tests/test3.yaml compile
#- python: "3.5.3" - env: TARGET=Cpp-Lint
# env: TARGET=Test3.5 dist: trusty
# install: pip install -U https://github.com/platformio/platformio-core/archive/develop.zip && pip install -e . && pip install flake8==3.6.0 pylint==2.3.0 pillow sudo: required
# script: addons:
# - esphome tests/test1.yaml compile apt:
# - esphome tests/test2.yaml compile sources:
- ubuntu-toolchain-r-test
- llvm-toolchain-trusty-7
packages:
- clang-tidy-7
- clang-format-7
before_script:
- pio init --ide atom
- |
if ! patch -R -p0 -s -f --dry-run <script/.neopixelbus.patch; then
patch -p0 < script/.neopixelbus.patch
fi
- clang-tidy-7 -version
- clang-format-7 -version
- clang-apply-replacements-7 -version
script:
- script/clang-tidy --all-headers -j 2 --fix
- script/clang-format -i -j 2
- script/ci-suggest-changes

692
LICENSE
View file

@ -1,6 +1,17 @@
MIT License # ESPHome License
Copyright (c) 2018 Otto Winter Copyright (c) 2019 ESPHome
The ESPHome License is made up of two base licenses: MIT and the GNU GENERAL PUBLIC LICENSE.
The C++/runtime codebase of the ESPHome project (file extensions .c, .cpp, .h, .hpp, .tcc, .ino) are
published under the GPLv3 license. The python codebase and all other parts of this codebase are
published under the MIT license.
Both MIT and GPLv3 licenses are attached to this document.
## MIT License
Copyright (c) 2019 ESPHome
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -19,3 +30,680 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
## GPLv3 License
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View file

@ -1,6 +1,5 @@
include LICENSE include LICENSE
include README.md include README.md
include esphome/dashboard/templates/*.html include esphome/dashboard/templates/*.html
include esphome/dashboard/static/*.js recursive-include esphome/dashboard/static *.ico *.js *.css
include esphome/dashboard/static/*.css recursive-include esphome *.cpp *.h *.tcc
include esphome/dashboard/static/*.ico

View file

@ -1,9 +1,8 @@
ARG BUILD_FROM=esphome/esphome-base-amd64:1.4.3 ARG BUILD_FROM=esphome/esphome-base-amd64:1.5.1
FROM ${BUILD_FROM} FROM ${BUILD_FROM}
COPY . . COPY . .
RUN \ RUN pip2 install --no-cache-dir -e .
pip2 install --no-cache-dir --no-binary :all: -e .
WORKDIR /config WORKDIR /config
ENTRYPOINT ["esphome"] ENTRYPOINT ["esphome"]

View file

@ -1,4 +1,4 @@
ARG BUILD_FROM=esphome/esphome-hassio-base-amd64:1.4.3 ARG BUILD_FROM
FROM ${BUILD_FROM} FROM ${BUILD_FROM}
# Copy root filesystem # Copy root filesystem
@ -6,8 +6,7 @@ COPY docker/rootfs/ /
COPY setup.py setup.cfg MANIFEST.in /opt/esphome/ COPY setup.py setup.cfg MANIFEST.in /opt/esphome/
COPY esphome /opt/esphome/esphome COPY esphome /opt/esphome/esphome
RUN \ RUN pip2 install --no-cache-dir -e /opt/esphome
pip2 install --no-cache-dir --no-binary :all: -e /opt/esphome
# Build arguments # Build arguments
ARG BUILD_VERSION=dev ARG BUILD_VERSION=dev

View file

@ -1,6 +1,18 @@
FROM python:2.7 FROM esphome/esphome-base-amd64:1.5.1
COPY requirements.txt /requirements.txt RUN \
apt-get update \
&& apt-get install -y --no-install-recommends \
clang-format-7 \
clang-tidy-7 \
patch \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \
/var/lib/apt/lists/*
RUN pip install -r /requirements.txt && \ COPY requirements_test.txt /requirements_test.txt
pip install flake8==3.6.0 pylint==1.9.4 pillow RUN pip2 install -r /requirements_test.txt
VOLUME ["/esphome"]
WORKDIR /esphome

View file

@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
python-pil \ python-pil \
git \ git \
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*rm -rf /var/lib/apt/lists/* /tmp/* && \ && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/*rm -rf /var/lib/apt/lists/* /tmp/* && \
pip install --no-cache-dir --no-binary :all: platformio && \ pip install --no-cache-dir platformio && \
platformio settings set enable_telemetry No && \ platformio settings set enable_telemetry No && \
platformio settings set check_libraries_interval 1000000 && \ platformio settings set check_libraries_interval 1000000 && \
platformio settings set check_platformio_interval 1000000 && \ platformio settings set check_platformio_interval 1000000 && \

View file

@ -1,18 +0,0 @@
#!/usr/bin/env bash
# the architecture to build
declare BUILD_ARCH
echo "BUILD_ARCH: ${BUILD_ARCH}"
if [[ ${BUILD_ARCH} = "amd64" ]]; then
echo "No qemu required..."
exit 0
fi
if [[ ${BUILD_ARCH} = "i386" ]]; then
echo "No qemu required..."
exit 0
fi
echo "Installing qemu..."
docker run --rm --privileged multiarch/qemu-user-static:register --reset

View file

@ -6,21 +6,29 @@
declare certfile declare certfile
declare keyfile declare keyfile
declare port declare direct_port
declare ingress_interface
declare ingress_port
mkdir -p /var/log/nginx mkdir -p /var/log/nginx
# Enable SSL direct_port=$(bashio::addon.port 6052)
if bashio::config.true 'ssl'; then if bashio::var.has_value "${direct_port}"; then
rm /etc/nginx/nginx.conf if bashio::config.true 'ssl'; then
mv /etc/nginx/nginx-ssl.conf /etc/nginx/nginx.conf
certfile=$(bashio::config 'certfile') certfile=$(bashio::config 'certfile')
keyfile=$(bashio::config 'keyfile') keyfile=$(bashio::config 'keyfile')
sed -i "s/%%certfile%%/${certfile}/g" /etc/nginx/nginx.conf mv /etc/nginx/servers/direct-ssl.disabled /etc/nginx/servers/direct.conf
sed -i "s/%%keyfile%%/${keyfile}/g" /etc/nginx/nginx.conf sed -i "s/%%certfile%%/${certfile}/g" /etc/nginx/servers/direct.conf
sed -i "s/%%keyfile%%/${keyfile}/g" /etc/nginx/servers/direct.conf
else
mv /etc/nginx/servers/direct.disabled /etc/nginx/servers/direct.conf
fi
sed -i "s/%%port%%/${direct_port}/g" /etc/nginx/servers/direct.conf
fi fi
port=$(bashio::config 'port') ingress_port=$(bashio::addon.ingress_port)
sed -i "s/%%port%%/${port}/g" /etc/nginx/nginx.conf ingress_interface=$(bashio::addon.ip_address)
sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf
sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf

View file

@ -10,6 +10,6 @@ if bashio::config.has_value 'esphome_version'; then
esphome_version=$(bashio::config 'esphome_version') esphome_version=$(bashio::config 'esphome_version')
full_url="https://github.com/esphome/esphome/archive/${esphome_version}.zip" full_url="https://github.com/esphome/esphome/archive/${esphome_version}.zip"
bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..." bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..."
pip2 install --no-cache-dir --no-binary :all: "${full_url}" \ pip2 install -U --no-cache-dir "${full_url}" \
|| bashio::exit.nok "Failed installing esphome pinned version." || bashio::exit.nok "Failed installing esphome pinned version."
fi fi

View file

@ -0,0 +1,96 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View file

@ -0,0 +1,16 @@
proxy_http_version 1.1;
proxy_ignore_client_abort off;
proxy_read_timeout 86400s;
proxy_redirect off;
proxy_send_timeout 86400s;
proxy_max_temp_file_size 0;
proxy_set_header Accept-Encoding "";
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization "";

View file

@ -0,0 +1,6 @@
root /dev/null;
server_name $hostname;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;

View file

@ -0,0 +1,9 @@
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

View file

@ -1,62 +0,0 @@
worker_processes 1;
pid /var/run/nginx.pid;
error_log stderr;
events {
worker_connections 1024;
}
http {
access_log stdout;
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream esphome {
ip_hash;
server unix:/var/run/esphome.sock;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
server_name hassio.local;
listen %%port%% default_server ssl;
root /dev/null;
ssl_certificate /ssl/%%certfile%%;
ssl_certificate_key /ssl/%%keyfile%%;
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
# Redirect http requests to https on the same port.
# https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/
error_page 497 https://$http_host$request_uri;
location / {
proxy_redirect off;
proxy_pass http://esphome;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Authorization "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
}
}
}

View file

@ -1,46 +1,33 @@
worker_processes 1; daemon off;
user root;
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
error_log stderr; worker_processes 1;
# Hass.io addon log
error_log /proc/1/fd/1 error;
events { events {
worker_connections 1024; worker_connections 1024;
} }
http { http {
include /etc/nginx/includes/mime.types;
access_log stdout; access_log stdout;
include mime.types;
default_type application/octet-stream; default_type application/octet-stream;
sendfile on; gzip on;
keepalive_timeout 65; keepalive_timeout 65;
sendfile on;
server_tokens off;
upstream esphome {
ip_hash;
server unix:/var/run/esphome.sock;
}
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default upgrade; default upgrade;
'' close; '' close;
} }
server { # Use Hass.io supervisor as resolver
server_name hassio.local; resolver 172.30.32.2;
listen %%port%% default_server;
root /dev/null;
location / { upstream esphome {
proxy_redirect off; server unix:/var/run/esphome.sock;
proxy_pass http://esphome;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Authorization "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
}
} }
include /etc/nginx/servers/*.conf;
} }

View file

@ -0,0 +1,17 @@
server {
listen %%port%% default_server ssl http2;
include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
include /etc/nginx/includes/ssl_params.conf;
# Clear Hass.io Ingress header
proxy_set_header X-Hassio-Ingress "";
# Redirect http requests to https on the same port.
# https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/
error_page 497 https://$http_host$request_uri;
location / {
proxy_pass http://esphome;
}
}

View file

@ -0,0 +1,12 @@
server {
listen %%port%% default_server;
include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
# Clear Hass.io Ingress header
proxy_set_header X-Hassio-Ingress "";
location / {
proxy_pass http://esphome;
}
}

View file

@ -0,0 +1,16 @@
server {
listen %%interface%%:%%port%% default_server;
include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
# Set Hass.io Ingress header
proxy_set_header X-Hassio-Ingress "YES";
location / {
# Only allow from Hass.io supervisor
allow 172.30.32.2;
deny all;
proxy_pass http://esphome;
}
}

View file

@ -4,5 +4,11 @@
# Runs the NGINX proxy # Runs the NGINX proxy
# ============================================================================== # ==============================================================================
bashio::log.info "Waiting for dashboard to come up..."
while [[ ! -S /var/run/esphome.sock ]]; do
sleep 0.5
done
bashio::log.info "Starting NGINX..." bashio::log.info "Starting NGINX..."
exec nginx -g "daemon off;" exec nginx

View file

@ -1,30 +1,24 @@
from __future__ import print_function from __future__ import print_function
import argparse import argparse
from datetime import datetime import functools
import logging import logging
import os import os
import random
import sys import sys
from datetime import datetime
from esphome import const, core_config, mqtt, platformio_api, wizard, writer, yaml_util from esphome import const, writer, yaml_util
from esphome.api.client import run_logs import esphome.codegen as cg
from esphome.config import get_component, iter_components, read_config, strip_default_ids from esphome.config import iter_components, read_config, strip_default_ids
from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_ESPHOME, CONF_LOGGER, \ from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \
CONF_USE_CUSTOM_CODE CONF_PASSWORD, CONF_PORT
from esphome.core import CORE, EsphomeError from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority
from esphome.cpp_generator import Expression, RawStatement, add, statement
from esphome.helpers import color, indent from esphome.helpers import color, indent
from esphome.py_compat import IS_PY2, safe_input, text_type from esphome.py_compat import IS_PY2, safe_input
from esphome.storage_json import StorageJSON, storage_path from esphome.util import run_external_command, run_external_process, safe_print
from esphome.util import run_external_command, run_external_process, safe_print, \
is_dev_esphome_version
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PRE_INITIALIZE = ['esphome', 'logger', 'wifi', 'ethernet', 'ota', 'mqtt', 'web_server', 'api',
'i2c']
def get_serial_ports(): def get_serial_ports():
# from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py
@ -94,6 +88,8 @@ def get_port_type(port):
def run_miniterm(config, port): def run_miniterm(config, port):
import serial import serial
from esphome import platformio_api
if CONF_LOGGER not in config: if CONF_LOGGER not in config:
_LOGGER.info("Logger is not enabled. Not starting UART logs.") _LOGGER.info("Logger is not enabled. Not starting UART logs.")
return return
@ -123,50 +119,45 @@ def run_miniterm(config, port):
config, line, backtrace_state=backtrace_state) config, line, backtrace_state=backtrace_state)
def wrap_to_code(name, comp):
coro = coroutine(comp.to_code)
@functools.wraps(comp.to_code)
@coroutine_with_priority(coro.priority)
def wrapped(conf):
cg.add(cg.LineComment(u"{}:".format(name)))
if comp.config_schema is not None:
conf_str = yaml_util.dump(conf)
if IS_PY2:
conf_str = conf_str.decode('utf-8')
cg.add(cg.LineComment(indent(conf_str)))
yield coro(conf)
return wrapped
def write_cpp(config): def write_cpp(config):
_LOGGER.info("Generating C++ source...") _LOGGER.info("Generating C++ source...")
CORE.add_job(core_config.to_code, config[CONF_ESPHOME], domain='esphome') for name, component, conf in iter_components(CORE.config):
for domain in PRE_INITIALIZE: if component.to_code is not None:
if domain == CONF_ESPHOME or domain not in config: coro = wrap_to_code(name, component)
continue CORE.add_job(coro, conf)
CORE.add_job(get_component(domain).to_code, config[domain], domain=domain)
for domain, component, conf in iter_components(config):
if domain in PRE_INITIALIZE or not hasattr(component, 'to_code'):
continue
CORE.add_job(component.to_code, conf, domain=domain)
CORE.flush_tasks() CORE.flush_tasks()
add(RawStatement(''))
add(RawStatement(''))
all_code = []
for exp in CORE.expressions:
if not config[CONF_ESPHOME][CONF_USE_CUSTOM_CODE]:
if isinstance(exp, Expression) and not exp.required:
continue
all_code.append(text_type(statement(exp)))
writer.write_platformio_project() writer.write_platformio_project()
code_s = indent('\n'.join(line.rstrip() for line in all_code)) code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s) writer.write_cpp(code_s)
return 0 return 0
def compile_program(args, config): def compile_program(args, config):
from esphome import platformio_api
_LOGGER.info("Compiling app...") _LOGGER.info("Compiling app...")
rc = platformio_api.run_compile(config, args.verbose) return platformio_api.run_compile(config, args.verbose)
if rc != 0 and CORE.is_dev_esphome_core_version and not is_dev_esphome_version():
_LOGGER.warning("You're using 'esphome_core_version: dev' but not using the "
"dev version of the ESPHome tool.")
_LOGGER.warning("Expect compile errors if these versions are out of sync.")
_LOGGER.warning("Please install the dev version of ESPHome too when using "
"'esphome_core_version: dev'.")
_LOGGER.warning(" - Hass.io: Install 'ESPHome (dev)' addon")
_LOGGER.warning(" - Docker: docker run [...] esphome/esphome:dev [...]")
_LOGGER.warning(" - PIP: pip install -U https://github.com/esphome/esphome/archive/dev.zip")
return rc
def upload_using_esptool(config, port): def upload_using_esptool(config, port):
@ -185,35 +176,19 @@ def upload_using_esptool(config, port):
def upload_program(config, args, host): def upload_program(config, args, host):
# if upload is to a serial port use platformio, otherwise assume ota # if upload is to a serial port use platformio, otherwise assume ota
if get_port_type(host) == 'SERIAL': if get_port_type(host) == 'SERIAL':
from esphome import platformio_api
if CORE.is_esp8266: if CORE.is_esp8266:
return upload_using_esptool(config, host) return upload_using_esptool(config, host)
return platformio_api.run_upload(config, args.verbose, host) return platformio_api.run_upload(config, args.verbose, host)
from esphome.components import ota
from esphome import espota2 from esphome import espota2
if args.host_port is not None: ota_conf = config[CONF_OTA]
host_port = args.host_port remote_port = ota_conf[CONF_PORT]
else: password = ota_conf[CONF_PASSWORD]
host_port = int(os.getenv('ESPHOME_OTA_HOST_PORT', random.randint(10000, 60000)))
verbose = args.verbose
remote_port = ota.get_port(config)
password = ota.get_auth(config)
storage = StorageJSON.load(storage_path())
res = espota2.run_ota(host, remote_port, password, CORE.firmware_bin) res = espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
if res == 0:
if storage is not None and storage.use_legacy_ota:
storage.use_legacy_ota = False
storage.save(storage_path())
return res return res
if storage is not None and not storage.use_legacy_ota:
return res
_LOGGER.warning("OTA v2 method failed. Trying with legacy OTA...")
return espota2.run_legacy_ota(verbose, host_port, host, remote_port, password,
CORE.firmware_bin)
def show_logs(config, args, port): def show_logs(config, args, port):
@ -223,19 +198,30 @@ def show_logs(config, args, port):
run_miniterm(config, port) run_miniterm(config, port)
return 0 return 0
if get_port_type(port) == 'NETWORK' and 'api' in config: if get_port_type(port) == 'NETWORK' and 'api' in config:
from esphome.api.client import run_logs
return run_logs(config, port) return run_logs(config, port)
if get_port_type(port) == 'MQTT' and 'mqtt' in config: if get_port_type(port) == 'MQTT' and 'mqtt' in config:
from esphome import mqtt
return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id) return mqtt.show_logs(config, args.topic, args.username, args.password, args.client_id)
raise ValueError raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
def clean_mqtt(config, args): def clean_mqtt(config, args):
from esphome import mqtt
return mqtt.clear_topic(config, args.topic, args.username, args.password, args.client_id) return mqtt.clear_topic(config, args.topic, args.username, args.password, args.client_id)
def setup_log(debug=False): def setup_log(debug=False, quiet=False):
log_level = logging.DEBUG if debug else logging.INFO if debug:
log_level = logging.DEBUG
elif quiet:
log_level = logging.CRITICAL
else:
log_level = logging.INFO
logging.basicConfig(level=log_level) logging.basicConfig(level=log_level)
fmt = "%(levelname)s %(message)s" fmt = "%(levelname)s %(message)s"
colorfmt = "%(log_color)s{}%(reset)s".format(fmt) colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
@ -262,6 +248,8 @@ def setup_log(debug=False):
def command_wizard(args): def command_wizard(args):
from esphome import wizard
return wizard.wizard(args.configuration) return wizard.wizard(args.configuration)
@ -273,6 +261,13 @@ def command_config(args, config):
return 0 return 0
def command_vscode(args):
from esphome import vscode
CORE.config_path = args.configuration
vscode.read_config(args)
def command_compile(args, config): def command_compile(args, config):
exit_code = write_cpp(config) exit_code = write_cpp(config)
if exit_code != 0: if exit_code != 0:
@ -329,6 +324,8 @@ def command_clean_mqtt(args, config):
def command_mqtt_fingerprint(args, config): def command_mqtt_fingerprint(args, config):
from esphome import mqtt
return mqtt.get_fingerprint(config) return mqtt.get_fingerprint(config)
@ -356,7 +353,8 @@ def command_dashboard(args):
PRE_CONFIG_ACTIONS = { PRE_CONFIG_ACTIONS = {
'wizard': command_wizard, 'wizard': command_wizard,
'version': command_version, 'version': command_version,
'dashboard': command_dashboard 'dashboard': command_dashboard,
'vscode': command_vscode,
} }
POST_CONFIG_ACTIONS = { POST_CONFIG_ACTIONS = {
@ -375,8 +373,9 @@ def parse_args(argv):
parser = argparse.ArgumentParser(prog='esphome') parser = argparse.ArgumentParser(prog='esphome')
parser.add_argument('-v', '--verbose', help="Enable verbose esphome logs.", parser.add_argument('-v', '--verbose', help="Enable verbose esphome logs.",
action='store_true') action='store_true')
parser.add_argument('--dashboard', help="Internal flag to set if the command is run from the " parser.add_argument('-q', '--quiet', help="Disable all esphome logs.",
"dashboard.", action='store_true') action='store_true')
parser.add_argument('--dashboard', help=argparse.SUPPRESS, action='store_true')
parser.add_argument('configuration', help='Your YAML configuration file.') parser.add_argument('configuration', help='Your YAML configuration file.')
subparsers = parser.add_subparsers(help='Commands', dest='command') subparsers = parser.add_subparsers(help='Commands', dest='command')
@ -393,7 +392,6 @@ def parse_args(argv):
'and upload the latest binary.') 'and upload the latest binary.')
parser_upload.add_argument('--upload-port', help="Manually specify the upload port to use. " parser_upload.add_argument('--upload-port', help="Manually specify the upload port to use. "
"For example /dev/cu.SLAB_USBtoUART.") "For example /dev/cu.SLAB_USBtoUART.")
parser_upload.add_argument('--host-port', help="Specify the host port.", type=int)
parser_logs = subparsers.add_parser('logs', help='Validate the configuration ' parser_logs = subparsers.add_parser('logs', help='Validate the configuration '
'and show all MQTT logs.') 'and show all MQTT logs.')
@ -408,7 +406,6 @@ def parse_args(argv):
'upload it, and start MQTT logs.') 'upload it, and start MQTT logs.')
parser_run.add_argument('--upload-port', help="Manually specify the upload port/ip to use. " parser_run.add_argument('--upload-port', help="Manually specify the upload port/ip to use. "
"For example /dev/cu.SLAB_USBtoUART.") "For example /dev/cu.SLAB_USBtoUART.")
parser_run.add_argument('--host-port', help="Specify the host port to use for OTA", type=int)
parser_run.add_argument('--no-logs', help='Disable starting MQTT logs.', parser_run.add_argument('--no-logs', help='Disable starting MQTT logs.',
action='store_true') action='store_true')
parser_run.add_argument('--topic', help='Manually set the topic to subscribe to for logs.') parser_run.add_argument('--topic', help='Manually set the topic to subscribe to for logs.')
@ -441,12 +438,14 @@ def parse_args(argv):
dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.", dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.",
action='store_true') action='store_true')
dashboard.add_argument("--hassio", dashboard.add_argument("--hassio",
help="Internal flag used to tell esphome is started as a Hass.io " help=argparse.SUPPRESS,
"add-on.",
action="store_true") action="store_true")
dashboard.add_argument("--socket", dashboard.add_argument("--socket",
help="Make the dashboard serve under a unix socket", type=str) help="Make the dashboard serve under a unix socket", type=str)
vscode = subparsers.add_parser('vscode', help=argparse.SUPPRESS)
vscode.add_argument('--ace', action='store_true')
return parser.parse_args(argv[1:]) return parser.parse_args(argv[1:])
@ -454,7 +453,7 @@ def run_esphome(argv):
args = parse_args(argv) args = parse_args(argv)
CORE.dashboard = args.dashboard CORE.dashboard = args.dashboard
setup_log(args.verbose) setup_log(args.verbose, args.quiet)
if args.command in PRE_CONFIG_ACTIONS: if args.command in PRE_CONFIG_ACTIONS:
try: try:
return PRE_CONFIG_ACTIONS[args.command](args) return PRE_CONFIG_ACTIONS[args.command](args)

View file

@ -1,144 +1,93 @@
import copy import esphome.codegen as cg
import voluptuous as vol
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ABOVE, CONF_ACTION_ID, CONF_AND, CONF_AUTOMATION_ID, CONF_BELOW, \ from esphome.const import CONF_AUTOMATION_ID, CONF_CONDITION, CONF_ELSE, CONF_ID, CONF_THEN, \
CONF_CONDITION, CONF_CONDITION_ID, CONF_DELAY, CONF_ELSE, CONF_ID, CONF_IF, CONF_LAMBDA, \ CONF_TRIGGER_ID, CONF_TYPE_ID, CONF_TIME
CONF_OR, CONF_RANGE, CONF_THEN, CONF_TRIGGER_ID, CONF_WAIT_UNTIL, CONF_WHILE from esphome.core import coroutine
from esphome.core import CORE from esphome.util import Registry
from esphome.cpp_generator import Pvariable, TemplateArguments, add, get_variable, \
process_lambda, templatable
from esphome.cpp_types import Action, App, Component, PollingComponent, Trigger, bool_, \
esphome_ns, float_, uint32, void
from esphome.util import ServiceRegistry
def maybe_simple_id(*validators): def maybe_simple_id(*validators):
validator = vol.All(*validators) validator = cv.All(*validators)
def validate(value): def validate(value):
if isinstance(value, dict): if isinstance(value, dict):
return validator(value) return validator(value)
with cv.remove_prepend_path([CONF_ID]):
return validator({CONF_ID: value}) return validator({CONF_ID: value})
return validate return validate
def validate_recursive_condition(value): def register_action(name, action_type, schema):
is_list = isinstance(value, list) return ACTION_REGISTRY.register(name, action_type, schema)
value = cv.ensure_list()(value)[:]
for i, item in enumerate(value):
path = [i] if is_list else []
item = copy.deepcopy(item)
if not isinstance(item, dict):
raise vol.Invalid(u"Condition must consist of key-value mapping! Got {}".format(item),
path)
key = next((x for x in item if x != CONF_CONDITION_ID), None)
if key is None:
raise vol.Invalid(u"Key missing from action! Got {}".format(item), path)
if key not in CONDITION_REGISTRY:
raise vol.Invalid(u"Unable to find condition with the name '{}', is the "
u"component loaded?".format(key), path + [key])
item.setdefault(CONF_CONDITION_ID, None)
key2 = next((x for x in item if x not in (CONF_CONDITION_ID, key)), None)
if key2 is not None:
raise vol.Invalid(u"Cannot have two conditions in one item. Key '{}' overrides '{}'! "
u"Did you forget to indent the block inside the condition?"
u"".format(key, key2), path)
validator = CONDITION_REGISTRY[key][0]
try:
condition = validator(item[key] or {})
except vol.Invalid as err:
err.prepend(path)
raise err
value[i] = {
CONF_CONDITION_ID: cv.declare_variable_id(Condition)(item[CONF_CONDITION_ID]),
key: condition,
}
return value
def validate_recursive_action(value): def register_condition(name, condition_type, schema):
is_list = isinstance(value, list) return CONDITION_REGISTRY.register(name, condition_type, schema)
if not is_list:
value = [value]
for i, item in enumerate(value):
path = [i] if is_list else []
item = copy.deepcopy(item)
if not isinstance(item, dict):
raise vol.Invalid(u"Action must consist of key-value mapping! Got {}".format(item),
path)
key = next((x for x in item if x != CONF_ACTION_ID), None)
if key is None:
raise vol.Invalid(u"Key missing from action! Got {}".format(item), path)
if key not in ACTION_REGISTRY:
raise vol.Invalid(u"Unable to find action with the name '{}', is the component loaded?"
u"".format(key), path + [key])
item.setdefault(CONF_ACTION_ID, None)
key2 = next((x for x in item if x not in (CONF_ACTION_ID, key)), None)
if key2 is not None:
raise vol.Invalid(u"Cannot have two actions in one item. Key '{}' overrides '{}'! "
u"Did you forget to indent the block inside the action?"
u"".format(key, key2), path)
validator = ACTION_REGISTRY[key][0]
try:
action = validator(item[key] or {})
except vol.Invalid as err:
err.prepend(path)
raise err
value[i] = {
CONF_ACTION_ID: cv.declare_variable_id(Action)(item[CONF_ACTION_ID]),
key: action,
}
return value
ACTION_REGISTRY = ServiceRegistry() Action = cg.esphome_ns.class_('Action')
CONDITION_REGISTRY = ServiceRegistry() Trigger = cg.esphome_ns.class_('Trigger')
ACTION_REGISTRY = Registry()
Condition = cg.esphome_ns.class_('Condition')
CONDITION_REGISTRY = Registry()
validate_action = cv.validate_registry_entry('action', ACTION_REGISTRY)
validate_action_list = cv.validate_registry('action', ACTION_REGISTRY)
validate_condition = cv.validate_registry_entry('condition', CONDITION_REGISTRY)
validate_condition_list = cv.validate_registry('condition', CONDITION_REGISTRY)
# pylint: disable=invalid-name
DelayAction = esphome_ns.class_('DelayAction', Action, Component)
LambdaAction = esphome_ns.class_('LambdaAction', Action)
IfAction = esphome_ns.class_('IfAction', Action)
WhileAction = esphome_ns.class_('WhileAction', Action)
WaitUntilAction = esphome_ns.class_('WaitUntilAction', Action, Component)
UpdateComponentAction = esphome_ns.class_('UpdateComponentAction', Action)
Automation = esphome_ns.class_('Automation')
Condition = esphome_ns.class_('Condition') def validate_potentially_and_condition(value):
AndCondition = esphome_ns.class_('AndCondition', Condition) if isinstance(value, list):
OrCondition = esphome_ns.class_('OrCondition', Condition) with cv.remove_prepend_path(['and']):
RangeCondition = esphome_ns.class_('RangeCondition', Condition) return validate_condition({
LambdaCondition = esphome_ns.class_('LambdaCondition', Condition) 'and': value
})
return validate_condition(value)
DelayAction = cg.esphome_ns.class_('DelayAction', Action, cg.Component)
LambdaAction = cg.esphome_ns.class_('LambdaAction', Action)
IfAction = cg.esphome_ns.class_('IfAction', Action)
WhileAction = cg.esphome_ns.class_('WhileAction', Action)
WaitUntilAction = cg.esphome_ns.class_('WaitUntilAction', Action, cg.Component)
UpdateComponentAction = cg.esphome_ns.class_('UpdateComponentAction', Action)
Automation = cg.esphome_ns.class_('Automation')
LambdaCondition = cg.esphome_ns.class_('LambdaCondition', Condition)
ForCondition = cg.esphome_ns.class_('ForCondition', Condition)
def validate_automation(extra_schema=None, extra_validators=None, single=False): def validate_automation(extra_schema=None, extra_validators=None, single=False):
if extra_schema is None: if extra_schema is None:
extra_schema = {} extra_schema = {}
if isinstance(extra_schema, vol.Schema): if isinstance(extra_schema, cv.Schema):
extra_schema = extra_schema.schema extra_schema = extra_schema.schema
schema = AUTOMATION_SCHEMA.extend(extra_schema) schema = AUTOMATION_SCHEMA.extend(extra_schema)
def validator_(value): def validator_(value):
if isinstance(value, list): if isinstance(value, list):
# List of items, there are two possible options here, either a sequence of
# actions (no then:) or a list of automations.
try: try:
# First try as a sequence of actions # First try as a sequence of actions
# If that succeeds, return immediately
with cv.remove_prepend_path([CONF_THEN]):
return [schema({CONF_THEN: value})] return [schema({CONF_THEN: value})]
except vol.Invalid as err: except cv.Invalid as err:
if err.path and err.path[0] == CONF_THEN:
err.path.pop(0)
# Next try as a sequence of automations # Next try as a sequence of automations
try: try:
return cv.Schema([schema])(value) return cv.Schema([schema])(value)
except vol.Invalid as err2: except cv.Invalid as err2:
if 'Unable to find action' in str(err): if u'extra keys not allowed' in str(err2) and len(err2.path) == 2:
raise err
if u'Unable to find action' in str(err):
raise err2 raise err2
raise vol.MultipleInvalid([err, err2]) raise cv.MultipleInvalid([err, err2])
elif isinstance(value, dict): elif isinstance(value, dict):
if CONF_THEN in value: if CONF_THEN in value:
return [schema(value)] return [schema(value)]
with cv.remove_prepend_path([CONF_THEN]):
return [schema({CONF_THEN: value})] return [schema({CONF_THEN: value})]
# This should only happen with invalid configs, but let's have a nice error message. # This should only happen with invalid configs, but let's have a nice error message.
return [schema(value)] return [schema(value)]
@ -149,7 +98,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
value = cv.Schema([extra_validators])(value) value = cv.Schema([extra_validators])(value)
if single: if single:
if len(value) != 1: if len(value) != 1:
raise vol.Invalid("Cannot have more than 1 automation for templates") raise cv.Invalid("Cannot have more than 1 automation for templates")
return value[0] return value[0]
return value return value
@ -157,228 +106,161 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
AUTOMATION_SCHEMA = cv.Schema({ AUTOMATION_SCHEMA = cv.Schema({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(Trigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger),
cv.GenerateID(CONF_AUTOMATION_ID): cv.declare_variable_id(Automation), cv.GenerateID(CONF_AUTOMATION_ID): cv.declare_id(Automation),
vol.Required(CONF_THEN): validate_recursive_action, cv.Required(CONF_THEN): validate_action_list,
}) })
AND_CONDITION_SCHEMA = validate_recursive_condition AndCondition = cg.esphome_ns.class_('AndCondition', Condition)
OrCondition = cg.esphome_ns.class_('OrCondition', Condition)
NotCondition = cg.esphome_ns.class_('NotCondition', Condition)
@CONDITION_REGISTRY.register(CONF_AND, AND_CONDITION_SCHEMA) @register_condition('and', AndCondition, validate_condition_list)
def and_condition_to_code(config, condition_id, template_arg, args): def and_condition_to_code(config, condition_id, template_arg, args):
for conditions in build_conditions(config, template_arg, args): conditions = yield build_condition_list(config, template_arg, args)
yield yield cg.new_Pvariable(condition_id, template_arg, conditions)
rhs = AndCondition.new(template_arg, conditions)
type = AndCondition.template(template_arg)
yield Pvariable(condition_id, rhs, type=type)
OR_CONDITION_SCHEMA = validate_recursive_condition @register_condition('or', OrCondition, validate_condition_list)
@CONDITION_REGISTRY.register(CONF_OR, OR_CONDITION_SCHEMA)
def or_condition_to_code(config, condition_id, template_arg, args): def or_condition_to_code(config, condition_id, template_arg, args):
for conditions in build_conditions(config, template_arg, args): conditions = yield build_condition_list(config, template_arg, args)
yield yield cg.new_Pvariable(condition_id, template_arg, conditions)
rhs = OrCondition.new(template_arg, conditions)
type = OrCondition.template(template_arg)
yield Pvariable(condition_id, rhs, type=type)
RANGE_CONDITION_SCHEMA = vol.All(cv.Schema({ @register_condition('not', NotCondition, validate_potentially_and_condition)
vol.Optional(CONF_ABOVE): cv.templatable(cv.float_), def not_condition_to_code(config, condition_id, template_arg, args):
vol.Optional(CONF_BELOW): cv.templatable(cv.float_), condition = yield build_condition(config, template_arg, args)
}), cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW)) yield cg.new_Pvariable(condition_id, template_arg, condition)
@CONDITION_REGISTRY.register(CONF_RANGE, RANGE_CONDITION_SCHEMA) @register_condition('lambda', LambdaCondition, cv.lambda_)
def range_condition_to_code(config, condition_id, template_arg, args): def lambda_condition_to_code(config, condition_id, template_arg, args):
for conditions in build_conditions(config, template_arg, args): lambda_ = yield cg.process_lambda(config, args, return_type=bool)
yield yield cg.new_Pvariable(condition_id, template_arg, lambda_)
rhs = RangeCondition.new(template_arg, conditions)
type = RangeCondition.template(template_arg)
condition = Pvariable(condition_id, rhs, type=type)
if CONF_ABOVE in config:
for template_ in templatable(config[CONF_ABOVE], args, float_):
yield
condition.set_min(template_)
if CONF_BELOW in config:
for template_ in templatable(config[CONF_BELOW], args, float_):
yield
condition.set_max(template_)
yield condition
DELAY_ACTION_SCHEMA = cv.templatable(cv.positive_time_period_milliseconds) @register_condition('for', ForCondition, cv.Schema({
cv.Required(CONF_TIME): cv.templatable(cv.positive_time_period_milliseconds),
cv.Required(CONF_CONDITION): validate_potentially_and_condition,
}).extend(cv.COMPONENT_SCHEMA))
def for_condition_to_code(config, condition_id, template_arg, args):
condition = yield build_condition(config[CONF_CONDITION], cg.TemplateArguments(), [])
var = cg.new_Pvariable(condition_id, template_arg, condition)
yield cg.register_component(var, config)
templ = yield cg.templatable(config[CONF_TIME], args, cg.uint32)
cg.add(var.set_time(templ))
yield var
@ACTION_REGISTRY.register(CONF_DELAY, DELAY_ACTION_SCHEMA) @register_action('delay', DelayAction, cv.templatable(cv.positive_time_period_milliseconds))
def delay_action_to_code(config, action_id, template_arg, args): def delay_action_to_code(config, action_id, template_arg, args):
rhs = App.register_component(DelayAction.new(template_arg)) var = cg.new_Pvariable(action_id, template_arg)
type = DelayAction.template(template_arg) yield cg.register_component(var, {})
action = Pvariable(action_id, rhs, type=type) template_ = yield cg.templatable(config, args, cg.uint32)
for template_ in templatable(config, args, uint32): cg.add(var.set_delay(template_))
yield yield var
add(action.set_delay(template_))
yield action
IF_ACTION_SCHEMA = vol.All({ @register_action('if', IfAction, cv.All({
vol.Required(CONF_CONDITION): validate_recursive_condition, cv.Required(CONF_CONDITION): validate_potentially_and_condition,
vol.Optional(CONF_THEN): validate_recursive_action, cv.Optional(CONF_THEN): validate_action_list,
vol.Optional(CONF_ELSE): validate_recursive_action, cv.Optional(CONF_ELSE): validate_action_list,
}, cv.has_at_least_one_key(CONF_THEN, CONF_ELSE)) }, cv.has_at_least_one_key(CONF_THEN, CONF_ELSE)))
@ACTION_REGISTRY.register(CONF_IF, IF_ACTION_SCHEMA)
def if_action_to_code(config, action_id, template_arg, args): def if_action_to_code(config, action_id, template_arg, args):
for conditions in build_conditions(config[CONF_CONDITION], template_arg, args): conditions = yield build_condition(config[CONF_CONDITION], template_arg, args)
yield None var = cg.new_Pvariable(action_id, template_arg, conditions)
rhs = IfAction.new(template_arg, conditions)
type = IfAction.template(template_arg)
action = Pvariable(action_id, rhs, type=type)
if CONF_THEN in config: if CONF_THEN in config:
for actions in build_actions(config[CONF_THEN], template_arg, args): actions = yield build_action_list(config[CONF_THEN], template_arg, args)
yield None cg.add(var.add_then(actions))
add(action.add_then(actions))
if CONF_ELSE in config: if CONF_ELSE in config:
for actions in build_actions(config[CONF_ELSE], template_arg, args): actions = yield build_action_list(config[CONF_ELSE], template_arg, args)
yield None cg.add(var.add_else(actions))
add(action.add_else(actions)) yield var
yield action
WHILE_ACTION_SCHEMA = cv.Schema({ @register_action('while', WhileAction, cv.Schema({
vol.Required(CONF_CONDITION): validate_recursive_condition, cv.Required(CONF_CONDITION): validate_potentially_and_condition,
vol.Required(CONF_THEN): validate_recursive_action, cv.Required(CONF_THEN): validate_action_list,
}) }))
@ACTION_REGISTRY.register(CONF_WHILE, WHILE_ACTION_SCHEMA)
def while_action_to_code(config, action_id, template_arg, args): def while_action_to_code(config, action_id, template_arg, args):
for conditions in build_conditions(config[CONF_CONDITION], template_arg, args): conditions = yield build_condition(config[CONF_CONDITION], template_arg, args)
yield None var = cg.new_Pvariable(action_id, template_arg, conditions)
rhs = WhileAction.new(template_arg, conditions) actions = yield build_action_list(config[CONF_THEN], template_arg, args)
type = WhileAction.template(template_arg) cg.add(var.add_then(actions))
action = Pvariable(action_id, rhs, type=type) yield var
for actions in build_actions(config[CONF_THEN], template_arg, args):
yield None
add(action.add_then(actions))
yield action
def validate_wait_until(value): def validate_wait_until(value):
schema = cv.Schema({ schema = cv.Schema({
vol.Required(CONF_CONDITION): validate_recursive_condition cv.Required(CONF_CONDITION): validate_potentially_and_condition,
}) })
if isinstance(value, dict) and CONF_CONDITION in value: if isinstance(value, dict) and CONF_CONDITION in value:
return schema(value) return schema(value)
return validate_wait_until({CONF_CONDITION: value}) return validate_wait_until({CONF_CONDITION: value})
WAIT_UNTIL_ACTION_SCHEMA = validate_wait_until @register_action('wait_until', WaitUntilAction, validate_wait_until)
@ACTION_REGISTRY.register(CONF_WAIT_UNTIL, WAIT_UNTIL_ACTION_SCHEMA)
def wait_until_action_to_code(config, action_id, template_arg, args): def wait_until_action_to_code(config, action_id, template_arg, args):
for conditions in build_conditions(config[CONF_CONDITION], template_arg, args): conditions = yield build_condition(config[CONF_CONDITION], template_arg, args)
yield None var = cg.new_Pvariable(action_id, template_arg, conditions)
rhs = WaitUntilAction.new(template_arg, conditions) yield cg.register_component(var, {})
type = WaitUntilAction.template(template_arg) yield var
action = Pvariable(action_id, rhs, type=type)
add(App.register_component(action))
yield action
LAMBDA_ACTION_SCHEMA = cv.lambda_ @register_action('lambda', LambdaAction, cv.lambda_)
@ACTION_REGISTRY.register(CONF_LAMBDA, LAMBDA_ACTION_SCHEMA)
def lambda_action_to_code(config, action_id, template_arg, args): def lambda_action_to_code(config, action_id, template_arg, args):
for lambda_ in process_lambda(config, args, return_type=void): lambda_ = yield cg.process_lambda(config, args, return_type=cg.void)
yield None yield cg.new_Pvariable(action_id, template_arg, lambda_)
rhs = LambdaAction.new(template_arg, lambda_)
type = LambdaAction.template(template_arg)
yield Pvariable(action_id, rhs, type=type)
LAMBDA_CONDITION_SCHEMA = cv.lambda_ @register_action('component.update', UpdateComponentAction, maybe_simple_id({
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}))
@CONDITION_REGISTRY.register(CONF_LAMBDA, LAMBDA_CONDITION_SCHEMA)
def lambda_condition_to_code(config, condition_id, template_arg, args):
for lambda_ in process_lambda(config, args, return_type=bool_):
yield
rhs = LambdaCondition.new(template_arg, lambda_)
type = LambdaCondition.template(template_arg)
yield Pvariable(condition_id, rhs, type=type)
CONF_COMPONENT_UPDATE = 'component.update'
COMPONENT_UPDATE_ACTION_SCHEMA = maybe_simple_id({
vol.Required(CONF_ID): cv.use_variable_id(PollingComponent),
})
@ACTION_REGISTRY.register(CONF_COMPONENT_UPDATE, COMPONENT_UPDATE_ACTION_SCHEMA)
def component_update_action_to_code(config, action_id, template_arg, args): def component_update_action_to_code(config, action_id, template_arg, args):
for var in get_variable(config[CONF_ID]): comp = yield cg.get_variable(config[CONF_ID])
yield None yield cg.new_Pvariable(action_id, template_arg, comp)
rhs = UpdateComponentAction.new(template_arg, var)
type = UpdateComponentAction.template(template_arg)
yield Pvariable(action_id, rhs, type=type)
@coroutine
def build_action(full_config, template_arg, args): def build_action(full_config, template_arg, args):
action_id = full_config[CONF_ACTION_ID] registry_entry, config = cg.extract_registry_entry_config(ACTION_REGISTRY, full_config)
key, config = next((k, v) for k, v in full_config.items() if k in ACTION_REGISTRY) action_id = full_config[CONF_TYPE_ID]
builder = registry_entry.coroutine_fun
builder = ACTION_REGISTRY[key][1] yield builder(config, action_id, template_arg, args)
for result in builder(config, action_id, template_arg, args):
yield None
yield result
def build_actions(config, templ, arg_type): @coroutine
def build_action_list(config, templ, arg_type):
actions = [] actions = []
for conf in config: for conf in config:
for action in build_action(conf, templ, arg_type): action = yield build_action(conf, templ, arg_type)
yield None
actions.append(action) actions.append(action)
yield actions yield actions
@coroutine
def build_condition(full_config, template_arg, args): def build_condition(full_config, template_arg, args):
action_id = full_config[CONF_CONDITION_ID] registry_entry, config = cg.extract_registry_entry_config(CONDITION_REGISTRY, full_config)
key, config = next((k, v) for k, v in full_config.items() if k in CONDITION_REGISTRY) action_id = full_config[CONF_TYPE_ID]
builder = registry_entry.coroutine_fun
builder = CONDITION_REGISTRY[key][1] yield builder(config, action_id, template_arg, args)
for result in builder(config, action_id, template_arg, args):
yield None
yield result
def build_conditions(config, templ, args): @coroutine
def build_condition_list(config, templ, args):
conditions = [] conditions = []
for conf in config: for conf in config:
for condition in build_condition(conf, templ, args): condition = yield build_condition(conf, templ, args)
yield None
conditions.append(condition) conditions.append(condition)
yield conditions yield conditions
def build_automation_(trigger, args, config): @coroutine
def build_automation(trigger, args, config):
arg_types = [arg[0] for arg in args] arg_types = [arg[0] for arg in args]
templ = TemplateArguments(*arg_types) templ = cg.TemplateArguments(*arg_types)
rhs = App.make_automation(templ, trigger) obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
type = Automation.template(templ) actions = yield build_action_list(config[CONF_THEN], templ, args)
obj = Pvariable(config[CONF_AUTOMATION_ID], rhs, type=type) cg.add(obj.add_actions(actions))
for actions in build_actions(config[CONF_THEN], templ, args):
yield None
add(obj.add_actions(actions))
yield obj yield obj
def build_automations(trigger, args, config):
CORE.add_job(build_automation_, trigger, args, config)

26
esphome/codegen.py Normal file
View file

@ -0,0 +1,26 @@
# Base file for all codegen-related imports
# All integrations should have a line in the import section like this
#
# >>> import esphome.codegen as cg
#
# Integrations should specifically *NOT* import directly from the
# other helper modules (cpp_generator etc) directly if they don't
# want to break suddenly due to a rename (this file will get backports for features).
# pylint: disable=unused-import
from esphome.cpp_generator import ( # noqa
Expression, RawExpression, RawStatement, TemplateArguments,
StructInitializer, ArrayInitializer, safe_exp, Statement, LineComment,
progmem_array, statement, variable, Pvariable, new_Pvariable,
add, add_global, add_library, add_build_flag, add_define,
get_variable, get_variable_with_full_id, process_lambda, is_template, templatable, MockObj,
MockObjClass)
from esphome.cpp_helpers import ( # noqa
gpio_pin_expression, register_component, build_registry_entry,
build_registry_list, extract_registry_entry_config, register_parented)
from esphome.cpp_types import ( # noqa
global_ns, void, nullptr, float_, double, bool_, std_ns, std_string,
std_vector, uint8, uint16, uint32, int32, const_char_ptr, NAN,
esphome_ns, App, Nameable, Component, ComponentPtr,
PollingComponent, Application, optional, arduino_json_ns, JsonObject,
JsonObjectRef, JsonObjectConstRef, Controller, GPIOPin)

View file

View file

@ -0,0 +1,49 @@
#include "a4988.h"
#include "esphome/core/log.h"
namespace esphome {
namespace a4988 {
static const char *TAG = "a4988.stepper";
void A4988::setup() {
ESP_LOGCONFIG(TAG, "Setting up A4988...");
if (this->sleep_pin_ != nullptr) {
this->sleep_pin_->setup();
this->sleep_pin_->digital_write(false);
}
this->step_pin_->setup();
this->step_pin_->digital_write(false);
this->dir_pin_->setup();
this->dir_pin_->digital_write(false);
}
void A4988::dump_config() {
ESP_LOGCONFIG(TAG, "A4988:");
LOG_PIN(" Step Pin: ", this->step_pin_);
LOG_PIN(" Dir Pin: ", this->dir_pin_);
LOG_PIN(" Sleep Pin: ", this->sleep_pin_);
LOG_STEPPER(this);
}
void A4988::loop() {
bool at_target = this->has_reached_target();
if (this->sleep_pin_ != nullptr) {
this->sleep_pin_->digital_write(!at_target);
}
if (at_target) {
this->high_freq_.stop();
} else {
this->high_freq_.start();
}
int32_t dir = this->should_step_();
if (dir == 0)
return;
this->dir_pin_->digital_write(dir == 1);
this->step_pin_->digital_write(true);
delayMicroseconds(5);
this->step_pin_->digital_write(false);
}
} // namespace a4988
} // namespace esphome

View file

@ -0,0 +1,28 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/esphal.h"
#include "esphome/components/stepper/stepper.h"
namespace esphome {
namespace a4988 {
class A4988 : public stepper::Stepper, public Component {
public:
void set_step_pin(GPIOPin *step_pin) { step_pin_ = step_pin; }
void set_dir_pin(GPIOPin *dir_pin) { dir_pin_ = dir_pin; }
void set_sleep_pin(GPIOPin *sleep_pin) { this->sleep_pin_ = sleep_pin; }
void setup() override;
void dump_config() override;
void loop() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
GPIOPin *step_pin_;
GPIOPin *dir_pin_;
GPIOPin *sleep_pin_{nullptr};
HighFrequencyLoopRequester high_freq_;
};
} // namespace a4988
} // namespace esphome

View file

@ -0,0 +1,31 @@
from esphome import pins
from esphome.components import stepper
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_DIR_PIN, CONF_ID, CONF_SLEEP_PIN, CONF_STEP_PIN
a4988_ns = cg.esphome_ns.namespace('a4988')
A4988 = a4988_ns.class_('A4988', stepper.Stepper, cg.Component)
CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend({
cv.Required(CONF_ID): cv.declare_id(A4988),
cv.Required(CONF_STEP_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_DIR_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_SLEEP_PIN): pins.gpio_output_pin_schema,
}).extend(cv.COMPONENT_SCHEMA)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield stepper.register_stepper(var, config)
step_pin = yield cg.gpio_pin_expression(config[CONF_STEP_PIN])
cg.add(var.set_step_pin(step_pin))
dir_pin = yield cg.gpio_pin_expression(config[CONF_DIR_PIN])
cg.add(var.set_dir_pin(dir_pin))
if CONF_SLEEP_PIN in config:
sleep_pin = yield cg.gpio_pin_expression(config[CONF_SLEEP_PIN])
cg.add(var.set_sleep_pin(sleep_pin))

View file

View file

@ -0,0 +1,92 @@
#include "adc_sensor.h"
#include "esphome/core/log.h"
#ifdef USE_ADC_SENSOR_VCC
ADC_MODE(ADC_VCC)
#endif
namespace esphome {
namespace adc {
static const char *TAG = "adc";
#ifdef ARDUINO_ARCH_ESP32
void ADCSensor::set_attenuation(adc_attenuation_t attenuation) { this->attenuation_ = attenuation; }
#endif
void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
GPIOPin(this->pin_, INPUT).setup();
#ifdef ARDUINO_ARCH_ESP32
analogSetPinAttenuation(this->pin_, this->attenuation_);
#endif
}
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
#ifdef ARDUINO_ARCH_ESP8266
#ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC");
#else
ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_);
#endif
#endif
#ifdef ARDUINO_ARCH_ESP32
ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_);
switch (this->attenuation_) {
case ADC_0db:
ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)");
break;
case ADC_2_5db:
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)");
break;
case ADC_6db:
ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)");
break;
case ADC_11db:
ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)");
break;
}
#endif
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
void ADCSensor::update() {
float value_v = this->sample();
ESP_LOGD(TAG, "'%s': Got voltage=%.2fV", this->get_name().c_str(), value_v);
this->publish_state(value_v);
}
float ADCSensor::sample() {
#ifdef ARDUINO_ARCH_ESP32
float value_v = analogRead(this->pin_) / 4095.0f;
switch (this->attenuation_) {
case ADC_0db:
value_v *= 1.1;
break;
case ADC_2_5db:
value_v *= 1.5;
break;
case ADC_6db:
value_v *= 2.2;
break;
case ADC_11db:
value_v *= 3.9;
break;
}
return value_v;
#endif
#ifdef ARDUINO_ARCH_ESP8266
#ifdef USE_ADC_SENSOR_VCC
return ESP.getVcc() / 1024.0f;
#else
return analogRead(this->pin_) / 1024.0f;
#endif
#endif
}
#ifdef ARDUINO_ARCH_ESP8266
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
#endif
} // namespace adc
} // namespace esphome

View file

@ -0,0 +1,42 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/esphal.h"
#include "esphome/core/defines.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
namespace esphome {
namespace adc {
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public:
#ifdef ARDUINO_ARCH_ESP32
/// Set the attenuation for this pin. Only available on the ESP32.
void set_attenuation(adc_attenuation_t attenuation);
#endif
/// Update adc values.
void update() override;
/// Setup ADc
void setup() override;
void dump_config() override;
/// `HARDWARE_LATE` setup priority.
float get_setup_priority() const override;
void set_pin(uint8_t pin) { this->pin_ = pin; }
float sample() override;
#ifdef ARDUINO_ARCH_ESP8266
std::string unique_id() override;
#endif
protected:
uint8_t pin_;
#ifdef ARDUINO_ARCH_ESP32
adc_attenuation_t attenuation_{ADC_0db};
#endif
};
} // namespace adc
} // namespace esphome

View file

@ -0,0 +1,48 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import sensor, voltage_sampler
from esphome.const import CONF_ATTENUATION, CONF_ID, CONF_PIN, ICON_FLASH, UNIT_VOLT
AUTO_LOAD = ['voltage_sampler']
ATTENUATION_MODES = {
'0db': cg.global_ns.ADC_0db,
'2.5db': cg.global_ns.ADC_2_5db,
'6db': cg.global_ns.ADC_6db,
'11db': cg.global_ns.ADC_11db,
}
def validate_adc_pin(value):
vcc = str(value).upper()
if vcc == 'VCC':
return cv.only_on_esp8266(vcc)
return pins.analog_pin(value)
adc_ns = cg.esphome_ns.namespace('adc')
ADCSensor = adc_ns.class_('ADCSensor', sensor.Sensor, cg.PollingComponent,
voltage_sampler.VoltageSampler)
CONFIG_SCHEMA = sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2).extend({
cv.GenerateID(): cv.declare_id(ADCSensor),
cv.Required(CONF_PIN): validate_adc_pin,
cv.SplitDefault(CONF_ATTENUATION, esp32='0db'):
cv.All(cv.only_on_esp32, cv.enum(ATTENUATION_MODES, lower=True)),
}).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)
if config[CONF_PIN] == 'VCC':
cg.add_define('USE_ADC_SENSOR_VCC')
else:
cg.add(var.set_pin(config[CONF_PIN]))
if CONF_ATTENUATION in config:
cg.add(var.set_attenuation(config[CONF_ATTENUATION]))

View file

@ -1,27 +0,0 @@
import voluptuous as vol
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from esphome.cpp_generator import Pvariable
from esphome.cpp_helpers import setup_component
from esphome.cpp_types import App, Component
DEPENDENCIES = ['i2c']
MULTI_CONF = True
ADS1115Component = sensor.sensor_ns.class_('ADS1115Component', Component, i2c.I2CDevice)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_variable_id(ADS1115Component),
vol.Required(CONF_ADDRESS): cv.i2c_address,
}).extend(cv.COMPONENT_SCHEMA.schema)
def to_code(config):
rhs = App.make_ads1115_component(config[CONF_ADDRESS])
var = Pvariable(config[CONF_ID], rhs)
setup_component(var, config)
BUILD_FLAGS = '-DUSE_ADS1115_SENSOR'

View file

@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID
DEPENDENCIES = ['i2c']
AUTO_LOAD = ['sensor', 'voltage_sampler']
MULTI_CONF = True
ads1115_ns = cg.esphome_ns.namespace('ads1115')
ADS1115Component = ads1115_ns.class_('ADS1115Component', cg.Component, i2c.I2CDevice)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(ADS1115Component),
}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(None))
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,164 @@
#include "ads1115.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ads1115 {
static const char *TAG = "ads1115";
static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111;
void ADS1115Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ADS1115...");
uint16_t value;
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
this->mark_failed();
return;
}
uint16_t config = 0;
// Clear single-shot bit
// 0b0xxxxxxxxxxxxxxx
config |= 0b0000000000000000;
// Setup multiplexer
// 0bx000xxxxxxxxxxxx
config |= ADS1115_MULTIPLEXER_P0_N1 << 12;
// Setup Gain
// 0bxxxx000xxxxxxxxx
config |= ADS1115_GAIN_6P144 << 9;
// Set singleshot mode
// 0bxxxxxxx1xxxxxxxx
config |= 0b0000000100000000;
// Set data rate - 860 samples per second (we're in singleshot mode)
// 0bxxxxxxxx100xxxxx
config |= ADS1115_DATA_RATE_860_SPS << 5;
// Set comparator mode - hysteresis
// 0bxxxxxxxxxxx0xxxx
config |= 0b0000000000000000;
// Set comparator polarity - active low
// 0bxxxxxxxxxxxx0xxx
config |= 0b0000000000000000;
// Set comparator latch enabled - false
// 0bxxxxxxxxxxxxx0xx
config |= 0b0000000000000000;
// Set comparator que mode - disabled
// 0bxxxxxxxxxxxxxx11
config |= 0b0000000000000011;
if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
this->mark_failed();
return;
}
for (auto *sensor : this->sensors_) {
this->set_interval(sensor->get_name(), sensor->update_interval(),
[this, sensor] { this->request_measurement(sensor); });
}
}
void ADS1115Component::dump_config() {
ESP_LOGCONFIG(TAG, "Setting up ADS1115...");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with ADS1115 failed!");
}
for (auto *sensor : this->sensors_) {
LOG_SENSOR(" ", "Sensor", sensor);
ESP_LOGCONFIG(TAG, " Multiplexer: %u", sensor->get_multiplexer());
ESP_LOGCONFIG(TAG, " Gain: %u", sensor->get_gain());
}
}
float ADS1115Component::get_setup_priority() const { return setup_priority::DATA; }
float ADS1115Component::request_measurement(ADS1115Sensor *sensor) {
uint16_t config;
if (!this->read_byte_16(ADS1115_REGISTER_CONFIG, &config)) {
this->status_set_warning();
return NAN;
}
// Multiplexer
// 0bxBBBxxxxxxxxxxxx
config &= 0b1000111111111111;
config |= (sensor->get_multiplexer() & 0b111) << 12;
// Gain
// 0bxxxxBBBxxxxxxxxx
config &= 0b1111000111111111;
config |= (sensor->get_gain() & 0b111) << 9;
// Start conversion
config |= 0b1000000000000000;
if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
this->status_set_warning();
return NAN;
}
// about 1.6 ms with 860 samples per second
delay(2);
uint32_t start = millis();
while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) {
if (millis() - start > 100) {
ESP_LOGW(TAG, "Reading ADS1115 timed out");
this->status_set_warning();
return NAN;
}
yield();
}
uint16_t raw_conversion;
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &raw_conversion)) {
this->status_set_warning();
return NAN;
}
auto signed_conversion = static_cast<int16_t>(raw_conversion);
float millivolts;
switch (sensor->get_gain()) {
case ADS1115_GAIN_6P144:
millivolts = signed_conversion * 0.187500f;
break;
case ADS1115_GAIN_4P096:
millivolts = signed_conversion * 0.125000f;
break;
case ADS1115_GAIN_2P048:
millivolts = signed_conversion * 0.062500f;
break;
case ADS1115_GAIN_1P024:
millivolts = signed_conversion * 0.031250f;
break;
case ADS1115_GAIN_0P512:
millivolts = signed_conversion * 0.015625f;
break;
case ADS1115_GAIN_0P256:
millivolts = signed_conversion * 0.007813f;
break;
default:
millivolts = NAN;
}
this->status_clear_warning();
return millivolts / 1e4f;
}
uint8_t ADS1115Sensor::get_multiplexer() const { return this->multiplexer_; }
void ADS1115Sensor::set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; }
uint8_t ADS1115Sensor::get_gain() const { return this->gain_; }
void ADS1115Sensor::set_gain(ADS1115Gain gain) { this->gain_ = gain; }
float ADS1115Sensor::sample() { return this->parent_->request_measurement(this); }
void ADS1115Sensor::update() {
float v = this->parent_->request_measurement(this);
if (!isnan(v)) {
ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v);
this->publish_state(v);
}
}
} // namespace ads1115
} // namespace esphome

View file

@ -0,0 +1,68 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
namespace esphome {
namespace ads1115 {
enum ADS1115Multiplexer {
ADS1115_MULTIPLEXER_P0_N1 = 0b000,
ADS1115_MULTIPLEXER_P0_N3 = 0b001,
ADS1115_MULTIPLEXER_P1_N3 = 0b010,
ADS1115_MULTIPLEXER_P2_N3 = 0b011,
ADS1115_MULTIPLEXER_P0_NG = 0b100,
ADS1115_MULTIPLEXER_P1_NG = 0b101,
ADS1115_MULTIPLEXER_P2_NG = 0b110,
ADS1115_MULTIPLEXER_P3_NG = 0b111,
};
enum ADS1115Gain {
ADS1115_GAIN_6P144 = 0b000,
ADS1115_GAIN_4P096 = 0b001,
ADS1115_GAIN_2P048 = 0b010,
ADS1115_GAIN_1P024 = 0b011,
ADS1115_GAIN_0P512 = 0b100,
ADS1115_GAIN_0P256 = 0b101,
};
class ADS1115Sensor;
class ADS1115Component : public Component, public i2c::I2CDevice {
public:
void register_sensor(ADS1115Sensor *obj) { this->sensors_.push_back(obj); }
/// Set up the internal sensor array.
void setup() override;
void dump_config() override;
/// HARDWARE_LATE setup priority
float get_setup_priority() const override;
/// Helper method to request a measurement from a sensor.
float request_measurement(ADS1115Sensor *sensor);
protected:
std::vector<ADS1115Sensor *> sensors_;
};
/// Internal holder class that is in instance of Sensor so that the hub can create individual sensors.
class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public:
ADS1115Sensor(ADS1115Component *parent) : parent_(parent) {}
void update() override;
void set_multiplexer(ADS1115Multiplexer multiplexer);
void set_gain(ADS1115Gain gain);
float sample() override;
uint8_t get_multiplexer() const;
uint8_t get_gain() const;
protected:
ADS1115Component *parent_;
ADS1115Multiplexer multiplexer_;
ADS1115Gain gain_;
};
} // namespace ads1115
} // namespace esphome

View file

@ -0,0 +1,63 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, voltage_sampler
from esphome.const import CONF_GAIN, CONF_MULTIPLEXER, ICON_FLASH, UNIT_VOLT, CONF_ID
from esphome.py_compat import string_types
from . import ads1115_ns, ADS1115Component
DEPENDENCIES = ['ads1115']
ADS1115Multiplexer = ads1115_ns.enum('ADS1115Multiplexer')
MUX = {
'A0_A1': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N1,
'A0_A3': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_N3,
'A1_A3': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_N3,
'A2_A3': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_N3,
'A0_GND': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P0_NG,
'A1_GND': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P1_NG,
'A2_GND': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P2_NG,
'A3_GND': ADS1115Multiplexer.ADS1115_MULTIPLEXER_P3_NG,
}
ADS1115Gain = ads1115_ns.enum('ADS1115Gain')
GAIN = {
'6.144': ADS1115Gain.ADS1115_GAIN_6P144,
'4.096': ADS1115Gain.ADS1115_GAIN_4P096,
'2.048': ADS1115Gain.ADS1115_GAIN_2P048,
'1.024': ADS1115Gain.ADS1115_GAIN_1P024,
'0.512': ADS1115Gain.ADS1115_GAIN_0P512,
'0.256': ADS1115Gain.ADS1115_GAIN_0P256,
}
def validate_gain(value):
if isinstance(value, float):
value = u'{:0.03f}'.format(value)
elif not isinstance(value, string_types):
raise cv.Invalid('invalid gain "{}"'.format(value))
return cv.enum(GAIN)(value)
ADS1115Sensor = ads1115_ns.class_('ADS1115Sensor', sensor.Sensor, cg.PollingComponent,
voltage_sampler.VoltageSampler)
CONF_ADS1115_ID = 'ads1115_id'
CONFIG_SCHEMA = sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 3).extend({
cv.GenerateID(): cv.declare_id(ADS1115Sensor),
cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component),
cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space='_'),
cv.Required(CONF_GAIN): validate_gain,
}).extend(cv.polling_component_schema('60s'))
def to_code(config):
paren = yield cg.get_variable(config[CONF_ADS1115_ID])
var = cg.new_Pvariable(config[CONF_ID], paren)
yield sensor.register_sensor(var, config)
yield cg.register_component(var, config)
cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
cg.add(var.set_gain(config[CONF_GAIN]))
cg.add(paren.register_sensor(var))

View file

View file

@ -0,0 +1,107 @@
// Implementation based on:
// - ESPEasy: https://github.com/letscontrolit/ESPEasy/blob/mega/src/_P034_DHT12.ino
// - DHT12_sensor_library: https://github.com/xreef/DHT12_sensor_library/blob/master/DHT12.cpp
// - Arduino - AM2320: https://github.com/EngDial/AM2320/blob/master/src/AM2320.cpp
#include "am2320.h"
#include "esphome/core/log.h"
namespace esphome {
namespace am2320 {
static const char *TAG = "am2320";
// ---=== Calc CRC16 ===---
uint16_t crc_16(uint8_t *ptr, uint8_t length) {
uint16_t crc = 0xFFFF;
uint8_t i;
//------------------------------
while (length--) {
crc ^= *ptr++;
for (i = 0; i < 8; i++)
if ((crc & 0x01) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else
crc >>= 1;
}
return crc;
}
void AM2320Component::update() {
uint8_t data[8];
data[0] = 0;
data[1] = 4;
if (!this->read_data_(data)) {
this->status_set_warning();
return;
}
float temperature = (((data[4] & 0x7F) << 8) + data[5]) / 10.0;
temperature = (data[4] & 0x80) ? -temperature : temperature;
float humidity = ((data[2] << 8) + data[3]) / 10.0;
ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temperature, humidity);
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
if (this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(humidity);
this->status_clear_warning();
}
void AM2320Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up AM2320...");
uint8_t data[8];
data[0] = 0;
data[1] = 4;
if (!this->read_data_(data)) {
this->mark_failed();
return;
}
}
void AM2320Component::dump_config() {
ESP_LOGD(TAG, "AM2320:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with AM2320 failed!");
}
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
}
float AM2320Component::get_setup_priority() const { return setup_priority::DATA; }
bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) {
if (!this->write_bytes(a_register, data, 2)) {
ESP_LOGW(TAG, "Writing bytes for AM2320 failed!");
return false;
}
if (conversion > 0)
delay(conversion);
return this->parent_->raw_receive(this->address_, data, len);
}
bool AM2320Component::read_data_(uint8_t *data) {
// Wake up
this->write_bytes(0, data, 0);
// Write instruction 3, 2 bytes, get 8 bytes back (2 preamble, 2 bytes temperature, 2 bytes humidity, 2 bytes CRC)
if (!this->read_bytes_(3, data, 8, 2)) {
ESP_LOGW(TAG, "Updating AM2320 failed!");
return false;
}
uint16_t checksum;
checksum = data[7] << 8;
checksum += data[6];
if (crc_16(data, 6) != checksum) {
ESP_LOGW(TAG, "AM2320 Checksum invalid!");
return false;
}
return true;
}
} // namespace am2320
} // namespace esphome

View file

@ -0,0 +1,29 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace am2320 {
class AM2320Component : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
void update() override;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
protected:
bool read_data_(uint8_t *data);
bool read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0);
sensor::Sensor *temperature_sensor_;
sensor::Sensor *humidity_sensor_;
};
} // namespace am2320
} // namespace esphome

View file

@ -0,0 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, \
UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT
DEPENDENCIES = ['i2c']
am2320_ns = cg.esphome_ns.namespace('am2320')
AM2320Component = am2320_ns.class_('AM2320Component', cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(AM2320Component),
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),
}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x5C))
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config:
sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(sens))

View file

@ -1,33 +0,0 @@
import voluptuous as vol
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID, CONF_UPDATE_INTERVAL
from esphome.cpp_generator import Pvariable, add
from esphome.cpp_helpers import setup_component
from esphome.cpp_types import App, PollingComponent
DEPENDENCIES = ['i2c']
MULTI_CONF = True
CONF_APDS9960_ID = 'apds9960_id'
APDS9960 = sensor.sensor_ns.class_('APDS9960', PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_variable_id(APDS9960),
vol.Optional(CONF_ADDRESS): cv.i2c_address,
vol.Optional(CONF_UPDATE_INTERVAL): cv.update_interval,
}).extend(cv.COMPONENT_SCHEMA.schema)
def to_code(config):
rhs = App.make_apds9960(config.get(CONF_UPDATE_INTERVAL))
var = Pvariable(config[CONF_ID], rhs)
if CONF_ADDRESS in config:
add(var.set_address(config[CONF_ADDRESS]))
setup_component(var, config)
BUILD_FLAGS = '-DUSE_APDS9960'

View file

@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import CONF_ID
DEPENDENCIES = ['i2c']
AUTO_LOAD = ['sensor', 'binary_sensor']
MULTI_CONF = True
CONF_APDS9960_ID = 'apds9960_id'
apds9960_nds = cg.esphome_ns.namespace('apds9960')
APDS9960 = apds9960_nds.class_('APDS9960', cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(APDS9960),
}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x39))
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield i2c.register_i2c_device(var, config)

View file

@ -0,0 +1,374 @@
#include "apds9960.h"
#include "esphome/core/log.h"
namespace esphome {
namespace apds9960 {
static const char *TAG = "apds9960";
#define APDS9960_ERROR_CHECK(func) \
if (!func) { \
this->mark_failed(); \
return; \
}
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
void APDS9960::setup() {
ESP_LOGCONFIG(TAG, "Setting up APDS9960...");
uint8_t id;
if (!this->read_byte(0x92, &id)) { // ID register
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
if (id != 0xAB && id != 0x9C) { // APDS9960 all should have one of these IDs
this->error_code_ = WRONG_ID;
this->mark_failed();
return;
}
// ATime (ADC integration time, 2.78ms increments, 0x81) -> 0xDB (103ms)
APDS9960_WRITE_BYTE(0x81, 0xDB);
// WTime (Wait time, 0x83) -> 0xF6 (27ms)
APDS9960_WRITE_BYTE(0x83, 0xF6);
// PPulse (0x8E) -> 0x87 (16us, 8 pulses)
APDS9960_WRITE_BYTE(0x8E, 0x87);
// POffset UR (0x9D) -> 0 (no offset)
APDS9960_WRITE_BYTE(0x9D, 0x00);
// POffset DL (0x9E) -> 0 (no offset)
APDS9960_WRITE_BYTE(0x9E, 0x00);
// Config 1 (0x8D) -> 0x60 (no wtime factor)
APDS9960_WRITE_BYTE(0x8D, 0x60);
// Control (0x8F) ->
uint8_t val = 0;
APDS9960_ERROR_CHECK(this->read_byte(0x8F, &val));
val &= 0b00111111;
uint8_t led_drive = 0; // led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
val |= (led_drive & 0b11) << 6;
val &= 0b11110011;
uint8_t proximity_gain = 2; // proximity gain, 0 -> 1x, 1 -> 2X, 2 -> 4X, 4 -> 8X
val |= (proximity_gain & 0b11) << 2;
val &= 0b11111100;
uint8_t ambient_gain = 1; // ambient light gain, 0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x
val |= (ambient_gain & 0b11) << 0;
APDS9960_WRITE_BYTE(0x8F, val);
// Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt)
APDS9960_WRITE_BYTE(0x8C, 0x11);
// Config 2 (0x90) -> 0x01 (no saturation interrupts or LED boost)
APDS9960_WRITE_BYTE(0x90, 0x01);
// Config 3 (0x9F) -> 0x00 (enable all photodiodes, no SAI)
APDS9960_WRITE_BYTE(0x9F, 0x00);
// GPenTh (0xA0, gesture enter threshold) -> 0x28 (also 0x32)
APDS9960_WRITE_BYTE(0xA0, 0x28);
// GPexTh (0xA1, gesture exit threshold) -> 0x1E
APDS9960_WRITE_BYTE(0xA1, 0x1E);
// GConf 1 (0xA2, gesture config 1) -> 0x40 (4 gesture events for interrupt (GFIFO 3), 1 for exit)
APDS9960_WRITE_BYTE(0xA2, 0x40);
// GConf 2 (0xA3, gesture config 2) ->
APDS9960_ERROR_CHECK(this->read_byte(0xA3, &val));
val &= 0b10011111;
uint8_t gesture_gain = 2; // gesture gain, 0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x
val |= (gesture_gain & 0b11) << 5;
val &= 0b11100111;
uint8_t gesture_led_drive = 0; // gesture led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA
val |= (gesture_led_drive & 0b11) << 3;
val &= 0b11111000;
// gesture wait time
// 0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms
// 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms
uint8_t gesture_wait_time = 1; // gesture wait time
val |= (gesture_wait_time & 0b111) << 0;
APDS9960_WRITE_BYTE(0xA3, val);
// GOffsetU (0xA4) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA4, 0x00);
// GOffsetD (0xA5) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA5, 0x00);
// GOffsetL (0xA7) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA7, 0x00);
// GOffsetR (0xA9) -> 0x00 (no offset)
APDS9960_WRITE_BYTE(0xA9, 0x00);
// GPulse (0xA6) -> 0xC9 (32 µs, 10 pulses)
APDS9960_WRITE_BYTE(0xA6, 0xC9);
// GConf 3 (0xAA, gesture config 3) -> 0x00 (all photodiodes active during gesture, all gesture dimensions enabled)
// 0x00 -> all dimensions, 0x01 -> up down, 0x02 -> left right
APDS9960_WRITE_BYTE(0xAA, 0x00);
// Enable (0x80) ->
val = 0;
val |= (0b1) << 0; // power on
val |= (this->is_color_enabled_() & 0b1) << 1;
val |= (this->is_proximity_enabled_() & 0b1) << 2;
val |= 0b0 << 3; // wait timer disabled
val |= 0b0 << 4; // color interrupt disabled
val |= 0b0 << 5; // proximity interrupt disabled
val |= (this->is_gesture_enabled_() & 0b1) << 6; // proximity is required for gestures
APDS9960_WRITE_BYTE(0x80, val);
}
bool APDS9960::is_color_enabled_() const {
return this->red_channel_ != nullptr || this->green_channel_ != nullptr || this->blue_channel_ != nullptr ||
this->clear_channel_ != nullptr;
}
void APDS9960::dump_config() {
ESP_LOGCONFIG(TAG, "APDS9960:");
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
if (this->is_failed()) {
switch (this->error_code_) {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication with APDS9960 failed!");
break;
case WRONG_ID:
ESP_LOGE(TAG, "APDS9960 has invalid id!");
break;
default:
ESP_LOGE(TAG, "Setting up APDS9960 registers failed!");
break;
}
}
}
#define APDS9960_WARNING_CHECK(func, warning) \
if (!(func)) { \
ESP_LOGW(TAG, warning); \
this->status_set_warning(); \
return; \
}
void APDS9960::update() {
uint8_t status;
APDS9960_WARNING_CHECK(this->read_byte(0x93, &status), "Reading status bit failed.");
this->status_clear_warning();
this->read_color_data_(status);
this->read_proximity_data_(status);
}
void APDS9960::loop() { this->read_gesture_data_(); }
void APDS9960::read_color_data_(uint8_t status) {
if (!this->is_color_enabled_())
return;
if ((status & 0x01) == 0x00) {
// color data not ready yet.
return;
}
uint8_t raw[8];
APDS9960_WARNING_CHECK(this->read_bytes(0x94, raw, 8), "Reading color values failed.");
uint16_t uint_clear = (uint16_t(raw[1]) << 8) | raw[0];
uint16_t uint_red = (uint16_t(raw[3]) << 8) | raw[2];
uint16_t uint_green = (uint16_t(raw[5]) << 8) | raw[4];
uint16_t uint_blue = (uint16_t(raw[7]) << 8) | raw[6];
float clear_perc = (uint_clear / float(UINT16_MAX)) * 100.0f;
float red_perc = (uint_red / float(UINT16_MAX)) * 100.0f;
float green_perc = (uint_green / float(UINT16_MAX)) * 100.0f;
float blue_perc = (uint_blue / float(UINT16_MAX)) * 100.0f;
ESP_LOGD(TAG, "Got clear=%.1f%% red=%.1f%% green=%.1f%% blue=%.1f%%", clear_perc, red_perc, green_perc, blue_perc);
if (this->clear_channel_ != nullptr)
this->clear_channel_->publish_state(clear_perc);
if (this->red_channel_ != nullptr)
this->red_channel_->publish_state(red_perc);
if (this->green_channel_ != nullptr)
this->green_channel_->publish_state(green_perc);
if (this->blue_channel_ != nullptr)
this->blue_channel_->publish_state(blue_perc);
}
void APDS9960::read_proximity_data_(uint8_t status) {
if (this->proximity_ == nullptr)
return;
if ((status & 0b10) == 0x00) {
// proximity data not ready yet.
return;
}
uint8_t prox;
APDS9960_WARNING_CHECK(this->read_byte(0x9C, &prox), "Reading proximity values failed.");
float prox_perc = (prox / float(UINT8_MAX)) * 100.0f;
ESP_LOGD(TAG, "Got proximity=%.1f%%", prox_perc);
this->proximity_->publish_state(prox_perc);
}
void APDS9960::read_gesture_data_() {
if (!this->is_gesture_enabled_())
return;
uint8_t status;
APDS9960_WARNING_CHECK(this->read_byte(0xAF, &status), "Reading gesture status failed.");
if ((status & 0b01) == 0) {
// GVALID is false
return;
}
if ((status & 0b10) == 0b10) {
ESP_LOGV(TAG, "FIFO buffer has filled to capacity!");
}
uint8_t fifo_level;
APDS9960_WARNING_CHECK(this->read_byte(0xAE, &fifo_level), "Reading FIFO level failed.");
if (fifo_level == 0)
// no data to process
return;
APDS9960_WARNING_CHECK(fifo_level <= 32, "FIFO level has invalid value.")
uint8_t buf[128];
for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
// The ESP's i2c driver has a limited buffer size.
// This way of retrieving the data should be wrong according to the datasheet
// but it seems to work.
uint8_t read = std::min(32, fifo_level * 4 - pos);
APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed.");
}
if (millis() - this->gesture_start_ > 500) {
this->gesture_up_started_ = false;
this->gesture_down_started_ = false;
this->gesture_left_started_ = false;
this->gesture_right_started_ = false;
}
for (uint32_t i = 0; i < fifo_level * 4; i += 4) {
const int up = buf[i + 0]; // NOLINT
const int down = buf[i + 1];
const int left = buf[i + 2];
const int right = buf[i + 3];
this->process_dataset_(up, down, left, right);
}
}
void APDS9960::report_gesture_(int gesture) {
binary_sensor::BinarySensor *bin;
switch (gesture) {
case 1:
bin = this->up_direction_;
this->gesture_up_started_ = false;
this->gesture_down_started_ = false;
ESP_LOGD(TAG, "Got gesture UP");
break;
case 2:
bin = this->down_direction_;
this->gesture_up_started_ = false;
this->gesture_down_started_ = false;
ESP_LOGD(TAG, "Got gesture DOWN");
break;
case 3:
bin = this->left_direction_;
this->gesture_left_started_ = false;
this->gesture_right_started_ = false;
ESP_LOGD(TAG, "Got gesture LEFT");
break;
case 4:
bin = this->right_direction_;
this->gesture_left_started_ = false;
this->gesture_right_started_ = false;
ESP_LOGD(TAG, "Got gesture RIGHT");
break;
default:
return;
}
if (bin != nullptr) {
bin->publish_state(true);
bin->publish_state(false);
}
}
void APDS9960::process_dataset_(int up, int down, int left, int right) {
/* Algorithm: (see Figure 11 in datasheet)
*
* Observation: When a gesture is started, we will see a short amount of time where
* the photodiode in the direction of the motion has a much higher count value
* than where the gesture originates.
*
* In this algorithm we continually check the difference between the count values of opposing
* directions. For example in the down/up direction we continually look at the difference of the
* up count and down count. When DOWN gesture begins, this difference will be positive with a
* high magnitude for a short amount of time (magic value here is the difference is at least 13).
*
* If we see such a pattern, we store that we saw the first part of a gesture (the leading edge).
* After that some time can pass during which the difference is zero again (though the count values
* are not zero). At the end of a gesture, we will see this difference go into the opposite direction
* for a short period of time.
*
* If a gesture is not ended within 500 milliseconds, we consider the initial trailing edge invalid
* and reset the state.
*
* This algorithm does work, but not too well. Some good signal processing algorithms could
* probably improve this a lot, especially since the incoming signal has such a characteristic
* and quite noise-free pattern.
*/
const int up_down_delta = up - down;
const int left_right_delta = left - right;
const bool up_down_significant = abs(up_down_delta) > 13;
const bool left_right_significant = abs(left_right_delta) > 13;
if (up_down_significant) {
if (up_down_delta < 0) {
if (this->gesture_up_started_) {
// trailing edge of gesture up
this->report_gesture_(1); // UP
} else {
// leading edge of gesture down
this->gesture_down_started_ = true;
this->gesture_start_ = millis();
}
} else {
if (this->gesture_down_started_) {
// trailing edge of gesture down
this->report_gesture_(2); // DOWN
} else {
// leading edge of gesture up
this->gesture_up_started_ = true;
this->gesture_start_ = millis();
}
}
}
if (left_right_significant) {
if (left_right_delta < 0) {
if (this->gesture_left_started_) {
// trailing edge of gesture left
this->report_gesture_(3); // LEFT
} else {
// leading edge of gesture right
this->gesture_right_started_ = true;
this->gesture_start_ = millis();
}
} else {
if (this->gesture_right_started_) {
// trailing edge of gesture right
this->report_gesture_(4); // RIGHT
} else {
// leading edge of gesture left
this->gesture_left_started_ = true;
this->gesture_start_ = millis();
}
}
}
}
float APDS9960::get_setup_priority() const { return setup_priority::DATA; }
bool APDS9960::is_proximity_enabled_() const { return this->proximity_ != nullptr || this->is_gesture_enabled_(); }
bool APDS9960::is_gesture_enabled_() const {
return this->up_direction_ != nullptr || this->left_direction_ != nullptr || this->down_direction_ != nullptr ||
this->right_direction_ != nullptr;
}
} // namespace apds9960
} // namespace esphome

View file

@ -0,0 +1,61 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace apds9960 {
class APDS9960 : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
void update() override;
void loop() override;
void set_red_channel(sensor::Sensor *red_channel) { red_channel_ = red_channel; }
void set_green_channel(sensor::Sensor *green_channel) { green_channel_ = green_channel; }
void set_blue_channel(sensor::Sensor *blue_channel) { blue_channel_ = blue_channel; }
void set_clear_channel(sensor::Sensor *clear_channel) { clear_channel_ = clear_channel; }
void set_up_direction(binary_sensor::BinarySensor *up_direction) { up_direction_ = up_direction; }
void set_right_direction(binary_sensor::BinarySensor *right_direction) { right_direction_ = right_direction; }
void set_down_direction(binary_sensor::BinarySensor *down_direction) { down_direction_ = down_direction; }
void set_left_direction(binary_sensor::BinarySensor *left_direction) { left_direction_ = left_direction; }
void set_proximity(sensor::Sensor *proximity) { proximity_ = proximity; }
protected:
bool is_color_enabled_() const;
bool is_proximity_enabled_() const;
bool is_gesture_enabled_() const;
void read_color_data_(uint8_t status);
void read_proximity_data_(uint8_t status);
void read_gesture_data_();
void report_gesture_(int gesture);
void process_dataset_(int up, int down, int left, int right);
sensor::Sensor *red_channel_{nullptr};
sensor::Sensor *green_channel_{nullptr};
sensor::Sensor *blue_channel_{nullptr};
sensor::Sensor *clear_channel_{nullptr};
binary_sensor::BinarySensor *up_direction_{nullptr};
binary_sensor::BinarySensor *right_direction_{nullptr};
binary_sensor::BinarySensor *down_direction_{nullptr};
binary_sensor::BinarySensor *left_direction_{nullptr};
sensor::Sensor *proximity_{nullptr};
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
WRONG_ID,
} error_code_{NONE};
bool gesture_up_started_{false};
bool gesture_down_started_{false};
bool gesture_left_started_{false};
bool gesture_right_started_{false};
uint32_t gesture_start_{0};
};
} // namespace apds9960
} // namespace esphome

View file

@ -0,0 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import CONF_DIRECTION, CONF_DEVICE_CLASS, DEVICE_CLASS_MOVING
from . import APDS9960, CONF_APDS9960_ID
DEPENDENCIES = ['apds9960']
DIRECTIONS = {
'UP': 'set_up_direction',
'DOWN': 'set_down_direction',
'LEFT': 'set_left_direction',
'RIGHT': 'set_right_direction',
}
CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({
cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True),
cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960),
cv.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_MOVING): binary_sensor.device_class,
})
def to_code(config):
hub = yield cg.get_variable(config[CONF_APDS9960_ID])
var = yield binary_sensor.new_binary_sensor(config)
func = getattr(hub, DIRECTIONS[config[CONF_DIRECTION]])
cg.add(func(var))

View file

@ -0,0 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import CONF_TYPE, UNIT_PERCENT, ICON_LIGHTBULB
from . import APDS9960, CONF_APDS9960_ID
DEPENDENCIES = ['apds9960']
TYPES = {
'CLEAR': 'set_clear_channel',
'RED': 'set_red_channel',
'GREEN': 'set_green_channel',
'BLUE': 'set_blue_channel',
'PROXIMITY': 'set_proximity',
}
CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PERCENT, ICON_LIGHTBULB, 1).extend({
cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True),
cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960),
})
def to_code(config):
hub = yield cg.get_variable(config[CONF_APDS9960_ID])
var = yield sensor.new_sensor(config)
func = getattr(hub, TYPES[config[CONF_TYPE]])
cg.add(func(var))

View file

@ -0,0 +1,118 @@
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_DATA, CONF_DATA_TEMPLATE, CONF_ID, CONF_PASSWORD, CONF_PORT, \
CONF_REBOOT_TIMEOUT, CONF_SERVICE, CONF_VARIABLES, CONF_SERVICES, CONF_TRIGGER_ID
from esphome.core import CORE, coroutine_with_priority
DEPENDENCIES = ['network']
api_ns = cg.esphome_ns.namespace('api')
APIServer = api_ns.class_('APIServer', cg.Component, cg.Controller)
HomeAssistantServiceCallAction = api_ns.class_('HomeAssistantServiceCallAction', automation.Action)
KeyValuePair = api_ns.class_('KeyValuePair')
TemplatableKeyValuePair = api_ns.class_('TemplatableKeyValuePair')
APIConnectedCondition = api_ns.class_('APIConnectedCondition', Condition)
UserService = api_ns.class_('UserService', automation.Trigger)
ServiceTypeArgument = api_ns.class_('ServiceTypeArgument')
ServiceArgType = api_ns.enum('ServiceArgType')
SERVICE_ARG_TYPES = {
'bool': ServiceArgType.SERVICE_ARG_TYPE_BOOL,
'int': ServiceArgType.SERVICE_ARG_TYPE_INT,
'float': ServiceArgType.SERVICE_ARG_TYPE_FLOAT,
'string': ServiceArgType.SERVICE_ARG_TYPE_STRING,
}
SERVICE_ARG_NATIVE_TYPES = {
'bool': bool,
'int': cg.int32,
'float': float,
'string': cg.std_string,
}
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(APIServer),
cv.Optional(CONF_PORT, default=6053): cv.port,
cv.Optional(CONF_PASSWORD, default=''): cv.string_strict,
cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds,
cv.Optional(CONF_SERVICES): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserService),
cv.Required(CONF_SERVICE): cv.valid_name,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema({
cv.validate_id_name: cv.one_of(*SERVICE_ARG_TYPES, lower=True),
}),
}),
}).extend(cv.COMPONENT_SCHEMA)
@coroutine_with_priority(40.0)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
for conf in config.get(CONF_SERVICES, []):
template_args = []
func_args = []
service_type_args = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
template_args.append(native)
func_args.append((native, name))
service_type_args.append(ServiceTypeArgument(name, SERVICE_ARG_TYPES[var_]))
templ = cg.TemplateArguments(*template_args)
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], templ,
conf[CONF_SERVICE], service_type_args)
cg.add(var.register_user_service(trigger))
yield automation.build_automation(trigger, func_args, conf)
cg.add_define('USE_API')
if CORE.is_esp32:
cg.add_library('AsyncTCP', '1.0.3')
elif CORE.is_esp8266:
cg.add_library('ESPAsyncTCP', '1.2.0')
HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema({
cv.GenerateID(): cv.use_id(APIServer),
cv.Required(CONF_SERVICE): cv.string,
cv.Optional(CONF_DATA): cv.Schema({
cv.string: cv.string,
}),
cv.Optional(CONF_DATA_TEMPLATE): cv.Schema({
cv.string: cv.string,
}),
cv.Optional(CONF_VARIABLES): cv.Schema({
cv.string: cv.returning_lambda,
}),
})
@automation.register_action('homeassistant.service', HomeAssistantServiceCallAction,
HOMEASSISTANT_SERVICE_ACTION_SCHEMA)
def homeassistant_service_to_code(config, action_id, template_arg, args):
serv = yield cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv)
cg.add(var.set_service(config[CONF_SERVICE]))
if CONF_DATA in config:
datas = [KeyValuePair(k, v) for k, v in config[CONF_DATA].items()]
cg.add(var.set_data(datas))
if CONF_DATA_TEMPLATE in config:
datas = [KeyValuePair(k, v) for k, v in config[CONF_DATA_TEMPLATE].items()]
cg.add(var.set_data_template(datas))
if CONF_VARIABLES in config:
datas = []
for key, value in config[CONF_VARIABLES].items():
value_ = yield cg.process_lambda(value, [])
datas.append(TemplatableKeyValuePair(key, value_))
cg.add(var.set_variables(datas))
yield var
@automation.register_condition('api.connected', APIConnectedCondition, {})
def api_connected_to_code(config, condition_id, template_arg, args):
yield cg.new_Pvariable(condition_id, template_arg)

View file

@ -0,0 +1,506 @@
syntax = "proto3";
// ==================== BASE PACKETS ====================
// The Home Assistant protocol is structured as a simple
// TCP socket with short binary messages encoded in the protocol buffers format
// First, a message in this protocol has a specific format:
// * VarInt denoting the size of the message object. (type is not part of this)
// * VarInt denoting the type of message.
// * The message object encoded as a ProtoBuf message
// The connection is established in 4 steps:
// * First, the client connects to the server and sends a "Hello Request" identifying itself
// * The server responds with a "Hello Response" and selects the protocol version
// * After receiving this message, the client attempts to authenticate itself using
// the password and a "Connect Request"
// * The server responds with a "Connect Response" and notifies of invalid password.
// If anything in this initial process fails, the connection must immediately closed
// by both sides and _no_ disconnection message is to be sent.
// Message sent at the beginning of each connection
// Can only be sent by the client and only at the beginning of the connection
// ID: 1
message HelloRequest {
// Description of client (like User Agent)
// For example "Home Assistant"
// Not strictly necessary to send but nice for debugging
// purposes.
string client_info = 1;
}
// Confirmation of successful connection request.
// Can only be sent by the server and only at the beginning of the connection
// ID: 2
message HelloResponse {
// The version of the API to use. The _client_ (for example Home Assistant) needs to check
// for compatibility and if necessary adopt to an older API.
// Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_
// Minor is for breaking changes in individual messages - a mismatch will lead to a warning message
uint32 api_version_major = 1;
uint32 api_version_minor = 2;
// A string identifying the server (ESP); like client info this may be empty
// and only exists for debugging/logging purposes.
// For example "ESPHome v1.10.0 on ESP8266"
string server_info = 3;
}
// Message sent at the beginning of each connection to authenticate the client
// Can only be sent by the client and only at the beginning of the connection
// ID: 3
message ConnectRequest {
// The password to log in with
string password = 1;
}
// Confirmation of successful connection. After this the connection is available for all traffic.
// Can only be sent by the server and only at the beginning of the connection
// ID: 4
message ConnectResponse {
bool invalid_password = 1;
}
// Request to close the connection.
// Can be sent by both the client and server
// ID: 5
message DisconnectRequest {
// Do not close the connection before the acknowledgement arrives
}
// ID: 6
message DisconnectResponse {
// Empty - Both parties are required to close the connection after this
// message has been received.
}
// ID: 7
message PingRequest {
// Empty
}
// ID: 8
message PingResponse {
// Empty
}
// ID: 9
message DeviceInfoRequest {
// Empty
}
// ID: 10
message DeviceInfoResponse {
bool uses_password = 1;
// The name of the node, given by "App.set_name()"
string name = 2;
// The mac address of the device. For example "AC:BC:32:89:0E:A9"
string mac_address = 3;
// A string describing the ESPHome version. For example "1.10.0"
string esphome_core_version = 4;
// A string describing the date of compilation, this is generated by the compiler
// and therefore may not be in the same format all the time.
// If the user isn't using ESPHome, this will also not be set.
string compilation_time = 5;
// The model of the board. For example NodeMCU
string model = 6;
bool has_deep_sleep = 7;
}
// ID: 11
message ListEntitiesRequest {
// Empty
}
// ID: 19
message ListEntitiesDoneResponse {
// Empty
}
// ID: 20
message SubscribeStatesRequest {
// Empty
}
// ==================== BINARY SENSOR ====================
// ID: 12
message ListEntitiesBinarySensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string device_class = 5;
bool is_status_binary_sensor = 6;
}
// ID: 21
message BinarySensorStateResponse {
fixed32 key = 1;
bool state = 2;
}
// ==================== COVER ====================
// ID: 13
message ListEntitiesCoverResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
bool assumed_state = 5;
bool supports_position = 6;
bool supports_tilt = 7;
string device_class = 8;
}
// ID: 22
message CoverStateResponse {
fixed32 key = 1;
// legacy: state has been removed in 1.13
// clients/servers must still send/accept it until the next protocol change
enum LegacyCoverState {
OPEN = 0;
CLOSED = 1;
}
LegacyCoverState legacy_state = 2;
float position = 3;
float tilt = 4;
enum CoverOperation {
IDLE = 0;
IS_OPENING = 1;
IS_CLOSING = 2;
}
CoverOperation current_operation = 5;
}
// ID: 30
message CoverCommandRequest {
fixed32 key = 1;
// legacy: command has been removed in 1.13
// clients/servers must still send/accept it until the next protocol change
enum LegacyCoverCommand {
OPEN = 0;
CLOSE = 1;
STOP = 2;
}
bool has_legacy_command = 2;
LegacyCoverCommand legacy_command = 3;
bool has_position = 4;
float position = 5;
bool has_tilt = 6;
float tilt = 7;
bool stop = 8;
}
// ==================== FAN ====================
// ID: 14
message ListEntitiesFanResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
bool supports_oscillation = 5;
bool supports_speed = 6;
}
enum FanSpeed {
LOW = 0;
MEDIUM = 1;
HIGH = 2;
}
// ID: 23
message FanStateResponse {
fixed32 key = 1;
bool state = 2;
bool oscillating = 3;
FanSpeed speed = 4;
}
// ID: 31
message FanCommandRequest {
fixed32 key = 1;
bool has_state = 2;
bool state = 3;
bool has_speed = 4;
FanSpeed speed = 5;
bool has_oscillating = 6;
bool oscillating = 7;
}
// ==================== LIGHT ====================
// ID: 15
message ListEntitiesLightResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
bool supports_brightness = 5;
bool supports_rgb = 6;
bool supports_white_value = 7;
bool supports_color_temperature = 8;
float min_mireds = 9;
float max_mireds = 10;
repeated string effects = 11;
}
// ID: 24
message LightStateResponse {
fixed32 key = 1;
bool state = 2;
float brightness = 3;
float red = 4;
float green = 5;
float blue = 6;
float white = 7;
float color_temperature = 8;
string effect = 9;
}
// ID: 32
message LightCommandRequest {
fixed32 key = 1;
bool has_state = 2;
bool state = 3;
bool has_brightness = 4;
float brightness = 5;
bool has_rgb = 6;
float red = 7;
float green = 8;
float blue = 9;
bool has_white = 10;
float white = 11;
bool has_color_temperature = 12;
float color_temperature = 13;
bool has_transition_length = 14;
uint32 transition_length = 15;
bool has_flash_length = 16;
uint32 flash_length = 17;
bool has_effect = 18;
string effect = 19;
}
// ==================== SENSOR ====================
// ID: 16
message ListEntitiesSensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
string unit_of_measurement = 6;
int32 accuracy_decimals = 7;
}
// ID: 25
message SensorStateResponse {
fixed32 key = 1;
float state = 2;
}
// ==================== SWITCH ====================
// ID: 17
message ListEntitiesSwitchResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool assumed_state = 6;
}
// ID: 26
message SwitchStateResponse {
fixed32 key = 1;
bool state = 2;
}
// ID: 33
message SwitchCommandRequest {
fixed32 key = 1;
bool state = 2;
}
// ==================== TEXT SENSOR ====================
// ID: 18
message ListEntitiesTextSensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
}
// ID: 27
message TextSensorStateResponse {
fixed32 key = 1;
string state = 2;
}
// ==================== SUBSCRIBE LOGS ====================
enum LogLevel {
NONE = 0;
ERROR = 1;
WARN = 2;
INFO = 3;
DEBUG = 4;
VERBOSE = 5;
VERY_VERBOSE = 6;
}
// ID: 28
message SubscribeLogsRequest {
LogLevel level = 1;
bool dump_config = 2;
}
// ID: 29
message SubscribeLogsResponse {
LogLevel level = 1;
string tag = 2;
string message = 3;
bool send_failed = 4;
}
// ==================== HOMEASSISTANT.SERVICE ====================
// ID: 34
message SubscribeServiceCallsRequest {
}
// ID: 35
message ServiceCallResponse {
string service = 1;
map<string, string> data = 2;
map<string, string> data_template = 3;
map<string, string> variables = 4;
}
// ==================== IMPORT HOME ASSISTANT STATES ====================
// 1. Client sends SubscribeHomeAssistantStatesRequest
// 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async)
// 3. Client sends HomeAssistantStateResponse for state changes.
// ID: 38
message SubscribeHomeAssistantStatesRequest {
}
// ID: 39
message SubscribeHomeAssistantStateResponse {
string entity_id = 1;
}
// ID: 40
message HomeAssistantStateResponse {
string entity_id = 1;
string state = 2;
}
// ==================== IMPORT TIME ====================
// ID: 36
message GetTimeRequest {
}
// ID: 37
message GetTimeResponse {
fixed32 epoch_seconds = 1;
}
// ==================== USER-DEFINES SERVICES ====================
message ListEntitiesServicesArgument {
string name = 1;
enum Type {
BOOL = 0;
INT = 1;
FLOAT = 2;
STRING = 3;
}
Type type = 2;
}
// ID: 41
message ListEntitiesServicesResponse {
string name = 1;
fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3;
}
message ExecuteServiceArgument {
bool bool_ = 1;
int32 int_ = 2;
float float_ = 3;
string string_ = 4;
}
// ID: 42
message ExecuteServiceRequest {
fixed32 key = 1;
repeated ExecuteServiceArgument args = 2;
}
// ==================== CAMERA ====================
// ID: 43
message ListEntitiesCameraResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
}
// ID: 44
message CameraImageResponse {
fixed32 key = 1;
bytes data = 2;
bool done = 3;
}
// ID: 45
message CameraImageRequest {
bool single = 1;
bool stream = 2;
}
// ==================== CLIMATE ====================
enum ClimateMode {
OFF = 0;
AUTO = 1;
COOL = 2;
HEAT = 3;
}
// ID: 46
message ListEntitiesClimateResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
bool supports_current_temperature = 5;
bool supports_two_point_target_temperature = 6;
repeated ClimateMode supported_modes = 7;
float visual_min_temperature = 8;
float visual_max_temperature = 9;
float visual_temperature_step = 10;
bool supports_away = 11;
}
// ID: 47
message ClimateStateResponse {
fixed32 key = 1;
ClimateMode mode = 2;
float current_temperature = 3;
float target_temperature = 4;
float target_temperature_low = 5;
float target_temperature_high = 6;
bool away = 7;
}
// ID: 48
message ClimateCommandRequest {
fixed32 key = 1;
bool has_mode = 2;
ClimateMode mode = 3;
bool has_target_temperature = 4;
float target_temperature = 5;
bool has_target_temperature_low = 6;
float target_temperature_low = 7;
bool has_target_temperature_high = 8;
float target_temperature_high = 9;
bool has_away = 10;
bool away = 11;
}

View file

@ -0,0 +1,87 @@
#include "api_message.h"
#include "esphome/core/log.h"
namespace esphome {
namespace api {
static const char *TAG = "api.message";
bool APIMessage::decode_varint(uint32_t field_id, uint32_t value) { return false; }
bool APIMessage::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) { return false; }
bool APIMessage::decode_32bit(uint32_t field_id, uint32_t value) { return false; }
void APIMessage::encode(APIBuffer &buffer) {}
void APIMessage::decode(const uint8_t *buffer, size_t length) {
uint32_t i = 0;
bool error = false;
while (i < length) {
uint32_t consumed;
auto res = proto_decode_varuint32(&buffer[i], length - i, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid field start at %u", i);
break;
}
uint32_t field_type = (*res) & 0b111;
uint32_t field_id = (*res) >> 3;
i += consumed;
switch (field_type) {
case 0: { // VarInt
res = proto_decode_varuint32(&buffer[i], length - i, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at %u", i);
error = true;
break;
}
if (!this->decode_varint(field_id, *res)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %u with value %u!", field_id, *res);
}
i += consumed;
break;
}
case 2: { // Length-delimited
res = proto_decode_varuint32(&buffer[i], length - i, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at %u", i);
error = true;
break;
}
i += consumed;
if (*res > length - i) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %u", i);
error = true;
break;
}
if (!this->decode_length_delimited(field_id, &buffer[i], *res)) {
ESP_LOGV(TAG, "Cannot decode Length Delimited field %u!", field_id);
}
i += *res;
break;
}
case 5: { // 32-bit
if (length - i < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %u", i);
error = true;
break;
}
uint32_t val = (uint32_t(buffer[i]) << 0) | (uint32_t(buffer[i + 1]) << 8) | (uint32_t(buffer[i + 2]) << 16) |
(uint32_t(buffer[i + 3]) << 24);
if (!this->decode_32bit(field_id, val)) {
ESP_LOGV(TAG, "Cannot decode 32-bit field %u with value %u!", field_id, val);
}
i += 4;
break;
}
default:
ESP_LOGV(TAG, "Invalid field type at %u", i);
error = true;
break;
}
if (error) {
break;
}
}
}
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,79 @@
#pragma once
#include "esphome/core/component.h"
#include "util.h"
namespace esphome {
namespace api {
enum class APIMessageType {
HELLO_REQUEST = 1,
HELLO_RESPONSE = 2,
CONNECT_REQUEST = 3,
CONNECT_RESPONSE = 4,
DISCONNECT_REQUEST = 5,
DISCONNECT_RESPONSE = 6,
PING_REQUEST = 7,
PING_RESPONSE = 8,
DEVICE_INFO_REQUEST = 9,
DEVICE_INFO_RESPONSE = 10,
LIST_ENTITIES_REQUEST = 11,
LIST_ENTITIES_BINARY_SENSOR_RESPONSE = 12,
LIST_ENTITIES_COVER_RESPONSE = 13,
LIST_ENTITIES_FAN_RESPONSE = 14,
LIST_ENTITIES_LIGHT_RESPONSE = 15,
LIST_ENTITIES_SENSOR_RESPONSE = 16,
LIST_ENTITIES_SWITCH_RESPONSE = 17,
LIST_ENTITIES_TEXT_SENSOR_RESPONSE = 18,
LIST_ENTITIES_SERVICE_RESPONSE = 41,
LIST_ENTITIES_CAMERA_RESPONSE = 43,
LIST_ENTITIES_CLIMATE_RESPONSE = 46,
LIST_ENTITIES_DONE_RESPONSE = 19,
SUBSCRIBE_STATES_REQUEST = 20,
BINARY_SENSOR_STATE_RESPONSE = 21,
COVER_STATE_RESPONSE = 22,
FAN_STATE_RESPONSE = 23,
LIGHT_STATE_RESPONSE = 24,
SENSOR_STATE_RESPONSE = 25,
SWITCH_STATE_RESPONSE = 26,
TEXT_SENSOR_STATE_RESPONSE = 27,
CAMERA_IMAGE_RESPONSE = 44,
CLIMATE_STATE_RESPONSE = 47,
SUBSCRIBE_LOGS_REQUEST = 28,
SUBSCRIBE_LOGS_RESPONSE = 29,
COVER_COMMAND_REQUEST = 30,
FAN_COMMAND_REQUEST = 31,
LIGHT_COMMAND_REQUEST = 32,
SWITCH_COMMAND_REQUEST = 33,
CAMERA_IMAGE_REQUEST = 45,
CLIMATE_COMMAND_REQUEST = 48,
SUBSCRIBE_SERVICE_CALLS_REQUEST = 34,
SERVICE_CALL_RESPONSE = 35,
GET_TIME_REQUEST = 36,
GET_TIME_RESPONSE = 37,
SUBSCRIBE_HOME_ASSISTANT_STATES_REQUEST = 38,
SUBSCRIBE_HOME_ASSISTANT_STATE_RESPONSE = 39,
HOME_ASSISTANT_STATE_RESPONSE = 40,
EXECUTE_SERVICE_REQUEST = 42,
};
class APIMessage {
public:
void decode(const uint8_t *buffer, size_t length);
virtual bool decode_varint(uint32_t field_id, uint32_t value);
virtual bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len);
virtual bool decode_32bit(uint32_t field_id, uint32_t value);
virtual APIMessageType message_type() const = 0;
virtual void encode(APIBuffer &buffer);
};
} // namespace api
} // namespace esphome

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,242 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include "util.h"
#include "api_message.h"
#include "basic_messages.h"
#include "list_entities.h"
#include "subscribe_state.h"
#include "subscribe_logs.h"
#include "command_messages.h"
#include "service_call_message.h"
#include "user_services.h"
#ifdef ARDUINO_ARCH_ESP32
#include <AsyncTCP.h>
#endif
#ifdef ARDUINO_ARCH_ESP8266
#include <ESPAsyncTCP.h>
#endif
namespace esphome {
namespace api {
class APIServer;
class APIConnection {
public:
APIConnection(AsyncClient *client, APIServer *parent);
~APIConnection();
void disconnect_client();
APIBuffer get_buffer();
bool send_buffer(APIMessageType type);
bool send_message(APIMessage &msg);
bool send_empty_message(APIMessageType type);
void loop();
#ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
#endif
#ifdef USE_COVER
bool send_cover_state(cover::Cover *cover);
#endif
#ifdef USE_FAN
bool send_fan_state(fan::FanState *fan);
#endif
#ifdef USE_LIGHT
bool send_light_state(light::LightState *light);
#endif
#ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor, float state);
#endif
#ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch, bool state);
#endif
#ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
#endif
#ifdef USE_ESP32_CAMERA
void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
#endif
bool send_log_message(int level, const char *tag, const char *line);
bool send_disconnect_request();
bool send_ping_request();
void send_service_call(ServiceCallResponse &call);
#ifdef USE_HOMEASSISTANT_TIME
void send_time_request();
#endif
protected:
friend APIServer;
void on_error_(int8_t error);
void on_disconnect_();
void on_timeout_(uint32_t time);
void on_data_(uint8_t *buf, size_t len);
void fatal_error_();
bool valid_rx_message_type_(uint32_t msg_type);
void read_message_(uint32_t size, uint32_t type, uint8_t *msg);
void parse_recv_buffer_();
// request types
void on_hello_request_(const HelloRequest &req);
void on_connect_request_(const ConnectRequest &req);
void on_disconnect_request_(const DisconnectRequest &req);
void on_disconnect_response_(const DisconnectResponse &req);
void on_ping_request_(const PingRequest &req);
void on_ping_response_(const PingResponse &req);
void on_device_info_request_(const DeviceInfoRequest &req);
void on_list_entities_request_(const ListEntitiesRequest &req);
void on_subscribe_states_request_(const SubscribeStatesRequest &req);
void on_subscribe_logs_request_(const SubscribeLogsRequest &req);
#ifdef USE_COVER
void on_cover_command_request_(const CoverCommandRequest &req);
#endif
#ifdef USE_FAN
void on_fan_command_request_(const FanCommandRequest &req);
#endif
#ifdef USE_LIGHT
void on_light_command_request_(const LightCommandRequest &req);
#endif
#ifdef USE_SWITCH
void on_switch_command_request_(const SwitchCommandRequest &req);
#endif
#ifdef USE_CLIMATE
void on_climate_command_request_(const ClimateCommandRequest &req);
#endif
void on_subscribe_service_calls_request_(const SubscribeServiceCallsRequest &req);
void on_subscribe_home_assistant_states_request_(const SubscribeHomeAssistantStatesRequest &req);
void on_home_assistant_state_response_(const HomeAssistantStateResponse &req);
void on_execute_service_(const ExecuteServiceRequest &req);
#ifdef USE_ESP32_CAMERA
void on_camera_image_request_(const CameraImageRequest &req);
#endif
enum class ConnectionState {
WAITING_FOR_HELLO,
WAITING_FOR_CONNECT,
CONNECTED,
} connection_state_{ConnectionState::WAITING_FOR_HELLO};
bool remove_{false};
std::vector<uint8_t> send_buffer_;
std::vector<uint8_t> recv_buffer_;
std::string client_info_;
#ifdef USE_ESP32_CAMERA
esp32_camera::CameraImageReader image_reader_;
#endif
bool state_subscription_{false};
int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
uint32_t last_traffic_;
bool sent_ping_{false};
bool service_call_subscription_{false};
AsyncClient *client_;
APIServer *parent_;
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
};
template<typename... Ts> class HomeAssistantServiceCallAction;
class APIServer : public Component, public Controller {
public:
APIServer();
void setup() override;
uint16_t get_port() const;
float get_setup_priority() const override;
void loop() override;
void dump_config() override;
void on_shutdown() override;
bool check_password(const std::string &password) const;
bool uses_password() const;
void set_port(uint16_t port);
void set_password(const std::string &password);
void set_reboot_timeout(uint32_t reboot_timeout);
void handle_disconnect(APIConnection *conn);
#ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
#endif
#ifdef USE_COVER
void on_cover_update(cover::Cover *obj) override;
#endif
#ifdef USE_FAN
void on_fan_update(fan::FanState *obj) override;
#endif
#ifdef USE_LIGHT
void on_light_update(light::LightState *obj) override;
#endif
#ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj, float state) override;
#endif
#ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj, bool state) override;
#endif
#ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj, std::string state) override;
#endif
#ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override;
#endif
void send_service_call(ServiceCallResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#ifdef USE_HOMEASSISTANT_TIME
void request_time();
#endif
bool is_connected() const;
struct HomeAssistantStateSubscription {
std::string entity_id;
std::function<void(std::string)> callback;
};
void subscribe_home_assistant_state(std::string entity_id, std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
protected:
AsyncServer server_{0};
uint16_t port_{6053};
uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
std::vector<APIConnection *> clients_;
std::string password_;
std::vector<HomeAssistantStateSubscription> state_subs_;
std::vector<UserServiceDescriptor *> user_services_;
};
extern APIServer *global_api_server;
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
public:
explicit HomeAssistantServiceCallAction(APIServer *parent) : parent_(parent) {}
void set_service(const std::string &service) { this->resp_.set_service(service); }
void set_data(const std::vector<KeyValuePair> &data) { this->resp_.set_data(data); }
void set_data_template(const std::vector<KeyValuePair> &data_template) {
this->resp_.set_data_template(data_template);
}
void set_variables(const std::vector<TemplatableKeyValuePair> &variables) { this->resp_.set_variables(variables); }
void play(Ts... x) override { this->parent_->send_service_call(this->resp_); }
protected:
APIServer *parent_;
ServiceCallResponse resp_;
};
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
public:
bool check(Ts... x) override { return global_api_server->is_connected(); }
};
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,57 @@
#include "basic_messages.h"
#include "esphome/core/log.h"
namespace esphome {
namespace api {
// Hello
bool HelloRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
switch (field_id) {
case 1: // string client_info = 1;
this->client_info_ = as_string(value, len);
return true;
default:
return false;
}
}
const std::string &HelloRequest::get_client_info() const { return this->client_info_; }
void HelloRequest::set_client_info(const std::string &client_info) { this->client_info_ = client_info; }
APIMessageType HelloRequest::message_type() const { return APIMessageType::HELLO_REQUEST; }
// Connect
bool ConnectRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
switch (field_id) {
case 1: // string password = 1;
this->password_ = as_string(value, len);
return true;
default:
return false;
}
}
const std::string &ConnectRequest::get_password() const { return this->password_; }
void ConnectRequest::set_password(const std::string &password) { this->password_ = password; }
APIMessageType ConnectRequest::message_type() const { return APIMessageType::CONNECT_REQUEST; }
APIMessageType DeviceInfoRequest::message_type() const { return APIMessageType::DEVICE_INFO_REQUEST; }
APIMessageType DisconnectRequest::message_type() const { return APIMessageType::DISCONNECT_REQUEST; }
bool DisconnectRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
switch (field_id) {
case 1: // string reason = 1;
this->reason_ = as_string(value, len);
return true;
default:
return false;
}
}
const std::string &DisconnectRequest::get_reason() const { return this->reason_; }
void DisconnectRequest::set_reason(const std::string &reason) { this->reason_ = reason; }
void DisconnectRequest::encode(APIBuffer &buffer) {
// string reason = 1;
buffer.encode_string(1, this->reason_);
}
APIMessageType DisconnectResponse::message_type() const { return APIMessageType::DISCONNECT_RESPONSE; }
APIMessageType PingRequest::message_type() const { return APIMessageType::PING_REQUEST; }
APIMessageType PingResponse::message_type() const { return APIMessageType::PING_RESPONSE; }
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,63 @@
#pragma once
#include "api_message.h"
namespace esphome {
namespace api {
class HelloRequest : public APIMessage {
public:
bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
const std::string &get_client_info() const;
void set_client_info(const std::string &client_info);
APIMessageType message_type() const override;
protected:
std::string client_info_;
};
class ConnectRequest : public APIMessage {
public:
bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
const std::string &get_password() const;
void set_password(const std::string &password);
APIMessageType message_type() const override;
protected:
std::string password_;
};
class DeviceInfoRequest : public APIMessage {
public:
APIMessageType message_type() const override;
};
class DisconnectRequest : public APIMessage {
public:
bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
void encode(APIBuffer &buffer) override;
APIMessageType message_type() const override;
const std::string &get_reason() const;
void set_reason(const std::string &reason);
protected:
std::string reason_;
};
class DisconnectResponse : public APIMessage {
public:
APIMessageType message_type() const override;
};
class PingRequest : public APIMessage {
public:
APIMessageType message_type() const override;
};
class PingResponse : public APIMessage {
public:
APIMessageType message_type() const override;
};
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,417 @@
#include "command_messages.h"
#include "esphome/core/log.h"
namespace esphome {
namespace api {
#ifdef USE_COVER
bool CoverCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 2:
// bool has_legacy_command = 2;
this->has_legacy_command_ = value;
return true;
case 3:
// enum LegacyCoverCommand {
// OPEN = 0;
// CLOSE = 1;
// STOP = 2;
// }
// LegacyCoverCommand legacy_command_ = 3;
this->legacy_command_ = static_cast<LegacyCoverCommand>(value);
return true;
case 4:
// bool has_position = 4;
this->has_position_ = value;
return true;
case 6:
// bool has_tilt = 6;
this->has_tilt_ = value;
return true;
case 8:
// bool stop = 8;
this->stop_ = value;
default:
return false;
}
}
bool CoverCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1:
// fixed32 key = 1;
this->key_ = value;
return true;
case 5:
// float position = 5;
this->position_ = as_float(value);
return true;
case 7:
// float tilt = 7;
this->tilt_ = as_float(value);
return true;
default:
return false;
}
}
APIMessageType CoverCommandRequest::message_type() const { return APIMessageType ::COVER_COMMAND_REQUEST; }
uint32_t CoverCommandRequest::get_key() const { return this->key_; }
optional<LegacyCoverCommand> CoverCommandRequest::get_legacy_command() const {
if (!this->has_legacy_command_)
return {};
return this->legacy_command_;
}
optional<float> CoverCommandRequest::get_position() const {
if (!this->has_position_)
return {};
return this->position_;
}
optional<float> CoverCommandRequest::get_tilt() const {
if (!this->has_tilt_)
return {};
return this->tilt_;
}
#endif
#ifdef USE_FAN
bool FanCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 2:
// bool has_state = 2;
this->has_state_ = value;
return true;
case 3:
// bool state = 3;
this->state_ = value;
return true;
case 4:
// bool has_speed = 4;
this->has_speed_ = value;
return true;
case 5:
// FanSpeed speed = 5;
this->speed_ = static_cast<fan::FanSpeed>(value);
return true;
case 6:
// bool has_oscillating = 6;
this->has_oscillating_ = value;
return true;
case 7:
// bool oscillating = 7;
this->oscillating_ = value;
return true;
default:
return false;
}
}
bool FanCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1:
// fixed32 key = 1;
this->key_ = value;
return true;
default:
return false;
}
}
APIMessageType FanCommandRequest::message_type() const { return APIMessageType::FAN_COMMAND_REQUEST; }
uint32_t FanCommandRequest::get_key() const { return this->key_; }
optional<bool> FanCommandRequest::get_state() const {
if (!this->has_state_)
return {};
return this->state_;
}
optional<fan::FanSpeed> FanCommandRequest::get_speed() const {
if (!this->has_speed_)
return {};
return this->speed_;
}
optional<bool> FanCommandRequest::get_oscillating() const {
if (!this->has_oscillating_)
return {};
return this->oscillating_;
}
#endif
#ifdef USE_LIGHT
bool LightCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 2:
// bool has_state = 2;
this->has_state_ = value;
return true;
case 3:
// bool state = 3;
this->state_ = value;
return true;
case 4:
// bool has_brightness = 4;
this->has_brightness_ = value;
return true;
case 6:
// bool has_rgb = 6;
this->has_rgb_ = value;
return true;
case 10:
// bool has_white = 10;
this->has_white_ = value;
return true;
case 12:
// bool has_color_temperature = 12;
this->has_color_temperature_ = value;
return true;
case 14:
// bool has_transition_length = 14;
this->has_transition_length_ = value;
return true;
case 15:
// uint32 transition_length = 15;
this->transition_length_ = value;
return true;
case 16:
// bool has_flash_length = 16;
this->has_flash_length_ = value;
return true;
case 17:
// uint32 flash_length = 17;
this->flash_length_ = value;
return true;
case 18:
// bool has_effect = 18;
this->has_effect_ = value;
return true;
default:
return false;
}
}
bool LightCommandRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
switch (field_id) {
case 19:
// string effect = 19;
this->effect_ = as_string(value, len);
return true;
default:
return false;
}
}
bool LightCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1:
// fixed32 key = 1;
this->key_ = value;
return true;
case 5:
// float brightness = 5;
this->brightness_ = as_float(value);
return true;
case 7:
// float red = 7;
this->red_ = as_float(value);
return true;
case 8:
// float green = 8;
this->green_ = as_float(value);
return true;
case 9:
// float blue = 9;
this->blue_ = as_float(value);
return true;
case 11:
// float white = 11;
this->white_ = as_float(value);
return true;
case 13:
// float color_temperature = 13;
this->color_temperature_ = as_float(value);
return true;
default:
return false;
}
}
APIMessageType LightCommandRequest::message_type() const { return APIMessageType::LIGHT_COMMAND_REQUEST; }
uint32_t LightCommandRequest::get_key() const { return this->key_; }
optional<bool> LightCommandRequest::get_state() const {
if (!this->has_state_)
return {};
return this->state_;
}
optional<float> LightCommandRequest::get_brightness() const {
if (!this->has_brightness_)
return {};
return this->brightness_;
}
optional<float> LightCommandRequest::get_red() const {
if (!this->has_rgb_)
return {};
return this->red_;
}
optional<float> LightCommandRequest::get_green() const {
if (!this->has_rgb_)
return {};
return this->green_;
}
optional<float> LightCommandRequest::get_blue() const {
if (!this->has_rgb_)
return {};
return this->blue_;
}
optional<float> LightCommandRequest::get_white() const {
if (!this->has_white_)
return {};
return this->white_;
}
optional<float> LightCommandRequest::get_color_temperature() const {
if (!this->has_color_temperature_)
return {};
return this->color_temperature_;
}
optional<uint32_t> LightCommandRequest::get_transition_length() const {
if (!this->has_transition_length_)
return {};
return this->transition_length_;
}
optional<uint32_t> LightCommandRequest::get_flash_length() const {
if (!this->has_flash_length_)
return {};
return this->flash_length_;
}
optional<std::string> LightCommandRequest::get_effect() const {
if (!this->has_effect_)
return {};
return this->effect_;
}
#endif
#ifdef USE_SWITCH
bool SwitchCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 2:
// bool state = 2;
this->state_ = value;
return true;
default:
return false;
}
}
bool SwitchCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1:
// fixed32 key = 1;
this->key_ = value;
return true;
default:
return false;
}
}
APIMessageType SwitchCommandRequest::message_type() const { return APIMessageType::SWITCH_COMMAND_REQUEST; }
uint32_t SwitchCommandRequest::get_key() const { return this->key_; }
bool SwitchCommandRequest::get_state() const { return this->state_; }
#endif
#ifdef USE_ESP32_CAMERA
bool CameraImageRequest::get_single() const { return this->single_; }
bool CameraImageRequest::get_stream() const { return this->stream_; }
bool CameraImageRequest::decode_varint(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1:
// bool single = 1;
this->single_ = value;
return true;
case 2:
// bool stream = 2;
this->stream_ = value;
return true;
default:
return false;
}
}
APIMessageType CameraImageRequest::message_type() const { return APIMessageType::CAMERA_IMAGE_REQUEST; }
#endif
#ifdef USE_CLIMATE
bool ClimateCommandRequest::decode_varint(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 2:
// bool has_mode = 2;
this->has_mode_ = value;
return true;
case 3:
// ClimateMode mode = 3;
this->mode_ = static_cast<climate::ClimateMode>(value);
return true;
case 4:
// bool has_target_temperature = 4;
this->has_target_temperature_ = value;
return true;
case 6:
// bool has_target_temperature_low = 6;
this->has_target_temperature_low_ = value;
return true;
case 8:
// bool has_target_temperature_high = 8;
this->has_target_temperature_high_ = value;
return true;
case 10:
// bool has_away = 10;
this->has_away_ = value;
return true;
case 11:
// bool away = 11;
this->away_ = value;
return true;
default:
return false;
}
}
bool ClimateCommandRequest::decode_32bit(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1:
// fixed32 key = 1;
this->key_ = value;
return true;
case 5:
// float target_temperature = 5;
this->target_temperature_ = as_float(value);
return true;
case 7:
// float target_temperature_low = 7;
this->target_temperature_low_ = as_float(value);
return true;
case 9:
// float target_temperature_high = 9;
this->target_temperature_high_ = as_float(value);
return true;
default:
return false;
}
}
APIMessageType ClimateCommandRequest::message_type() const { return APIMessageType::CLIMATE_COMMAND_REQUEST; }
uint32_t ClimateCommandRequest::get_key() const { return this->key_; }
optional<climate::ClimateMode> ClimateCommandRequest::get_mode() const {
if (!this->has_mode_)
return {};
return this->mode_;
}
optional<float> ClimateCommandRequest::get_target_temperature() const {
if (!this->has_target_temperature_)
return {};
return this->target_temperature_;
}
optional<float> ClimateCommandRequest::get_target_temperature_low() const {
if (!this->has_target_temperature_low_)
return {};
return this->target_temperature_low_;
}
optional<float> ClimateCommandRequest::get_target_temperature_high() const {
if (!this->has_target_temperature_high_)
return {};
return this->target_temperature_high_;
}
optional<bool> ClimateCommandRequest::get_away() const {
if (!this->has_away_)
return {};
return this->away_;
}
#endif
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,162 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "api_message.h"
namespace esphome {
namespace api {
#ifdef USE_COVER
enum LegacyCoverCommand {
LEGACY_COVER_COMMAND_OPEN = 0,
LEGACY_COVER_COMMAND_CLOSE = 1,
LEGACY_COVER_COMMAND_STOP = 2,
};
class CoverCommandRequest : public APIMessage {
public:
bool decode_varint(uint32_t field_id, uint32_t value) override;
bool decode_32bit(uint32_t field_id, uint32_t value) override;
APIMessageType message_type() const override;
uint32_t get_key() const;
optional<LegacyCoverCommand> get_legacy_command() const;
optional<float> get_position() const;
optional<float> get_tilt() const;
bool get_stop() const { return this->stop_; }
protected:
uint32_t key_{0};
bool has_legacy_command_{false};
LegacyCoverCommand legacy_command_{LEGACY_COVER_COMMAND_OPEN};
bool has_position_{false};
float position_{0.0f};
bool has_tilt_{false};
float tilt_{0.0f};
bool stop_{false};
};
#endif
#ifdef USE_FAN
class FanCommandRequest : public APIMessage {
public:
bool decode_varint(uint32_t field_id, uint32_t value) override;
bool decode_32bit(uint32_t field_id, uint32_t value) override;
APIMessageType message_type() const override;
uint32_t get_key() const;
optional<bool> get_state() const;
optional<fan::FanSpeed> get_speed() const;
optional<bool> get_oscillating() const;
protected:
uint32_t key_{0};
bool has_state_{false};
bool state_{false};
bool has_speed_{false};
fan::FanSpeed speed_{fan::FAN_SPEED_LOW};
bool has_oscillating_{false};
bool oscillating_{false};
};
#endif
#ifdef USE_LIGHT
class LightCommandRequest : public APIMessage {
public:
bool decode_varint(uint32_t field_id, uint32_t value) override;
bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
bool decode_32bit(uint32_t field_id, uint32_t value) override;
APIMessageType message_type() const override;
uint32_t get_key() const;
optional<bool> get_state() const;
optional<float> get_brightness() const;
optional<float> get_red() const;
optional<float> get_green() const;
optional<float> get_blue() const;
optional<float> get_white() const;
optional<float> get_color_temperature() const;
optional<uint32_t> get_transition_length() const;
optional<uint32_t> get_flash_length() const;
optional<std::string> get_effect() const;
protected:
uint32_t key_{0};
bool has_state_{false};
bool state_{false};
bool has_brightness_{false};
float brightness_{0.0f};
bool has_rgb_{false};
float red_{0.0f};
float green_{0.0f};
float blue_{0.0f};
bool has_white_{false};
float white_{0.0f};
bool has_color_temperature_{false};
float color_temperature_{0.0f};
bool has_transition_length_{false};
uint32_t transition_length_{0};
bool has_flash_length_{false};
uint32_t flash_length_{0};
bool has_effect_{false};
std::string effect_{};
};
#endif
#ifdef USE_SWITCH
class SwitchCommandRequest : public APIMessage {
public:
bool decode_varint(uint32_t field_id, uint32_t value) override;
bool decode_32bit(uint32_t field_id, uint32_t value) override;
APIMessageType message_type() const override;
uint32_t get_key() const;
bool get_state() const;
protected:
uint32_t key_{0};
bool state_{false};
};
#endif
#ifdef USE_ESP32_CAMERA
class CameraImageRequest : public APIMessage {
public:
bool decode_varint(uint32_t field_id, uint32_t value) override;
bool get_single() const;
bool get_stream() const;
APIMessageType message_type() const override;
protected:
bool single_{false};
bool stream_{false};
};
#endif
#ifdef USE_CLIMATE
class ClimateCommandRequest : public APIMessage {
public:
bool decode_varint(uint32_t field_id, uint32_t value) override;
bool decode_32bit(uint32_t field_id, uint32_t value) override;
APIMessageType message_type() const override;
uint32_t get_key() const;
optional<climate::ClimateMode> get_mode() const;
optional<float> get_target_temperature() const;
optional<float> get_target_temperature_low() const;
optional<float> get_target_temperature_high() const;
optional<bool> get_away() const;
protected:
uint32_t key_{0};
bool has_mode_{false};
climate::ClimateMode mode_{climate::CLIMATE_MODE_OFF};
bool has_target_temperature_{false};
float target_temperature_{0.0f};
bool has_target_temperature_low_{false};
float target_temperature_low_{0.0f};
bool has_target_temperature_high_{false};
float target_temperature_high_{0.0f};
bool has_away_{false};
bool away_{false};
};
#endif
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,190 @@
#include "list_entities.h"
#include "esphome/core/util.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace api {
std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
return App.get_name() + component_type + nameable->get_object_id();
}
#ifdef USE_BINARY_SENSOR
bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(binary_sensor);
// string unique_id = 4;
buffer.encode_string(4, get_default_unique_id("binary_sensor", binary_sensor));
// string device_class = 5;
buffer.encode_string(5, binary_sensor->get_device_class());
// bool is_status_binary_sensor = 6;
buffer.encode_bool(6, binary_sensor->is_status_binary_sensor());
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_BINARY_SENSOR_RESPONSE);
}
#endif
#ifdef USE_COVER
bool ListEntitiesIterator::on_cover(cover::Cover *cover) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(cover);
// string unique_id = 4;
buffer.encode_string(4, get_default_unique_id("cover", cover));
auto traits = cover->get_traits();
// bool assumed_state = 5;
buffer.encode_bool(5, traits.get_is_assumed_state());
// bool supports_position = 6;
buffer.encode_bool(6, traits.get_supports_position());
// bool supports_tilt = 7;
buffer.encode_bool(7, traits.get_supports_tilt());
// string device_class = 8;
buffer.encode_string(8, cover->get_device_class());
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_COVER_RESPONSE);
}
#endif
#ifdef USE_FAN
bool ListEntitiesIterator::on_fan(fan::FanState *fan) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(fan);
// string unique_id = 4;
buffer.encode_string(4, get_default_unique_id("fan", fan));
// bool supports_oscillation = 5;
buffer.encode_bool(5, fan->get_traits().supports_oscillation());
// bool supports_speed = 6;
buffer.encode_bool(6, fan->get_traits().supports_speed());
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_FAN_RESPONSE);
}
#endif
#ifdef USE_LIGHT
bool ListEntitiesIterator::on_light(light::LightState *light) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(light);
// string unique_id = 4;
buffer.encode_string(4, get_default_unique_id("light", light));
// bool supports_brightness = 5;
auto traits = light->get_traits();
buffer.encode_bool(5, traits.get_supports_brightness());
// bool supports_rgb = 6;
buffer.encode_bool(6, traits.get_supports_rgb());
// bool supports_white_value = 7;
buffer.encode_bool(7, traits.get_supports_rgb_white_value());
// bool supports_color_temperature = 8;
buffer.encode_bool(8, traits.get_supports_color_temperature());
if (traits.get_supports_color_temperature()) {
// float min_mireds = 9;
buffer.encode_float(9, traits.get_min_mireds());
// float max_mireds = 10;
buffer.encode_float(10, traits.get_max_mireds());
}
// repeated string effects = 11;
if (light->supports_effects()) {
buffer.encode_string(11, "None");
for (auto *effect : light->get_effects()) {
buffer.encode_string(11, effect->get_name());
}
}
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_LIGHT_RESPONSE);
}
#endif
#ifdef USE_SENSOR
bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(sensor);
// string unique_id = 4;
std::string unique_id = sensor->unique_id();
if (unique_id.empty())
unique_id = get_default_unique_id("sensor", sensor);
buffer.encode_string(4, unique_id);
// string icon = 5;
buffer.encode_string(5, sensor->get_icon());
// string unit_of_measurement = 6;
buffer.encode_string(6, sensor->get_unit_of_measurement());
// int32 accuracy_decimals = 7;
buffer.encode_int32(7, sensor->get_accuracy_decimals());
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SENSOR_RESPONSE);
}
#endif
#ifdef USE_SWITCH
bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(a_switch);
// string unique_id = 4;
buffer.encode_string(4, get_default_unique_id("switch", a_switch));
// string icon = 5;
buffer.encode_string(5, a_switch->get_icon());
// bool assumed_state = 6;
buffer.encode_bool(6, a_switch->assumed_state());
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SWITCH_RESPONSE);
}
#endif
#ifdef USE_TEXT_SENSOR
bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(text_sensor);
// string unique_id = 4;
std::string unique_id = text_sensor->unique_id();
if (unique_id.empty())
unique_id = get_default_unique_id("text_sensor", text_sensor);
buffer.encode_string(4, unique_id);
// string icon = 5;
buffer.encode_string(5, text_sensor->get_icon());
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_TEXT_SENSOR_RESPONSE);
}
#endif
bool ListEntitiesIterator::on_end() {
return this->client_->send_empty_message(APIMessageType::LIST_ENTITIES_DONE_RESPONSE);
}
ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client)
: ComponentIterator(server), client_(client) {}
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto buffer = this->client_->get_buffer();
service->encode_list_service_response(buffer);
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_SERVICE_RESPONSE);
}
#ifdef USE_ESP32_CAMERA
bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(camera);
// string unique_id = 4;
buffer.encode_string(4, get_default_unique_id("camera", camera));
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_CAMERA_RESPONSE);
}
#endif
#ifdef USE_CLIMATE
bool ListEntitiesIterator::on_climate(climate::Climate *climate) {
auto buffer = this->client_->get_buffer();
buffer.encode_nameable(climate);
// string unique_id = 4;
buffer.encode_string(4, get_default_unique_id("climate", climate));
auto traits = climate->get_traits();
// bool supports_current_temperature = 5;
buffer.encode_bool(5, traits.get_supports_current_temperature());
// bool supports_two_point_target_temperature = 6;
buffer.encode_bool(6, traits.get_supports_two_point_target_temperature());
// repeated ClimateMode supported_modes = 7;
for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT}) {
if (traits.supports_mode(mode))
buffer.encode_uint32(7, mode, true);
}
// float visual_min_temperature = 8;
buffer.encode_float(8, traits.get_visual_min_temperature());
// float visual_max_temperature = 9;
buffer.encode_float(9, traits.get_visual_max_temperature());
// float visual_temperature_step = 10;
buffer.encode_float(10, traits.get_visual_temperature_step());
// bool supports_away = 11;
buffer.encode_bool(11, traits.get_supports_away());
return this->client_->send_buffer(APIMessageType::LIST_ENTITIES_CLIMATE_RESPONSE);
}
#endif
APIMessageType ListEntitiesRequest::message_type() const { return APIMessageType::LIST_ENTITIES_REQUEST; }
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,57 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "api_message.h"
namespace esphome {
namespace api {
class ListEntitiesRequest : public APIMessage {
public:
APIMessageType message_type() const override;
};
class APIConnection;
class ListEntitiesIterator : public ComponentIterator {
public:
ListEntitiesIterator(APIServer *server, APIConnection *client);
#ifdef USE_BINARY_SENSOR
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
#endif
#ifdef USE_COVER
bool on_cover(cover::Cover *cover) override;
#endif
#ifdef USE_FAN
bool on_fan(fan::FanState *fan) override;
#endif
#ifdef USE_LIGHT
bool on_light(light::LightState *light) override;
#endif
#ifdef USE_SENSOR
bool on_sensor(sensor::Sensor *sensor) override;
#endif
#ifdef USE_SWITCH
bool on_switch(switch_::Switch *a_switch) override;
#endif
#ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
#endif
bool on_service(UserServiceDescriptor *service) override;
#ifdef USE_ESP32_CAMERA
bool on_camera(esp32_camera::ESP32Camera *camera) override;
#endif
#ifdef USE_CLIMATE
bool on_climate(climate::Climate *climate) override;
#endif
bool on_end() override;
protected:
APIConnection *client_;
};
} // namespace api
} // namespace esphome
#include "api_server.h"

View file

@ -0,0 +1,49 @@
#include "service_call_message.h"
#include "esphome/core/log.h"
namespace esphome {
namespace api {
APIMessageType SubscribeServiceCallsRequest::message_type() const {
return APIMessageType::SUBSCRIBE_SERVICE_CALLS_REQUEST;
}
APIMessageType ServiceCallResponse::message_type() const { return APIMessageType::SERVICE_CALL_RESPONSE; }
void ServiceCallResponse::encode(APIBuffer &buffer) {
// string service = 1;
buffer.encode_string(1, this->service_);
// map<string, string> data = 2;
for (auto &it : this->data_) {
auto nested = buffer.begin_nested(2);
buffer.encode_string(1, it.key);
buffer.encode_string(2, it.value);
buffer.end_nested(nested);
}
// map<string, string> data_template = 3;
for (auto &it : this->data_template_) {
auto nested = buffer.begin_nested(3);
buffer.encode_string(1, it.key);
buffer.encode_string(2, it.value);
buffer.end_nested(nested);
}
// map<string, string> variables = 4;
for (auto &it : this->variables_) {
auto nested = buffer.begin_nested(4);
buffer.encode_string(1, it.key);
buffer.encode_string(2, it.value());
buffer.end_nested(nested);
}
}
void ServiceCallResponse::set_service(const std::string &service) { this->service_ = service; }
void ServiceCallResponse::set_data(const std::vector<KeyValuePair> &data) { this->data_ = data; }
void ServiceCallResponse::set_data_template(const std::vector<KeyValuePair> &data_template) {
this->data_template_ = data_template;
}
void ServiceCallResponse::set_variables(const std::vector<TemplatableKeyValuePair> &variables) {
this->variables_ = variables;
}
KeyValuePair::KeyValuePair(const std::string &key, const std::string &value) : key(key), value(value) {}
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,53 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/core/automation.h"
#include "api_message.h"
namespace esphome {
namespace api {
class SubscribeServiceCallsRequest : public APIMessage {
public:
APIMessageType message_type() const override;
};
class KeyValuePair {
public:
KeyValuePair(const std::string &key, const std::string &value);
std::string key;
std::string value;
};
class TemplatableKeyValuePair {
public:
template<typename T> TemplatableKeyValuePair(std::string key, T func);
std::string key;
std::function<std::string()> value;
};
template<typename T> TemplatableKeyValuePair::TemplatableKeyValuePair(std::string key, T func) : key(key) {
this->value = [func]() -> std::string { return to_string(func()); };
}
class ServiceCallResponse : public APIMessage {
public:
APIMessageType message_type() const override;
void encode(APIBuffer &buffer) override;
void set_service(const std::string &service);
void set_data(const std::vector<KeyValuePair> &data);
void set_data_template(const std::vector<KeyValuePair> &data_template);
void set_variables(const std::vector<TemplatableKeyValuePair> &variables);
protected:
std::string service_;
std::vector<KeyValuePair> data_;
std::vector<KeyValuePair> data_template_;
std::vector<TemplatableKeyValuePair> variables_;
};
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,26 @@
#include "subscribe_logs.h"
#include "esphome/core/log.h"
namespace esphome {
namespace api {
APIMessageType SubscribeLogsRequest::message_type() const { return APIMessageType::SUBSCRIBE_LOGS_REQUEST; }
bool SubscribeLogsRequest::decode_varint(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1: // LogLevel level = 1;
this->level_ = value;
return true;
case 2: // bool dump_config = 2;
this->dump_config_ = value;
return true;
default:
return false;
}
}
uint32_t SubscribeLogsRequest::get_level() const { return this->level_; }
void SubscribeLogsRequest::set_level(uint32_t level) { this->level_ = level; }
bool SubscribeLogsRequest::get_dump_config() const { return this->dump_config_; }
void SubscribeLogsRequest::set_dump_config(bool dump_config) { this->dump_config_ = dump_config; }
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,24 @@
#pragma once
#include "esphome/core/component.h"
#include "api_message.h"
namespace esphome {
namespace api {
class SubscribeLogsRequest : public APIMessage {
public:
bool decode_varint(uint32_t field_id, uint32_t value) override;
APIMessageType message_type() const override;
uint32_t get_level() const;
void set_level(uint32_t level);
bool get_dump_config() const;
void set_dump_config(bool dump_config);
protected:
uint32_t level_{6};
bool dump_config_{false};
};
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,77 @@
#include "subscribe_state.h"
#include "esphome/core/log.h"
namespace esphome {
namespace api {
#ifdef USE_BINARY_SENSOR
bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
if (!binary_sensor->has_state())
return true;
return this->client_->send_binary_sensor_state(binary_sensor, binary_sensor->state);
}
#endif
#ifdef USE_COVER
bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); }
#endif
#ifdef USE_FAN
bool InitialStateIterator::on_fan(fan::FanState *fan) { return this->client_->send_fan_state(fan); }
#endif
#ifdef USE_LIGHT
bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); }
#endif
#ifdef USE_SENSOR
bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) {
if (!sensor->has_state())
return true;
return this->client_->send_sensor_state(sensor, sensor->state);
}
#endif
#ifdef USE_SWITCH
bool InitialStateIterator::on_switch(switch_::Switch *a_switch) {
return this->client_->send_switch_state(a_switch, a_switch->state);
}
#endif
#ifdef USE_TEXT_SENSOR
bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
if (!text_sensor->has_state())
return true;
return this->client_->send_text_sensor_state(text_sensor, text_sensor->state);
}
#endif
#ifdef USE_CLIMATE
bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); }
#endif
InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
: ComponentIterator(server), client_(client) {}
APIMessageType SubscribeStatesRequest::message_type() const { return APIMessageType::SUBSCRIBE_STATES_REQUEST; }
bool HomeAssistantStateResponse::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
switch (field_id) {
case 1:
// string entity_id = 1;
this->entity_id_ = as_string(value, len);
return true;
case 2:
// string state = 2;
this->state_ = as_string(value, len);
return true;
default:
return false;
}
}
APIMessageType HomeAssistantStateResponse::message_type() const {
return APIMessageType::HOME_ASSISTANT_STATE_RESPONSE;
}
const std::string &HomeAssistantStateResponse::get_entity_id() const { return this->entity_id_; }
const std::string &HomeAssistantStateResponse::get_state() const { return this->state_; }
APIMessageType SubscribeHomeAssistantStatesRequest::message_type() const {
return APIMessageType::SUBSCRIBE_HOME_ASSISTANT_STATES_REQUEST;
}
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,70 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/defines.h"
#include "util.h"
#include "api_message.h"
namespace esphome {
namespace api {
class SubscribeStatesRequest : public APIMessage {
public:
APIMessageType message_type() const override;
};
class APIConnection;
class InitialStateIterator : public ComponentIterator {
public:
InitialStateIterator(APIServer *server, APIConnection *client);
#ifdef USE_BINARY_SENSOR
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
#endif
#ifdef USE_COVER
bool on_cover(cover::Cover *cover) override;
#endif
#ifdef USE_FAN
bool on_fan(fan::FanState *fan) override;
#endif
#ifdef USE_LIGHT
bool on_light(light::LightState *light) override;
#endif
#ifdef USE_SENSOR
bool on_sensor(sensor::Sensor *sensor) override;
#endif
#ifdef USE_SWITCH
bool on_switch(switch_::Switch *a_switch) override;
#endif
#ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
#endif
#ifdef USE_CLIMATE
bool on_climate(climate::Climate *climate) override;
#endif
protected:
APIConnection *client_;
};
class SubscribeHomeAssistantStatesRequest : public APIMessage {
public:
APIMessageType message_type() const override;
};
class HomeAssistantStateResponse : public APIMessage {
public:
bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
APIMessageType message_type() const override;
const std::string &get_entity_id() const;
const std::string &get_state() const;
protected:
std::string entity_id_;
std::string state_;
};
} // namespace api
} // namespace esphome
#include "api_server.h"

View file

@ -0,0 +1,74 @@
#include "user_services.h"
#include "esphome/core/log.h"
namespace esphome {
namespace api {
template<> bool ExecuteServiceArgument::get_value<bool>() { return this->value_bool_; }
template<> int ExecuteServiceArgument::get_value<int>() { return this->value_int_; }
template<> float ExecuteServiceArgument::get_value<float>() { return this->value_float_; }
template<> std::string ExecuteServiceArgument::get_value<std::string>() { return this->value_string_; }
APIMessageType ExecuteServiceArgument::message_type() const { return APIMessageType::EXECUTE_SERVICE_REQUEST; }
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1: // bool bool_ = 1;
this->value_bool_ = value;
return true;
case 2: // int32 int_ = 2;
this->value_int_ = value;
return true;
default:
return false;
}
}
bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 3: // float float_ = 3;
this->value_float_ = as_float(value);
return true;
default:
return false;
}
}
bool ExecuteServiceArgument::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
switch (field_id) {
case 4: // string string_ = 4;
this->value_string_ = as_string(value, len);
return true;
default:
return false;
}
}
bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, uint32_t value) {
switch (field_id) {
case 1: // fixed32 key = 1;
this->key_ = value;
return true;
default:
return false;
}
}
bool ExecuteServiceRequest::decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) {
switch (field_id) {
case 2: { // repeated ExecuteServiceArgument args = 2;
ExecuteServiceArgument arg;
arg.decode(value, len);
this->args_.push_back(arg);
return true;
}
default:
return false;
}
}
APIMessageType ExecuteServiceRequest::message_type() const { return APIMessageType::EXECUTE_SERVICE_REQUEST; }
const std::vector<ExecuteServiceArgument> &ExecuteServiceRequest::get_args() const { return this->args_; }
uint32_t ExecuteServiceRequest::get_key() const { return this->key_; }
ServiceTypeArgument::ServiceTypeArgument(const std::string &name, ServiceArgType type) : name_(name), type_(type) {}
const std::string &ServiceTypeArgument::get_name() const { return this->name_; }
ServiceArgType ServiceTypeArgument::get_type() const { return this->type_; }
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,125 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "api_message.h"
namespace esphome {
namespace api {
enum ServiceArgType {
SERVICE_ARG_TYPE_BOOL = 0,
SERVICE_ARG_TYPE_INT = 1,
SERVICE_ARG_TYPE_FLOAT = 2,
SERVICE_ARG_TYPE_STRING = 3,
};
class ServiceTypeArgument {
public:
ServiceTypeArgument(const std::string &name, ServiceArgType type);
const std::string &get_name() const;
ServiceArgType get_type() const;
protected:
std::string name_;
ServiceArgType type_;
};
class ExecuteServiceArgument : public APIMessage {
public:
APIMessageType message_type() const override;
template<typename T> T get_value();
bool decode_varint(uint32_t field_id, uint32_t value) override;
bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
bool decode_32bit(uint32_t field_id, uint32_t value) override;
protected:
bool value_bool_{false};
int value_int_{0};
float value_float_{0.0f};
std::string value_string_{};
};
class ExecuteServiceRequest : public APIMessage {
public:
bool decode_length_delimited(uint32_t field_id, const uint8_t *value, size_t len) override;
bool decode_32bit(uint32_t field_id, uint32_t value) override;
APIMessageType message_type() const override;
uint32_t get_key() const;
const std::vector<ExecuteServiceArgument> &get_args() const;
protected:
uint32_t key_;
std::vector<ExecuteServiceArgument> args_;
};
class UserServiceDescriptor {
public:
virtual void encode_list_service_response(APIBuffer &buffer) = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
};
template<typename... Ts> class UserService : public UserServiceDescriptor, public Trigger<Ts...> {
public:
UserService(const std::string &name, const std::array<ServiceTypeArgument, sizeof...(Ts)> &args);
void encode_list_service_response(APIBuffer &buffer) override;
bool execute_service(const ExecuteServiceRequest &req) override;
protected:
template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...>);
std::string name_;
uint32_t key_{0};
std::array<ServiceTypeArgument, sizeof...(Ts)> args_;
};
template<typename... Ts>
template<int... S>
void UserService<Ts...>::execute_(std::vector<ExecuteServiceArgument> args, seq<S...>) {
this->trigger((args[S].get_value<Ts>())...);
}
template<typename... Ts> void UserService<Ts...>::encode_list_service_response(APIBuffer &buffer) {
// string name = 1;
buffer.encode_string(1, this->name_);
// fixed32 key = 2;
buffer.encode_fixed32(2, this->key_);
// repeated ListServicesArgument args = 3;
for (auto &arg : this->args_) {
auto nested = buffer.begin_nested(3);
// string name = 1;
buffer.encode_string(1, arg.get_name());
// Type type = 2;
buffer.encode_int32(2, arg.get_type());
buffer.end_nested(nested);
}
}
template<typename... Ts> bool UserService<Ts...>::execute_service(const ExecuteServiceRequest &req) {
if (req.get_key() != this->key_)
return false;
if (req.get_args().size() != this->args_.size()) {
return false;
}
this->execute_(req.get_args(), typename gens<sizeof...(Ts)>::type());
return true;
}
template<typename... Ts>
UserService<Ts...>::UserService(const std::string &name, const std::array<ServiceTypeArgument, sizeof...(Ts)> &args)
: name_(name), args_(args) {
this->key_ = fnv1_hash(this->name_);
}
template<> bool ExecuteServiceArgument::get_value<bool>();
template<> int ExecuteServiceArgument::get_value<int>();
template<> float ExecuteServiceArgument::get_value<float>();
template<> std::string ExecuteServiceArgument::get_value<std::string>();
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,353 @@
#include "util.h"
#include "api_server.h"
#include "user_services.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace api {
APIBuffer::APIBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
size_t APIBuffer::get_length() const { return this->buffer_->size(); }
void APIBuffer::write(uint8_t value) { this->buffer_->push_back(value); }
void APIBuffer::encode_uint32(uint32_t field, uint32_t value, bool force) {
if (value == 0 && !force)
return;
this->encode_field_raw(field, 0);
this->encode_varint_raw(value);
}
void APIBuffer::encode_int32(uint32_t field, int32_t value, bool force) {
this->encode_uint32(field, static_cast<uint32_t>(value), force);
}
void APIBuffer::encode_bool(uint32_t field, bool value, bool force) {
if (!value && !force)
return;
this->encode_field_raw(field, 0);
this->write(0x01);
}
void APIBuffer::encode_string(uint32_t field, const std::string &value) {
this->encode_string(field, value.data(), value.size());
}
void APIBuffer::encode_bytes(uint32_t field, const uint8_t *data, size_t len) {
this->encode_string(field, reinterpret_cast<const char *>(data), len);
}
void APIBuffer::encode_string(uint32_t field, const char *string, size_t len) {
if (len == 0)
return;
this->encode_field_raw(field, 2);
this->encode_varint_raw(len);
const uint8_t *data = reinterpret_cast<const uint8_t *>(string);
for (size_t i = 0; i < len; i++) {
this->write(data[i]);
}
}
void APIBuffer::encode_fixed32(uint32_t field, uint32_t value, bool force) {
if (value == 0 && !force)
return;
this->encode_field_raw(field, 5);
this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF);
}
void APIBuffer::encode_float(uint32_t field, float value, bool force) {
if (value == 0.0f && !force)
return;
union {
float value_f;
uint32_t value_raw;
} val;
val.value_f = value;
this->encode_fixed32(field, val.value_raw);
}
void APIBuffer::encode_field_raw(uint32_t field, uint32_t type) {
uint32_t val = (field << 3) | (type & 0b111);
this->encode_varint_raw(val);
}
void APIBuffer::encode_varint_raw(uint32_t value) {
if (value <= 0x7F) {
this->write(value);
return;
}
while (value) {
uint8_t temp = value & 0x7F;
value >>= 7;
if (value) {
this->write(temp | 0x80);
} else {
this->write(temp);
}
}
}
void APIBuffer::encode_sint32(uint32_t field, int32_t value, bool force) {
if (value < 0)
this->encode_uint32(field, ~(uint32_t(value) << 1), force);
else
this->encode_uint32(field, uint32_t(value) << 1, force);
}
void APIBuffer::encode_nameable(Nameable *nameable) {
// string object_id = 1;
this->encode_string(1, nameable->get_object_id());
// fixed32 key = 2;
this->encode_fixed32(2, nameable->get_object_id_hash());
// string name = 3;
this->encode_string(3, nameable->get_name());
}
size_t APIBuffer::begin_nested(uint32_t field) {
this->encode_field_raw(field, 2);
return this->buffer_->size();
}
void APIBuffer::end_nested(size_t begin_index) {
const uint32_t nested_length = this->buffer_->size() - begin_index;
// add varint
std::vector<uint8_t> var;
uint32_t val = nested_length;
if (val <= 0x7F) {
var.push_back(val);
} else {
while (val) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
var.push_back(temp | 0x80);
} else {
var.push_back(temp);
}
}
}
this->buffer_->insert(this->buffer_->begin() + begin_index, var.begin(), var.end());
}
optional<uint32_t> proto_decode_varuint32(const uint8_t *buf, size_t len, uint32_t *consumed) {
if (len == 0)
return {};
uint32_t result = 0;
uint8_t bitpos = 0;
for (uint32_t i = 0; i < len; i++) {
uint8_t val = buf[i];
result |= uint32_t(val & 0x7F) << bitpos;
bitpos += 7;
if ((val & 0x80) == 0) {
if (consumed != nullptr) {
*consumed = i + 1;
}
return result;
}
}
return {};
}
std::string as_string(const uint8_t *value, size_t len) {
return std::string(reinterpret_cast<const char *>(value), len);
}
int32_t as_sint32(uint32_t val) {
if (val & 1)
return uint32_t(~(val >> 1));
else
return uint32_t(val >> 1);
}
float as_float(uint32_t val) {
static_assert(sizeof(uint32_t) == sizeof(float), "float must be 32bit long");
union {
uint32_t raw;
float value;
} x;
x.raw = val;
return x.value;
}
ComponentIterator::ComponentIterator(APIServer *server) : server_(server) {}
void ComponentIterator::begin() {
this->state_ = IteratorState::BEGIN;
this->at_ = 0;
}
void ComponentIterator::advance() {
bool advance_platform = false;
bool success = true;
switch (this->state_) {
case IteratorState::NONE:
// not started
return;
case IteratorState::BEGIN:
if (this->on_begin()) {
advance_platform = true;
} else {
return;
}
break;
#ifdef USE_BINARY_SENSOR
case IteratorState::BINARY_SENSOR:
if (this->at_ >= App.get_binary_sensors().size()) {
advance_platform = true;
} else {
auto *binary_sensor = App.get_binary_sensors()[this->at_];
if (binary_sensor->is_internal()) {
success = true;
break;
} else {
success = this->on_binary_sensor(binary_sensor);
}
}
break;
#endif
#ifdef USE_COVER
case IteratorState::COVER:
if (this->at_ >= App.get_covers().size()) {
advance_platform = true;
} else {
auto *cover = App.get_covers()[this->at_];
if (cover->is_internal()) {
success = true;
break;
} else {
success = this->on_cover(cover);
}
}
break;
#endif
#ifdef USE_FAN
case IteratorState::FAN:
if (this->at_ >= App.get_fans().size()) {
advance_platform = true;
} else {
auto *fan = App.get_fans()[this->at_];
if (fan->is_internal()) {
success = true;
break;
} else {
success = this->on_fan(fan);
}
}
break;
#endif
#ifdef USE_LIGHT
case IteratorState::LIGHT:
if (this->at_ >= App.get_lights().size()) {
advance_platform = true;
} else {
auto *light = App.get_lights()[this->at_];
if (light->is_internal()) {
success = true;
break;
} else {
success = this->on_light(light);
}
}
break;
#endif
#ifdef USE_SENSOR
case IteratorState::SENSOR:
if (this->at_ >= App.get_sensors().size()) {
advance_platform = true;
} else {
auto *sensor = App.get_sensors()[this->at_];
if (sensor->is_internal()) {
success = true;
break;
} else {
success = this->on_sensor(sensor);
}
}
break;
#endif
#ifdef USE_SWITCH
case IteratorState::SWITCH:
if (this->at_ >= App.get_switches().size()) {
advance_platform = true;
} else {
auto *a_switch = App.get_switches()[this->at_];
if (a_switch->is_internal()) {
success = true;
break;
} else {
success = this->on_switch(a_switch);
}
}
break;
#endif
#ifdef USE_TEXT_SENSOR
case IteratorState::TEXT_SENSOR:
if (this->at_ >= App.get_text_sensors().size()) {
advance_platform = true;
} else {
auto *text_sensor = App.get_text_sensors()[this->at_];
if (text_sensor->is_internal()) {
success = true;
break;
} else {
success = this->on_text_sensor(text_sensor);
}
}
break;
#endif
case IteratorState ::SERVICE:
if (this->at_ >= this->server_->get_user_services().size()) {
advance_platform = true;
} else {
auto *service = this->server_->get_user_services()[this->at_];
success = this->on_service(service);
}
break;
#ifdef USE_ESP32_CAMERA
case IteratorState::CAMERA:
if (esp32_camera::global_esp32_camera == nullptr) {
advance_platform = true;
} else {
if (esp32_camera::global_esp32_camera->is_internal()) {
advance_platform = success = true;
break;
} else {
advance_platform = success = this->on_camera(esp32_camera::global_esp32_camera);
}
}
break;
#endif
#ifdef USE_CLIMATE
case IteratorState::CLIMATE:
if (this->at_ >= App.get_climates().size()) {
advance_platform = true;
} else {
auto *climate = App.get_climates()[this->at_];
if (climate->is_internal()) {
success = true;
break;
} else {
success = this->on_climate(climate);
}
}
break;
#endif
case IteratorState::MAX:
if (this->on_end()) {
this->state_ = IteratorState::NONE;
}
return;
}
if (advance_platform) {
this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1);
this->at_ = 0;
} else if (success) {
this->at_++;
}
}
bool ComponentIterator::on_end() { return true; }
bool ComponentIterator::on_begin() { return true; }
bool ComponentIterator::on_service(UserServiceDescriptor *service) { return true; }
#ifdef USE_ESP32_CAMERA
bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; }
#endif
} // namespace api
} // namespace esphome

View file

@ -0,0 +1,127 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#ifdef USE_ESP32_CAMERA
#include "esphome/components/esp32_camera/esp32_camera.h"
#endif
namespace esphome {
namespace api {
class APIBuffer {
public:
APIBuffer(std::vector<uint8_t> *buffer);
size_t get_length() const;
void write(uint8_t value);
void encode_int32(uint32_t field, int32_t value, bool force = false);
void encode_uint32(uint32_t field, uint32_t value, bool force = false);
void encode_sint32(uint32_t field, int32_t value, bool force = false);
void encode_bool(uint32_t field, bool value, bool force = false);
void encode_string(uint32_t field, const std::string &value);
void encode_string(uint32_t field, const char *string, size_t len);
void encode_bytes(uint32_t field, const uint8_t *data, size_t len);
void encode_fixed32(uint32_t field, uint32_t value, bool force = false);
void encode_float(uint32_t field, float value, bool force = false);
void encode_nameable(Nameable *nameable);
size_t begin_nested(uint32_t field);
void end_nested(size_t begin_index);
void encode_field_raw(uint32_t field, uint32_t type);
void encode_varint_raw(uint32_t value);
protected:
std::vector<uint8_t> *buffer_;
};
optional<uint32_t> proto_decode_varuint32(const uint8_t *buf, size_t len, uint32_t *consumed = nullptr);
std::string as_string(const uint8_t *value, size_t len);
int32_t as_sint32(uint32_t val);
float as_float(uint32_t val);
class APIServer;
class UserServiceDescriptor;
class ComponentIterator {
public:
ComponentIterator(APIServer *server);
void begin();
void advance();
virtual bool on_begin();
#ifdef USE_BINARY_SENSOR
virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0;
#endif
#ifdef USE_COVER
virtual bool on_cover(cover::Cover *cover) = 0;
#endif
#ifdef USE_FAN
virtual bool on_fan(fan::FanState *fan) = 0;
#endif
#ifdef USE_LIGHT
virtual bool on_light(light::LightState *light) = 0;
#endif
#ifdef USE_SENSOR
virtual bool on_sensor(sensor::Sensor *sensor) = 0;
#endif
#ifdef USE_SWITCH
virtual bool on_switch(switch_::Switch *a_switch) = 0;
#endif
#ifdef USE_TEXT_SENSOR
virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0;
#endif
virtual bool on_service(UserServiceDescriptor *service);
#ifdef USE_ESP32_CAMERA
virtual bool on_camera(esp32_camera::ESP32Camera *camera);
#endif
#ifdef USE_CLIMATE
virtual bool on_climate(climate::Climate *climate) = 0;
#endif
virtual bool on_end();
protected:
enum class IteratorState {
NONE = 0,
BEGIN,
#ifdef USE_BINARY_SENSOR
BINARY_SENSOR,
#endif
#ifdef USE_COVER
COVER,
#endif
#ifdef USE_FAN
FAN,
#endif
#ifdef USE_LIGHT
LIGHT,
#endif
#ifdef USE_SENSOR
SENSOR,
#endif
#ifdef USE_SWITCH
SWITCH,
#endif
#ifdef USE_TEXT_SENSOR
TEXT_SENSOR,
#endif
SERVICE,
#ifdef USE_ESP32_CAMERA
CAMERA,
#endif
#ifdef USE_CLIMATE
CLIMATE,
#endif
MAX,
} state_{IteratorState::NONE};
size_t at_{0};
APIServer *server_;
};
} // namespace api
} // namespace esphome

View file

View file

@ -0,0 +1,156 @@
#include "bang_bang_climate.h"
#include "esphome/core/log.h"
namespace esphome {
namespace bang_bang {
static const char *TAG = "bang_bang.climate";
void BangBangClimate::setup() {
this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
// control may have changed, recompute
this->compute_state_();
// current temperature changed, publish state
this->publish_state();
});
this->current_temperature = this->sensor_->state;
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->to_call(this).perform();
} else {
// restore from defaults, change_away handles those for us
this->mode = climate::CLIMATE_MODE_AUTO;
this->change_away_(false);
}
}
void BangBangClimate::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value())
this->mode = *call.get_mode();
if (call.get_target_temperature_low().has_value())
this->target_temperature_low = *call.get_target_temperature_low();
if (call.get_target_temperature_high().has_value())
this->target_temperature_high = *call.get_target_temperature_high();
if (call.get_away().has_value())
this->change_away_(*call.get_away());
this->compute_state_();
this->publish_state();
}
climate::ClimateTraits BangBangClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.set_supports_auto_mode(true);
traits.set_supports_cool_mode(this->supports_cool_);
traits.set_supports_heat_mode(this->supports_heat_);
traits.set_supports_two_point_target_temperature(true);
traits.set_supports_away(this->supports_away_);
return traits;
}
void BangBangClimate::compute_state_() {
if (this->mode != climate::CLIMATE_MODE_AUTO) {
// in non-auto mode
this->switch_to_mode_(this->mode);
return;
}
// auto mode, compute target mode
if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) {
// if any control values are nan, go to OFF (idle) mode
this->switch_to_mode_(climate::CLIMATE_MODE_OFF);
return;
}
const bool too_cold = this->current_temperature < this->target_temperature_low;
const bool too_hot = this->current_temperature > this->target_temperature_high;
climate::ClimateMode target_mode;
if (too_cold) {
// too cold -> enable heating if possible, else idle
if (this->supports_heat_)
target_mode = climate::CLIMATE_MODE_HEAT;
else
target_mode = climate::CLIMATE_MODE_OFF;
} else if (too_hot) {
// too hot -> enable cooling if possible, else idle
if (this->supports_cool_)
target_mode = climate::CLIMATE_MODE_COOL;
else
target_mode = climate::CLIMATE_MODE_OFF;
} else {
// neither too hot nor too cold -> in range
if (this->supports_cool_ && this->supports_heat_) {
// if supports both ends, go to idle mode
target_mode = climate::CLIMATE_MODE_OFF;
} else {
// else use current mode and don't change (hysteresis)
target_mode = this->internal_mode_;
}
}
this->switch_to_mode_(target_mode);
}
void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) {
if (mode == this->internal_mode_)
// already in target mode
return;
if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop();
this->prev_trigger_ = nullptr;
}
Trigger<> *trig;
switch (mode) {
case climate::CLIMATE_MODE_OFF:
trig = this->idle_trigger_;
break;
case climate::CLIMATE_MODE_COOL:
trig = this->cool_trigger_;
break;
case climate::CLIMATE_MODE_HEAT:
trig = this->heat_trigger_;
break;
default:
trig = nullptr;
}
if (trig != nullptr) {
// trig should never be null, but still check so that we don't crash
trig->trigger();
this->internal_mode_ = mode;
this->prev_trigger_ = trig;
this->publish_state();
}
}
void BangBangClimate::change_away_(bool away) {
if (!away) {
this->target_temperature_low = this->normal_config_.default_temperature_low;
this->target_temperature_high = this->normal_config_.default_temperature_high;
} else {
this->target_temperature_low = this->away_config_.default_temperature_low;
this->target_temperature_high = this->away_config_.default_temperature_high;
}
this->away = away;
}
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
this->normal_config_ = normal_config;
}
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
this->supports_away_ = true;
this->away_config_ = away_config;
}
BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default;
BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature_low,
float default_temperature_high)
: default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {}
} // namespace bang_bang
} // namespace esphome

View file

@ -0,0 +1,89 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace bang_bang {
struct BangBangClimateTargetTempConfig {
public:
BangBangClimateTargetTempConfig();
BangBangClimateTargetTempConfig(float default_temperature_low, float default_temperature_high);
float default_temperature_low{NAN};
float default_temperature_high{NAN};
};
class BangBangClimate : public climate::Climate, public Component {
public:
BangBangClimate();
void setup() override;
void set_sensor(sensor::Sensor *sensor);
Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const;
void set_supports_cool(bool supports_cool);
Trigger<> *get_heat_trigger() const;
void set_supports_heat(bool supports_heat);
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
protected:
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
/// Change the away setting, will reset target temperatures to defaults.
void change_away_(bool away);
/// Return the traits of this controller.
climate::ClimateTraits traits() override;
/// Re-compute the state of this climate controller.
void compute_state_();
/// Switch the climate device to the given climate mode.
void switch_to_mode_(climate::ClimateMode mode);
/// The sensor used for getting the current temperature
sensor::Sensor *sensor_{nullptr};
/** The trigger to call when the controller should switch to idle mode.
*
* In idle mode, the controller is assumed to have both heating and cooling disabled.
*/
Trigger<> *idle_trigger_;
/** The trigger to call when the controller should switch to cooling mode.
*/
Trigger<> *cool_trigger_;
/** Whether the controller supports cooling.
*
* A false value for this attribute means that the controller has no cooling action
* (for example a thermostat, where only heating and not-heating is possible).
*/
bool supports_cool_{false};
/** The trigger to call when the controller should switch to heating mode.
*
* A null value for this attribute means that the controller has no heating action
* For example window blinds, where only cooling (blinds closed) and not-cooling
* (blinds open) is possible.
*/
Trigger<> *heat_trigger_{nullptr};
bool supports_heat_{false};
/** A reference to the trigger that was previously active.
*
* This is so that the previous trigger can be stopped before enabling a new one.
*/
Trigger<> *prev_trigger_{nullptr};
/** The climate mode that is currently active - for a `.mode = AUTO` this will
* contain the actual mode the device
*
*/
climate::ClimateMode internal_mode_{climate::CLIMATE_MODE_OFF};
BangBangClimateTargetTempConfig normal_config_{};
bool supports_away_{false};
BangBangClimateTargetTempConfig away_config_{};
};
} // namespace bang_bang
} // namespace esphome

View file

@ -0,0 +1,57 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.components import climate, sensor
from esphome.const import CONF_AWAY_CONFIG, CONF_COOL_ACTION, \
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, \
CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR
bang_bang_ns = cg.esphome_ns.namespace('bang_bang')
BangBangClimate = bang_bang_ns.class_('BangBangClimate', climate.Climate, cg.Component)
BangBangClimateTargetTempConfig = bang_bang_ns.struct('BangBangClimateTargetTempConfig')
CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(BangBangClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_AWAY_CONFIG): cv.Schema({
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
}),
}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION))
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield climate.register_climate(var, config)
sens = yield cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens))
normal_config = BangBangClimateTargetTempConfig(
config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]
)
cg.add(var.set_normal_config(normal_config))
yield automation.build_automation(var.get_idle_trigger(), [], config[CONF_IDLE_ACTION])
if CONF_COOL_ACTION in config:
yield automation.build_automation(var.get_cool_trigger(), [], config[CONF_COOL_ACTION])
cg.add(var.set_supports_cool(True))
if CONF_HEAT_ACTION in config:
yield automation.build_automation(var.get_heat_trigger(), [], config[CONF_HEAT_ACTION])
cg.add(var.set_supports_heat(True))
if CONF_AWAY_CONFIG in config:
away = config[CONF_AWAY_CONFIG]
away_config = BangBangClimateTargetTempConfig(
away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]
)
cg.add(var.set_away_config(away_config))

View file

View file

@ -0,0 +1,78 @@
#include "bh1750.h"
#include "esphome/core/log.h"
namespace esphome {
namespace bh1750 {
static const char *TAG = "bh1750.sensor";
static const uint8_t BH1750_COMMAND_POWER_ON = 0b00000001;
void BH1750Sensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up BH1750 '%s'...", this->name_.c_str());
if (!this->write_bytes(BH1750_COMMAND_POWER_ON, nullptr, 0)) {
this->mark_failed();
return;
}
}
void BH1750Sensor::dump_config() {
LOG_SENSOR("", "BH1750", this);
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with BH1750 failed!");
}
const char *resolution_s;
switch (this->resolution_) {
case BH1750_RESOLUTION_0P5_LX:
resolution_s = "0.5";
break;
case BH1750_RESOLUTION_1P0_LX:
resolution_s = "1";
break;
case BH1750_RESOLUTION_4P0_LX:
resolution_s = "4";
break;
default:
resolution_s = "Unknown";
break;
}
ESP_LOGCONFIG(TAG, " Resolution: %s", resolution_s);
LOG_UPDATE_INTERVAL(this);
}
void BH1750Sensor::update() {
if (!this->write_bytes(this->resolution_, nullptr, 0))
return;
uint32_t wait = 0;
// use max conversion times
switch (this->resolution_) {
case BH1750_RESOLUTION_0P5_LX:
case BH1750_RESOLUTION_1P0_LX:
wait = 180;
break;
case BH1750_RESOLUTION_4P0_LX:
wait = 24;
break;
}
this->set_timeout("illuminance", wait, [this]() { this->read_data_(); });
}
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
void BH1750Sensor::read_data_() {
uint16_t raw_value;
if (!this->parent_->raw_receive_16(this->address_, &raw_value, 1)) {
this->status_set_warning();
return;
}
float lx = float(raw_value) / 1.2f;
ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx);
this->publish_state(lx);
this->status_clear_warning();
}
void BH1750Sensor::set_resolution(BH1750Resolution resolution) { this->resolution_ = resolution; }
} // namespace bh1750
} // namespace esphome

View file

@ -0,0 +1,46 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bh1750 {
/// Enum listing all resolutions that can be used with the BH1750
enum BH1750Resolution {
BH1750_RESOLUTION_4P0_LX = 0b00100011, // one-time low resolution mode
BH1750_RESOLUTION_1P0_LX = 0b00100000, // one-time high resolution mode 1
BH1750_RESOLUTION_0P5_LX = 0b00100001, // one-time high resolution mode 2
};
/// This class implements support for the i2c-based BH1750 ambient light sensor.
class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
/** Set the resolution of this sensor.
*
* Possible values are:
*
* - `BH1750_RESOLUTION_4P0_LX`
* - `BH1750_RESOLUTION_1P0_LX`
* - `BH1750_RESOLUTION_0P5_LX` (default)
*
* @param resolution The new resolution of the sensor.
*/
void set_resolution(BH1750Resolution resolution);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void setup() override;
void dump_config() override;
void update() override;
float get_setup_priority() const override;
protected:
void read_data_();
BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX};
};
} // namespace bh1750
} // namespace esphome

View file

@ -0,0 +1,30 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import CONF_ID, CONF_RESOLUTION, UNIT_LUX, ICON_BRIGHTNESS_5
DEPENDENCIES = ['i2c']
bh1750_ns = cg.esphome_ns.namespace('bh1750')
BH1750Resolution = bh1750_ns.enum('BH1750Resolution')
BH1750_RESOLUTIONS = {
4.0: BH1750Resolution.BH1750_RESOLUTION_4P0_LX,
1.0: BH1750Resolution.BH1750_RESOLUTION_1P0_LX,
0.5: BH1750Resolution.BH1750_RESOLUTION_0P5_LX,
}
BH1750Sensor = bh1750_ns.class_('BH1750Sensor', sensor.Sensor, cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 1).extend({
cv.GenerateID(): cv.declare_id(BH1750Sensor),
cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum(BH1750_RESOLUTIONS, float=True),
}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x23))
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield sensor.register_sensor(var, config)
yield i2c.register_i2c_device(var, config)
cg.add(var.set_resolution(config[CONF_RESOLUTION]))

View file

@ -0,0 +1,3 @@
import esphome.codegen as cg
binary_ns = cg.esphome_ns.namespace('binary')

View file

@ -0,0 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import fan, output
from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_OUTPUT_ID
from .. import binary_ns
BinaryFan = binary_ns.class_('BinaryFan', cg.Component)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan),
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
}).extend(cv.COMPONENT_SCHEMA)
def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
yield cg.register_component(var, config)
fan_ = yield fan.create_fan_state(config)
cg.add(var.set_fan(fan_))
output_ = yield cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(output_))
if CONF_OSCILLATION_OUTPUT in config:
oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT])
cg.add(var.set_oscillation(oscillation_output))

View file

@ -0,0 +1,48 @@
#include "binary_fan.h"
#include "esphome/core/log.h"
namespace esphome {
namespace binary {
static const char *TAG = "binary.fan";
void binary::BinaryFan::dump_config() {
ESP_LOGCONFIG(TAG, "Fan '%s':", this->fan_->get_name().c_str());
if (this->fan_->get_traits().supports_oscillation()) {
ESP_LOGCONFIG(TAG, " Oscillation: YES");
}
}
void BinaryFan::setup() {
auto traits = fan::FanTraits(this->oscillating_ != nullptr, false);
this->fan_->set_traits(traits);
this->fan_->add_on_state_callback([this]() { this->next_update_ = true; });
}
void BinaryFan::loop() {
if (!this->next_update_) {
return;
}
this->next_update_ = false;
{
bool enable = this->fan_->state;
if (enable)
this->output_->turn_on();
else
this->output_->turn_off();
ESP_LOGD(TAG, "Setting binary state: %s", ONOFF(enable));
}
if (this->oscillating_ != nullptr) {
bool enable = this->fan_->oscillating;
if (enable) {
this->oscillating_->turn_on();
} else {
this->oscillating_->turn_off();
}
ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable));
}
}
float BinaryFan::get_setup_priority() const { return setup_priority::DATA; }
} // namespace binary
} // namespace esphome

View file

@ -0,0 +1,28 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/fan/fan_state.h"
namespace esphome {
namespace binary {
class BinaryFan : public Component {
public:
void set_fan(fan::FanState *fan) { fan_ = fan; }
void set_output(output::BinaryOutput *output) { output_ = output; }
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override;
void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; }
protected:
fan::FanState *fan_;
output::BinaryOutput *output_;
output::BinaryOutput *oscillating_{nullptr};
bool next_update_{true};
};
} // namespace binary
} // namespace esphome

View file

@ -0,0 +1,20 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import light, output
from esphome.const import CONF_OUTPUT_ID, CONF_OUTPUT
from .. import binary_ns
BinaryLightOutput = binary_ns.class_('BinaryLightOutput', light.LightOutput)
CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend({
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryLightOutput),
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
})
def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
yield light.register_light(var, config)
out = yield cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(out))

View file

@ -0,0 +1,32 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/light/light_output.h"
namespace esphome {
namespace binary {
class BinaryLightOutput : public light::LightOutput {
public:
void set_output(output::BinaryOutput *output) { output_ = output; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
traits.set_supports_brightness(false);
return traits;
}
void write_state(light::LightState *state) override {
bool binary;
state->current_values_as_binary(&binary);
if (binary)
this->output_->turn_on();
else
this->output_->turn_off();
}
protected:
output::BinaryOutput *output_;
};
} // namespace binary
} // namespace esphome

View file

@ -1,19 +1,16 @@
import voluptuous as vol import esphome.codegen as cg
from esphome import automation, core
from esphome.automation import CONDITION_REGISTRY, Condition, maybe_simple_id
from esphome.components import mqtt
from esphome.components.mqtt import setup_mqtt_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DELAYED_OFF, CONF_DELAYED_ON, CONF_DEVICE_CLASS, CONF_FILTERS, \ from esphome import automation, core
CONF_HEARTBEAT, CONF_ID, CONF_INTERNAL, CONF_INVALID_COOLDOWN, CONF_INVERT, CONF_INVERTED, \ from esphome.automation import Condition, maybe_simple_id
CONF_LAMBDA, CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_MQTT_ID, CONF_ON_CLICK, \ from esphome.components import mqtt
from esphome.const import CONF_DEVICE_CLASS, CONF_FILTERS, \
CONF_ID, CONF_INTERNAL, CONF_INVALID_COOLDOWN, CONF_INVERTED, \
CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_ON_CLICK, \
CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_ON_STATE, \ CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_ON_STATE, \
CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, CONF_FOR CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, CONF_FOR, CONF_NAME, CONF_MQTT_ID
from esphome.core import CORE from esphome.core import CORE, coroutine, coroutine_with_priority
from esphome.cpp_generator import Pvariable, StructInitializer, add, get_variable, process_lambda
from esphome.cpp_types import App, Component, Nameable, Trigger, bool_, esphome_ns, optional
from esphome.py_compat import string_types from esphome.py_compat import string_types
from esphome.util import Registry
DEVICE_CLASSES = [ DEVICE_CLASSES = [
'', 'battery', 'cold', 'connectivity', 'door', 'garage_door', 'gas', '', 'battery', 'cold', 'connectivity', 'door', 'garage_door', 'gas',
@ -22,50 +19,67 @@ DEVICE_CLASSES = [
'sound', 'vibration', 'window' 'sound', 'vibration', 'window'
] ]
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ IS_PLATFORM_COMPONENT = True
}) binary_sensor_ns = cg.esphome_ns.namespace('binary_sensor')
BinarySensor = binary_sensor_ns.class_('BinarySensor', cg.Nameable)
binary_sensor_ns = esphome_ns.namespace('binary_sensor')
BinarySensor = binary_sensor_ns.class_('BinarySensor', Nameable)
BinarySensorPtr = BinarySensor.operator('ptr') BinarySensorPtr = BinarySensor.operator('ptr')
MQTTBinarySensorComponent = binary_sensor_ns.class_('MQTTBinarySensorComponent', mqtt.MQTTComponent)
# Triggers # Triggers
PressTrigger = binary_sensor_ns.class_('PressTrigger', Trigger.template()) PressTrigger = binary_sensor_ns.class_('PressTrigger', automation.Trigger.template())
ReleaseTrigger = binary_sensor_ns.class_('ReleaseTrigger', Trigger.template()) ReleaseTrigger = binary_sensor_ns.class_('ReleaseTrigger', automation.Trigger.template())
ClickTrigger = binary_sensor_ns.class_('ClickTrigger', Trigger.template()) ClickTrigger = binary_sensor_ns.class_('ClickTrigger', automation.Trigger.template())
DoubleClickTrigger = binary_sensor_ns.class_('DoubleClickTrigger', Trigger.template()) DoubleClickTrigger = binary_sensor_ns.class_('DoubleClickTrigger', automation.Trigger.template())
MultiClickTrigger = binary_sensor_ns.class_('MultiClickTrigger', Trigger.template(), Component) MultiClickTrigger = binary_sensor_ns.class_('MultiClickTrigger', automation.Trigger.template(),
cg.Component)
MultiClickTriggerEvent = binary_sensor_ns.struct('MultiClickTriggerEvent') MultiClickTriggerEvent = binary_sensor_ns.struct('MultiClickTriggerEvent')
StateTrigger = binary_sensor_ns.class_('StateTrigger', Trigger.template(bool_)) StateTrigger = binary_sensor_ns.class_('StateTrigger', automation.Trigger.template(bool))
BinarySensorPublishAction = binary_sensor_ns.class_('BinarySensorPublishAction', automation.Action)
# Condition # Condition
BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Condition) BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Condition)
# Filters # Filters
Filter = binary_sensor_ns.class_('Filter') Filter = binary_sensor_ns.class_('Filter')
DelayedOnFilter = binary_sensor_ns.class_('DelayedOnFilter', Filter, Component) DelayedOnFilter = binary_sensor_ns.class_('DelayedOnFilter', Filter, cg.Component)
DelayedOffFilter = binary_sensor_ns.class_('DelayedOffFilter', Filter, Component) DelayedOffFilter = binary_sensor_ns.class_('DelayedOffFilter', Filter, cg.Component)
HeartbeatFilter = binary_sensor_ns.class_('HeartbeatFilter', Filter, Component)
InvertFilter = binary_sensor_ns.class_('InvertFilter', Filter) InvertFilter = binary_sensor_ns.class_('InvertFilter', Filter)
LambdaFilter = binary_sensor_ns.class_('LambdaFilter', Filter) LambdaFilter = binary_sensor_ns.class_('LambdaFilter', Filter)
FILTER_KEYS = [CONF_INVERT, CONF_DELAYED_ON, CONF_DELAYED_OFF, CONF_LAMBDA, CONF_HEARTBEAT] FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry('filter', FILTER_REGISTRY)
FILTERS_SCHEMA = cv.ensure_list({
vol.Optional(CONF_INVERT): None,
vol.Optional(CONF_DELAYED_ON): cv.positive_time_period_milliseconds,
vol.Optional(CONF_DELAYED_OFF): cv.positive_time_period_milliseconds,
vol.Optional(CONF_LAMBDA): cv.lambda_,
vol.Optional(CONF_HEARTBEAT): cv.invalid("The heartbeat filter has been removed in 1.11.0"), @FILTER_REGISTRY.register('invert', InvertFilter, {})
}, cv.has_exactly_one_key(*FILTER_KEYS)) def invert_filter_to_code(config, filter_id):
yield cg.new_Pvariable(filter_id)
@FILTER_REGISTRY.register('delayed_on', DelayedOnFilter,
cv.positive_time_period_milliseconds)
def delayed_on_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id, config)
yield cg.register_component(var, {})
yield var
@FILTER_REGISTRY.register('delayed_off', DelayedOffFilter, cv.positive_time_period_milliseconds)
def delayed_off_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id, config)
yield cg.register_component(var, {})
yield var
@FILTER_REGISTRY.register('lambda', LambdaFilter, cv.returning_lambda)
def lambda_filter_to_code(config, filter_id):
lambda_ = yield cg.process_lambda(config, [(bool, 'x')], return_type=cg.optional.template(bool))
yield cg.new_Pvariable(filter_id, lambda_)
MULTI_CLICK_TIMING_SCHEMA = cv.Schema({ MULTI_CLICK_TIMING_SCHEMA = cv.Schema({
vol.Optional(CONF_STATE): cv.boolean, cv.Optional(CONF_STATE): cv.boolean,
vol.Optional(CONF_MIN_LENGTH): cv.positive_time_period_milliseconds, cv.Optional(CONF_MIN_LENGTH): cv.positive_time_period_milliseconds,
vol.Optional(CONF_MAX_LENGTH): cv.positive_time_period_milliseconds, cv.Optional(CONF_MAX_LENGTH): cv.positive_time_period_milliseconds,
}) })
@ -75,15 +89,15 @@ def parse_multi_click_timing_str(value):
parts = value.lower().split(' ') parts = value.lower().split(' ')
if len(parts) != 5: if len(parts) != 5:
raise vol.Invalid("Multi click timing grammar consists of exactly 5 words, not {}" raise cv.Invalid("Multi click timing grammar consists of exactly 5 words, not {}"
"".format(len(parts))) "".format(len(parts)))
try: try:
state = cv.boolean(parts[0]) state = cv.boolean(parts[0])
except vol.Invalid: except cv.Invalid:
raise vol.Invalid(u"First word must either be ON or OFF, not {}".format(parts[0])) raise cv.Invalid(u"First word must either be ON or OFF, not {}".format(parts[0]))
if parts[1] != 'for': if parts[1] != 'for':
raise vol.Invalid(u"Second word must be 'for', got {}".format(parts[1])) raise cv.Invalid(u"Second word must be 'for', got {}".format(parts[1]))
if parts[2] == 'at': if parts[2] == 'at':
if parts[3] == 'least': if parts[3] == 'least':
@ -91,29 +105,29 @@ def parse_multi_click_timing_str(value):
elif parts[3] == 'most': elif parts[3] == 'most':
key = CONF_MAX_LENGTH key = CONF_MAX_LENGTH
else: else:
raise vol.Invalid(u"Third word after at must either be 'least' or 'most', got {}" raise cv.Invalid(u"Third word after at must either be 'least' or 'most', got {}"
u"".format(parts[3])) u"".format(parts[3]))
try: try:
length = cv.positive_time_period_milliseconds(parts[4]) length = cv.positive_time_period_milliseconds(parts[4])
except vol.Invalid as err: except cv.Invalid as err:
raise vol.Invalid(u"Multi Click Grammar Parsing length failed: {}".format(err)) raise cv.Invalid(u"Multi Click Grammar Parsing length failed: {}".format(err))
return { return {
CONF_STATE: state, CONF_STATE: state,
key: str(length) key: str(length)
} }
if parts[3] != 'to': if parts[3] != 'to':
raise vol.Invalid("Multi click grammar: 4th word must be 'to'") raise cv.Invalid("Multi click grammar: 4th word must be 'to'")
try: try:
min_length = cv.positive_time_period_milliseconds(parts[2]) min_length = cv.positive_time_period_milliseconds(parts[2])
except vol.Invalid as err: except cv.Invalid as err:
raise vol.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err)) raise cv.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err))
try: try:
max_length = cv.positive_time_period_milliseconds(parts[4]) max_length = cv.positive_time_period_milliseconds(parts[4])
except vol.Invalid as err: except cv.Invalid as err:
raise vol.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err)) raise cv.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err))
return { return {
CONF_STATE: state, CONF_STATE: state,
@ -124,7 +138,7 @@ def parse_multi_click_timing_str(value):
def validate_multi_click_timing(value): def validate_multi_click_timing(value):
if not isinstance(value, list): if not isinstance(value, list):
raise vol.Invalid("Timing option must be a *list* of times!") raise cv.Invalid("Timing option must be a *list* of times!")
timings = [] timings = []
state = None state = None
for i, v_ in enumerate(value): for i, v_ in enumerate(value):
@ -132,16 +146,16 @@ def validate_multi_click_timing(value):
min_length = v_.get(CONF_MIN_LENGTH) min_length = v_.get(CONF_MIN_LENGTH)
max_length = v_.get(CONF_MAX_LENGTH) max_length = v_.get(CONF_MAX_LENGTH)
if min_length is None and max_length is None: if min_length is None and max_length is None:
raise vol.Invalid("At least one of min_length and max_length is required!") raise cv.Invalid("At least one of min_length and max_length is required!")
if min_length is None and max_length is not None: if min_length is None and max_length is not None:
min_length = core.TimePeriodMilliseconds(milliseconds=0) min_length = core.TimePeriodMilliseconds(milliseconds=0)
new_state = v_.get(CONF_STATE, not state) new_state = v_.get(CONF_STATE, not state)
if new_state == state: if new_state == state:
raise vol.Invalid("Timings must have alternating state. Indices {} and {} have " raise cv.Invalid("Timings must have alternating state. Indices {} and {} have "
"the same state {}".format(i, i + 1, state)) "the same state {}".format(i, i + 1, state))
if max_length is not None and max_length < min_length: if max_length is not None and max_length < min_length:
raise vol.Invalid("Max length ({}) must be larger than min length ({})." raise cv.Invalid("Max length ({}) must be larger than min length ({})."
"".format(max_length, min_length)) "".format(max_length, min_length))
state = new_state state = new_state
@ -155,167 +169,140 @@ def validate_multi_click_timing(value):
return timings return timings
device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space='_')
BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({
cv.GenerateID(CONF_MQTT_ID): cv.declare_variable_id(MQTTBinarySensorComponent), cv.GenerateID(): cv.declare_id(BinarySensor),
cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTBinarySensorComponent),
vol.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_DEVICE_CLASS): device_class,
vol.Optional(CONF_FILTERS): FILTERS_SCHEMA, cv.Optional(CONF_FILTERS): validate_filters,
vol.Optional(CONF_ON_PRESS): automation.validate_automation({ cv.Optional(CONF_ON_PRESS): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(PressTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger),
}), }),
vol.Optional(CONF_ON_RELEASE): automation.validate_automation({ cv.Optional(CONF_ON_RELEASE): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(ReleaseTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger),
}), }),
vol.Optional(CONF_ON_CLICK): automation.validate_automation({ cv.Optional(CONF_ON_CLICK): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(ClickTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
vol.Optional(CONF_MIN_LENGTH, default='50ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_MIN_LENGTH, default='50ms'): cv.positive_time_period_milliseconds,
vol.Optional(CONF_MAX_LENGTH, default='350ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_MAX_LENGTH, default='350ms'): cv.positive_time_period_milliseconds,
}), }),
vol.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation({ cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(DoubleClickTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger),
vol.Optional(CONF_MIN_LENGTH, default='50ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_MIN_LENGTH, default='50ms'): cv.positive_time_period_milliseconds,
vol.Optional(CONF_MAX_LENGTH, default='350ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_MAX_LENGTH, default='350ms'): cv.positive_time_period_milliseconds,
}), }),
vol.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation({ cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(MultiClickTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger),
vol.Required(CONF_TIMING): vol.All([parse_multi_click_timing_str], cv.Required(CONF_TIMING): cv.All([parse_multi_click_timing_str],
validate_multi_click_timing), validate_multi_click_timing),
vol.Optional(CONF_INVALID_COOLDOWN): cv.positive_time_period_milliseconds, cv.Optional(CONF_INVALID_COOLDOWN, default='1s'): cv.positive_time_period_milliseconds,
}), }),
vol.Optional(CONF_ON_STATE): automation.validate_automation({ cv.Optional(CONF_ON_STATE): automation.validate_automation({
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_variable_id(StateTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
}), }),
vol.Optional(CONF_INVERTED): cv.invalid( cv.Optional(CONF_INVERTED): cv.invalid(
"The inverted binary_sensor property has been replaced by the " "The inverted binary_sensor property has been replaced by the "
"new 'invert' binary sensor filter. Please see " "new 'invert' binary sensor filter. Please see "
"https://esphome.io/components/binary_sensor/index.html." "https://esphome.io/components/binary_sensor/index.html."
), ),
}) })
BINARY_SENSOR_PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(BINARY_SENSOR_SCHEMA.schema)
@coroutine
def setup_filter(config): def setup_binary_sensor_core_(var, config):
if CONF_INVERT in config: cg.add(var.set_name(config[CONF_NAME]))
yield InvertFilter.new()
elif CONF_DELAYED_OFF in config:
yield App.register_component(DelayedOffFilter.new(config[CONF_DELAYED_OFF]))
elif CONF_DELAYED_ON in config:
yield App.register_component(DelayedOnFilter.new(config[CONF_DELAYED_ON]))
elif CONF_LAMBDA in config:
for lambda_ in process_lambda(config[CONF_LAMBDA], [(bool_, 'x')],
return_type=optional.template(bool_)):
yield None
yield LambdaFilter.new(lambda_)
def setup_filters(config):
filters = []
for conf in config:
for filter in setup_filter(conf):
yield None
filters.append(filter)
yield filters
def setup_binary_sensor_core_(binary_sensor_var, config):
if CONF_INTERNAL in config: if CONF_INTERNAL in config:
add(binary_sensor_var.set_internal(CONF_INTERNAL)) cg.add(var.set_internal(CONF_INTERNAL))
if CONF_DEVICE_CLASS in config: if CONF_DEVICE_CLASS in config:
add(binary_sensor_var.set_device_class(config[CONF_DEVICE_CLASS])) cg.add(var.set_device_class(config[CONF_DEVICE_CLASS]))
if CONF_INVERTED in config: if CONF_INVERTED in config:
add(binary_sensor_var.set_inverted(config[CONF_INVERTED])) cg.add(var.set_inverted(config[CONF_INVERTED]))
if CONF_FILTERS in config: if CONF_FILTERS in config:
for filters in setup_filters(config[CONF_FILTERS]): filters = yield cg.build_registry_list(FILTER_REGISTRY, config[CONF_FILTERS])
yield cg.add(var.add_filters(filters))
add(binary_sensor_var.add_filters(filters))
for conf in config.get(CONF_ON_PRESS, []): for conf in config.get(CONF_ON_PRESS, []):
rhs = binary_sensor_var.make_press_trigger() trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) yield automation.build_automation(trigger, [], conf)
automation.build_automations(trigger, [], conf)
for conf in config.get(CONF_ON_RELEASE, []): for conf in config.get(CONF_ON_RELEASE, []):
rhs = binary_sensor_var.make_release_trigger() trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) yield automation.build_automation(trigger, [], conf)
automation.build_automations(trigger, [], conf)
for conf in config.get(CONF_ON_CLICK, []): for conf in config.get(CONF_ON_CLICK, []):
rhs = binary_sensor_var.make_click_trigger(conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH]) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var,
trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH])
automation.build_automations(trigger, [], conf) yield automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DOUBLE_CLICK, []): for conf in config.get(CONF_ON_DOUBLE_CLICK, []):
rhs = binary_sensor_var.make_double_click_trigger(conf[CONF_MIN_LENGTH], trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var,
conf[CONF_MAX_LENGTH]) conf[CONF_MIN_LENGTH], conf[CONF_MAX_LENGTH])
trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) yield automation.build_automation(trigger, [], conf)
automation.build_automations(trigger, [], conf)
for conf in config.get(CONF_ON_MULTI_CLICK, []): for conf in config.get(CONF_ON_MULTI_CLICK, []):
timings = [] timings = []
for tim in conf[CONF_TIMING]: for tim in conf[CONF_TIMING]:
timings.append(StructInitializer( timings.append(cg.StructInitializer(
MultiClickTriggerEvent, MultiClickTriggerEvent,
('state', tim[CONF_STATE]), ('state', tim[CONF_STATE]),
('min_length', tim[CONF_MIN_LENGTH]), ('min_length', tim[CONF_MIN_LENGTH]),
('max_length', tim.get(CONF_MAX_LENGTH, 4294967294)), ('max_length', tim.get(CONF_MAX_LENGTH, 4294967294)),
)) ))
rhs = App.register_component(binary_sensor_var.make_multi_click_trigger(timings)) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings)
trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs)
if CONF_INVALID_COOLDOWN in conf: if CONF_INVALID_COOLDOWN in conf:
add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))
automation.build_automations(trigger, [], conf) yield cg.register_component(trigger, conf)
yield automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_STATE, []): for conf in config.get(CONF_ON_STATE, []):
rhs = binary_sensor_var.make_state_trigger() trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) yield automation.build_automation(trigger, [(bool, 'x')], conf)
automation.build_automations(trigger, [(bool_, 'x')], conf)
setup_mqtt_component(binary_sensor_var.Pget_mqtt(), config) if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
yield mqtt.register_mqtt_component(mqtt_, config)
def setup_binary_sensor(binary_sensor_obj, config):
if not CORE.has_id(config[CONF_ID]):
binary_sensor_obj = Pvariable(config[CONF_ID], binary_sensor_obj, has_side_effects=True)
CORE.add_job(setup_binary_sensor_core_, binary_sensor_obj, config)
@coroutine
def register_binary_sensor(var, config): def register_binary_sensor(var, config):
binary_sensor_var = Pvariable(config[CONF_ID], var, has_side_effects=True) if not CORE.has_id(config[CONF_ID]):
add(App.register_binary_sensor(binary_sensor_var)) var = cg.Pvariable(config[CONF_ID], var)
CORE.add_job(setup_binary_sensor_core_, binary_sensor_var, config) cg.add(cg.App.register_binary_sensor(var))
yield setup_binary_sensor_core_(var, config)
BUILD_FLAGS = '-DUSE_BINARY_SENSOR' @coroutine
def new_binary_sensor(config):
var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME])
yield register_binary_sensor(var, config)
yield var
CONF_BINARY_SENSOR_IS_ON = 'binary_sensor.is_on'
BINARY_SENSOR_IS_ON_CONDITION_SCHEMA = maybe_simple_id({ BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id({
vol.Required(CONF_ID): cv.use_variable_id(BinarySensor), cv.Required(CONF_ID): cv.use_id(BinarySensor),
vol.Optional(CONF_FOR): cv.positive_time_period_milliseconds, cv.Optional(CONF_FOR): cv.invalid("This option has been removed in 1.13, please use the "
"'for' condition instead."),
}) })
@CONDITION_REGISTRY.register(CONF_BINARY_SENSOR_IS_ON, BINARY_SENSOR_IS_ON_CONDITION_SCHEMA) @automation.register_condition('binary_sensor.is_on', BinarySensorCondition,
BINARY_SENSOR_CONDITION_SCHEMA)
def binary_sensor_is_on_to_code(config, condition_id, template_arg, args): def binary_sensor_is_on_to_code(config, condition_id, template_arg, args):
for var in get_variable(config[CONF_ID]): paren = yield cg.get_variable(config[CONF_ID])
yield None yield cg.new_Pvariable(condition_id, template_arg, paren, True)
rhs = var.make_binary_sensor_is_on_condition(template_arg, config.get(CONF_FOR))
type = BinarySensorCondition.template(template_arg)
yield Pvariable(condition_id, rhs, type=type)
CONF_BINARY_SENSOR_IS_OFF = 'binary_sensor.is_off' @automation.register_condition('binary_sensor.is_off', BinarySensorCondition,
BINARY_SENSOR_IS_OFF_CONDITION_SCHEMA = maybe_simple_id({ BINARY_SENSOR_CONDITION_SCHEMA)
vol.Required(CONF_ID): cv.use_variable_id(BinarySensor),
vol.Optional(CONF_FOR): cv.positive_time_period_milliseconds,
})
@CONDITION_REGISTRY.register(CONF_BINARY_SENSOR_IS_OFF, BINARY_SENSOR_IS_OFF_CONDITION_SCHEMA)
def binary_sensor_is_off_to_code(config, condition_id, template_arg, args): def binary_sensor_is_off_to_code(config, condition_id, template_arg, args):
for var in get_variable(config[CONF_ID]): paren = yield cg.get_variable(config[CONF_ID])
yield None yield cg.new_Pvariable(condition_id, template_arg, paren, False)
rhs = var.make_binary_sensor_is_off_condition(template_arg, config.get(CONF_FOR))
type = BinarySensorCondition.template(template_arg)
yield Pvariable(condition_id, rhs, type=type) @coroutine_with_priority(100.0)
def to_code(config):
cg.add_define('USE_BINARY_SENSOR')
cg.add_global(binary_sensor_ns.using)

View file

@ -1,32 +0,0 @@
import voluptuous as vol
from esphome.components import binary_sensor, sensor
from esphome.components.apds9960 import APDS9960, CONF_APDS9960_ID
import esphome.config_validation as cv
from esphome.const import CONF_DIRECTION, CONF_NAME
from esphome.cpp_generator import get_variable
DEPENDENCIES = ['apds9960']
APDS9960GestureDirectionBinarySensor = sensor.sensor_ns.class_(
'APDS9960GestureDirectionBinarySensor', binary_sensor.BinarySensor)
DIRECTIONS = {
'UP': 'make_up_direction',
'DOWN': 'make_down_direction',
'LEFT': 'make_left_direction',
'RIGHT': 'make_right_direction',
}
PLATFORM_SCHEMA = cv.nameable(binary_sensor.BINARY_SENSOR_PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(APDS9960GestureDirectionBinarySensor),
vol.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True),
cv.GenerateID(CONF_APDS9960_ID): cv.use_variable_id(APDS9960)
}))
def to_code(config):
for hub in get_variable(config[CONF_APDS9960_ID]):
yield
func = getattr(hub, DIRECTIONS[config[CONF_DIRECTION]])
rhs = func(config[CONF_NAME])
binary_sensor.register_binary_sensor(rhs, config)

View file

@ -0,0 +1,113 @@
#include "automation.h"
#include "esphome/core/log.h"
namespace esphome {
namespace binary_sensor {
static const char *TAG = "binary_sensor.automation";
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
// Handle duplicate events
if (state == this->last_state_) {
return;
}
this->last_state_ = state;
// Cooldown: Do not immediately try matching after having invalid timing
if (this->is_in_cooldown_) {
return;
}
if (!this->at_index_.has_value()) {
// Start matching
MultiClickTriggerEvent evt = this->timing_[0];
if (evt.state == state) {
ESP_LOGV(TAG, "START min=%u max=%u", evt.min_length, evt.max_length);
ESP_LOGV(TAG, "Multi Click: Starting multi click action!");
this->at_index_ = 1;
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
} else {
this->schedule_is_valid_(evt.min_length);
this->schedule_is_not_valid_(evt.max_length);
}
} else {
ESP_LOGV(TAG, "Multi Click: action not started because first level does not match!");
}
return;
}
if (!this->is_valid_) {
this->schedule_cooldown_();
return;
}
if (*this->at_index_ == this->timing_.size()) {
this->trigger_();
return;
}
MultiClickTriggerEvent evt = this->timing_[*this->at_index_];
if (evt.max_length != 4294967294UL) {
ESP_LOGV(TAG, "A i=%u min=%u max=%u", *this->at_index_, evt.min_length, evt.max_length); // NOLINT
this->schedule_is_valid_(evt.min_length);
this->schedule_is_not_valid_(evt.max_length);
} else if (*this->at_index_ + 1 != this->timing_.size()) {
ESP_LOGV(TAG, "B i=%u min=%u", *this->at_index_, evt.min_length); // NOLINT
this->cancel_timeout("is_not_valid");
this->schedule_is_valid_(evt.min_length);
} else {
ESP_LOGV(TAG, "C i=%u min=%u", *this->at_index_, evt.min_length); // NOLINT
this->is_valid_ = false;
this->cancel_timeout("is_not_valid");
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
}
*this->at_index_ = *this->at_index_ + 1;
}
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %u ms...", this->invalid_cooldown_);
this->is_in_cooldown_ = true;
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again.");
this->is_in_cooldown_ = false;
});
this->at_index_.reset();
this->cancel_timeout("trigger");
this->cancel_timeout("is_valid");
this->cancel_timeout("is_not_valid");
}
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
this->is_valid_ = false;
this->set_timeout("is_valid", min_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = true;
});
}
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout("is_not_valid", max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false;
this->schedule_cooldown_();
});
}
void binary_sensor::MultiClickTrigger::trigger_() {
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
this->at_index_.reset();
this->cancel_timeout("trigger");
this->cancel_timeout("is_valid");
this->cancel_timeout("is_not_valid");
this->trigger();
}
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
if (max_length == 0) {
return length >= min_length;
} else {
return length >= min_length && length <= max_length;
}
}
} // namespace binary_sensor
} // namespace esphome

View file

@ -0,0 +1,150 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace binary_sensor {
struct MultiClickTriggerEvent {
bool state;
uint32_t min_length;
uint32_t max_length;
};
class PressTrigger : public Trigger<> {
public:
explicit PressTrigger(BinarySensor *parent) {
parent->add_on_state_callback([this](bool state) {
if (state)
this->trigger();
});
}
};
class ReleaseTrigger : public Trigger<> {
public:
explicit ReleaseTrigger(BinarySensor *parent) {
parent->add_on_state_callback([this](bool state) {
if (!state)
this->trigger();
});
}
};
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length);
class ClickTrigger : public Trigger<> {
public:
explicit ClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
: min_length_(min_length), max_length_(max_length) {
parent->add_on_state_callback([this](bool state) {
if (state) {
this->start_time_ = millis();
} else {
const uint32_t length = millis() - this->start_time_;
if (match_interval(this->min_length_, this->max_length_, length))
this->trigger();
}
});
}
protected:
uint32_t start_time_{0}; /// The millis() time when the click started.
uint32_t min_length_; /// Minimum length of click. 0 means no minimum.
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
};
class DoubleClickTrigger : public Trigger<> {
public:
explicit DoubleClickTrigger(BinarySensor *parent, uint32_t min_length, uint32_t max_length)
: min_length_(min_length), max_length_(max_length) {
parent->add_on_state_callback([this](bool state) {
const uint32_t now = millis();
if (state && this->start_time_ != 0 && this->end_time_ != 0) {
if (match_interval(this->min_length_, this->max_length_, this->end_time_ - this->start_time_) &&
match_interval(this->min_length_, this->max_length_, now - this->end_time_)) {
this->trigger();
this->start_time_ = 0;
this->end_time_ = 0;
return;
}
}
this->start_time_ = this->end_time_;
this->end_time_ = now;
});
}
protected:
uint32_t start_time_{0};
uint32_t end_time_{0};
uint32_t min_length_; /// Minimum length of click. 0 means no minimum.
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
};
class MultiClickTrigger : public Trigger<>, public Component {
public:
explicit MultiClickTrigger(BinarySensor *parent, const std::vector<MultiClickTriggerEvent> &timing)
: parent_(parent), timing_(timing) {}
void setup() override {
this->last_state_ = this->parent_->state;
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
this->parent_->add_on_state_callback(f);
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void set_invalid_cooldown(uint32_t invalid_cooldown) { this->invalid_cooldown_ = invalid_cooldown; }
protected:
void on_state_(bool state);
void schedule_cooldown_();
void schedule_is_valid_(uint32_t min_length);
void schedule_is_not_valid_(uint32_t max_length);
void trigger_();
BinarySensor *parent_;
std::vector<MultiClickTriggerEvent> timing_;
uint32_t invalid_cooldown_{1000};
optional<size_t> at_index_{};
bool last_state_{false};
bool is_in_cooldown_{false};
bool is_valid_{false};
};
class StateTrigger : public Trigger<bool> {
public:
explicit StateTrigger(BinarySensor *parent) {
parent->add_on_state_callback([this](bool state) { this->trigger(state); });
}
};
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
bool check(Ts... x) override { return this->parent_->state == this->state_; }
protected:
BinarySensor *parent_;
bool state_;
};
template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...> {
public:
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
TEMPLATABLE_VALUE(bool, state)
void play(Ts... x) override {
auto val = this->state_.value(x...);
this->sensor_->publish_state(val);
}
protected:
BinarySensor *sensor_;
};
} // namespace binary_sensor
} // namespace esphome

View file

@ -0,0 +1,71 @@
#include "binary_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace binary_sensor {
static const char *TAG = "binary_sensor";
void BinarySensor::add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, false);
} else {
this->filter_list_->input(state, false);
}
}
void BinarySensor::publish_initial_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
}
}
void BinarySensor::send_state_internal(bool state, bool is_initial) {
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state ? "ON" : "OFF");
this->has_state_ = true;
this->state = state;
if (!is_initial) {
this->state_callback_.call(state);
}
}
std::string BinarySensor::device_class() { return ""; }
BinarySensor::BinarySensor(const std::string &name) : Nameable(name), state(false) {}
BinarySensor::BinarySensor() : BinarySensor("") {}
void BinarySensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; }
std::string BinarySensor::get_device_class() {
if (this->device_class_.has_value())
return *this->device_class_;
return this->device_class();
}
void BinarySensor::add_filter(Filter *filter) {
filter->parent_ = this;
if (this->filter_list_ == nullptr) {
this->filter_list_ = filter;
} else {
Filter *last_filter = this->filter_list_;
while (last_filter->next_ != nullptr)
last_filter = last_filter->next_;
last_filter->next_ = filter;
}
}
void BinarySensor::add_filters(std::vector<Filter *> filters) {
for (Filter *filter : filters) {
this->add_filter(filter);
}
}
bool BinarySensor::has_state() const { return this->has_state_; }
uint32_t BinarySensor::hash_base() { return 1210250844UL; }
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace binary_sensor
} // namespace esphome

View file

@ -0,0 +1,90 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/filter.h"
namespace esphome {
namespace binary_sensor {
#define LOG_BINARY_SENSOR(prefix, type, obj) \
if (obj != nullptr) { \
ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \
if (!obj->get_device_class().empty()) { \
ESP_LOGCONFIG(TAG, prefix " Device Class: '%s'", obj->get_device_class().c_str()); \
} \
}
/** Base class for all binary_sensor-type classes.
*
* This class includes a callback that components such as MQTT can subscribe to for state changes.
* The sub classes should notify the front-end of new states via the publish_state() method which
* handles inverted inputs for you.
*/
class BinarySensor : public Nameable {
public:
explicit BinarySensor();
/** Construct a binary sensor with the specified name
*
* @param name Name of this binary sensor.
*/
explicit BinarySensor(const std::string &name);
/** Add a callback to be notified of state changes.
*
* @param callback The void(bool) callback.
*/
void add_on_state_callback(std::function<void(bool)> &&callback);
/** Publish a new state to the front-end.
*
* @param state The new state.
*/
void publish_state(bool state);
/** Publish the initial state, this will not make the callback manager send callbacks
* and is meant only for the initial state on boot.
*
* @param state The new state.
*/
void publish_initial_state(bool state);
/// The current reported state of the binary sensor.
bool state;
/// Manually set the Home Assistant device class (see binary_sensor::device_class)
void set_device_class(const std::string &device_class);
/// Get the device class for this binary sensor, using the manual override if specified.
std::string get_device_class();
void add_filter(Filter *filter);
void add_filters(std::vector<Filter *> filters);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void send_state_internal(bool state, bool is_initial);
/// Return whether this binary sensor has outputted a state.
bool has_state() const;
virtual bool is_status_binary_sensor() const;
// ========== OVERRIDE METHODS ==========
// (You'll only need this when creating your own custom binary sensor)
/// Get the default device class for this sensor, or empty string for no default.
virtual std::string device_class();
protected:
uint32_t hash_base() override;
CallbackManager<void(bool)> state_callback_{};
optional<std::string> device_class_{}; ///< Stores the override of the device class
Filter *filter_list_{nullptr};
bool has_state_{false};
Deduplicator<bool> publish_dedup_;
};
} // namespace binary_sensor
} // namespace esphome

View file

@ -1,35 +0,0 @@
import voluptuous as vol
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_BINARY_SENSORS, CONF_ID, CONF_LAMBDA, CONF_NAME
from esphome.cpp_generator import add, process_lambda, variable
from esphome.cpp_types import std_vector
CustomBinarySensorConstructor = binary_sensor.binary_sensor_ns.class_(
'CustomBinarySensorConstructor')
PLATFORM_SCHEMA = binary_sensor.PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(CustomBinarySensorConstructor),
vol.Required(CONF_LAMBDA): cv.lambda_,
vol.Required(CONF_BINARY_SENSORS):
cv.ensure_list(binary_sensor.BINARY_SENSOR_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(binary_sensor.BinarySensor),
})),
})
def to_code(config):
for template_ in process_lambda(config[CONF_LAMBDA], [],
return_type=std_vector.template(binary_sensor.BinarySensorPtr)):
yield
rhs = CustomBinarySensorConstructor(template_)
custom = variable(config[CONF_ID], rhs)
for i, conf in enumerate(config[CONF_BINARY_SENSORS]):
rhs = custom.Pget_binary_sensor(i)
add(rhs.set_name(conf[CONF_NAME]))
binary_sensor.register_binary_sensor(rhs, conf)
BUILD_FLAGS = '-DUSE_CUSTOM_BINARY_SENSOR'

View file

@ -1,25 +0,0 @@
import voluptuous as vol
from esphome.components import binary_sensor
from esphome.components.esp32_ble_tracker import CONF_ESP32_BLE_ID, ESP32BLETracker, \
make_address_array
import esphome.config_validation as cv
from esphome.const import CONF_MAC_ADDRESS, CONF_NAME
from esphome.cpp_generator import get_variable
from esphome.cpp_types import esphome_ns
DEPENDENCIES = ['esp32_ble_tracker']
ESP32BLEPresenceDevice = esphome_ns.class_('ESP32BLEPresenceDevice', binary_sensor.BinarySensor)
PLATFORM_SCHEMA = cv.nameable(binary_sensor.BINARY_SENSOR_PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(ESP32BLEPresenceDevice),
vol.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.GenerateID(CONF_ESP32_BLE_ID): cv.use_variable_id(ESP32BLETracker)
}))
def to_code(config):
for hub in get_variable(config[CONF_ESP32_BLE_ID]):
yield
rhs = hub.make_presence_sensor(config[CONF_NAME], make_address_array(config[CONF_MAC_ADDRESS]))
binary_sensor.register_binary_sensor(rhs, config)

View file

@ -1,58 +0,0 @@
import voluptuous as vol
from esphome.components import binary_sensor
from esphome.components.esp32_touch import ESP32TouchComponent
import esphome.config_validation as cv
from esphome.const import CONF_NAME, CONF_PIN, CONF_THRESHOLD, ESP_PLATFORM_ESP32
from esphome.cpp_generator import get_variable
from esphome.cpp_types import global_ns
from esphome.pins import validate_gpio_pin
ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
DEPENDENCIES = ['esp32_touch']
CONF_ESP32_TOUCH_ID = 'esp32_touch_id'
TOUCH_PADS = {
4: global_ns.TOUCH_PAD_NUM0,
0: global_ns.TOUCH_PAD_NUM1,
2: global_ns.TOUCH_PAD_NUM2,
15: global_ns.TOUCH_PAD_NUM3,
13: global_ns.TOUCH_PAD_NUM4,
12: global_ns.TOUCH_PAD_NUM5,
14: global_ns.TOUCH_PAD_NUM6,
27: global_ns.TOUCH_PAD_NUM7,
33: global_ns.TOUCH_PAD_NUM8,
32: global_ns.TOUCH_PAD_NUM9,
}
def validate_touch_pad(value):
value = validate_gpio_pin(value)
if value not in TOUCH_PADS:
raise vol.Invalid("Pin {} does not support touch pads.".format(value))
return value
ESP32TouchBinarySensor = binary_sensor.binary_sensor_ns.class_('ESP32TouchBinarySensor',
binary_sensor.BinarySensor)
PLATFORM_SCHEMA = cv.nameable(binary_sensor.BINARY_SENSOR_PLATFORM_SCHEMA.extend({
cv.GenerateID(): cv.declare_variable_id(ESP32TouchBinarySensor),
vol.Required(CONF_PIN): validate_touch_pad,
vol.Required(CONF_THRESHOLD): cv.uint16_t,
cv.GenerateID(CONF_ESP32_TOUCH_ID): cv.use_variable_id(ESP32TouchComponent),
}))
def to_code(config):
hub = None
for hub in get_variable(config[CONF_ESP32_TOUCH_ID]):
yield
touch_pad = TOUCH_PADS[config[CONF_PIN]]
rhs = hub.make_touch_pad(config[CONF_NAME], touch_pad, config[CONF_THRESHOLD])
binary_sensor.register_binary_sensor(rhs, config)
BUILD_FLAGS = '-DUSE_ESP32_TOUCH_BINARY_SENSOR'

View file

@ -0,0 +1,68 @@
#include "filter.h"
#include "binary_sensor.h"
namespace esphome {
namespace binary_sensor {
static const char *TAG = "sensor.filter";
void Filter::output(bool value, bool is_initial) {
if (!this->dedup_.next(value))
return;
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value, is_initial);
} else {
this->next_->input(value, is_initial);
}
}
void Filter::input(bool value, bool is_initial) {
auto b = this->new_value(value, is_initial);
if (b.has_value()) {
this->output(*b, is_initial);
}
}
DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {}
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
if (value) {
this->set_timeout("ON", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
return {};
} else {
this->cancel_timeout("ON");
return false;
}
}
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
DelayedOffFilter::DelayedOffFilter(uint32_t delay) : delay_(delay) {}
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
if (!value) {
this->set_timeout("OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
return {};
} else {
this->cancel_timeout("OFF");
return true;
}
}
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
LambdaFilter::LambdaFilter(const std::function<optional<bool>(bool)> &f) : f_(f) {}
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> UniqueFilter::new_value(bool value, bool is_initial) {
if (this->last_value_.has_value() && *this->last_value_ == value) {
return {};
} else {
this->last_value_ = value;
return value;
}
}
} // namespace binary_sensor
} // namespace esphome

View file

@ -0,0 +1,77 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace binary_sensor {
class BinarySensor;
class Filter {
public:
virtual optional<bool> new_value(bool value, bool is_initial) = 0;
void input(bool value, bool is_initial);
void output(bool value, bool is_initial);
protected:
friend BinarySensor;
Filter *next_{nullptr};
BinarySensor *parent_{nullptr};
Deduplicator<bool> dedup_;
};
class DelayedOnFilter : public Filter, public Component {
public:
explicit DelayedOnFilter(uint32_t delay);
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
protected:
uint32_t delay_;
};
class DelayedOffFilter : public Filter, public Component {
public:
explicit DelayedOffFilter(uint32_t delay);
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
protected:
uint32_t delay_;
};
class InvertFilter : public Filter {
public:
optional<bool> new_value(bool value, bool is_initial) override;
};
class LambdaFilter : public Filter {
public:
explicit LambdaFilter(const std::function<optional<bool>(bool)> &f);
optional<bool> new_value(bool value, bool is_initial) override;
protected:
std::function<optional<bool>(bool)> f_;
};
class UniqueFilter : public Filter {
public:
optional<bool> new_value(bool value, bool is_initial) override;
protected:
optional<bool> last_value_{};
};
} // namespace binary_sensor
} // namespace esphome

Some files were not shown because too many files have changed in this diff Show more