diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..4add58dfbe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +blank_issues_enabled: false +contact_links: + - name: Issue Tracker + url: https://github.com/esphome/issues + about: Please create bug reports in the dedicated issue tracker. + - name: Feature Request Tracker + url: https://github.com/esphome/feature-requests + about: Please create feature requests in the dedicated feature request tracker. + - name: Frequently Asked Question + url: https://esphome.io/guides/faq.html + about: Please view the FAQ for common questions and what to include in a bug report. + diff --git a/.github/ci-reporter.yml b/.github/ci-reporter.yml deleted file mode 100644 index 243e671532..0000000000 --- a/.github/ci-reporter.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Set to false to create a new comment instead of updating the app's first one -updateComment: true - -# Use a custom string, or set to false to disable -before: "✨ Good work on this PR so far! ✨ Unfortunately, the [ build]() is failing as of . Here's the output:" - -# Use a custom string, or set to false to disable -after: "Thanks for contributing to this project!" diff --git a/.github/config.yml b/.github/config.yml deleted file mode 100644 index f2b357cc95..0000000000 --- a/.github/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Configuration for sentiment-bot - https://github.com/behaviorbot/sentiment-bot - -# *Required* toxicity threshold between 0 and .99 with the higher numbers being the most toxic -# Anything higher than this threshold will be marked as toxic and commented on -sentimentBotToxicityThreshold: .8 - -# *Required* Comment to reply with -sentimentBotReplyComment: > - Please be sure to review the code of conduct and be respectful of other users. - -# Note: the bot will only work if your repository has a Code of Conduct diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..d73adbfa30 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + ignore: + # Hypotehsis is only used for testing and is updated quite often + - dependency-name: hypothesis diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml new file mode 100644 index 0000000000..8036470f52 --- /dev/null +++ b/.github/workflows/ci-docker.yml @@ -0,0 +1,54 @@ +name: CI for docker images + +# Only run when docker paths change +on: + push: + branches: [dev, beta, master] + paths: + - 'docker/**' + - '.github/workflows/**' + + pull_request: + paths: + - 'docker/**' + - '.github/workflows/**' + +jobs: + check-docker: + name: Build docker containers + runs-on: ubuntu-latest + strategy: + matrix: + arch: [amd64, armv7, aarch64] + build_type: ["hassio", "docker"] + steps: + - uses: actions/checkout@v2 + - name: Set up env variables + run: | + base_version="2.6.0" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + fi + + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:dev" || true + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=ci" \ + --cache-from "${BUILD_TO}:dev" \ + --file "${DOCKERFILE}" \ + . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..a6b3c5566b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,176 @@ +# THESE JOBS ARE COPIED IN release.yml and release-dev.yml +# PLEASE ALSO UPDATE THOSE FILES WHEN CHANGING LINES HERE +name: CI + +on: + push: + # On dev branch release-dev already performs CI checks + # On other branches the `pull_request` trigger will be used + branches: [beta, master] + + pull_request: + +jobs: + lint-clang-format: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:latest + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:latest + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # Don't use the esphome-lint docker image because it may contain outdated requirements. + # This way, all dependencies are cached via the cache action. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Lint Custom + run: script/ci-custom.py + - name: Lint Python + run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests diff --git a/.github/workflows/matchers/ci-custom.json b/.github/workflows/matchers/ci-custom.json new file mode 100644 index 0000000000..4e1eafff5e --- /dev/null +++ b/.github/workflows/matchers/ci-custom.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "ci-custom", + "pattern": [ + { + "regexp": "^ERROR (.*):(\\d+):(\\d+) - (.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/clang-tidy.json b/.github/workflows/matchers/clang-tidy.json new file mode 100644 index 0000000000..03e77341a5 --- /dev/null +++ b/.github/workflows/matchers/clang-tidy.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "clang-tidy", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*) \\[([a-z0-9,\\-]+)\\]\\s*$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/gcc.json b/.github/workflows/matchers/gcc.json new file mode 100644 index 0000000000..899239f816 --- /dev/null +++ b/.github/workflows/matchers/gcc.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "gcc", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/lint-python.json b/.github/workflows/matchers/lint-python.json new file mode 100644 index 0000000000..decbe36c4a --- /dev/null +++ b/.github/workflows/matchers/lint-python.json @@ -0,0 +1,28 @@ +{ + "problemMatcher": [ + { + "owner": "flake8", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+) - ([EFCDNW]\\d{3}.*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + }, + { + "owner": "pylint", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+) - (\\[[EFCRW]\\d{4}\\(.*\\),.*\\].*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/python.json b/.github/workflows/matchers/python.json new file mode 100644 index 0000000000..9c3095c0c9 --- /dev/null +++ b/.github/workflows/matchers/python.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "python", + "pattern": [ + { + "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", + "message": 2 + } + ] + } + ] +} diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml new file mode 100644 index 0000000000..81c3a80b05 --- /dev/null +++ b/.github/workflows/release-dev.yml @@ -0,0 +1,262 @@ +name: Publish dev releases to docker hub + +on: + push: + branches: + - dev + +jobs: + # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml + + lint-clang-format: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:latest + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:latest + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # Don't use the esphome-lint docker image because it may contain outdated requirements. + # This way, all dependencies are cached via the cache action. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Lint Custom + run: script/ci-custom.py + - name: Lint Python + run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests + + deploy-docker: + name: Build and publish docker containers + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + strategy: + matrix: + arch: [amd64, armv7, aarch64] + # Hassio dev image doesn't use esphome/esphome-hassio-$arch and uses base directly + build_type: ["docker"] + steps: + - uses: actions/checkout@v2 + - name: Set TAG + run: | + TAG="${GITHUB_SHA:0:7}" + echo "::set-env name=TAG::${TAG}" + - name: Set up env variables + run: | + base_version="2.6.0" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + fi + + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:dev" || true + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=${TAG}" \ + --tag "${BUILD_TO}:${TAG}" \ + --tag "${BUILD_TO}:dev" \ + --cache-from "${BUILD_TO}:dev" \ + --file "${DOCKERFILE}" \ + . + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - run: | + docker push "${BUILD_TO}:${TAG}" + docker push "${BUILD_TO}:dev" + + + deploy-docker-manifest: + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - name: Enable experimental manifest support + run: | + mkdir -p ~/.docker + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - name: Set TAG + run: | + TAG="${GITHUB_SHA:0:7}" + echo "::set-env name=TAG::${TAG}" + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - name: "Create the manifest" + run: | + docker manifest create esphome/esphome:${TAG} \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:${TAG} + + docker manifest create esphome/esphome:dev \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..7ac80355f2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,325 @@ +name: Publish Release + +on: + release: + types: [published] + +jobs: + # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml + + lint-clang-format: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:latest + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:latest + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # Don't use the esphome-lint docker image because it may contain outdated requirements. + # This way, all dependencies are cached via the cache action. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Lint Custom + run: script/ci-custom.py + - name: Lint Python + run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests + + deploy-pypi: + name: Build and publish to PyPi + if: github.repository == 'esphome/esphome' + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Set up python environment + run: | + script/setup + pip install setuptools wheel twine + - name: Build + run: python setup.py sdist bdist_wheel + - name: Upload + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: twine upload dist/* + + deploy-docker: + name: Build and publish docker containers + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + strategy: + matrix: + arch: [amd64, armv7, aarch64] + build_type: ["hassio", "docker"] + steps: + - uses: actions/checkout@v2 + - name: Set TAG + run: | + TAG="${GITHUB_REF#refs/tags/v}" + echo "::set-env name=TAG::${TAG}" + - name: Set up env variables + run: | + base_version="2.6.0" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + fi + + if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then + cache_tag="beta" + else + cache_tag="latest" + fi + + # Set env variables so these values don't need to be calculated again + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + echo "::set-env name=CACHE_TAG::${cache_tag}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:${CACHE_TAG}" || true + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=${TAG}" \ + --tag "${BUILD_TO}:${TAG}" \ + --cache-from "${BUILD_TO}:${CACHE_TAG}" \ + --file "${DOCKERFILE}" \ + . + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - run: docker push "${BUILD_TO}:${TAG}" + + # Always publish to beta tag (also full releases) + - name: Publish docker beta tag + run: | + docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:beta" + docker push "${BUILD_TO}:beta" + + - if: ${{ !github.event.release.prerelease }} + name: Publish docker latest tag + run: | + docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:latest" + docker push "${BUILD_TO}:latest" + + deploy-docker-manifest: + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - name: Enable experimental manifest support + run: | + mkdir -p ~/.docker + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - name: Set TAG + run: | + TAG="${GITHUB_REF#refs/tags/v}" + echo "::set-env name=TAG::${TAG}" + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - name: "Create the manifest" + run: | + docker manifest create esphome/esphome:${TAG} \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:${TAG} + + - name: Publish docker beta tag + run: | + docker manifest create esphome/esphome:beta \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:beta + + - name: Publish docker latest tag + if: ${{ !github.event.release.prerelease }} + run: | + docker manifest create esphome/esphome:latest \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:latest + + deploy-hassio-repo: + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - env: + TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} + run: | + TAG="${GITHUB_REF#refs/tags/v}" + curl \ + -u ":$TOKEN" \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ + -d "{\"ref\":\"master\",\"inputs\":{\"version\":\"$TAG\"}}" diff --git a/.gitignore b/.gitignore index 2de9dd40e5..11a80a647c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ htmlcov/ .esphome nosetests.xml coverage.xml +cov.xml *.cover .hypothesis/ .pytest_cache/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 3db0b982ae..0000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,342 +0,0 @@ ---- -# Based on https://gitlab.com/hassio-addons/addon-node-red/blob/master/.gitlab-ci.yml -variables: - DOCKER_DRIVER: overlay2 - DOCKER_HOST: tcp://docker:2375/ - BASE_VERSION: '2.0.1' - TZ: UTC - -stages: - - lint - - test - - deploy - -.lint: &lint - image: esphome/esphome-lint:latest - stage: lint - before_script: - - script/setup - tags: - - docker - -.test: &test - image: esphome/esphome-lint:latest - stage: test - before_script: - - script/setup - tags: - - docker - -.docker-base: &docker-base - image: esphome/esphome-base-builder - before_script: - - docker info - - docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" - script: - - docker run --rm --privileged multiarch/qemu-user-static:4.1.0-1 --reset -p yes - - TAG="${CI_COMMIT_TAG#v}" - - TAG="${TAG:-${CI_COMMIT_SHA:0:7}}" - - echo "Tag ${TAG}" - - - | - if [[ "${IS_HASSIO}" == "YES" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:${BASE_VERSION} - BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH} - DOCKERFILE=docker/Dockerfile.hassio - else - BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:${BASE_VERSION} - if [[ "${BUILD_ARCH}" == "amd64" ]]; then - BUILD_TO=esphome/esphome - else - BUILD_TO=esphome/esphome-${BUILD_ARCH} - fi - DOCKERFILE=docker/Dockerfile - fi - - - | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${BUILD_TO}:${TAG}" \ - --file "${DOCKERFILE}" \ - . - - | - if [[ "${RELEASE}" = "YES" ]]; then - echo "Pushing to ${BUILD_TO}:${TAG}" - docker push "${BUILD_TO}:${TAG}" - fi - - | - if [[ "${LATEST}" = "YES" ]]; then - echo "Pushing to :latest" - docker tag ${BUILD_TO}:${TAG} ${BUILD_TO}:latest - docker push ${BUILD_TO}:latest - fi - - | - if [[ "${BETA}" = "YES" ]]; then - echo "Pushing to :beta" - docker tag \ - ${BUILD_TO}:${TAG} \ - ${BUILD_TO}:beta - docker push ${BUILD_TO}:beta - fi - - | - if [[ "${DEV}" = "YES" ]]; then - echo "Pushing to :dev" - docker tag \ - ${BUILD_TO}:${TAG} \ - ${BUILD_TO}:dev - docker push ${BUILD_TO}:dev - fi - services: - - docker:dind - tags: - - docker - stage: deploy - -lint-custom: - <<: *lint - script: - - script/ci-custom.py - -lint-python: - <<: *lint - script: - - script/lint-python - -lint-tidy: - <<: *lint - script: - - pio init --ide atom - - script/clang-tidy --all-headers --fix - - script/ci-suggest-changes - -lint-format: - <<: *lint - script: - - script/clang-format -i - - script/ci-suggest-changes - -test1: - <<: *test - script: - - esphome tests/test1.yaml compile - -test2: - <<: *test - script: - - esphome tests/test2.yaml compile - -test3: - <<: *test - script: - - esphome tests/test3.yaml compile - -.deploy-pypi: &deploy-pypi - <<: *lint - stage: deploy - script: - - pip install twine wheel - - python setup.py sdist bdist_wheel - - twine upload dist/* - -deploy-release:pypi: - <<: *deploy-pypi - only: - - /^v\d+\.\d+\.\d+$/ - except: - - /^(?!master).+@/ - -deploy-beta:pypi: - <<: *deploy-pypi - only: - - /^v\d+\.\d+\.\d+b\d+$/ - except: - - /^(?!rc).+@/ - -.latest: &latest - <<: *docker-base - only: - - /^v([0-9\.]+)$/ - except: - - branches - -.beta: &beta - <<: *docker-base - only: - - /^v([0-9\.]+b\d+)$/ - except: - - branches - -.dev: &dev - <<: *docker-base - only: - - dev - -aarch64-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "NO" - RELEASE: "YES" -aarch64-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "YES" - RELEASE: "YES" -aarch64-dev-docker: - <<: *dev - variables: - BUILD_ARCH: aarch64 - DEV: "YES" - IS_HASSIO: "NO" -aarch64-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: aarch64 - DEV: "YES" - IS_HASSIO: "YES" -aarch64-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -aarch64-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -amd64-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "NO" - RELEASE: "YES" -amd64-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "YES" - RELEASE: "YES" -amd64-dev-docker: - <<: *dev - variables: - BUILD_ARCH: amd64 - DEV: "YES" - IS_HASSIO: "NO" -amd64-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: amd64 - DEV: "YES" - IS_HASSIO: "YES" -amd64-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -amd64-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -armv7-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "NO" - RELEASE: "YES" -armv7-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "YES" - RELEASE: "YES" -armv7-dev-docker: - <<: *dev - variables: - BUILD_ARCH: armv7 - DEV: "YES" - IS_HASSIO: "NO" -armv7-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: armv7 - DEV: "YES" - IS_HASSIO: "YES" -armv7-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -armv7-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -i386-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "NO" - RELEASE: "YES" -i386-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "YES" - RELEASE: "YES" -i386-dev-docker: - <<: *dev - variables: - BUILD_ARCH: i386 - DEV: "YES" - IS_HASSIO: "NO" -i386-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: i386 - DEV: "YES" - IS_HASSIO: "YES" -i386-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -i386-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ca0a3082db..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -sudo: false -language: python -python: '3.6' -install: script/setup -cache: - directories: - - "~/.platformio" - -matrix: - fast_finish: true - include: - - python: "3.7" - env: TARGET=Lint3.7 - script: - - script/ci-custom.py - - flake8 esphome - - pylint esphome - - python: "3.6" - env: TARGET=Test3.6 - script: - - esphome tests/test1.yaml compile - - esphome tests/test2.yaml compile - - esphome tests/test3.yaml compile - - env: TARGET=Cpp-Lint - dist: trusty - sudo: required - addons: - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-trusty-7 - packages: - - clang-tidy-7 - - clang-format-7 - before_script: - - pio init --ide atom - - 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 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..1722f482ac --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,69 @@ +# This file is generated by script/build_codeowners.py +# People marked here will be automatically requested for a review +# when the code that they own is touched. +# +# Every time an issue is created with a label corresponding to an integration, +# the integration's code owner is automatically notified. + +# Core Code +setup.py @esphome/core +esphome/*.py @esphome/core +esphome/core/* @esphome/core + +# Integrations +esphome/components/ac_dimmer/* @glmnet +esphome/components/adc/* @esphome/core +esphome/components/api/* @OttoWinter +esphome/components/async_tcp/* @OttoWinter +esphome/components/bang_bang/* @OttoWinter +esphome/components/binary_sensor/* @esphome/core +esphome/components/captive_portal/* @OttoWinter +esphome/components/climate/* @esphome/core +esphome/components/climate_ir/* @glmnet +esphome/components/coolix/* @glmnet +esphome/components/cover/* @esphome/core +esphome/components/ct_clamp/* @jesserockz +esphome/components/debug/* @OttoWinter +esphome/components/dfplayer/* @glmnet +esphome/components/dht/* @OttoWinter +esphome/components/exposure_notifications/* @OttoWinter +esphome/components/fastled_base/* @OttoWinter +esphome/components/globals/* @esphome/core +esphome/components/gpio/* @esphome/core +esphome/components/homeassistant/* @OttoWinter +esphome/components/i2c/* @esphome/core +esphome/components/integration/* @OttoWinter +esphome/components/interval/* @esphome/core +esphome/components/json/* @OttoWinter +esphome/components/ledc/* @OttoWinter +esphome/components/light/* @esphome/core +esphome/components/logger/* @esphome/core +esphome/components/network/* @esphome/core +esphome/components/ota/* @esphome/core +esphome/components/output/* @esphome/core +esphome/components/pid/* @OttoWinter +esphome/components/pn532/* @OttoWinter +esphome/components/power_supply/* @esphome/core +esphome/components/restart/* @esphome/core +esphome/components/rf_bridge/* @jesserockz +esphome/components/rtttl/* @glmnet +esphome/components/script/* @esphome/core +esphome/components/sensor/* @esphome/core +esphome/components/shutdown/* @esphome/core +esphome/components/sim800l/* @glmnet +esphome/components/spi/* @esphome/core +esphome/components/substitutions/* @esphome/core +esphome/components/sun/* @OttoWinter +esphome/components/switch/* @esphome/core +esphome/components/tcl112/* @glmnet +esphome/components/time/* @OttoWinter +esphome/components/tm1637/* @glmnet +esphome/components/tuya/binary_sensor/* @jesserockz +esphome/components/tuya/climate/* @jesserockz +esphome/components/tuya/sensor/* @jesserockz +esphome/components/tuya/switch/* @jesserockz +esphome/components/uart/* @esphome/core +esphome/components/ultrasonic/* @OttoWinter +esphome/components/version/* @esphome/core +esphome/components/web_server_base/* @OttoWinter +esphome/components/whirlpool/* @glmnet diff --git a/MANIFEST.in b/MANIFEST.in index cdea2df2a6..0fe80762b3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include LICENSE include README.md +include requirements.txt include esphome/dashboard/templates/*.html recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE recursive-include esphome *.cpp *.h *.tcc diff --git a/docker/Dockerfile b/docker/Dockerfile index 11bbeeda2b..865741a39f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,21 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:2.0.1 +ARG BUILD_FROM=esphome/esphome-base-amd64:2.6.0 FROM ${BUILD_FROM} +# First install requirements to leverage caching when requirements don't change +COPY requirements.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt + +# Then copy esphome and install COPY . . RUN pip3 install --no-cache-dir -e . -ENV USERNAME="" -ENV PASSWORD="" +# Settings for dashboard +ENV USERNAME="" PASSWORD="" +# The directory the user should mount their configuration files to WORKDIR /config +# Set entrypoint to esphome so that the user doesn't have to type 'esphome' +# in every docker command twice ENTRYPOINT ["esphome"] +# When no arguments given, start the dashboard in the workdir CMD ["/config", "dashboard"] diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index a3871e2513..989802ab1e 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM esphome/esphome-base-amd64:2.0.1 +FROM esphome/esphome-base-amd64:2.6.0 COPY . . diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio index e5c9625680..eb7ef23001 100644 --- a/docker/Dockerfile.hassio +++ b/docker/Dockerfile.hassio @@ -1,11 +1,15 @@ ARG BUILD_FROM FROM ${BUILD_FROM} +# First install requirements to leverage caching when requirements don't change +COPY requirements.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt + # Copy root filesystem COPY docker/rootfs/ / -COPY setup.py setup.cfg MANIFEST.in /opt/esphome/ -COPY esphome /opt/esphome/esphome +# Then copy esphome and install +COPY . /opt/esphome/ RUN pip3 install --no-cache-dir -e /opt/esphome # Build arguments diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 2d77502dc2..27f04dd33d 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,18 +1,7 @@ -FROM esphome/esphome-base-amd64:2.0.1 +FROM esphome/esphome-lint-base:2.6.0 -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/* - -COPY requirements_test.txt /requirements_test.txt -RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt +COPY requirements.txt requirements_test.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt -r /requirements_test.txt VOLUME ["/esphome"] WORKDIR /esphome diff --git a/docker/rootfs/etc/cont-init.d/30-esphome.sh b/docker/rootfs/etc/cont-init.d/30-esphome.sh old mode 100644 new mode 100755 diff --git a/docker/rootfs/etc/cont-init.d/40-migrate.sh b/docker/rootfs/etc/cont-init.d/40-migrate.sh old mode 100644 new mode 100755 diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf old mode 100755 new mode 100644 diff --git a/esphome/__main__.py b/esphome/__main__.py index 73723dfa00..79488e9b55 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -12,29 +12,17 @@ from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority from esphome.helpers import color, indent -from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files +from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files, \ + get_serial_ports _LOGGER = logging.getLogger(__name__) -def get_serial_ports(): - # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py - from serial.tools.list_ports import comports - result = [] - for port, desc, info in comports(include_links=True): - if not port: - continue - if "VID:PID" in info: - result.append((port, desc)) - result.sort(key=lambda x: x[0]) - return result - - def choose_prompt(options): if not options: raise EsphomeError("Found no valid options for upload/logging, please make sure relevant " - "sections (ota, mqtt, ...) are in your configuration and/or the device " - "is plugged in.") + "sections (ota, api, mqtt, ...) are in your configuration and/or the " + "device is plugged in.") if len(options) == 1: return options[0][1] @@ -60,8 +48,8 @@ def choose_prompt(options): def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): options = [] - for res, desc in get_serial_ports(): - options.append((f"{res} ({desc})", res)) + for port in get_serial_ports(): + options.append((f"{port.path} ({port.description})", port.path)) if (show_ota and 'ota' in CORE.config) or (show_api and 'api' in CORE.config): options.append((f"Over The Air ({CORE.address})", CORE.address)) if default == 'OTA': @@ -131,6 +119,11 @@ def wrap_to_code(name, comp): def write_cpp(config): + generate_cpp_contents(config) + return write_cpp_file() + + +def generate_cpp_contents(config): _LOGGER.info("Generating C++ source...") for name, component, conf in iter_components(CORE.config): @@ -140,6 +133,8 @@ def write_cpp(config): CORE.flush_tasks() + +def write_cpp_file(): writer.write_platformio_project() code_s = indent(CORE.cpp_main_section) @@ -428,6 +423,8 @@ def parse_args(argv): parser.add_argument('-q', '--quiet', help="Disable all esphome logs.", action='store_true') parser.add_argument('--dashboard', help=argparse.SUPPRESS, action='store_true') + parser.add_argument('-s', '--substitution', nargs=2, action='append', + help='Add a substitution', metavar=('key', 'value')) parser.add_argument('configuration', help='Your YAML configuration file.', nargs='*') subparsers = parser.add_subparsers(help='Commands', dest='command') @@ -532,7 +529,7 @@ def run_esphome(argv): CORE.config_path = conf_path CORE.dashboard = args.dashboard - config = read_config() + config = read_config(dict(args.substitution) if args.substitution else {}) if config is None: return 1 CORE.config = config diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 16f04ac984..17dcd8ac26 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -4,6 +4,8 @@ from esphome import pins from esphome.components import output from esphome.const import CONF_ID, CONF_MIN_POWER, CONF_METHOD +CODEOWNERS = ['@glmnet'] + ac_dimmer_ns = cg.esphome_ns.namespace('ac_dimmer') AcDimmer = ac_dimmer_ns.class_('AcDimmer', output.FloatOutput, cg.Component) diff --git a/esphome/components/adalight/__init__.py b/esphome/components/adalight/__init__.py new file mode 100644 index 0000000000..66fae17f1e --- /dev/null +++ b/esphome/components/adalight/__init__.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_UART_ID + +DEPENDENCIES = ['uart'] + +adalight_ns = cg.esphome_ns.namespace('adalight') +AdalightLightEffect = adalight_ns.class_( + 'AdalightLightEffect', uart.UARTDevice, AddressableLightEffect) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect('adalight', AdalightLightEffect, "Adalight", { + cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent) +}) +def adalight_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + yield uart.register_uart_device(effect, config) + + yield effect diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp new file mode 100644 index 0000000000..1bf357e308 --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -0,0 +1,140 @@ +#include "adalight_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace adalight { + +static const char *TAG = "adalight_light_effect"; + +static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; +static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; + +AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void AdalightLightEffect::start() { + AddressableLightEffect::start(); + + last_ack_ = 0; + last_byte_ = 0; + last_reset_ = 0; +} + +void AdalightLightEffect::stop() { + frame_.resize(0); + + AddressableLightEffect::stop(); +} + +int AdalightLightEffect::get_frame_size_(int led_count) const { + // 3 bytes: Ada + // 2 bytes: LED count + // 1 byte: checksum + // 3 bytes per LED + return 3 + 2 + 1 + led_count * 3; +} + +void AdalightLightEffect::reset_frame_(light::AddressableLight &it) { + int buffer_capacity = get_frame_size_(it.size()); + + frame_.clear(); + frame_.reserve(buffer_capacity); +} + +void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(light::ESPColor::BLACK); + } +} + +void AdalightLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + const uint32_t now = millis(); + + if (now - this->last_ack_ >= ADALIGHT_ACK_INTERVAL) { + ESP_LOGV(TAG, "Sending ACK"); + this->write_str("Ada\n"); + this->last_ack_ = now; + } + + if (!this->last_reset_) { + ESP_LOGW(TAG, "Frame: Reset."); + reset_frame_(it); + blank_all_leds_(it); + this->last_reset_ = now; + } + + if (!this->frame_.empty() && now - this->last_byte_ >= ADALIGHT_RECEIVE_TIMEOUT) { + ESP_LOGW(TAG, "Frame: Receive timeout (size=%zu).", this->frame_.size()); + reset_frame_(it); + blank_all_leds_(it); + } + + if (this->available() > 0) { + ESP_LOGV(TAG, "Frame: Available (size=%d).", this->available()); + } + + while (this->available() != 0) { + uint8_t data; + if (!this->read_byte(&data)) + break; + this->frame_.push_back(data); + this->last_byte_ = now; + + switch (this->parse_frame_(it)) { + case INVALID: + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%d).", this->frame_.size(), this->frame_[0]); + reset_frame_(it); + break; + + case PARTIAL: + break; + + case CONSUMED: + ESP_LOGV(TAG, "Frame: Consumed (size=%zu).", this->frame_.size()); + reset_frame_(it); + break; + } + } +} + +AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableLight &it) { + if (frame_.empty()) + return INVALID; + + // Check header: `Ada` + if (frame_[0] != 'A') + return INVALID; + if (frame_.size() > 1 && frame_[1] != 'd') + return INVALID; + if (frame_.size() > 2 && frame_[2] != 'a') + return INVALID; + + // 3 bytes: Count Hi, Count Lo, Checksum + if (frame_.size() < 6) + return PARTIAL; + + // Check checksum + uint16_t checksum = frame_[3] ^ frame_[4] ^ 0x55; + if (checksum != frame_[5]) + return INVALID; + + // Check if we received the full frame + uint16_t led_count = (frame_[3] << 8) + frame_[4] + 1; + auto buffer_size = get_frame_size_(led_count); + if (frame_.size() < buffer_size) + return PARTIAL; + + // Apply lights + auto accepted_led_count = std::min(led_count, it.size()); + uint8_t *led_data = &frame_[6]; + + for (int led = 0; led < accepted_led_count; led++, led_data += 3) { + auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]); + + it[led].set(light::ESPColor(led_data[0], led_data[1], led_data[2], white)); + } + + return CONSUMED; +} + +} // namespace adalight +} // namespace esphome diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h new file mode 100644 index 0000000000..4f77394ebc --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace adalight { + +class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { + public: + AdalightLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + + protected: + enum Frame { + INVALID, + PARTIAL, + CONSUMED, + }; + + int get_frame_size_(int led_count) const; + void reset_frame_(light::AddressableLight &it); + void blank_all_leds_(light::AddressableLight &it); + Frame parse_frame_(light::AddressableLight &it); + + protected: + uint32_t last_ack_{0}; + uint32_t last_byte_{0}; + uint32_t last_reset_{0}; + std::vector frame_; +}; + +} // namespace adalight +} // namespace esphome diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 2c448d0392..9b1f452131 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -58,7 +58,7 @@ void ADCSensor::update() { } float ADCSensor::sample() { #ifdef ARDUINO_ARCH_ESP32 - float value_v = analogRead(this->pin_) / 4095.0f; + float value_v = analogRead(this->pin_) / 4095.0f; // NOLINT switch (this->attenuation_) { case ADC_0db: value_v *= 1.1; @@ -80,7 +80,7 @@ float ADCSensor::sample() { #ifdef USE_ADC_SENSOR_VCC return ESP.getVcc() / 1024.0f; #else - return analogRead(this->pin_) / 1024.0f; + return analogRead(this->pin_) / 1024.0f; // NOLINT #endif #endif } diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp index 9316d9cad0..c4752abf39 100644 --- a/esphome/components/ade7953/ade7953.cpp +++ b/esphome/components/ade7953/ade7953.cpp @@ -18,7 +18,7 @@ void ADE7953::dump_config() { } #define ADE_PUBLISH_(name, factor) \ - if (name) { \ + if (name && this->name##_sensor_) { \ float value = *name / factor; \ this->name##_sensor_->publish_state(value); \ } diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index eef60602ba..23cfa51d2b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -8,6 +8,7 @@ from esphome.core import coroutine_with_priority DEPENDENCIES = ['network'] AUTO_LOAD = ['async_tcp'] +CODEOWNERS = ['@OttoWinter'] api_ns = cg.esphome_ns.namespace('api') APIServer = api_ns.class_('APIServer', cg.Component, cg.Controller) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4bb7d1b555..403242d236 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -301,12 +301,17 @@ message ListEntitiesFanResponse { bool supports_oscillation = 5; bool supports_speed = 6; + bool supports_direction = 7; } enum FanSpeed { FAN_SPEED_LOW = 0; FAN_SPEED_MEDIUM = 1; FAN_SPEED_HIGH = 2; } +enum FanDirection { + FAN_DIRECTION_FORWARD = 0; + FAN_DIRECTION_REVERSE = 1; +} message FanStateResponse { option (id) = 23; option (source) = SOURCE_SERVER; @@ -317,6 +322,7 @@ message FanStateResponse { bool state = 2; bool oscillating = 3; FanSpeed speed = 4; + FanDirection direction = 5; } message FanCommandRequest { option (id) = 31; @@ -331,6 +337,8 @@ message FanCommandRequest { FanSpeed speed = 5; bool has_oscillating = 6; bool oscillating = 7; + bool has_direction = 8; + FanDirection direction = 9; } // ==================== LIGHT ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index beccf91d27..1956f3119d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -248,6 +248,8 @@ bool APIConnection::send_fan_state(fan::FanState *fan) { resp.oscillating = fan->oscillating; if (traits.supports_speed()) resp.speed = static_cast(fan->speed); + if (traits.supports_direction()) + resp.direction = static_cast(fan->direction); return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::FanState *fan) { @@ -259,6 +261,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.unique_id = get_default_unique_id("fan", fan); msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); + msg.supports_direction = traits.supports_direction(); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -273,6 +276,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { call.set_oscillating(msg.oscillating); if (msg.has_speed) call.set_speed(static_cast(msg.speed)); + if (msg.has_direction) + call.set_direction(static_cast(msg.direction)); call.perform(); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6b98f95f53..c659561aa8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -52,6 +52,16 @@ template<> const char *proto_enum_to_string(enums::FanSpeed val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::FanDirection value) { + switch (value) { + case enums::FAN_DIRECTION_FORWARD: + return "FAN_DIRECTION_FORWARD"; + case enums::FAN_DIRECTION_REVERSE: + return "FAN_DIRECTION_REVERSE"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: @@ -760,6 +770,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->supports_speed = value.as_bool(); return true; } + case 7: { + this->supports_direction = value.as_bool(); + return true; + } default: return false; } @@ -799,6 +813,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); + buffer.encode_bool(7, this->supports_direction); } void ListEntitiesFanResponse::dump_to(std::string &out) const { char buffer[64]; @@ -827,6 +842,10 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" supports_speed: "); out.append(YESNO(this->supports_speed)); out.append("\n"); + + out.append(" supports_direction: "); + out.append(YESNO(this->supports_direction)); + out.append("\n"); out.append("}"); } bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -843,6 +862,10 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed = value.as_enum(); return true; } + case 5: { + this->direction = value.as_enum(); + return true; + } default: return false; } @@ -862,6 +885,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_enum(4, this->speed); + buffer.encode_enum(5, this->direction); } void FanStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -882,6 +906,10 @@ void FanStateResponse::dump_to(std::string &out) const { out.append(" speed: "); out.append(proto_enum_to_string(this->speed)); out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); out.append("}"); } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -910,6 +938,14 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->oscillating = value.as_bool(); return true; } + case 8: { + this->has_direction = value.as_bool(); + return true; + } + case 9: { + this->direction = value.as_enum(); + return true; + } default: return false; } @@ -932,6 +968,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(5, this->speed); buffer.encode_bool(6, this->has_oscillating); buffer.encode_bool(7, this->oscillating); + buffer.encode_bool(8, this->has_direction); + buffer.encode_enum(9, this->direction); } void FanCommandRequest::dump_to(std::string &out) const { char buffer[64]; @@ -964,6 +1002,14 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(" oscillating: "); out.append(YESNO(this->oscillating)); out.append("\n"); + + out.append(" has_direction: "); + out.append(YESNO(this->has_direction)); + out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); out.append("}"); } bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8be89f0365..306bdbf5a9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -28,6 +28,10 @@ enum FanSpeed : uint32_t { FAN_SPEED_MEDIUM = 1, FAN_SPEED_HIGH = 2, }; +enum FanDirection : uint32_t { + FAN_DIRECTION_FORWARD = 0, + FAN_DIRECTION_REVERSE = 1, +}; enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, LOG_LEVEL_ERROR = 1, @@ -279,6 +283,7 @@ class ListEntitiesFanResponse : public ProtoMessage { std::string unique_id{}; // NOLINT bool supports_oscillation{false}; // NOLINT bool supports_speed{false}; // NOLINT + bool supports_direction{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -289,10 +294,11 @@ class ListEntitiesFanResponse : public ProtoMessage { }; class FanStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT - bool oscillating{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + bool oscillating{false}; // NOLINT + enums::FanSpeed speed{}; // NOLINT + enums::FanDirection direction{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -302,13 +308,15 @@ class FanStateResponse : public ProtoMessage { }; class FanCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_state{false}; // NOLINT - bool state{false}; // NOLINT - bool has_speed{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT - bool has_oscillating{false}; // NOLINT - bool oscillating{false}; // NOLINT + uint32_t key{0}; // NOLINT + bool has_state{false}; // NOLINT + bool state{false}; // NOLINT + bool has_speed{false}; // NOLINT + enums::FanSpeed speed{}; // NOLINT + bool has_oscillating{false}; // NOLINT + bool oscillating{false}; // NOLINT + bool has_direction{false}; // NOLINT + enums::FanDirection direction{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d68dac3b61..8a72765195 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -29,6 +29,7 @@ template class HomeAssistantServiceCallAction : public Action void add_variable(std::string key, T value) { this->variables_.push_back(TemplatableKeyValuePair(key, value)); } + void play(Ts... x) override { HomeassistantServiceResponse resp; resp.service = this->service_.value(x...); diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py index de25060623..51958048ca 100644 --- a/esphome/components/as3935/__init__.py +++ b/esphome/components/as3935/__init__.py @@ -25,7 +25,7 @@ AS3935_SCHEMA = cv.Schema({ cv.Optional(CONF_SPIKE_REJECTION, default=2): cv.int_range(min=1, max=11), cv.Optional(CONF_LIGHTNING_THRESHOLD, default=1): cv.one_of(1, 5, 9, 16, int=True), cv.Optional(CONF_MASK_DISTURBER, default=False): cv.boolean, - cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 22, 64, 128, int=True), + cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 32, 64, 128, int=True), cv.Optional(CONF_CAPACITANCE, default=0): cv.int_range(min=0, max=15), }) diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index f8272e6036..9446a2fdd6 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -26,6 +26,9 @@ void AS3935Component::setup() { void AS3935Component::dump_config() { ESP_LOGCONFIG(TAG, "AS3935:"); LOG_PIN(" Interrupt Pin: ", this->irq_pin_); + LOG_BINARY_SENSOR(" ", "Thunder alert", this->thunder_alert_binary_sensor_); + LOG_SENSOR(" ", "Distance", this->distance_sensor_); + LOG_SENSOR(" ", "Lightning energy", this->energy_sensor_); } float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 3374ada6a8..016df8f2a1 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -27,4 +27,4 @@ def to_code(config): if CONF_LIGHTNING_ENERGY in config: conf = config[CONF_LIGHTNING_ENERGY] lightning_energy_sensor = yield sensor.new_sensor(conf) - cg.add(hub.set_distance_sensor(lightning_energy_sensor)) + cg.add(hub.set_energy_sensor(lightning_energy_sensor)) diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py index fa27c2b0f5..30c2240c27 100644 --- a/esphome/components/as3935_spi/__init__.py +++ b/esphome/components/as3935_spi/__init__.py @@ -10,8 +10,8 @@ as3935_spi_ns = cg.esphome_ns.namespace('as3935_spi') SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935.AS3935, spi.SPIDevice) CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SPIAS3935) -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA)) + cv.GenerateID(): cv.declare_id(SPIAS3935), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(cs_pin_required=True))) def to_code(config): diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index cf9d2f1585..b2307d5a7b 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.core import CORE, coroutine_with_priority +CODEOWNERS = ['@OttoWinter'] + @coroutine_with_priority(200.0) def to_code(config): @@ -10,4 +12,4 @@ def to_code(config): cg.add_library('AsyncTCP-esphome', '1.1.1') elif CORE.is_esp8266: # https://github.com/OttoWinter/ESPAsyncTCP - cg.add_library('ESPAsyncTCP-esphome', '1.2.2') + cg.add_library('ESPAsyncTCP-esphome', '1.2.3') diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index fc526dfbc0..f27fd3126a 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -55,7 +55,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), cv.Optional(CONF_CURRENT_PHASES, default='3'): cv.enum(CURRENT_PHASES, upper=True), cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/bang_bang/__init__.py b/esphome/components/bang_bang/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/bang_bang/__init__.py +++ b/esphome/components/bang_bang/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index cf527988fe..45d5174390 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -108,7 +108,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { } if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); + this->prev_trigger_->stop_action(); this->prev_trigger_ = nullptr; } Trigger<> *trig; diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 9cd152e1ef..2ec297a6f4 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -7,6 +7,8 @@ namespace bh1750 { static const char *TAG = "bh1750.sensor"; static const uint8_t BH1750_COMMAND_POWER_ON = 0b00000001; +static const uint8_t BH1750_COMMAND_MT_REG_HI = 0b01000000; // last 3 bits +static const uint8_t BH1750_COMMAND_MT_REG_LO = 0b01100000; // last 5 bits void BH1750Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up BH1750 '%s'...", this->name_.c_str()); @@ -14,7 +16,13 @@ void BH1750Sensor::setup() { this->mark_failed(); return; } + + uint8_t mtreg_hi = (this->measurement_time_ >> 5) & 0b111; + uint8_t mtreg_lo = (this->measurement_time_ >> 0) & 0b11111; + this->write_bytes(BH1750_COMMAND_MT_REG_HI | mtreg_hi, nullptr, 0); + this->write_bytes(BH1750_COMMAND_MT_REG_LO | mtreg_lo, nullptr, 0); } + void BH1750Sensor::dump_config() { LOG_SENSOR("", "BH1750", this); LOG_I2C_DEVICE(this); @@ -59,6 +67,7 @@ void BH1750Sensor::update() { 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; @@ -68,10 +77,12 @@ void BH1750Sensor::read_data_() { } float lx = float(raw_value) / 1.2f; + lx *= 69.0f / this->measurement_time_; 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 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 8df0bda02a..00abd53e92 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -28,6 +28,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: * @param resolution The new resolution of the sensor. */ void set_resolution(BH1750Resolution resolution); + void set_measurement_time(uint8_t measurement_time) { measurement_time_ = measurement_time; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -40,6 +41,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void read_data_(); BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX}; + uint8_t measurement_time_; }; } // namespace bh1750 diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index b3ce0eaf88..54735616d5 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -15,9 +15,11 @@ BH1750_RESOLUTIONS = { BH1750Sensor = bh1750_ns.class_('BH1750Sensor', sensor.Sensor, cg.PollingComponent, i2c.I2CDevice) +CONF_MEASUREMENT_TIME = 'measurement_time' 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), + cv.Optional(CONF_MEASUREMENT_TIME, default=69): cv.int_range(min=31, max=254), }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x23)) @@ -28,3 +30,4 @@ def to_code(config): yield i2c.register_i2c_device(var, config) cg.add(var.set_resolution(config[CONF_RESOLUTION])) + cg.add(var.set_measurement_time(config[CONF_MEASUREMENT_TIME])) diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index dbfe1a8286..6969c1dbbf 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -1,7 +1,8 @@ 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 esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, \ + CONF_OUTPUT, CONF_OUTPUT_ID from .. import binary_ns BinaryFan = binary_ns.class_('BinaryFan', cg.Component) @@ -9,6 +10,7 @@ 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_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), }).extend(cv.COMPONENT_SCHEMA) @@ -25,3 +27,7 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index 986902efe5..5fd1867e7f 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -11,9 +11,12 @@ void binary::BinaryFan::dump_config() { if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void BinaryFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, false); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -41,6 +44,16 @@ void BinaryFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } } float BinaryFan::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/binary/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h index 980d2629f6..93294b8dee 100644 --- a/esphome/components/binary/fan/binary_fan.h +++ b/esphome/components/binary/fan/binary_fan.h @@ -16,11 +16,13 @@ class BinaryFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } protected: fan::FanState *fan_; output::BinaryOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; bool next_update_{true}; }; diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 7c78c3a369..8361ee8004 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -11,6 +11,7 @@ from esphome.const import CONF_DEVICE_CLASS, CONF_FILTERS, \ from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry +CODEOWNERS = ['@esphome/core'] DEVICE_CLASSES = [ '', 'battery', 'cold', 'connectivity', 'door', 'garage_door', 'gas', 'heat', 'light', 'lock', 'moisture', 'motion', 'moving', 'occupancy', @@ -224,7 +225,7 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ def setup_binary_sensor_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) if CONF_INTERNAL in config: - cg.add(var.set_internal(CONF_INTERNAL)) + cg.add(var.set_internal(config[CONF_INTERNAL])) if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) if CONF_INVERTED in config: diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index e9ff37446d..6b0321628c 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -137,6 +137,7 @@ template class BinarySensorPublishAction : public Action 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); diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 52885ae449..bb8b2198a1 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -7,6 +7,7 @@ from esphome.core import coroutine_with_priority AUTO_LOAD = ['web_server_base'] DEPENDENCIES = ['wifi'] +CODEOWNERS = ['@OttoWinter'] captive_portal_ns = cg.esphome_ns.namespace('captive_portal') CaptivePortal = captive_portal_ns.class_('CaptivePortal', cg.Component) diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index a8020c77f7..0f755d5c11 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ - UNIT_PARTS_PER_BILLION, CONF_TEMPERATURE, CONF_HUMIDITY, ICON_PERIODIC_TABLE_CO2 + UNIT_PARTS_PER_BILLION, CONF_TEMPERATURE, CONF_HUMIDITY, ICON_MOLECULE_CO2 DEPENDENCIES = ['i2c'] @@ -15,7 +15,7 @@ CONF_BASELINE = 'baseline' CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(CCS811Component), - cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, + cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 843b888218..38e254bb9d 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -10,6 +10,7 @@ from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True +CODEOWNERS = ['@esphome/core'] climate_ns = cg.esphome_ns.namespace('climate') Climate = climate_ns.class_('Climate', cg.Nameable) diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 1163705faa..40ab3f22e8 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR from esphome.core import coroutine AUTO_LOAD = ['sensor', 'remote_base'] +CODEOWNERS = ['@glmnet'] climate_ir_ns = cg.esphome_ns.namespace('climate_ir') ClimateIR = climate_ir_ns.class_('ClimateIR', climate.Climate, cg.Component, diff --git a/esphome/components/climate_ir_lg/__init__.py b/esphome/components/climate_ir_lg/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py new file mode 100644 index 0000000000..37bf9e2628 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +climate_ir_lg_ns = cg.esphome_ns.namespace('climate_ir_lg') +LgIrClimate = climate_ir_lg_ns.class_('LgIrClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(LgIrClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp new file mode 100644 index 0000000000..80677e6de3 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -0,0 +1,204 @@ +#include "climate_ir_lg.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace climate_ir_lg { + +static const char *TAG = "climate.climate_ir_lg"; + +const uint32_t COMMAND_ON = 0x00000; +const uint32_t COMMAND_ON_AI = 0x03000; +const uint32_t COMMAND_COOL = 0x08000; +const uint32_t COMMAND_OFF = 0xC0000; +const uint32_t COMMAND_SWING = 0x10000; +// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. +const uint32_t COMMAND_AUTO = 0x0B000; +const uint32_t COMMAND_DRY_FAN = 0x09000; + +const uint32_t COMMAND_MASK = 0xFF000; + +const uint32_t FAN_MASK = 0xF0; +const uint32_t FAN_AUTO = 0x50; +const uint32_t FAN_MIN = 0x00; +const uint32_t FAN_MED = 0x20; +const uint32_t FAN_MAX = 0x40; + +// Temperature +const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1; +const uint32_t TEMP_MASK = 0XF00; +const uint32_t TEMP_SHIFT = 8; + +// Constants +static const uint32_t HEADER_HIGH_US = 8000; +static const uint32_t HEADER_LOW_US = 4000; +static const uint32_t BIT_HIGH_US = 600; +static const uint32_t BIT_ONE_LOW_US = 1600; +static const uint32_t BIT_ZERO_LOW_US = 550; + +const uint16_t BITS = 28; + +void LgIrClimate::transmit_state() { + uint32_t remote_state = 0x8800000; + + // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_); + if (send_swing_cmd_) { + send_swing_cmd_ = false; + remote_state |= COMMAND_SWING; + } else { + if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_AUTO) { + remote_state |= COMMAND_ON_AI; + } else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) { + remote_state |= COMMAND_ON; + this->mode = climate::CLIMATE_MODE_COOL; + } else { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state |= COMMAND_COOL; + break; + case climate::CLIMATE_MODE_AUTO: + remote_state |= COMMAND_AUTO; + break; + case climate::CLIMATE_MODE_DRY: + remote_state |= COMMAND_DRY_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state |= COMMAND_OFF; + break; + } + } + mode_before_ = this->mode; + + ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode); + + if (this->mode == climate::CLIMATE_MODE_OFF) { + remote_state |= FAN_AUTO; + } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY) { + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state |= FAN_MAX; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state |= FAN_MIN; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state |= FAN_AUTO; + break; + } + } + + if (this->mode == climate::CLIMATE_MODE_AUTO) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + // remote_state |= FAN_MODE_AUTO_DRY; + } + if (this->mode == climate::CLIMATE_MODE_COOL) { + auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); + remote_state |= ((temp - 15) << TEMP_SHIFT); + } + } + transmit_(remote_state); + this->publish_state(); +} + +bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t nbits = 0; + uint32_t remote_state = 0; + + if (!data.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return false; + + for (nbits = 0; nbits < 32; nbits++) { + if (data.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + remote_state = (remote_state << 1) | 1; + } else if (data.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + remote_state = (remote_state << 1) | 0; + } else if (nbits == BITS) { + break; + } else { + return false; + } + } + + ESP_LOGD(TAG, "Decoded 0x%02X", remote_state); + if ((remote_state & 0xFF00000) != 0x8800000) + return false; + + if ((remote_state & COMMAND_MASK) == COMMAND_ON) { + this->mode = climate::CLIMATE_MODE_COOL; + } else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) { + this->mode = climate::CLIMATE_MODE_AUTO; + } + + if ((remote_state & COMMAND_MASK) == COMMAND_OFF) { + this->mode = climate::CLIMATE_MODE_OFF; + } else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) { + this->swing_mode = + this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; + } else { + if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) + this->mode = climate::CLIMATE_MODE_AUTO; + else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) { + this->mode = climate::CLIMATE_MODE_DRY; + } else { + this->mode = climate::CLIMATE_MODE_COOL; + } + } + + // Temperature + if (this->mode == climate::CLIMATE_MODE_COOL) + this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15; + + // Fan Speed + if (this->mode == climate::CLIMATE_MODE_AUTO) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY) { + if ((remote_state & FAN_MASK) == FAN_AUTO) + this->fan_mode = climate::CLIMATE_FAN_AUTO; + else if ((remote_state & FAN_MASK) == FAN_MIN) + this->fan_mode = climate::CLIMATE_FAN_LOW; + else if ((remote_state & FAN_MASK) == FAN_MED) + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + else if ((remote_state & FAN_MASK) == FAN_MAX) + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } + this->publish_state(); + + return true; +} +void LgIrClimate::transmit_(uint32_t value) { + calc_checksum_(value); + ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02X", value); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + data->reserve(2 + BITS * 2u); + + data->item(HEADER_HIGH_US, HEADER_LOW_US); + + for (uint32_t mask = 1UL << (BITS - 1); mask != 0; mask >>= 1) { + if (value & mask) + data->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + data->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + data->mark(BIT_HIGH_US); + transmit.perform(); +} +void LgIrClimate::calc_checksum_(uint32_t &value) { + uint32_t mask = 0xF; + uint32_t sum = 0; + for (uint8_t i = 1; i < 8; i++) { + sum += (value & (mask << (i * 4))) >> (i * 4); + } + + value |= (sum & mask); +} + +} // namespace climate_ir_lg +} // namespace esphome diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h new file mode 100644 index 0000000000..204482e7a8 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace climate_ir_lg { + +// Temperature +const uint8_t TEMP_MIN = 18; // Celsius +const uint8_t TEMP_MAX = 30; // Celsius + +class LgIrClimate : public climate_ir::ClimateIR { + public: + LgIrClimate() + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, false, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + // swing resets after unit powered off + if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + this->swing_mode = climate::CLIMATE_SWING_OFF; + climate_ir::ClimateIR::control(call); + } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + + bool send_swing_cmd_{false}; + + void calc_checksum_(uint32_t &value); + void transmit_(uint32_t value); + + climate::ClimateMode mode_before_{climate::CLIMATE_MODE_OFF}; +}; + +} // namespace climate_ir_lg +} // namespace esphome diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py new file mode 100644 index 0000000000..3e2e7b2c07 --- /dev/null +++ b/esphome/components/color/__init__.py @@ -0,0 +1,23 @@ +from esphome import config_validation as cv +from esphome import codegen as cg +from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE + +ColorStruct = cg.esphome_ns.struct('Color') + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(ColorStruct), + cv.Optional(CONF_RED, default=0.0): cv.percentage, + cv.Optional(CONF_GREEN, default=0.0): cv.percentage, + cv.Optional(CONF_BLUE, default=0.0): cv.percentage, + cv.Optional(CONF_WHITE, default=0.0): cv.percentage, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + cg.variable(config[CONF_ID], cg.StructInitializer( + ColorStruct, + ('r', config[CONF_RED]), + ('g', config[CONF_GREEN]), + ('b', config[CONF_BLUE]), + ('w', config[CONF_WHITE]))) diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 81412bb586..075fad5a4b 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -4,6 +4,7 @@ from esphome.components import climate_ir from esphome.const import CONF_ID AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] coolix_ns = cg.esphome_ns.namespace('coolix') CoolixClimate = coolix_ns.class_('CoolixClimate', climate_ir.ClimateIR) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index ed6ebe98fa..f39e7ba540 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -9,9 +9,10 @@ from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True +CODEOWNERS = ['@esphome/core'] DEVICE_CLASSES = [ '', 'awning', 'blind', 'curtain', 'damper', 'door', 'garage', - 'shade', 'shutter', 'window' + 'gate', 'shade', 'shutter', 'window' ] cover_ns = cg.esphome_ns.namespace('cover') diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index a8eb0cdf99..0092f987f2 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -41,6 +41,10 @@ template class ControlAction : public Action { public: explicit ControlAction(Cover *cover) : cover_(cover) {} + TEMPLATABLE_VALUE(bool, stop) + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + void play(Ts... x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) @@ -52,10 +56,6 @@ template class ControlAction : public Action { call.perform(); } - TEMPLATABLE_VALUE(bool, stop) - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - protected: Cover *cover_; }; @@ -63,6 +63,10 @@ template class ControlAction : public Action { template class CoverPublishAction : public Action { public: CoverPublishAction(Cover *cover) : cover_(cover) {} + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + TEMPLATABLE_VALUE(CoverOperation, current_operation) + void play(Ts... x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); @@ -73,10 +77,6 @@ template class CoverPublishAction : public Action { this->cover_->publish_state(); } - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - TEMPLATABLE_VALUE(CoverOperation, current_operation) - protected: Cover *cover_; }; diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py index 9f41f8c614..42a3b66497 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -4,6 +4,7 @@ from esphome.components import sensor, voltage_sampler from esphome.const import CONF_SENSOR, CONF_ID, ICON_FLASH, UNIT_AMPERE AUTO_LOAD = ['voltage_sampler'] +CODEOWNERS = ['@jesserockz'] CONF_SAMPLE_DURATION = 'sample_duration' diff --git a/esphome/components/dallas/__init__.py b/esphome/components/dallas/__init__.py index 28fc9785ad..85ab4300ee 100644 --- a/esphome/components/dallas/__init__.py +++ b/esphome/components/dallas/__init__.py @@ -1,18 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ID, CONF_PIN, \ - CONF_RESOLUTION, CONF_UNIT_OF_MEASUREMENT, UNIT_CELSIUS, \ - CONF_ICON, ICON_THERMOMETER, CONF_ACCURACY_DECIMALS +from esphome.const import CONF_ID, CONF_PIN MULTI_CONF = True AUTO_LOAD = ['sensor'] CONF_ONE_WIRE_ID = 'one_wire_id' -CONF_AUTO_SETUP_SENSORS = 'auto_setup_sensors' -CONF_SENSOR_NAME_TEMPLATE = 'sensor_name_template' -SENSOR_NAME_TEMPLATE_DEFAULT = '%s.%s' - dallas_ns = cg.esphome_ns.namespace('dallas') DallasComponent = dallas_ns.class_('DallasComponent', cg.PollingComponent) ESPOneWire = dallas_ns.class_('ESPOneWire') @@ -21,12 +15,6 @@ CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(DallasComponent), cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire), cv.Required(CONF_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_AUTO_SETUP_SENSORS, default=False): cv.boolean, - cv.Optional(CONF_SENSOR_NAME_TEMPLATE, default=SENSOR_NAME_TEMPLATE_DEFAULT): cv.string_strict, - cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12), - cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_CELSIUS): cv.string_strict, - cv.Optional(CONF_ICON, default=ICON_THERMOMETER): cv.icon, - cv.Optional(CONF_ACCURACY_DECIMALS, default=1): cv.int_, }).extend(cv.polling_component_schema('60s')) @@ -34,16 +22,4 @@ def to_code(config): pin = yield cg.gpio_pin_expression(config[CONF_PIN]) one_wire = cg.new_Pvariable(config[CONF_ONE_WIRE_ID], pin) var = cg.new_Pvariable(config[CONF_ID], one_wire) - if CONF_AUTO_SETUP_SENSORS in config: - cg.add(var.set_auto_setup_sensors(config[CONF_AUTO_SETUP_SENSORS])) - if CONF_SENSOR_NAME_TEMPLATE in config: - cg.add(var.set_sensor_name_template(config[CONF_SENSOR_NAME_TEMPLATE])) - if CONF_RESOLUTION in config: - cg.add(var.set_resolution(config[CONF_RESOLUTION])) - if CONF_UNIT_OF_MEASUREMENT in config: - cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) - if CONF_ICON in config: - cg.add(var.set_icon(config[CONF_ICON])) - if CONF_ACCURACY_DECIMALS in config: - cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) yield cg.register_component(var, config) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index d657f6c498..aa839e7331 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -1,6 +1,5 @@ #include "dallas_component.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace dallas { @@ -53,29 +52,6 @@ void DallasComponent::setup() { continue; } this->found_sensors_.push_back(address); - - if (this->auto_setup_sensors_) { - // avoid re-generating pre-configured sensors - bool skip = false; - for (auto sensor : this->sensors_) { - if (sensor->get_address() == address) { - skip = true; - break; - } - } - if (!skip) { - auto dallastemperaturesensor = this->get_sensor_by_address(address, this->resolution_); - char sensor_name[64]; - snprintf(sensor_name, sizeof(sensor_name), this->sensor_name_template_.c_str(), App.get_name().c_str(), - s.c_str()); - dallastemperaturesensor->set_name(sensor_name); - dallastemperaturesensor->set_unit_of_measurement(this->unit_of_measurement_); - dallastemperaturesensor->set_icon(this->icon_); - dallastemperaturesensor->set_accuracy_decimals(this->accuracy_decimals_); - dallastemperaturesensor->set_force_update(false); - App.register_sensor(dallastemperaturesensor); - } - } } for (auto sensor : this->sensors_) { @@ -180,25 +156,12 @@ void DallasComponent::update() { } } DallasComponent::DallasComponent(ESPOneWire *one_wire) : one_wire_(one_wire) {} -void DallasComponent::set_auto_setup_sensors(bool auto_setup_sensors) { - this->auto_setup_sensors_ = auto_setup_sensors; -} -void DallasComponent::set_sensor_name_template(const std::string &sensor_name_template) { - this->sensor_name_template_ = sensor_name_template; -} -void DallasComponent::set_resolution(uint8_t resolution) { this->resolution_ = resolution; } -void DallasComponent::set_unit_of_measurement(const std::string &unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} -void DallasComponent::set_icon(const std::string &icon) { this->icon_ = icon; } -void DallasComponent::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } DallasTemperatureSensor::DallasTemperatureSensor(uint64_t address, uint8_t resolution, DallasComponent *parent) : parent_(parent) { this->set_address(address); this->set_resolution(resolution); } -const uint64_t &DallasTemperatureSensor::get_address() const { return this->address_; } void DallasTemperatureSensor::set_address(uint64_t address) { this->address_ = address; } uint8_t DallasTemperatureSensor::get_resolution() const { return this->resolution_; } void DallasTemperatureSensor::set_resolution(uint8_t resolution) { this->resolution_ = resolution; } diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h index f2acf09d75..d32aec1758 100644 --- a/esphome/components/dallas/dallas_component.h +++ b/esphome/components/dallas/dallas_component.h @@ -22,30 +22,12 @@ class DallasComponent : public PollingComponent { void update() override; - /// Automatic sensors instantiation - bool get_auto_setup_sensors() const; - void set_auto_setup_sensors(bool auto_setup_sensors); - - /// Get/Set properties for automatically generated sensors. - void set_sensor_name_template(const std::string &sensor_name_template); - void set_resolution(uint8_t resolution); - void set_unit_of_measurement(const std::string &unit_of_measurement); - void set_icon(const std::string &icon); - void set_accuracy_decimals(int8_t accuracy_decimals); - protected: friend DallasTemperatureSensor; ESPOneWire *one_wire_; std::vector sensors_; std::vector found_sensors_; - - bool auto_setup_sensors_; - std::string sensor_name_template_; - uint8_t resolution_; - std::string unit_of_measurement_; - std::string icon_; - int8_t accuracy_decimals_; }; /// Internal class that helps us create multiple sensors for one Dallas hub. @@ -58,8 +40,6 @@ class DallasTemperatureSensor : public sensor::Sensor { /// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29". const std::string &get_address_name(); - /// Get the 64-bit unsigned address of this sensor. - const uint64_t &get_address() const; /// Set the 64-bit unsigned address for this sensor. void set_address(uint64_t address); /// Get the index of this sensor. (0 if using address.) diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index d90b10894d..92b7317f7c 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -30,7 +30,7 @@ bool HOT ICACHE_RAM_ATTR ESPOneWire::reset() { // Switch into RX mode, letting the pin float this->pin_->pin_mode(INPUT_PULLUP); - // after 15µs-60µs wait time, slave pulls low for 60µs-240µs + // after 15µs-60µs wait time, responder pulls low for 60µs-240µs // let's have 70µs just in case delayMicroseconds(70); diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index a40dadb5c2..d43b0de06a 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -2,6 +2,7 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_ID +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['logger'] debug_ns = cg.esphome_ns.namespace('debug') diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 890c2bede4..680a6b89ec 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE from esphome.components import uart DEPENDENCIES = ['uart'] +CODEOWNERS = ['@glmnet'] dfplayer_ns = cg.esphome_ns.namespace('dfplayer') DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component) diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 22ca11c3be..cb9686bb64 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -104,7 +104,6 @@ class DFPlayer : public uart::UARTDevice, public Component { #define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ template class ACTION_CLASS : public Action, public Parented { \ - public: \ void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ }; @@ -115,6 +114,7 @@ template class PlayFileAction : public Action, public Par public: TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); @@ -131,6 +131,7 @@ template class PlayFolderAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, folder) TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { auto folder = this->folder_.value(x...); auto file = this->file_.value(x...); @@ -146,6 +147,7 @@ template class PlayFolderAction : public Action, public P template class SetDeviceAction : public Action, public Parented { public: TEMPLATABLE_VALUE(Device, device) + void play(Ts... x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); @@ -155,6 +157,7 @@ template class SetDeviceAction : public Action, public Pa template class SetVolumeAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, volume) + void play(Ts... x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); @@ -164,6 +167,7 @@ template class SetVolumeAction : public Action, public Pa template class SetEqAction : public Action, public Parented { public: TEMPLATABLE_VALUE(EqPreset, eq) + void play(Ts... x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); diff --git a/esphome/components/dht/__init__.py b/esphome/components/dht/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/dht/__init__.py +++ b/esphome/components/dht/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 453e114338..bd468dcbc3 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -1,4 +1,5 @@ #include "display_buffer.h" +#include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -7,8 +8,8 @@ namespace display { static const char *TAG = "display"; -const uint8_t COLOR_OFF = 0; -const uint8_t COLOR_ON = 1; +const Color COLOR_OFF(0, 0, 0, 0); +const Color COLOR_ON(1, 1, 1, 1); void DisplayBuffer::init_internal_(uint32_t buffer_length) { this->buffer_ = new uint8_t[buffer_length]; @@ -18,7 +19,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { } this->clear(); } -void DisplayBuffer::fill(int color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } +void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { switch (this->rotation_) { @@ -43,7 +44,7 @@ int DisplayBuffer::get_height() { } } void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } -void HOT DisplayBuffer::draw_pixel_at(int x, int y, int color) { +void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: break; @@ -63,7 +64,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, int color) { this->draw_absolute_pixel_internal(x, y, color); App.feed_wdt(); } -void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, int color) { +void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, Color color) { const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; int32_t err = dx + dy; @@ -83,29 +84,29 @@ void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, int color) { } } } -void HOT DisplayBuffer::horizontal_line(int x, int y, int width, int color) { +void HOT DisplayBuffer::horizontal_line(int x, int y, int width, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = x; i < x + width; i++) this->draw_pixel_at(i, y, color); } -void HOT DisplayBuffer::vertical_line(int x, int y, int height, int color) { +void HOT DisplayBuffer::vertical_line(int x, int y, int height, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = y; i < y + height; i++) this->draw_pixel_at(x, i, color); } -void DisplayBuffer::rectangle(int x1, int y1, int width, int height, int color) { +void DisplayBuffer::rectangle(int x1, int y1, int width, int height, Color color) { this->horizontal_line(x1, y1, width, color); this->horizontal_line(x1, y1 + height - 1, width, color); this->vertical_line(x1, y1, height, color); this->vertical_line(x1 + width - 1, y1, height, color); } -void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, int color) { +void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, Color color) { // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. for (int i = y1; i < y1 + height; i++) { this->horizontal_line(x1, i, width, color); } } -void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, int color) { +void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, Color color) { int dx = -radius; int dy = 0; int err = 2 - 2 * radius; @@ -128,7 +129,7 @@ void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, int colo } } while (dx <= 0); } -void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, int color) { +void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color color) { int dx = -int32_t(radius); int dy = 0; int err = 2 - 2 * radius; @@ -155,7 +156,7 @@ void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, int co } while (dx <= 0); } -void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, const char *text) { +void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) { int x_start, y_start; int width, height; this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); @@ -197,19 +198,39 @@ void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, i += match_length; } } -void DisplayBuffer::vprintf_(int x, int y, Font *font, int color, TextAlign align, const char *format, va_list arg) { +void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) { char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::image(int x, int y, Image *image) { - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? COLOR_ON : COLOR_OFF); - } + +void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { + switch (image->get_type()) { + case IMAGE_TYPE_BINARY: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : color_off); + } + } + break; + case IMAGE_TYPE_GRAYSCALE: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); + } + } + break; + case IMAGE_TYPE_RGB24: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); + } + } + break; } } + void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width, int *height) { int x_offset, baseline; @@ -248,7 +269,7 @@ void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, break; } } -void DisplayBuffer::print(int x, int y, Font *font, int color, const char *text) { +void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text); } void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) { @@ -257,13 +278,13 @@ void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char void DisplayBuffer::print(int x, int y, Font *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } -void DisplayBuffer::printf(int x, int y, Font *font, int color, TextAlign align, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, align, format, arg); va_end(arg); } -void DisplayBuffer::printf(int x, int y, Font *font, int color, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); @@ -306,14 +327,14 @@ void DisplayBuffer::do_update_() { } } #ifdef USE_TIME -void DisplayBuffer::strftime(int x, int y, Font *font, int color, TextAlign align, const char *format, +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::strftime(int x, int y, Font *font, int color, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); } void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { @@ -431,10 +452,27 @@ bool Image::get_pixel(int x, int y) const { const uint32_t pos = x + y * width_8; return pgm_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } +Color Image::get_color_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t pos = (x + y * this->width_) * 3; + const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | + (pgm_read_byte(this->data_start_ + pos + 1) << 8) | + (pgm_read_byte(this->data_start_ + pos + 0) << 16); + return Color(color32); +} +Color Image::get_grayscale_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t pos = (x + y * this->width_); + const uint8_t gray = pgm_read_byte(this->data_start_ + pos); + return Color(gray | gray << 8 | gray << 16 | gray << 24); +} int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } -Image::Image(const uint8_t *data_start, int width, int height) - : width_(width), height_(height), data_start_(data_start) {} +ImageType Image::get_type() const { return this->type_; } +Image::Image(const uint8_t *data_start, int width, int height, ImageType type) + : width_(width), height_(height), type_(type), data_start_(data_start) {} DisplayPage::DisplayPage(const display_writer_t &writer) : writer_(writer) {} void DisplayPage::show() { this->parent_->show_page(this); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b12fad8c8a..e402b4b021 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/automation.h" +#include "esphome/core/color.h" #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" @@ -63,9 +64,11 @@ enum class TextAlign { }; /// Turn the pixel OFF. -extern const uint8_t COLOR_OFF; +extern const Color COLOR_OFF; /// Turn the pixel ON. -extern const uint8_t COLOR_ON; +extern const Color COLOR_ON; + +enum ImageType { IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_RGB24 = 2 }; enum DisplayRotation { DISPLAY_ROTATION_0_DEGREES = 0, @@ -91,7 +94,7 @@ using display_writer_t = std::function; class DisplayBuffer { public: /// Fill the entire screen with the given color. - virtual void fill(int color); + virtual void fill(Color color); /// Clear the entire screen by filling it with OFF pixels. void clear(); @@ -100,29 +103,29 @@ class DisplayBuffer { /// Get the height of the image in pixels with rotation applied. int get_height(); /// Set a single pixel at the specified coordinates to the given color. - void draw_pixel_at(int x, int y, int color = COLOR_ON); + void draw_pixel_at(int x, int y, Color color = COLOR_ON); /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. - void line(int x1, int y1, int x2, int y2, int color = COLOR_ON); + void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color. - void horizontal_line(int x, int y, int width, int color = COLOR_ON); + void horizontal_line(int x, int y, int width, Color color = COLOR_ON); /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color. - void vertical_line(int x, int y, int height, int color = COLOR_ON); + void vertical_line(int x, int y, int height, Color color = COLOR_ON); /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at /// [x1+width,y1+height]. - void rectangle(int x1, int y1, int width, int height, int color = COLOR_ON); + void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height]. - void filled_rectangle(int x1, int y1, int width, int height, int color = COLOR_ON); + void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color. - void circle(int center_x, int center_xy, int radius, int color = COLOR_ON); + void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON); /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. - void filled_circle(int center_x, int center_y, int radius, int color = COLOR_ON); + void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -133,7 +136,7 @@ class DisplayBuffer { * @param align The alignment of the text. * @param text The text to draw. */ - void print(int x, int y, Font *font, int color, TextAlign align, const char *text); + void print(int x, int y, Font *font, Color color, TextAlign align, const char *text); /** Print `text` with the top left at [x,y] with `font`. * @@ -143,7 +146,7 @@ class DisplayBuffer { * @param color The color to draw the text with. * @param text The text to draw. */ - void print(int x, int y, Font *font, int color, const char *text); + void print(int x, int y, Font *font, Color color, const char *text); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -174,7 +177,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, int color, TextAlign align, const char *format, ...) + void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) __attribute__((format(printf, 7, 8))); /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. @@ -186,7 +189,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, int color, const char *format, ...) __attribute__((format(printf, 6, 7))); + void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. * @@ -220,7 +223,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, int color, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) __attribute__((format(strftime, 7, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -232,7 +235,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, int color, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. @@ -259,8 +262,15 @@ class DisplayBuffer { __attribute__((format(strftime, 5, 0))); #endif - /// Draw the `image` with the top-left corner at [x,y] to the screen. - void image(int x, int y, Image *image); + /** Draw the `image` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param image The image to draw + * @param color_on The color to replace in binary images for the on bits. + * @param color_off The color to replace in binary images for the off bits. + */ + void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); /** Get the text bounds of the given string. * @@ -290,9 +300,9 @@ class DisplayBuffer { void set_rotation(DisplayRotation rotation); protected: - void vprintf_(int x, int y, Font *font, int color, TextAlign align, const char *format, va_list arg); + void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); - virtual void draw_absolute_pixel_internal(int x, int y, int color) = 0; + virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; virtual int get_height_internal() = 0; @@ -377,20 +387,25 @@ class Font { class Image { public: - Image(const uint8_t *data_start, int width, int height); + Image(const uint8_t *data_start, int width, int height, ImageType type); bool get_pixel(int x, int y) const; + Color get_color_pixel(int x, int y) const; + Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; + ImageType get_type() const; protected: int width_; int height_; + ImageType type_; const uint8_t *data_start_; }; template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) + void play(Ts... x) override { auto *page = this->page_.value(x...); if (page != nullptr) { @@ -402,18 +417,18 @@ template class DisplayPageShowAction : public Action { template class DisplayPageShowNextAction : public Action { public: DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {} + void play(Ts... x) override { this->buffer_->show_next_page(); } - protected: DisplayBuffer *buffer_; }; template class DisplayPageShowPrevAction : public Action { public: DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {} + void play(Ts... x) override { this->buffer_->show_prev_page(); } - protected: DisplayBuffer *buffer_; }; diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py new file mode 100644 index 0000000000..0ba16bc928 --- /dev/null +++ b/esphome/components/e131/__init__.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS + +e131_ns = cg.esphome_ns.namespace('e131') +E131AddressableLightEffect = e131_ns.class_('E131AddressableLightEffect', AddressableLightEffect) +E131Component = e131_ns.class_('E131Component', cg.Component) + +METHODS = { + 'UNICAST': e131_ns.E131_UNICAST, + 'MULTICAST': e131_ns.E131_MULTICAST +} + +CHANNELS = { + 'MONO': e131_ns.E131_MONO, + 'RGB': e131_ns.E131_RGB, + 'RGBW': e131_ns.E131_RGBW +} + +CONF_UNIVERSE = 'universe' +CONF_E131_ID = 'e131_id' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(E131Component), + cv.Optional(CONF_METHOD, default='MULTICAST'): cv.one_of(*METHODS, upper=True), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + cg.add(var.set_method(METHODS[config[CONF_METHOD]])) + + +@register_addressable_effect('e131', E131AddressableLightEffect, "E1.31", { + cv.GenerateID(CONF_E131_ID): cv.use_id(E131Component), + cv.Required(CONF_UNIVERSE): cv.int_range(min=1, max=512), + cv.Optional(CONF_CHANNELS, default='RGB'): cv.one_of(*CHANNELS, upper=True) +}) +def e131_light_effect_to_code(config, effect_id): + parent = yield cg.get_variable(config[CONF_E131_ID]) + + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_first_universe(config[CONF_UNIVERSE])) + cg.add(effect.set_channels(CHANNELS[config[CONF_CHANNELS]])) + cg.add(effect.set_e131(parent)) + yield effect diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp new file mode 100644 index 0000000000..8a293e9067 --- /dev/null +++ b/esphome/components/e131/e131.cpp @@ -0,0 +1,106 @@ +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131"; +static const int PORT = 5568; + +E131Component::E131Component() {} + +E131Component::~E131Component() { + if (udp_) { + udp_->stop(); + } +} + +void E131Component::setup() { + udp_.reset(new WiFiUDP()); + + if (!udp_->begin(PORT)) { + ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); + mark_failed(); + return; + } + + join_igmp_groups_(); +} + +void E131Component::loop() { + std::vector payload; + E131Packet packet; + int universe = 0; + + while (uint16_t packet_size = udp_->parsePacket()) { + payload.resize(packet_size); + + if (!udp_->read(&payload[0], payload.size())) { + continue; + } + + if (!packet_(payload, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet recevied of size %zu.", payload.size()); + continue; + } + + if (!process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +} + +void E131Component::add_effect(E131AddressableLightEffect *light_effect) { + if (light_effects_.count(light_effect)) { + return; + } + + ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), + light_effect->get_first_universe(), light_effect->get_last_universe()); + + light_effects_.insert(light_effect); + + for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { + join_(universe); + } +} + +void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { + if (!light_effects_.count(light_effect)) { + return; + } + + ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), + light_effect->get_first_universe(), light_effect->get_last_universe()); + + light_effects_.erase(light_effect); + + for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { + leave_(universe); + } +} + +bool E131Component::process_(int universe, const E131Packet &packet) { + bool handled = false; + + ESP_LOGV(TAG, "Received E1.31 packet for %d universe, with %d bytes", universe, packet.count); + + for (auto light_effect : light_effects_) { + handled = light_effect->process_(universe, packet) || handled; + } + + return handled; +} + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h new file mode 100644 index 0000000000..3f647edbf1 --- /dev/null +++ b/esphome/components/e131/e131.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/component.h" + +#include +#include +#include + +class UDP; + +namespace esphome { +namespace e131 { + +class E131AddressableLightEffect; + +enum E131ListenMethod { E131_MULTICAST, E131_UNICAST }; + +const int E131_MAX_PROPERTY_VALUES_COUNT = 513; + +struct E131Packet { + uint16_t count; + uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; +}; + +class E131Component : public esphome::Component { + public: + E131Component(); + ~E131Component(); + + void setup() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + public: + void add_effect(E131AddressableLightEffect *light_effect); + void remove_effect(E131AddressableLightEffect *light_effect); + + public: + void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } + + protected: + bool packet_(const std::vector &data, int &universe, E131Packet &packet); + bool process_(int universe, const E131Packet &packet); + bool join_igmp_groups_(); + void join_(int universe); + void leave_(int universe); + + protected: + E131ListenMethod listen_method_{E131_MULTICAST}; + std::unique_ptr udp_; + std::set light_effects_; + std::map universe_consumers_; + std::map universe_packets_; +}; + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp new file mode 100644 index 0000000000..8657d828c5 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -0,0 +1,90 @@ +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131_addressable_light_effect"; +static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); + +E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } + +int E131AddressableLightEffect::get_lights_per_universe() const { return MAX_DATA_SIZE / channels_; } + +int E131AddressableLightEffect::get_first_universe() const { return first_universe_; } + +int E131AddressableLightEffect::get_last_universe() const { return first_universe_ + get_universe_count() - 1; } + +int E131AddressableLightEffect::get_universe_count() const { + // Round up to lights_per_universe + auto lights = get_lights_per_universe(); + return (get_addressable_()->size() + lights - 1) / lights; +} + +void E131AddressableLightEffect::start() { + AddressableLightEffect::start(); + + if (this->e131_) { + this->e131_->add_effect(this); + } +} + +void E131AddressableLightEffect::stop() { + if (this->e131_) { + this->e131_->remove_effect(this); + } + + AddressableLightEffect::stop(); +} + +void E131AddressableLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + // ignore, it is run by `E131Component::update()` +} + +bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet) { + auto it = get_addressable_(); + + // check if this is our universe and data are valid + if (universe < first_universe_ || universe > get_last_universe()) + return false; + + int output_offset = (universe - first_universe_) * get_lights_per_universe(); + // limit amount of lights per universe and received + int output_end = std::min(it->size(), std::min(output_offset + get_lights_per_universe(), packet.count - 1)); + auto input_data = packet.values + 1; + + ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %d-%d.", get_name().c_str(), universe, output_offset, + output_end); + + switch (channels_) { + case E131_MONO: + for (; output_offset < output_end; output_offset++, input_data++) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[0], input_data[0], input_data[0])); + } + break; + + case E131_RGB: + for (; output_offset < output_end; output_offset++, input_data += 3) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], + (input_data[0] + input_data[1] + input_data[2]) / 3)); + } + break; + + case E131_RGBW: + for (; output_offset < output_end; output_offset++, input_data += 4) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], input_data[3])); + } + break; + } + + return true; +} + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h new file mode 100644 index 0000000000..85af4fe7a9 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" + +namespace esphome { +namespace e131 { + +class E131Component; +struct E131Packet; + +enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; + +class E131AddressableLightEffect : public light::AddressableLightEffect { + public: + E131AddressableLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + + public: + int get_data_per_universe() const; + int get_lights_per_universe() const; + int get_first_universe() const; + int get_last_universe() const; + int get_universe_count() const; + + public: + void set_first_universe(int universe) { this->first_universe_ = universe; } + void set_channels(E131LightChannels channels) { this->channels_ = channels; } + void set_e131(E131Component *e131) { this->e131_ = e131; } + + protected: + bool process_(int universe, const E131Packet &packet); + + protected: + int first_universe_{0}; + int last_universe_{0}; + E131LightChannels channels_{E131_RGB}; + E131Component *e131_{nullptr}; + + friend class E131Component; +}; + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp new file mode 100644 index 0000000000..ca68f5126d --- /dev/null +++ b/esphome/components/e131/e131_packet.cpp @@ -0,0 +1,136 @@ +#include "e131.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131"; + +static const uint8_t ACN_ID[12] = {0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00}; +static const uint32_t VECTOR_ROOT = 4; +static const uint32_t VECTOR_FRAME = 2; +static const uint8_t VECTOR_DMP = 2; + +// E1.31 Packet Structure +union E131RawPacket { + struct { + // Root Layer + uint16_t preamble_size; + uint16_t postamble_size; + uint8_t acn_id[12]; + uint16_t root_flength; + uint32_t root_vector; + uint8_t cid[16]; + + // Frame Layer + uint16_t frame_flength; + uint32_t frame_vector; + uint8_t source_name[64]; + uint8_t priority; + uint16_t reserved; + uint8_t sequence_number; + uint8_t options; + uint16_t universe; + + // DMP Layer + uint16_t dmp_flength; + uint8_t dmp_vector; + uint8_t type; + uint16_t first_address; + uint16_t address_increment; + uint16_t property_value_count; + uint8_t property_values[E131_MAX_PROPERTY_VALUES_COUNT]; + } __attribute__((packed)); + + uint8_t raw[638]; +}; + +// We need to have at least one `1` value +// Get the offset of `property_values[1]` +const long E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); + +bool E131Component::join_igmp_groups_() { + if (listen_method_ != E131_MULTICAST) + return false; + if (!udp_) + return false; + + for (auto universe : universe_consumers_) { + if (!universe.second) + continue; + + ip4_addr_t multicast_addr = { + static_cast(IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)))}; + + auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); + + if (err) { + ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); + } + } + + return true; +} + +void E131Component::join_(int universe) { + // store only latest received packet for the given universe + auto consumers = ++universe_consumers_[universe]; + + if (consumers > 1) { + return; // we already joined before + } + + if (join_igmp_groups_()) { + ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); + } +} + +void E131Component::leave_(int universe) { + auto consumers = --universe_consumers_[universe]; + + if (consumers > 0) { + return; // we have other consumers of the given universe + } + + if (listen_method_ == E131_MULTICAST) { + ip4_addr_t multicast_addr = { + static_cast(IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))}; + + igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); + } + + ESP_LOGD(TAG, "Left %d universe for E1.31.", universe); +} + +bool E131Component::packet_(const std::vector &data, int &universe, E131Packet &packet) { + if (data.size() < E131_MIN_PACKET_SIZE) + return false; + + auto sbuff = reinterpret_cast(&data[0]); + + if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0) + return false; + if (htonl(sbuff->root_vector) != VECTOR_ROOT) + return false; + if (htonl(sbuff->frame_vector) != VECTOR_FRAME) + return false; + if (sbuff->dmp_vector != VECTOR_DMP) + return false; + if (sbuff->property_values[0] != 0) + return false; + + universe = htons(sbuff->universe); + packet.count = htons(sbuff->property_value_count); + if (packet.count > E131_MAX_PROPERTY_VALUES_COUNT) + return false; + + memcpy(packet.values, sbuff->property_values, packet.count); + return true; +} + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 1c239226c1..8e20cb6a29 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -94,7 +94,7 @@ void EndstopCover::dump_config() { float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } void EndstopCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5109af21fa..a8185a8c67 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -99,14 +99,14 @@ bool ESP32BLETracker::ble_setup() { return false; } + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + // Initialize the bluetooth controller with the default configuration if (!btStart()) { ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); return false; } - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - err = esp_bluedroid_init(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 8d011abfe3..eef7930b78 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -28,6 +28,7 @@ class ESPBTUUID { bool contains(uint8_t data1, uint8_t data2) const; bool operator==(const ESPBTUUID &uuid) const; + bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } esp_bt_uuid_t get_uuid(); @@ -74,6 +75,8 @@ class ESPBTDevice { uint64_t address_uint64() const; + const uint8_t *address() const { return address_; } + esp_ble_addr_type_t get_address_type() const { return this->address_type_; } int get_rssi() const { return rssi_; } const std::string &get_name() const { return this->name_; } diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index b6839985b0..661db6611f 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -13,7 +13,7 @@ class ESP8266PWM : public output::FloatOutput, public Component { void set_pin(GPIOPin *pin) { pin_ = pin; } void set_frequency(float frequency) { this->frequency_ = frequency; } /// Dynamically update frequency - void update_frequency(float frequency) { + void update_frequency(float frequency) override { this->set_frequency(frequency); this->write_state(this->last_output_); } @@ -43,7 +43,6 @@ template class SetFrequencyAction : public Action { this->parent_->update_frequency(freq); } - protected: ESP8266PWM *parent_; }; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index d5548fc377..0553d66273 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -37,7 +37,7 @@ void EthernetComponent::setup() { } void EthernetComponent::loop() { const uint32_t now = millis(); - if (!this->connected_ && !this->last_connected_ && now - this->last_connected_ > 15000) { + if (!this->connected_ && !this->last_connected_ && now - this->connect_begin_ > 15000) { ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); this->start_connect_(); return; diff --git a/esphome/components/exposure_notifications/__init__.py b/esphome/components/exposure_notifications/__init__.py new file mode 100644 index 0000000000..8175a2d3aa --- /dev/null +++ b/esphome/components/exposure_notifications/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_TRIGGER_ID + +CODEOWNERS = ['@OttoWinter'] +DEPENDENCIES = ['esp32_ble_tracker'] + +exposure_notifications_ns = cg.esphome_ns.namespace('exposure_notifications') +ExposureNotification = exposure_notifications_ns.struct('ExposureNotification') +ExposureNotificationTrigger = exposure_notifications_ns.class_( + 'ExposureNotificationTrigger', esp32_ble_tracker.ESPBTDeviceListener, + automation.Trigger.template(ExposureNotification)) + +CONF_ON_EXPOSURE_NOTIFICATION = 'on_exposure_notification' + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ON_EXPOSURE_NOTIFICATION): automation.validate_automation(cv.Schema({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ExposureNotificationTrigger), + }).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)), +}) + + +def to_code(config): + for conf in config.get(CONF_ON_EXPOSURE_NOTIFICATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield automation.build_automation(trigger, [(ExposureNotification, 'x')], conf) + yield esp32_ble_tracker.register_ble_device(trigger, conf) diff --git a/esphome/components/exposure_notifications/exposure_notifications.cpp b/esphome/components/exposure_notifications/exposure_notifications.cpp new file mode 100644 index 0000000000..1d8cf83a0e --- /dev/null +++ b/esphome/components/exposure_notifications/exposure_notifications.cpp @@ -0,0 +1,49 @@ +#include "exposure_notifications.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace exposure_notifications { + +using namespace esp32_ble_tracker; + +static const char *TAG = "exposure_notifications"; + +bool ExposureNotificationTrigger::parse_device(const ESPBTDevice &device) { + // See also https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf + if (device.get_service_uuids().size() != 1) + return false; + + // Exposure notifications have Service UUID FD 6F + ESPBTUUID uuid = device.get_service_uuids()[0]; + // constant service identifier + const ESPBTUUID expected_uuid = ESPBTUUID::from_uint16(0xFD6F); + if (uuid != expected_uuid) + return false; + if (device.get_service_datas().size() != 1) + return false; + + // The service data should be 20 bytes + // First 16 bytes are the rolling proximity identifier (RPI) + // Then 4 bytes of encrypted metadata follow which can be used to get the transmit power level. + ServiceData service_data = device.get_service_datas()[0]; + if (service_data.uuid != expected_uuid) + return false; + auto data = service_data.data; + if (data.size() != 20) + return false; + ExposureNotification notification{}; + memcpy(¬ification.address[0], device.address(), 6); + memcpy(¬ification.rolling_proximity_identifier[0], &data[0], 16); + memcpy(¬ification.associated_encrypted_metadata[0], &data[16], 4); + notification.rssi = device.get_rssi(); + this->trigger(notification); + return true; +} + +} // namespace exposure_notifications +} // namespace esphome + +#endif diff --git a/esphome/components/exposure_notifications/exposure_notifications.h b/esphome/components/exposure_notifications/exposure_notifications.h new file mode 100644 index 0000000000..6b9f61b2a0 --- /dev/null +++ b/esphome/components/exposure_notifications/exposure_notifications.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace exposure_notifications { + +struct ExposureNotification { + std::array address; + int rssi; + std::array rolling_proximity_identifier; + std::array associated_encrypted_metadata; +}; + +class ExposureNotificationTrigger : public Trigger, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace exposure_notifications +} // namespace esphome + +#endif diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 50fdb1c2c9..7b0a79a0d3 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -34,6 +34,10 @@ FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ cv.publish_topic), cv.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.All(cv.requires_component('mqtt'), cv.subscribe_topic), + cv.Optional(CONF_SPEED_STATE_TOPIC): cv.All(cv.requires_component('mqtt'), + cv.publish_topic), + cv.Optional(CONF_SPEED_COMMAND_TOPIC): cv.All(cv.requires_component('mqtt'), + cv.subscribe_topic), }) diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index dfa72a3ea6..d96ed994e8 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -25,7 +25,6 @@ template class TurnOnAction : public Action { call.perform(); } - protected: FanState *state_; }; @@ -35,7 +34,6 @@ template class TurnOffAction : public Action { void play(Ts... x) override { this->state_->turn_off().perform(); } - protected: FanState *state_; }; @@ -45,7 +43,6 @@ template class ToggleAction : public Action { void play(Ts... x) override { this->state_->toggle().perform(); } - protected: FanState *state_; }; diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index af170a755c..ae58b04150 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -22,6 +22,7 @@ struct FanStateRTCState { bool state; FanSpeed speed; bool oscillating; + FanDirection direction; }; void FanState::setup() { @@ -34,6 +35,7 @@ void FanState::setup() { call.set_state(recovered.state); call.set_speed(recovered.speed); call.set_oscillating(recovered.oscillating); + call.set_direction(recovered.direction); call.perform(); } float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } @@ -46,6 +48,9 @@ void FanStateCall::perform() const { if (this->oscillating_.has_value()) { this->state_->oscillating = *this->oscillating_; } + if (this->direction_.has_value()) { + this->state_->direction = *this->direction_; + } if (this->speed_.has_value()) { switch (*this->speed_) { case FAN_SPEED_LOW: @@ -63,6 +68,7 @@ void FanStateCall::perform() const { saved.state = this->state_->state; saved.speed = this->state_->speed; saved.oscillating = this->state_->oscillating; + saved.direction = this->state_->direction; this->state_->rtc_.save(&saved); this->state_->state_callback_.call(); diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index 4e937c68bd..7ab8337e94 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -15,6 +15,9 @@ enum FanSpeed { FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. }; +/// Simple enum to represent the direction of a fan +enum FanDirection { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1 }; + class FanState; class FanStateCall { @@ -46,6 +49,14 @@ class FanStateCall { return *this; } FanStateCall &set_speed(const char *speed); + FanStateCall &set_direction(FanDirection direction) { + this->direction_ = direction; + return *this; + } + FanStateCall &set_direction(optional direction) { + this->direction_ = direction; + return *this; + } void perform() const; @@ -54,6 +65,7 @@ class FanStateCall { optional binary_state_; optional oscillating_{}; optional speed_{}; + optional direction_{}; }; class FanState : public Nameable, public Component { @@ -76,6 +88,8 @@ class FanState : public Nameable, public Component { bool oscillating{false}; /// The current fan speed. FanSpeed speed{FAN_SPEED_HIGH}; + /// The current direction of the fan + FanDirection direction{FAN_DIRECTION_FORWARD}; FanStateCall turn_on(); FanStateCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index c46adbf013..75663484c5 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -6,7 +6,8 @@ namespace fan { class FanTraits { public: FanTraits() = default; - FanTraits(bool oscillation, bool speed) : oscillation_(oscillation), speed_(speed) {} + FanTraits(bool oscillation, bool speed, bool direction) + : oscillation_(oscillation), speed_(speed), direction_(direction) {} /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } @@ -16,10 +17,15 @@ class FanTraits { bool supports_speed() const { return this->speed_; } /// Set whether this fan supports speed modes. void set_speed(bool speed) { this->speed_ = speed; } + /// Return if this fan supports changing direction + bool supports_direction() const { return this->direction_; } + /// Set whether this fan supports changing direction + void set_direction(bool direction) { this->direction_ = direction; } protected: bool oscillation_{false}; bool speed_{false}; + bool direction_{false}; }; } // namespace fan diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index ffa49f43c2..ab78f7537f 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -4,6 +4,7 @@ from esphome.components import light from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_RGB_ORDER, CONF_MAX_REFRESH_RATE from esphome.core import coroutine +CODEOWNERS = ['@OttoWinter'] fastled_base_ns = cg.esphome_ns.namespace('fastled_base') FastLEDLightOutput = fastled_base_ns.class_('FastLEDLightOutput', light.AddressableLight) @@ -35,5 +36,7 @@ def new_fastled_light(config): yield light.register_light(var, config) # https://github.com/FastLED/FastLED/blob/master/library.json - cg.add_library('FastLED', '3.3.3') + # 3.3.3 has an issue on ESP32 with RMT and fastled_clockless: + # https://github.com/esphome/issues/issues/1375 + cg.add_library('FastLED', '3.3.2') yield var diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index e59a7e6acb..f1c4f4faf2 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -5,6 +5,7 @@ from esphome import codegen as cg from esphome.const import CONF_ID, CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_TYPE, CONF_VALUE from esphome.core import coroutine_with_priority +CODEOWNERS = ['@esphome/core'] globals_ns = cg.esphome_ns.namespace('globals') GlobalsComponent = globals_ns.class_('GlobalsComponent', cg.Component) GlobalVarSetAction = globals_ns.class_('GlobalVarSetAction', automation.Action) diff --git a/esphome/components/gpio/__init__.py b/esphome/components/gpio/__init__.py index ccb920e654..c36ba8f433 100644 --- a/esphome/components/gpio/__init__.py +++ b/esphome/components/gpio/__init__.py @@ -1,3 +1,4 @@ import esphome.codegen as cg +CODEOWNERS = ['@esphome/core'] gpio_ns = cg.esphome_ns.namespace('gpio') diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h new file mode 100644 index 0000000000..f160a91148 --- /dev/null +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Arduino.h" + +namespace esphome { +namespace hm3301 { + +class AbstractAQICalculator { + public: + virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator.cpp b/esphome/components/hm3301/aqi_calculator.cpp new file mode 100644 index 0000000000..6b70c5d4fd --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator.cpp @@ -0,0 +1,46 @@ +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class AQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 6; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 51}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 45}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, 604}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + return ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS - 1; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/hm3301/aqi_calculator_factory.h new file mode 100644 index 0000000000..483a158822 --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator_factory.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Arduino.h" +#include "caqi_calculator.cpp" +#include "aqi_calculator.cpp" + +namespace esphome { +namespace hm3301 { + +enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 }; + +class AQICalculatorFactory { + public: + AbstractAQICalculator *get_calculator(AQICalculatorType type) { + if (type == 0) { + return caqi_calculator_; + } else if (type == 1) { + return aqi_calculator_; + } + + return nullptr; + } + + protected: + CAQICalculator *caqi_calculator_ = new CAQICalculator(); + AQICalculator *aqi_calculator_ = new AQICalculator(); +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/caqi_calculator.cpp b/esphome/components/hm3301/caqi_calculator.cpp new file mode 100644 index 0000000000..511179cb71 --- /dev/null +++ b/esphome/components/hm3301/caqi_calculator.cpp @@ -0,0 +1,52 @@ +#include "esphome/core/log.h" +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class CAQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 5; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + if (grid_index == -1) { + return -1; + } + + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + int aqi = ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + + return aqi; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 6456ee354a..cbce714012 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -1,5 +1,5 @@ -#include "hm3301.h" #include "esphome/core/log.h" +#include "hm3301.h" namespace esphome { namespace hm3301 { @@ -30,6 +30,7 @@ void HM3301Component::dump_config() { LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "AQI", this->aqi_sensor_); } float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } @@ -47,17 +48,38 @@ void HM3301Component::update() { return; } + int16_t pm_1_0_value = -1; if (this->pm_1_0_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); - this->pm_1_0_sensor_->publish_state(value); + pm_1_0_value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); } + + int16_t pm_2_5_value = -1; if (this->pm_2_5_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); - this->pm_2_5_sensor_->publish_state(value); + pm_2_5_value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); } + + int16_t pm_10_0_value = -1; if (this->pm_10_0_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); - this->pm_10_0_sensor_->publish_state(value); + pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); + } + + int8_t aqi_value = -1; + if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { + AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); + aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); + } + + if (pm_1_0_value != -1) { + this->pm_1_0_sensor_->publish_state(pm_1_0_value); + } + if (pm_2_5_value != -1) { + this->pm_2_5_sensor_->publish_state(pm_2_5_value); + } + if (pm_10_0_value != -1) { + this->pm_10_0_sensor_->publish_state(pm_10_0_value); + } + if (aqi_value != -1) { + this->aqi_sensor_->publish_state(aqi_value); } this->status_clear_warning(); diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 0fbb32612e..5594f1719c 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "aqi_calculator_factory.h" #include @@ -16,6 +17,9 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; } + + void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; } void setup() override; void dump_config() override; @@ -32,6 +36,10 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *aqi_sensor_{nullptr}; + + AQICalculatorType aqi_calc_type_; + AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory(); bool read_sensor_value_(uint8_t *); bool validate_checksum_(const uint8_t *); diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 718d0a20bb..ef7669bc03 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -8,6 +8,25 @@ DEPENDENCIES = ['i2c'] hm3301_ns = cg.esphome_ns.namespace('hm3301') HM3301Component = hm3301_ns.class_('HM3301Component', cg.PollingComponent, i2c.I2CDevice) +AQICalculatorType = hm3301_ns.enum('AQICalculatorType') + +CONF_AQI = 'aqi' +CONF_CALCULATION_TYPE = 'calculation_type' +UNIT_INDEX = 'index' + +AQI_CALCULATION_TYPE = { + 'CAQI': AQICalculatorType.CAQI_TYPE, + 'AQI': AQICalculatorType.AQI_TYPE +} + + +def validate(config): + if CONF_AQI in config and CONF_PM_2_5 not in config: + raise cv.Invalid("AQI sensor requires PM 2.5") + if CONF_AQI in config and CONF_PM_10_0 not in config: + raise cv.Invalid("AQI sensor requires PM 10 sensors") + return config + CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(HM3301Component), @@ -18,8 +37,11 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), cv.Optional(CONF_PM_10_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40))) + cv.Optional(CONF_AQI): + sensor.sensor_schema(UNIT_INDEX, ICON_CHEMICAL_WEAPON, 0).extend({ + cv.Required(CONF_CALCULATION_TYPE): cv.enum(AQI_CALCULATION_TYPE, upper=True), + }) +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)), validate) def to_code(config): @@ -39,5 +61,10 @@ def to_code(config): sens = yield sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) + if CONF_AQI in config: + sens = yield sensor.new_sensor(config[CONF_AQI]) + cg.add(var.set_aqi_sensor(sens)) + cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) + # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301 cg.add_library('6306', '1.0.3') diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 9fb3836d49..69d759b977 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,3 +1,4 @@ import esphome.codegen as cg +CODEOWNERS = ['@OttoWinter'] homeassistant_ns = cg.esphome_ns.namespace('homeassistant') diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index e79df12a6c..b003736d89 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -4,7 +4,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \ - CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_1, CONF_URL + CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266, CONF_URL from esphome.core import CORE, Lambda from esphome.core_config import PLATFORMIO_ESP8266_LUT @@ -34,7 +34,7 @@ def validate_framework(config): return config framework = PLATFORMIO_ESP8266_LUT[version] if version in PLATFORMIO_ESP8266_LUT else version - if framework < ARDUINO_VERSION_ESP8266_2_5_1: + if framework < ARDUINO_VERSION_ESP8266['2.5.1']: raise cv.Invalid('This component is not supported on arduino framework version below 2.5.1') return config diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 390867c948..46b0910b5e 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -15,15 +15,15 @@ void HttpRequestComponent::dump_config() { void HttpRequestComponent::send() { bool begin_status = false; this->client_.setReuse(true); - static const String URL = this->url_.c_str(); + const String url = this->url_.c_str(); #ifdef ARDUINO_ARCH_ESP32 - begin_status = this->client_.begin(URL); + begin_status = this->client_.begin(url); #endif #ifdef ARDUINO_ARCH_ESP8266 #ifndef CLANG_TIDY this->client_.setFollowRedirects(true); this->client_.setRedirectLimit(3); - begin_status = this->client_.begin(*this->get_wifi_client_(), URL); + begin_status = this->client_.begin(*this->get_wifi_client_(), url); #endif #endif diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 0c71f18019..91c6a97190 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_SCAN, CONF_SCL, CONF_SDA CONF_I2C_ID from esphome.core import coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] i2c_ns = cg.esphome_ns.namespace('i2c') I2CComponent = i2c_ns.class_('I2CComponent', cg.Component) I2CDevice = i2c_ns.class_('I2CDevice') diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index d0a41a7379..dfe387afd1 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -4,13 +4,21 @@ from esphome import core from esphome.components import display, font import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_ID, CONF_RESIZE +from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['display'] MULTI_CONF = True + +ImageType = display.display_ns.enum('ImageType') +IMAGE_TYPE = { + 'BINARY': ImageType.IMAGE_TYPE_BINARY, + 'GRAYSCALE': ImageType.IMAGE_TYPE_GRAYSCALE, + 'RGB24': ImageType.IMAGE_TYPE_RGB24, +} + Image_ = display.display_ns.class_('Image') CONF_RAW_DATA_ID = 'raw_data_id' @@ -19,6 +27,7 @@ IMAGE_SCHEMA = cv.Schema({ cv.Required(CONF_ID): cv.declare_id(Image_), cv.Required(CONF_FILE): cv.file_, cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default='BINARY'): cv.enum(IMAGE_TYPE, upper=True), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), }) @@ -34,23 +43,50 @@ def to_code(config): except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") + width, height = image.size + if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) + width, height = image.size + else: + if width > 500 or height > 500: + _LOGGER.warning("The image you requested is very big. Please consider using" + " the resize parameter.") - image = image.convert('1', dither=Image.NONE) - width, height = image.size - if width > 500 or height > 500: - _LOGGER.warning("The image you requested is very big. Please consider using the resize " - "parameter") - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) + if config[CONF_TYPE] == 'GRAYSCALE': + image = image.convert('L', dither=Image.NONE) + pixels = list(image.getdata()) + data = [0 for _ in range(height * width)] + pos = 0 + for pix in pixels: + data[pos] = pix + pos += 1 + + elif config[CONF_TYPE] == 'RGB24': + image = image.convert('RGB') + pixels = list(image.getdata()) + data = [0 for _ in range(height * width * 3)] + pos = 0 + for pix in pixels: + data[pos] = pix[0] + pos += 1 + data[pos] = pix[1] + pos += 1 + data[pos] = pix[2] + pos += 1 + + elif config[CONF_TYPE] == 'BINARY': + image = image.convert('1', dither=Image.NONE) + width8 = ((width + 7) // 8) * 8 + data = [0 for _ in range(height * width8 // 8)] + for y in range(height): + for x in range(width): + if image.getpixel((x, y)): + continue + pos = x + y * width8 + data[pos // 8] |= 0x80 >> (pos % 8) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable(config[CONF_ID], prog_arr, width, height) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, + IMAGE_TYPE[config[CONF_TYPE]]) diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 1150f7c661..44d6501e36 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -120,7 +120,7 @@ void INA219Component::setup() { } this->calibration_lsb_ = lsb; - auto calibration = uint32_t(0.04096f / (0.0001 * lsb * this->shunt_resistance_ohm_)); + auto calibration = uint32_t(0.04096f / (0.000001 * lsb * this->shunt_resistance_ohm_)); ESP_LOGV(TAG, " Using LSB=%u calibration=%u", lsb, calibration); if (!this->write_byte_16(INA219_REGISTER_CALIBRATION, calibration)) { this->mark_failed(); diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index 3bd568f37d..17492433e3 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -42,7 +42,7 @@ void INA3221Component::setup() { config |= 0b0001000000000000; } // 0b0000xxx000000000 << 9 Averaging Mode (0 -> 1 sample, 111 -> 1024 samples) - config |= 0b0000111000000000; + config |= 0b0000000000000000; // 0b0000000xxx000000 << 6 Bus Voltage Conversion time (100 -> 1.1ms, 111 -> 8.244 ms) config |= 0b0000000111000000; // 0b0000000000xxx000 << 3 Shunt Voltage Conversion time (same as above) @@ -100,7 +100,7 @@ void INA3221Component::update() { this->status_set_warning(); return; } - const float shunt_voltage_v = int16_t(raw) * 40.0f / 1000000.0f; + const float shunt_voltage_v = int16_t(raw) * 40.0f / 8.0f / 1000000.0f; if (channel.shunt_voltage_sensor_ != nullptr) channel.shunt_voltage_sensor_->publish_state(shunt_voltage_v); current_a = shunt_voltage_v / channel.shunt_resistance_; diff --git a/esphome/components/integration/__init__.py b/esphome/components/integration/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/integration/__init__.py +++ b/esphome/components/integration/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index f9b5a43870..806c0ce567 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,7 +10,9 @@ static const char *TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); - this->rtc_.load(&this->result_); + float preference_value = 0; + this->rtc_.load(&preference_value); + this->result_ = preference_value; } this->last_update_ = millis(); diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py index e0816b7407..be37526ad1 100644 --- a/esphome/components/interval/__init__.py +++ b/esphome/components/interval/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import CONF_ID, CONF_INTERVAL +CODEOWNERS = ['@esphome/core'] interval_ns = cg.esphome_ns.namespace('interval') IntervalTrigger = interval_ns.class_('IntervalTrigger', automation.Trigger.template(), cg.PollingComponent) diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index f719b05340..63bc16dfa2 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.core import coroutine_with_priority +CODEOWNERS = ['@OttoWinter'] json_ns = cg.esphome_ns.namespace('json') diff --git a/esphome/components/ledc/__init__.py b/esphome/components/ledc/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/ledc/__init__.py +++ b/esphome/components/ledc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 2b1c181a62..d4e3327bb1 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -22,7 +22,7 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { - this->apply_frequency(this->frequency_); + this->update_frequency(this->frequency_); this->turn_off(); // Attach pin after setting default value ledcAttachPin(this->pin_->get_pin(), this->channel_); @@ -50,7 +50,7 @@ optional ledc_bit_depth_for_frequency(float frequency) { return {}; } -void LEDCOutput::apply_frequency(float frequency) { +void LEDCOutput::update_frequency(float frequency) { auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); if (!bit_depth_opt.has_value()) { ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index 3f56f502b0..b3b14fe855 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -19,7 +19,7 @@ class LEDCOutput : public output::FloatOutput, public Component { void set_channel(uint8_t channel) { this->channel_ = channel; } void set_frequency(float frequency) { this->frequency_ = frequency; } /// Dynamically change frequency at runtime - void apply_frequency(float frequency); + void update_frequency(float frequency) override; /// Setup LEDC. void setup() override; @@ -45,7 +45,7 @@ template class SetFrequencyAction : public Action { void play(Ts... x) { float freq = this->frequency_.value(x...); - this->parent_->apply_frequency(freq); + this->parent_->update_frequency(freq); } protected: diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 2a44b044b9..9d58fa47bf 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -14,6 +14,7 @@ from .types import ( # noqa LightState, AddressableLightState, light_ns, LightOutput, AddressableLight, \ LightTurnOnTrigger, LightTurnOffTrigger) +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True LightRestoreMode = light_ns.enum('LightRestoreMode') diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index dfab780658..b240f84e8f 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -103,10 +103,14 @@ class LightTurnOnTrigger : public Trigger<> { LightTurnOnTrigger(LightState *a_light) { a_light->add_new_remote_values_callback([this, a_light]() { auto is_on = a_light->current_values.is_on(); - if (is_on && !last_on_) { + // only trigger when going from off to on + auto should_trigger = is_on && !last_on_; + // Set new state immediately so that trigger() doesn't devolve + // into infinite loop + last_on_ = is_on; + if (should_trigger) { this->trigger(); } - last_on_ = is_on; }); last_on_ = a_light->current_values.is_on(); } @@ -120,10 +124,14 @@ class LightTurnOffTrigger : public Trigger<> { LightTurnOffTrigger(LightState *a_light) { a_light->add_new_remote_values_callback([this, a_light]() { auto is_on = a_light->current_values.is_on(); - if (!is_on && last_on_) { + // only trigger when going from on to off + auto should_trigger = !is_on && last_on_; + // Set new state immediately so that trigger() doesn't devolve + // into infinite loop + last_on_ = is_on; + if (should_trigger) { this->trigger(); } - last_on_ = is_on; }); last_on_ = a_light->current_values.is_on(); } diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index dcef60397d..d6d930e9d4 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -67,9 +67,9 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: AutomationLightEffect(const std::string &name) : LightEffect(name) {} - void stop() override { this->trig_->stop(); } + void stop() override { this->trig_->stop_action(); } void apply() override { - if (!this->trig_->is_running()) { + if (!this->trig_->is_action_running()) { this->trig_->trigger(); } } diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 39a93cbbcd..cdd05ae7b7 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -17,12 +17,13 @@ namespace light { * Not all values have to be populated though, for example a simple monochromatic light only needs * to access the state and brightness attributes. * - * PLease note all float values are automatically clamped. + * Please note all float values are automatically clamped. * * state - Whether the light should be on/off. Represented as a float for transitions. * brightness - The brightness of the light. * red, green, blue - RGB values. * white - The white value for RGBW lights. + * color_temperature - Temperature of the white value, range from 0.0 (cold) to 1.0 (warm) */ class LightColorValues { public: @@ -173,30 +174,39 @@ class LightColorValues { void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } /// Convert these light color values to a brightness-only representation and write them to brightness. - void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; } + void as_brightness(float *brightness, float gamma = 0) const { + *brightness = gamma_correct(this->state_ * this->brightness_, gamma); + } /// Convert these light color values to an RGB representation and write them to red, green, blue. - void as_rgb(float *red, float *green, float *blue) const { - *red = this->state_ * this->brightness_ * this->red_; - *green = this->state_ * this->brightness_ * this->green_; - *blue = this->state_ * this->brightness_ * this->blue_; + void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { + float brightness = this->state_ * this->brightness_; + if (color_interlock) { + brightness = brightness * (1.0f - this->white_); + } + *red = gamma_correct(brightness * this->red_, gamma); + *green = gamma_correct(brightness * this->green_, gamma); + *blue = gamma_correct(brightness * this->blue_, gamma); } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. - void as_rgbw(float *red, float *green, float *blue, float *white) const { - this->as_rgb(red, green, blue); - *white = this->state_ * this->brightness_ * this->white_; + void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, + bool color_interlock = false) const { + this->as_rgb(red, green, blue, gamma, color_interlock); + *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); } /// Convert these light color values to an RGBWW representation with the given parameters. void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *cold_white, float *warm_white, bool constant_brightness = false) const { - this->as_rgb(red, green, blue); + float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false, + bool color_interlock = false) const { + this->as_rgb(red, green, blue, gamma, color_interlock); const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; - *cold_white = this->state_ * this->brightness_ * this->white_ * cw_fraction; - *warm_white = this->state_ * this->brightness_ * this->white_ * ww_fraction; + const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *cold_white = white_level * cw_fraction; + *warm_white = white_level * ww_fraction; if (!constant_brightness) { const float max_cw_ww = std::max(ww_fraction, cw_fraction); *cold_white /= max_cw_ww; @@ -206,12 +216,13 @@ class LightColorValues { /// Convert these light color values to an CWWW representation with the given parameters. void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white, - bool constant_brightness = false) const { + float gamma = 0, bool constant_brightness = false) const { const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; - *cold_white = this->state_ * this->brightness_ * cw_fraction; - *warm_white = this->state_ * this->brightness_ * ww_fraction; + const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *cold_white = white_level * cw_fraction; + *warm_white = white_level * ww_fraction; if (!constant_brightness) { const float max_cw_ww = std::max(ww_fraction, cw_fraction); *cold_white /= max_cw_ww; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index c6e7df0811..d34bc88f53 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -400,26 +400,51 @@ LightColorValues LightCall::validate_() { this->green_ = optional(1.0f); this->blue_ = optional(1.0f); } + // make white values binary aka 0.0f or 1.0f...this allows brightness to do its job + if (traits.get_supports_color_interlock()) { + if (*this->white_ > 0.0f) { + this->white_ = optional(1.0f); + } else { + this->white_ = optional(0.0f); + } + } } - // White to 0% if (exclusively) setting any RGB value + // White to 0% if (exclusively) setting any RGB value that isn't 255,255,255 else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!this->white_.has_value()) { + if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f && traits.get_supports_rgb_white_value() && + traits.get_supports_color_interlock()) { + this->white_ = optional(1.0f); + } else if (!this->white_.has_value() || !traits.get_supports_rgb_white_value()) { this->white_ = optional(0.0f); } } // if changing Kelvin alone, change to white light else if (this->color_temperature_.has_value()) { - if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); + if (!traits.get_supports_color_interlock()) { + if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } } // if setting Kelvin from color (i.e. switching to white light), set White to 100% auto cv = this->parent_->remote_values; bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; bool now_white = *this->red_ == 1.0f && *this->blue_ == 1.0f && *this->green_ == 1.0f; - if (!this->white_.has_value() && was_color && now_white) { - this->white_ = optional(1.0f); + if (traits.get_supports_color_interlock()) { + if (cv.get_white() < 1.0f) { + this->white_ = optional(1.0f); + } + + if (was_color && !this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } + } else { + if (!this->white_.has_value() && was_color && now_white) { + this->white_ = optional(1.0f); + } } } @@ -702,39 +727,27 @@ LightOutput *LightState::get_output() const { return this->output_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { - this->current_values.as_brightness(brightness); - *brightness = gamma_correct(*brightness, this->gamma_correct_); + this->current_values.as_brightness(brightness, this->gamma_correct_); } -void LightState::current_values_as_rgb(float *red, float *green, float *blue) { - this->current_values.as_rgb(red, green, blue); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); +void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { + auto traits = this->get_traits(); + this->current_values.as_rgb(red, green, blue, this->gamma_correct_, traits.get_supports_color_interlock()); } -void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) { - this->current_values.as_rgbw(red, green, blue, white); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); - *white = gamma_correct(*white, this->gamma_correct_); +void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { + auto traits = this->get_traits(); + this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, traits.get_supports_color_interlock()); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, - bool constant_brightness) { + bool constant_brightness, bool color_interlock) { auto traits = this->get_traits(); this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white, constant_brightness); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); - *cold_white = gamma_correct(*cold_white, this->gamma_correct_); - *warm_white = gamma_correct(*warm_white, this->gamma_correct_); + warm_white, this->gamma_correct_, constant_brightness, + traits.get_supports_color_interlock()); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white, - constant_brightness); - *cold_white = gamma_correct(*cold_white, this->gamma_correct_); - *warm_white = gamma_correct(*warm_white, this->gamma_correct_); + this->gamma_correct_, constant_brightness); } void LightState::add_new_remote_values_callback(std::function &&send_callback) { this->remote_values_callback_.add(std::move(send_callback)); diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index f399cc2be4..e48cf9f864 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -266,12 +266,12 @@ class LightState : public Nameable, public Component { void current_values_as_brightness(float *brightness); - void current_values_as_rgb(float *red, float *green, float *blue); + void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false); - void current_values_as_rgbw(float *red, float *green, float *blue, float *white); + void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false); void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, - bool constant_brightness = false); + bool constant_brightness = false, bool color_interlock = false); void current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false); diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 2052b55c8e..ed9c0d44ea 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -20,6 +20,10 @@ class LightTraits { void set_supports_color_temperature(bool supports_color_temperature) { this->supports_color_temperature_ = supports_color_temperature; } + bool get_supports_color_interlock() const { return this->supports_color_interlock_; } + void set_supports_color_interlock(bool supports_color_interlock) { + this->supports_color_interlock_ = supports_color_interlock; + } float get_min_mireds() const { return this->min_mireds_; } void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; } float get_max_mireds() const { return this->max_mireds_; } @@ -32,6 +36,7 @@ class LightTraits { bool supports_color_temperature_{false}; float min_mireds_{0}; float max_mireds_{0}; + bool supports_color_interlock_{false}; }; } // namespace light diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 329c515aee..c447b0eee1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -8,6 +8,7 @@ from esphome.const import CONF_ARGS, CONF_BAUD_RATE, CONF_FORMAT, CONF_HARDWARE_ CONF_LEVEL, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] logger_ns = cg.esphome_ns.namespace('logger') LOG_LEVELS = { 'NONE': cg.global_ns.ESPHOME_LOG_LEVEL_NONE, diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index bc6951c9b9..140b8f26c1 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -103,7 +103,17 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { const char *msg = this->tx_buffer_ + offset; if (this->baud_rate_ > 0) this->hw_serial_->println(msg); +#ifdef ARDUINO_ARCH_ESP32 + // Suppress network-logging if memory constrained, but still log to serial + // ports. In some configurations (eg BLE enabled) there may be some transient + // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping + // here usually allows the stack to recover instead. + // See issue #1234 for analysis. + if (xPortGetFreeHeapSize() > 2048) + this->log_callback_.call(level, tag, msg); +#else this->log_callback_.call(level, tag, msg); +#endif } Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index dce28bd542..d1b5649b3e 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -11,7 +11,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX31855Sensor), cv.Optional(CONF_REFERENCE_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 523b5301d4..4e1411a2a4 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -16,7 +16,7 @@ FILTER = { CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX31856Sensor), cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py index ff1df9c5c8..7df36dfde4 100644 --- a/esphome/components/max31865/sensor.py +++ b/esphome/components/max31865/sensor.py @@ -20,7 +20,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2).extend({ cv.Required(CONF_RTD_NOMINAL_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=1000)), cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), cv.Optional(CONF_RTD_WIRES, default=4): cv.int_range(min=2, max=4), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py index 59d24a5283..7f0c943399 100644 --- a/esphome/components/max6675/sensor.py +++ b/esphome/components/max6675/sensor.py @@ -9,7 +9,7 @@ MAX6675Sensor = max6675_ns.class_('MAX6675Sensor', sensor.Sensor, cg.PollingComp CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX6675Sensor), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index c96454ce8c..0657f3f042 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -14,7 +14,7 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=255), cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max7219digit/__init__.py b/esphome/components/max7219digit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py new file mode 100644 index 0000000000..eb6d112a64 --- /dev/null +++ b/esphome/components/max7219digit/display.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, spi +from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS + +DEPENDENCIES = ['spi'] + +CONF_ROTATE_CHIP = 'rotate_chip' +CONF_SCROLL_SPEED = 'scroll_speed' +CONF_SCROLL_DWELL = 'scroll_dwell' +CONF_SCROLL_DELAY = 'scroll_delay' +CONF_SCROLL_ENABLE = 'scroll_enable' +CONF_SCROLL_MODE = 'scroll_mode' + +SCROLL_MODES = { + 'CONTINUOUS': 0, + 'STOP': 1, +} + +CHIP_MODES = { + '0': 0, + '90': 1, + '180': 2, + '270': 3, +} + +max7219_ns = cg.esphome_ns.namespace('max7219digit') +MAX7219Component = max7219_ns.class_('MAX7219Component', cg.PollingComponent, spi.SPIDevice, + display.DisplayBuffer) +MAX7219ComponentRef = MAX7219Component.operator('ref') + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MAX7219Component), + cv.Optional(CONF_NUM_CHIPS, default=4): cv.int_range(min=1, max=255), + cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), + cv.Optional(CONF_ROTATE_CHIP, default='0'): cv.enum(CHIP_MODES, upper=True), + cv.Optional(CONF_SCROLL_MODE, default='CONTINUOUS'): cv.enum(SCROLL_MODES, upper=True), + cv.Optional(CONF_SCROLL_ENABLE, default=True): cv.boolean, + cv.Optional(CONF_SCROLL_SPEED, default='250ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCROLL_DELAY, default='1000ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCROLL_DWELL, default='1000ms'): cv.positive_time_period_milliseconds, +}).extend(cv.polling_component_schema('500ms')).extend(spi.spi_device_schema(cs_pin_required=True)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + yield display.register_display(var, config) + + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) + cg.add(var.set_intensity(config[CONF_INTENSITY])) + cg.add(var.set_chip_orientation(config[CONF_ROTATE_CHIP])) + cg.add(var.set_scroll_speed(config[CONF_SCROLL_SPEED])) + cg.add(var.set_scroll_dwell(config[CONF_SCROLL_DWELL])) + cg.add(var.set_scroll_delay(config[CONF_SCROLL_DELAY])) + cg.add(var.set_scroll(config[CONF_SCROLL_ENABLE])) + cg.add(var.set_scroll_mode(config[CONF_SCROLL_MODE])) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(MAX7219ComponentRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp new file mode 100644 index 0000000000..ab9be4ad36 --- /dev/null +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -0,0 +1,296 @@ +#include "max7219digit.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "max7219font.h" + +namespace esphome { +namespace max7219digit { + +static const char *TAG = "max7219DIGIT"; + +static const uint8_t MAX7219_REGISTER_NOOP = 0x00; +static const uint8_t MAX7219_REGISTER_DECODE_MODE = 0x09; +static const uint8_t MAX7219_REGISTER_INTENSITY = 0x0A; +static const uint8_t MAX7219_REGISTER_SCAN_LIMIT = 0x0B; +static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; +static const uint8_t MAX7219_REGISTER_DISPLAY_TEST = 0x0F; +constexpr uint8_t MAX7219_NO_SHUTDOWN = 0x00; +constexpr uint8_t MAX7219_SHUTDOWN = 0x01; +constexpr uint8_t MAX7219_NO_DISPLAY_TEST = 0x00; +constexpr uint8_t MAX7219_DISPLAY_TEST = 0x01; + +float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void MAX7219Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX7219_DIGITS..."); + this->spi_setup(); + this->stepsleft_ = 0; + this->max_displaybuffer_.reserve(500); // Create base space to write buffer + // Initialize buffer with 0 for display so all non written pixels are blank + this->max_displaybuffer_.resize(this->num_chips_ * 8, 0); + // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway + this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); + // let's use our own ASCII -> led pattern encoding + this->send_to_all_(MAX7219_REGISTER_DECODE_MODE, 0); + // No display test with all the pixels on + this->send_to_all_(MAX7219_REGISTER_DISPLAY_TEST, MAX7219_NO_DISPLAY_TEST); + // SET Intsity of display + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); + // this->send_to_all_(MAX7219_REGISTER_INTENSITY, 1); + this->display(); + // power up + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); +} + +void MAX7219Component::dump_config() { + ESP_LOGCONFIG(TAG, "MAX7219DIGIT:"); + ESP_LOGCONFIG(TAG, " Number of Chips: %u", this->num_chips_); + ESP_LOGCONFIG(TAG, " Intensity: %u", this->intensity_); + ESP_LOGCONFIG(TAG, " Scroll Mode: %u", this->scroll_mode_); + ESP_LOGCONFIG(TAG, " Scroll Speed: %u", this->scroll_speed_); + ESP_LOGCONFIG(TAG, " Scroll Dwell: %u", this->scroll_dwell_); + ESP_LOGCONFIG(TAG, " Scroll Delay: %u", this->scroll_delay_); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_UPDATE_INTERVAL(this); +} + +void MAX7219Component::loop() { + unsigned long now = millis(); + + // check if the buffer has shrunk past the current position since last update + if ((this->max_displaybuffer_.size() >= this->old_buffer_size_ + 3) || + (this->max_displaybuffer_.size() <= this->old_buffer_size_ - 3)) { + this->stepsleft_ = 0; + this->display(); + this->old_buffer_size_ = this->max_displaybuffer_.size(); + } + + // Reset the counter back to 0 when full string has been displayed. + if (this->stepsleft_ > this->max_displaybuffer_.size()) + this->stepsleft_ = 0; + + // Return if there is no need to scroll or scroll is off + if (!this->scroll_ || (this->max_displaybuffer_.size() <= this->num_chips_ * 8)) { + this->display(); + return; + } + + if ((this->stepsleft_ == 0) && (now - this->last_scroll_ < this->scroll_delay_)) { + this->display(); + return; + } + + // Dwell time at end of string in case of stop at end + if (this->scroll_mode_ == 1) { + if (this->stepsleft_ >= this->max_displaybuffer_.size() - this->num_chips_ * 8 + 1) { + if (now - this->last_scroll_ >= this->scroll_dwell_) { + this->stepsleft_ = 0; + this->last_scroll_ = now; + this->display(); + } + return; + } + } + + // Actual call to scroll left action + if (now - this->last_scroll_ >= this->scroll_speed_) { + this->last_scroll_ = now; + this->scroll_left(); + this->display(); + } +} + +void MAX7219Component::display() { + uint8_t pixels[8]; + // Run this loop for every MAX CHIP (GRID OF 64 leds) + // Run this routine for the rows of every chip 8x row 0 top to 7 bottom + // Fill the pixel parameter with diplay data + // Send the data to the chip + for (uint8_t i = 0; i < this->num_chips_; i++) { + for (uint8_t j = 0; j < 8; j++) { + pixels[j] = this->max_displaybuffer_[i * 8 + j]; + } + this->send64pixels(i, pixels); + } +} + +int MAX7219Component::get_height_internal() { + return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHE + // TO BE DONE -> CREATE Virtual size of screen and scroll +} + +int MAX7219Component::get_width_internal() { return this->num_chips_ * 8; } + +size_t MAX7219Component::get_buffer_length_() { return this->num_chips_ * 8; } + +void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x + 1 > this->max_displaybuffer_.size()) { // Extend the display buffer in case required + this->max_displaybuffer_.resize(x + 1, this->bckgrnd_); + } + + if (y >= this->get_height_internal() || y < 0) // If pixel is outside display then dont draw + return; + + uint16_t pos = x; // X is starting at 0 top left + uint8_t subpos = y; // Y is starting at 0 top left + + if (color.is_on()) { + this->max_displaybuffer_[pos] |= (1 << subpos); + } else { + this->max_displaybuffer_[pos] &= ~(1 << subpos); + } +} + +void MAX7219Component::send_byte_(uint8_t a_register, uint8_t data) { + this->write_byte(a_register); // Write register value to MAX + this->write_byte(data); // Followed by actual data +} +void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { + this->enable(); // Enable SPI + for (uint8_t i = 0; i < this->num_chips_; i++) // Run the loop for every MAX chip in the stack + this->send_byte_(a_register, data); // Send the data to the chips + this->disable(); // Disable SPI +} +void MAX7219Component::update() { + this->update_ = true; + this->max_displaybuffer_.clear(); + this->max_displaybuffer_.resize(this->num_chips_ * 8, this->bckgrnd_); + if (this->writer_local_.has_value()) // insert Labda function if available + (*this->writer_local_)(*this); +} + +void MAX7219Component::invert_on_off(bool on_off) { this->invert_ = on_off; }; +void MAX7219Component::invert_on_off() { this->invert_ = !this->invert_; }; + +void MAX7219Component::turn_on_off(bool on_off) { + if (on_off) { + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); + } else { + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 0); + } +} + +void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell) { + this->set_scroll(on_off); + this->set_scroll_mode(mode); + this->set_scroll_speed(speed); + this->set_scroll_dwell(dwell); + this->set_scroll_delay(delay); +} + +void MAX7219Component::scroll(bool on_off, uint8_t mode) { + this->set_scroll(on_off); + this->set_scroll_mode(mode); +} + +void MAX7219Component::intensity(uint8_t intensity) { + this->intensity_ = intensity; + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); +} + +void MAX7219Component::scroll(bool on_off) { this->set_scroll(on_off); } + +void MAX7219Component::scroll_left() { + if (this->update_) { + this->max_displaybuffer_.push_back(this->bckgrnd_); + for (uint16_t i = 0; i < this->stepsleft_; i++) { + this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); + this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); + this->update_ = false; + } + } else { + this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); + this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); + } + this->stepsleft_++; +} + +void MAX7219Component::send_char(uint8_t chip, uint8_t data) { + // get this character from PROGMEM + for (uint8_t i = 0; i < 8; i++) + this->max_displaybuffer_[chip * 8 + i] = pgm_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); +} // end of send_char + +// send one character (data) to position (chip) + +void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { + for (uint8_t col = 0; col < 8; col++) { // RUN THIS LOOP 8 times until column is 7 + this->enable(); // start sending by enabling SPI + for (uint8_t i = 0; i < chip; i++) // send extra NOPs to push the pixels out to extra displays + this->send_byte_(MAX7219_REGISTER_NOOP, + MAX7219_REGISTER_NOOP); // run this loop unit the matching chip is reached + uint8_t b = 0; // rotate pixels 90 degrees -- set byte to 0 + if (this->orientation_ == 0) { + for (uint8_t i = 0; i < 8; i++) { + // run this loop 8 times for all the pixels[8] received + b |= ((pixels[i] >> col) & 1) << (7 - i); // change the column bits into row bits + } + } else if (this->orientation_ == 1) { + b = pixels[col]; + } else if (this->orientation_ == 2) { + for (uint8_t i = 0; i < 8; i++) { + b |= ((pixels[i] >> (7 - col)) << (7 - i)); + } + } else { + b = pixels[7 - col]; + } + // send this byte to dispay at selected chip + if (this->invert_) { + this->send_byte_(col + 1, ~b); + } else { + this->send_byte_(col + 1, b); + } + for (int i = 0; i < this->num_chips_ - chip - 1; i++) // end with enough NOPs so later chips don't update + this->send_byte_(MAX7219_REGISTER_NOOP, MAX7219_REGISTER_NOOP); + this->disable(); // all done disable SPI + } // end of for each column +} // end of send64pixels + +uint8_t MAX7219Component::printdigit(const char *str) { return this->printdigit(0, str); } + +uint8_t MAX7219Component::printdigit(uint8_t start_pos, const char *s) { + uint8_t chip = start_pos; + for (; chip < this->num_chips_ && *s; chip++) + send_char(chip, *s++); + // space out rest + while (chip < (this->num_chips_)) + send_char(chip++, ' '); + return 0; +} // end of sendString + +uint8_t MAX7219Component::printdigitf(uint8_t pos, const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->printdigit(pos, buffer); + return 0; +} +uint8_t MAX7219Component::printdigitf(const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->printdigit(buffer); + return 0; +} + +#ifdef USE_TIME +uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, time::ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + return this->printdigit(pos, buffer); + return 0; +} +uint8_t MAX7219Component::strftimedigit(const char *format, time::ESPTime time) { + return this->strftimedigit(0, format, time); +} +#endif + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h new file mode 100644 index 0000000000..dfd61e84e5 --- /dev/null +++ b/esphome/components/max7219digit/max7219digit.h @@ -0,0 +1,107 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace max7219digit { + +class MAX7219Component; + +using max7219_writer_t = std::function; + +class MAX7219Component : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_writer(max7219_writer_t &&writer) { this->writer_local_ = writer; }; + + void setup() override; + + void loop() override; + + void dump_config() override; + + void update() override; + + float get_setup_priority() const override; + + void display(); + + void invert_on_off(bool on_off); + void invert_on_off(); + + void turn_on_off(bool on_off); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + int get_height_internal() override; + int get_width_internal() override; + + void set_intensity(uint8_t intensity) { this->intensity_ = intensity; }; + void set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }; + void set_chip_orientation(uint8_t rotate) { this->orientation_ = rotate; }; + void set_scroll_speed(uint16_t speed) { this->scroll_speed_ = speed; }; + void set_scroll_dwell(uint16_t dwell) { this->scroll_dwell_ = dwell; }; + void set_scroll_delay(uint16_t delay) { this->scroll_delay_ = delay; }; + void set_scroll(bool on_off) { this->scroll_ = on_off; }; + void set_scroll_mode(uint8_t mode) { this->scroll_mode_ = mode; }; + + void send_char(byte chip, byte data); + void send64pixels(byte chip, const byte pixels[8]); + + void scroll_left(); + void scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell); + void scroll(bool on_off, uint8_t mode); + void scroll(bool on_off); + void intensity(uint8_t intensity); + + /// Evaluate the printf-format and print the result at the given position. + uint8_t printdigitf(uint8_t pos, const char *format, ...) __attribute__((format(printf, 3, 4))); + /// Evaluate the printf-format and print the result at position 0. + uint8_t printdigitf(const char *format, ...) __attribute__((format(printf, 2, 3))); + + /// Print `str` at the given position. + uint8_t printdigit(uint8_t pos, const char *str); + /// Print `str` at position 0. + uint8_t printdigit(const char *str); + +#ifdef USE_TIME + /// Evaluate the strftime-format and print the result at the given position. + uint8_t strftimedigit(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + + /// Evaluate the strftime-format and print the result at position 0. + uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); +#endif + + protected: + void send_byte_(uint8_t a_register, uint8_t data); + void send_to_all_(uint8_t a_register, uint8_t data); + + uint8_t intensity_; /// Intensity of the display from 0 to 15 (most) + uint8_t num_chips_; + bool scroll_; + bool update_{false}; + uint16_t scroll_speed_; + uint16_t scroll_delay_; + uint16_t scroll_dwell_; + uint16_t old_buffer_size_ = 0; + uint8_t scroll_mode_; + bool invert_ = false; + uint8_t orientation_; + uint8_t bckgrnd_ = 0x0; + std::vector max_displaybuffer_; + unsigned long last_scroll_ = 0; + uint16_t stepsleft_; + size_t get_buffer_length_(); + optional writer_local_{}; +}; + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h new file mode 100644 index 0000000000..3d42d1cc2d --- /dev/null +++ b/esphome/components/max7219digit/max7219font.h @@ -0,0 +1,268 @@ +#pragma once + +namespace esphome { +namespace max7219digit { + +// bit patterns for the CP437 font + +const byte MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 + {0x7E, 0x81, 0x95, 0xB1, 0xB1, 0x95, 0x81, 0x7E}, // 0x01 + {0x7E, 0xFF, 0xEB, 0xCF, 0xCF, 0xEB, 0xFF, 0x7E}, // 0x02 + {0x0E, 0x1F, 0x3F, 0x7E, 0x3F, 0x1F, 0x0E, 0x00}, // 0x03 + {0x08, 0x1C, 0x3E, 0x7F, 0x3E, 0x1C, 0x08, 0x00}, // 0x04 + {0x18, 0xBA, 0xFF, 0xFF, 0xFF, 0xBA, 0x18, 0x00}, // 0x05 + {0x10, 0xB8, 0xFC, 0xFF, 0xFC, 0xB8, 0x10, 0x00}, // 0x06 + {0x00, 0x00, 0x18, 0x3C, 0x3C, 0x18, 0x00, 0x00}, // 0x07 + {0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF}, // 0x08 + {0x00, 0x3C, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00}, // 0x09 + {0xFF, 0xC3, 0x99, 0xBD, 0xBD, 0x99, 0xC3, 0xFF}, // 0x0A + {0x70, 0xF8, 0x88, 0x88, 0xFD, 0x7F, 0x07, 0x0F}, // 0x0B + {0x00, 0x4E, 0x5F, 0xF1, 0xF1, 0x5F, 0x4E, 0x00}, // 0x0C + {0xC0, 0xE0, 0xFF, 0x7F, 0x05, 0x05, 0x07, 0x07}, // 0x0D + {0xC0, 0xFF, 0x7F, 0x05, 0x05, 0x65, 0x7F, 0x3F}, // 0x0E + {0x99, 0x5A, 0x3C, 0xE7, 0xE7, 0x3C, 0x5A, 0x99}, // 0x0F + {0x7F, 0x3E, 0x3E, 0x1C, 0x1C, 0x08, 0x08, 0x00}, // 0x10 + {0x08, 0x08, 0x1C, 0x1C, 0x3E, 0x3E, 0x7F, 0x00}, // 0x11 + {0x00, 0x24, 0x66, 0xFF, 0xFF, 0x66, 0x24, 0x00}, // 0x12 + {0x00, 0x5F, 0x5F, 0x00, 0x00, 0x5F, 0x5F, 0x00}, // 0x13 + {0x06, 0x0F, 0x09, 0x7F, 0x7F, 0x01, 0x7F, 0x7F}, // 0x14 + {0x40, 0xDA, 0xBF, 0xA5, 0xFD, 0x59, 0x03, 0x02}, // 0x15 + {0x00, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x00}, // 0x16 + {0x80, 0x94, 0xB6, 0xFF, 0xFF, 0xB6, 0x94, 0x80}, // 0x17 + {0x00, 0x04, 0x06, 0x7F, 0x7F, 0x06, 0x04, 0x00}, // 0x18 + {0x00, 0x10, 0x30, 0x7F, 0x7F, 0x30, 0x10, 0x00}, // 0x19 + {0x08, 0x08, 0x08, 0x2A, 0x3E, 0x1C, 0x08, 0x00}, // 0x1A + {0x08, 0x1C, 0x3E, 0x2A, 0x08, 0x08, 0x08, 0x00}, // 0x1B + {0x3C, 0x3C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00}, // 0x1C + {0x08, 0x1C, 0x3E, 0x08, 0x08, 0x3E, 0x1C, 0x08}, // 0x1D + {0x30, 0x38, 0x3C, 0x3E, 0x3E, 0x3C, 0x38, 0x30}, // 0x1E + {0x06, 0x0E, 0x1E, 0x3E, 0x3E, 0x1E, 0x0E, 0x06}, // 0x1F + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' + {0x00, 0x06, 0x5F, 0x5F, 0x06, 0x00, 0x00, 0x00}, // '!' + {0x00, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x00}, // '"' + {0x14, 0x7F, 0x7F, 0x14, 0x7F, 0x7F, 0x14, 0x00}, // '#' + {0x24, 0x2E, 0x6B, 0x6B, 0x3A, 0x12, 0x00, 0x00}, // '$' + {0x46, 0x66, 0x30, 0x18, 0x0C, 0x66, 0x62, 0x00}, // '%' + {0x30, 0x7A, 0x4F, 0x5D, 0x37, 0x7A, 0x48, 0x00}, // '&' + {0x04, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // ''' + {0x00, 0x1C, 0x3E, 0x63, 0x41, 0x00, 0x00, 0x00}, // '(' + {0x00, 0x41, 0x63, 0x3E, 0x1C, 0x00, 0x00, 0x00}, // ')' + {0x08, 0x2A, 0x3E, 0x1C, 0x1C, 0x3E, 0x2A, 0x08}, // '*' + {0x08, 0x08, 0x3E, 0x3E, 0x08, 0x08, 0x00, 0x00}, // '+' + {0x00, 0x80, 0xE0, 0x60, 0x00, 0x00, 0x00, 0x00}, // ',' + {0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00}, // '-' + {0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00}, // '.' + {0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // '/' + {0x3E, 0x7F, 0x71, 0x59, 0x4D, 0x7F, 0x3E, 0x00}, // '0' + {0x40, 0x42, 0x7F, 0x7F, 0x40, 0x40, 0x00, 0x00}, // '1' + {0x62, 0x73, 0x59, 0x49, 0x6F, 0x66, 0x00, 0x00}, // '2' + {0x22, 0x63, 0x49, 0x49, 0x7F, 0x36, 0x00, 0x00}, // '3' + {0x18, 0x1C, 0x16, 0x53, 0x7F, 0x7F, 0x50, 0x00}, // '4' + {0x27, 0x67, 0x45, 0x45, 0x7D, 0x39, 0x00, 0x00}, // '5' + {0x3C, 0x7E, 0x4B, 0x49, 0x79, 0x30, 0x00, 0x00}, // '6' + {0x03, 0x03, 0x71, 0x79, 0x0F, 0x07, 0x00, 0x00}, // '7' + {0x36, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00, 0x00}, // '8' + {0x06, 0x4F, 0x49, 0x69, 0x3F, 0x1E, 0x00, 0x00}, // '9' + {0x00, 0x00, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00}, // ':' + {0x00, 0x80, 0xE6, 0x66, 0x00, 0x00, 0x00, 0x00}, // ';' + {0x08, 0x1C, 0x36, 0x63, 0x41, 0x00, 0x00, 0x00}, // '<' + {0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x00, 0x00}, // '=' + {0x00, 0x41, 0x63, 0x36, 0x1C, 0x08, 0x00, 0x00}, // '>' + {0x02, 0x03, 0x51, 0x59, 0x0F, 0x06, 0x00, 0x00}, // '?' + {0x3E, 0x7F, 0x41, 0x5D, 0x5D, 0x1F, 0x1E, 0x00}, // '@' + {0x7C, 0x7E, 0x13, 0x13, 0x7E, 0x7C, 0x00, 0x00}, // 'A' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}, // 'B' + {0x1C, 0x3E, 0x63, 0x41, 0x41, 0x63, 0x22, 0x00}, // 'C' + {0x41, 0x7F, 0x7F, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 'D' + {0x41, 0x7F, 0x7F, 0x49, 0x5D, 0x41, 0x63, 0x00}, // 'E' + {0x41, 0x7F, 0x7F, 0x49, 0x1D, 0x01, 0x03, 0x00}, // 'F' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x7F, 0x7F, 0x08, 0x08, 0x7F, 0x7F, 0x00, 0x00}, // 'H' + {0x00, 0x41, 0x7F, 0x7F, 0x41, 0x00, 0x00, 0x00}, // 'I' + {0x30, 0x70, 0x40, 0x41, 0x7F, 0x3F, 0x01, 0x00}, // 'J' + {0x41, 0x7F, 0x7F, 0x08, 0x1C, 0x77, 0x63, 0x00}, // 'K' + {0x41, 0x7F, 0x7F, 0x41, 0x40, 0x60, 0x70, 0x00}, // 'L' + {0x7F, 0x7F, 0x0E, 0x1C, 0x0E, 0x7F, 0x7F, 0x00}, // 'M' + {0x7F, 0x7F, 0x06, 0x0C, 0x18, 0x7F, 0x7F, 0x00}, // 'N' + {0x1C, 0x3E, 0x63, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 'O' + {0x41, 0x7F, 0x7F, 0x49, 0x09, 0x0F, 0x06, 0x00}, // 'P' + {0x1E, 0x3F, 0x21, 0x71, 0x7F, 0x5E, 0x00, 0x00}, // 'Q' + {0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x26, 0x6F, 0x4D, 0x59, 0x73, 0x32, 0x00, 0x00}, // 'S' + {0x03, 0x41, 0x7F, 0x7F, 0x41, 0x03, 0x00, 0x00}, // 'T' + {0x7F, 0x7F, 0x40, 0x40, 0x7F, 0x7F, 0x00, 0x00}, // 'U' + {0x1F, 0x3F, 0x60, 0x60, 0x3F, 0x1F, 0x00, 0x00}, // 'V' + {0x7F, 0x7F, 0x30, 0x18, 0x30, 0x7F, 0x7F, 0x00}, // 'W' + {0x43, 0x67, 0x3C, 0x18, 0x3C, 0x67, 0x43, 0x00}, // 'X' + {0x07, 0x4F, 0x78, 0x78, 0x4F, 0x07, 0x00, 0x00}, // 'Y' + {0x47, 0x63, 0x71, 0x59, 0x4D, 0x67, 0x73, 0x00}, // 'Z' + {0x00, 0x7F, 0x7F, 0x41, 0x41, 0x00, 0x00, 0x00}, // '[' + {0x01, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00}, // backslash + {0x00, 0x41, 0x41, 0x7F, 0x7F, 0x00, 0x00, 0x00}, // ']' + {0x08, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x08, 0x00}, // '^' + {0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}, // '_' + {0x00, 0x00, 0x03, 0x07, 0x04, 0x00, 0x00, 0x00}, // '`' + {0x20, 0x74, 0x54, 0x54, 0x3C, 0x78, 0x40, 0x00}, // 'a' + {0x41, 0x7F, 0x3F, 0x48, 0x48, 0x78, 0x30, 0x00}, // 'b' + {0x38, 0x7C, 0x44, 0x44, 0x6C, 0x28, 0x00, 0x00}, // 'c' + {0x30, 0x78, 0x48, 0x49, 0x3F, 0x7F, 0x40, 0x00}, // 'd' + {0x38, 0x7C, 0x54, 0x54, 0x5C, 0x18, 0x00, 0x00}, // 'e' + {0x48, 0x7E, 0x7F, 0x49, 0x03, 0x02, 0x00, 0x00}, // 'f' + {0x98, 0xBC, 0xA4, 0xA4, 0xF8, 0x7C, 0x04, 0x00}, // 'g' + {0x41, 0x7F, 0x7F, 0x08, 0x04, 0x7C, 0x78, 0x00}, // 'h' + {0x00, 0x44, 0x7D, 0x7D, 0x40, 0x00, 0x00, 0x00}, // 'i' + {0x60, 0xE0, 0x80, 0x80, 0xFD, 0x7D, 0x00, 0x00}, // 'j' + {0x41, 0x7F, 0x7F, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 'k' + {0x00, 0x41, 0x7F, 0x7F, 0x40, 0x00, 0x00, 0x00}, // 'l' + {0x7C, 0x7C, 0x18, 0x38, 0x1C, 0x7C, 0x78, 0x00}, // 'm' + {0x7C, 0x7C, 0x04, 0x04, 0x7C, 0x78, 0x00, 0x00}, // 'n' + {0x38, 0x7C, 0x44, 0x44, 0x7C, 0x38, 0x00, 0x00}, // 'o' + {0x84, 0xFC, 0xF8, 0xA4, 0x24, 0x3C, 0x18, 0x00}, // 'p' + {0x18, 0x3C, 0x24, 0xA4, 0xF8, 0xFC, 0x84, 0x00}, // 'q' + {0x44, 0x7C, 0x78, 0x4C, 0x04, 0x1C, 0x18, 0x00}, // 'r' + {0x48, 0x5C, 0x54, 0x54, 0x74, 0x24, 0x00, 0x00}, // 's' + {0x00, 0x04, 0x3E, 0x7F, 0x44, 0x24, 0x00, 0x00}, // 't' + {0x3C, 0x7C, 0x40, 0x40, 0x3C, 0x7C, 0x40, 0x00}, // 'u' + {0x1C, 0x3C, 0x60, 0x60, 0x3C, 0x1C, 0x00, 0x00}, // 'v' + {0x3C, 0x7C, 0x70, 0x38, 0x70, 0x7C, 0x3C, 0x00}, // 'w' + {0x44, 0x6C, 0x38, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 'x' + {0x9C, 0xBC, 0xA0, 0xA0, 0xFC, 0x7C, 0x00, 0x00}, // 'y' + {0x4C, 0x64, 0x74, 0x5C, 0x4C, 0x64, 0x00, 0x00}, // 'z' + {0x08, 0x08, 0x3E, 0x77, 0x41, 0x41, 0x00, 0x00}, // '{' + {0x00, 0x00, 0x00, 0x77, 0x77, 0x00, 0x00, 0x00}, // '|' + {0x41, 0x41, 0x77, 0x3E, 0x08, 0x08, 0x00, 0x00}, // '}' + {0x02, 0x03, 0x01, 0x03, 0x02, 0x03, 0x01, 0x00}, // '~' + {0x70, 0x78, 0x4C, 0x46, 0x4C, 0x78, 0x70, 0x00}, // 0x7F + {0x0E, 0x9F, 0x91, 0xB1, 0xFB, 0x4A, 0x00, 0x00}, // 0x80 + {0x3A, 0x7A, 0x40, 0x40, 0x7A, 0x7A, 0x40, 0x00}, // 0x81 + {0x38, 0x7C, 0x54, 0x55, 0x5D, 0x19, 0x00, 0x00}, // 0x82 + {0x02, 0x23, 0x75, 0x55, 0x55, 0x7D, 0x7B, 0x42}, // 0x83 + {0x21, 0x75, 0x54, 0x54, 0x7D, 0x79, 0x40, 0x00}, // 0x84 + {0x21, 0x75, 0x55, 0x54, 0x7C, 0x78, 0x40, 0x00}, // 0x85 + {0x20, 0x74, 0x57, 0x57, 0x7C, 0x78, 0x40, 0x00}, // 0x86 + {0x18, 0x3C, 0xA4, 0xA4, 0xE4, 0x40, 0x00, 0x00}, // 0x87 + {0x02, 0x3B, 0x7D, 0x55, 0x55, 0x5D, 0x1B, 0x02}, // 0x88 + {0x39, 0x7D, 0x54, 0x54, 0x5D, 0x19, 0x00, 0x00}, // 0x89 + {0x39, 0x7D, 0x55, 0x54, 0x5C, 0x18, 0x00, 0x00}, // 0x8A + {0x01, 0x45, 0x7C, 0x7C, 0x41, 0x01, 0x00, 0x00}, // 0x8B + {0x02, 0x03, 0x45, 0x7D, 0x7D, 0x43, 0x02, 0x00}, // 0x8C + {0x01, 0x45, 0x7D, 0x7C, 0x40, 0x00, 0x00, 0x00}, // 0x8D + {0x79, 0x7D, 0x16, 0x12, 0x16, 0x7D, 0x79, 0x00}, // 0x8E + {0x70, 0x78, 0x2B, 0x2B, 0x78, 0x70, 0x00, 0x00}, // 0x8F + {0x44, 0x7C, 0x7C, 0x55, 0x55, 0x45, 0x00, 0x00}, // 0x90 + {0x20, 0x74, 0x54, 0x54, 0x7C, 0x7C, 0x54, 0x54}, // 0x91 + {0x7C, 0x7E, 0x0B, 0x09, 0x7F, 0x7F, 0x49, 0x00}, // 0x92 + {0x32, 0x7B, 0x49, 0x49, 0x7B, 0x32, 0x00, 0x00}, // 0x93 + {0x32, 0x7A, 0x48, 0x48, 0x7A, 0x32, 0x00, 0x00}, // 0x94 + {0x32, 0x7A, 0x4A, 0x48, 0x78, 0x30, 0x00, 0x00}, // 0x95 + {0x3A, 0x7B, 0x41, 0x41, 0x7B, 0x7A, 0x40, 0x00}, // 0x96 + {0x3A, 0x7A, 0x42, 0x40, 0x78, 0x78, 0x40, 0x00}, // 0x97 + {0x9A, 0xBA, 0xA0, 0xA0, 0xFA, 0x7A, 0x00, 0x00}, // 0x98 + {0x01, 0x19, 0x3C, 0x66, 0x66, 0x3C, 0x19, 0x01}, // 0x99 + {0x3D, 0x7D, 0x40, 0x40, 0x7D, 0x3D, 0x00, 0x00}, // 0x9A + {0x18, 0x3C, 0x24, 0xE7, 0xE7, 0x24, 0x24, 0x00}, // 0x9B + {0x68, 0x7E, 0x7F, 0x49, 0x43, 0x66, 0x20, 0x00}, // 0x9C + {0x2B, 0x2F, 0xFC, 0xFC, 0x2F, 0x2B, 0x00, 0x00}, // 0x9D + {0xFF, 0xFF, 0x09, 0x09, 0x2F, 0xF6, 0xF8, 0xA0}, // 0x9E + {0x40, 0xC0, 0x88, 0xFE, 0x7F, 0x09, 0x03, 0x02}, // 0x9F + {0x20, 0x74, 0x54, 0x55, 0x7D, 0x79, 0x40, 0x00}, // 0xA0 + {0x00, 0x44, 0x7D, 0x7D, 0x41, 0x00, 0x00, 0x00}, // 0xA1 + {0x30, 0x78, 0x48, 0x4A, 0x7A, 0x32, 0x00, 0x00}, // 0xA2 + {0x38, 0x78, 0x40, 0x42, 0x7A, 0x7A, 0x40, 0x00}, // 0xA3 + {0x7A, 0x7A, 0x0A, 0x0A, 0x7A, 0x70, 0x00, 0x00}, // 0xA4 + {0x7D, 0x7D, 0x19, 0x31, 0x7D, 0x7D, 0x00, 0x00}, // 0xA5 + {0x00, 0x26, 0x2F, 0x29, 0x2F, 0x2F, 0x28, 0x00}, // 0xA6 + {0x00, 0x26, 0x2F, 0x29, 0x2F, 0x26, 0x00, 0x00}, // 0xA7 + {0x30, 0x78, 0x4D, 0x45, 0x60, 0x20, 0x00, 0x00}, // 0xA8 + {0x38, 0x38, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00}, // 0xA9 + {0x08, 0x08, 0x08, 0x08, 0x38, 0x38, 0x00, 0x00}, // 0xAA + {0x4F, 0x6F, 0x30, 0x18, 0xCC, 0xEE, 0xBB, 0x91}, // 0xAB + {0x4F, 0x6F, 0x30, 0x18, 0x6C, 0x76, 0xFB, 0xF9}, // 0xAC + {0x00, 0x00, 0x00, 0x7B, 0x7B, 0x00, 0x00, 0x00}, // 0xAD + {0x08, 0x1C, 0x36, 0x22, 0x08, 0x1C, 0x36, 0x22}, // 0xAE + {0x22, 0x36, 0x1C, 0x08, 0x22, 0x36, 0x1C, 0x08}, // 0xAF + {0xAA, 0x00, 0x55, 0x00, 0xAA, 0x00, 0x55, 0x00}, // 0xB0 + {0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55}, // 0xB1 + {0xDD, 0xFF, 0xAA, 0x77, 0xDD, 0xAA, 0xFF, 0x77}, // 0xB2 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB3 + {0x10, 0x10, 0x10, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB4 + {0x14, 0x14, 0x14, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB5 + {0x10, 0x10, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}, // 0xB6 + {0x10, 0x10, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x00}, // 0xB7 + {0x14, 0x14, 0x14, 0xFC, 0xFC, 0x00, 0x00, 0x00}, // 0xB8 + {0x14, 0x14, 0xF7, 0xF7, 0x00, 0xFF, 0xFF, 0x00}, // 0xB9 + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}, // 0xBA + {0x14, 0x14, 0xF4, 0xF4, 0x04, 0xFC, 0xFC, 0x00}, // 0xBB + {0x14, 0x14, 0x17, 0x17, 0x10, 0x1F, 0x1F, 0x00}, // 0xBC + {0x10, 0x10, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x00}, // 0xBD + {0x14, 0x14, 0x14, 0x1F, 0x1F, 0x00, 0x00, 0x00}, // 0xBE + {0x10, 0x10, 0x10, 0xF0, 0xF0, 0x00, 0x00, 0x00}, // 0xBF + {0x00, 0x00, 0x00, 0x1F, 0x1F, 0x10, 0x10, 0x10}, // 0xC0 + {0x10, 0x10, 0x10, 0x1F, 0x1F, 0x10, 0x10, 0x10}, // 0xC1 + {0x10, 0x10, 0x10, 0xF0, 0xF0, 0x10, 0x10, 0x10}, // 0xC2 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x10, 0x10, 0x10}, // 0xC3 + {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}, // 0xC4 + {0x10, 0x10, 0x10, 0xFF, 0xFF, 0x10, 0x10, 0x10}, // 0xC5 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x14, 0x14, 0x14}, // 0xC6 + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x10}, // 0xC7 + {0x00, 0x00, 0x1F, 0x1F, 0x10, 0x17, 0x17, 0x14}, // 0xC8 + {0x00, 0x00, 0xFC, 0xFC, 0x04, 0xF4, 0xF4, 0x14}, // 0xC9 + {0x14, 0x14, 0x17, 0x17, 0x10, 0x17, 0x17, 0x14}, // 0xCA + {0x14, 0x14, 0xF4, 0xF4, 0x04, 0xF4, 0xF4, 0x14}, // 0xCB + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xF7, 0xF7, 0x14}, // 0xCC + {0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14}, // 0xCD + {0x14, 0x14, 0xF7, 0xF7, 0x00, 0xF7, 0xF7, 0x14}, // 0xCE + {0x14, 0x14, 0x14, 0x17, 0x17, 0x14, 0x14, 0x14}, // 0xCF + {0x10, 0x10, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x10}, // 0xD0 + {0x14, 0x14, 0x14, 0xF4, 0xF4, 0x14, 0x14, 0x14}, // 0xD1 + {0x10, 0x10, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x10}, // 0xD2 + {0x00, 0x00, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x10}, // 0xD3 + {0x00, 0x00, 0x00, 0x1F, 0x1F, 0x14, 0x14, 0x14}, // 0xD4 + {0x00, 0x00, 0x00, 0xFC, 0xFC, 0x14, 0x14, 0x14}, // 0xD5 + {0x00, 0x00, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x10}, // 0xD6 + {0x10, 0x10, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0x10}, // 0xD7 + {0x14, 0x14, 0x14, 0xFF, 0xFF, 0x14, 0x14, 0x14}, // 0xD8 + {0x10, 0x10, 0x10, 0x1F, 0x1F, 0x00, 0x00, 0x00}, // 0xD9 + {0x00, 0x00, 0x00, 0xF0, 0xF0, 0x10, 0x10, 0x10}, // 0xDA + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // 0xDB + {0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0}, // 0xDC + {0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00}, // 0xDD + {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}, // 0xDE + {0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F}, // 0xDF + {0x38, 0x7C, 0x44, 0x6C, 0x38, 0x6C, 0x44, 0x00}, // 0xE0 + {0xFC, 0xFE, 0x2A, 0x2A, 0x3E, 0x14, 0x00, 0x00}, // 0xE1 + {0x7E, 0x7E, 0x02, 0x02, 0x06, 0x06, 0x00, 0x00}, // 0xE2 + {0x02, 0x7E, 0x7E, 0x02, 0x7E, 0x7E, 0x02, 0x00}, // 0xE3 + {0x63, 0x77, 0x5D, 0x49, 0x63, 0x63, 0x00, 0x00}, // 0xE4 + {0x38, 0x7C, 0x44, 0x7C, 0x3C, 0x04, 0x04, 0x00}, // 0xE5 + {0x80, 0xFE, 0x7E, 0x20, 0x20, 0x3E, 0x1E, 0x00}, // 0xE6 + {0x04, 0x06, 0x02, 0x7E, 0x7C, 0x06, 0x02, 0x00}, // 0xE7 + {0x99, 0xBD, 0xE7, 0xE7, 0xBD, 0x99, 0x00, 0x00}, // 0xE8 + {0x1C, 0x3E, 0x6B, 0x49, 0x6B, 0x3E, 0x1C, 0x00}, // 0xE9 + {0x4C, 0x7E, 0x73, 0x01, 0x73, 0x7E, 0x4C, 0x00}, // 0xEA + {0x30, 0x78, 0x4A, 0x4F, 0x7D, 0x39, 0x00, 0x00}, // 0xEB + {0x18, 0x3C, 0x24, 0x3C, 0x3C, 0x24, 0x3C, 0x18}, // 0xEC + {0x98, 0xFC, 0x64, 0x3C, 0x3E, 0x27, 0x3D, 0x18}, // 0xED + {0x1C, 0x3E, 0x6B, 0x49, 0x49, 0x00, 0x00, 0x00}, // 0xEE + {0x7E, 0x7F, 0x01, 0x01, 0x7F, 0x7E, 0x00, 0x00}, // 0xEF + {0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00}, // 0xF0 + {0x44, 0x44, 0x5F, 0x5F, 0x44, 0x44, 0x00, 0x00}, // 0xF1 + {0x40, 0x51, 0x5B, 0x4E, 0x44, 0x40, 0x00, 0x00}, // 0xF2 + {0x40, 0x44, 0x4E, 0x5B, 0x51, 0x40, 0x00, 0x00}, // 0xF3 + {0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x07, 0x06}, // 0xF4 + {0x60, 0xE0, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0x00}, // 0xF5 + {0x08, 0x08, 0x6B, 0x6B, 0x08, 0x08, 0x00, 0x00}, // 0xF6 + {0x24, 0x36, 0x12, 0x36, 0x24, 0x36, 0x12, 0x00}, // 0xF7 + {0x00, 0x06, 0x0F, 0x09, 0x0F, 0x06, 0x00, 0x00}, // 0xF8 + {0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00}, // 0xF9 + {0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00}, // 0xFA + {0x10, 0x30, 0x70, 0xC0, 0xFF, 0xFF, 0x01, 0x01}, // 0xFB + {0x00, 0x1F, 0x1F, 0x01, 0x1F, 0x1E, 0x00, 0x00}, // 0xFC + {0x00, 0x19, 0x1D, 0x17, 0x12, 0x00, 0x00, 0x00}, // 0xFD + {0x00, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0x00}, // 0xFE + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0xFF +}; // end of MAX7219_Dot_Matrix_font + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/mcp3008/__init__.py b/esphome/components/mcp3008/__init__.py new file mode 100644 index 0000000000..acacc96159 --- /dev/null +++ b/esphome/components/mcp3008/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID + +DEPENDENCIES = ['spi'] +AUTO_LOAD = ['sensor'] +MULTI_CONF = True + +CONF_MCP3008 = 'mcp3008' + +mcp3008_ns = cg.esphome_ns.namespace('mcp3008') +MCP3008 = mcp3008_ns.class_('MCP3008', cg.Component, spi.SPIDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(MCP3008), +}).extend(spi.spi_device_schema(cs_pin_required=True)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp new file mode 100644 index 0000000000..a4d019ab8f --- /dev/null +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -0,0 +1,52 @@ +#include "mcp3008.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3008 { + +static const char *TAG = "mcp3008"; + +float MCP3008::get_setup_priority() const { return setup_priority::HARDWARE; } + +void MCP3008::setup() { + ESP_LOGCONFIG(TAG, "Setting up mcp3008"); + this->spi_setup(); +} + +void MCP3008::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008:"); + LOG_PIN(" CS Pin: ", this->cs_); +} + +float MCP3008::read_data_(uint8_t pin) { + uint8_t data_msb = 0; + uint8_t data_lsb = 0; + + uint8_t command = ((0x01 << 7) | // start bit + ((pin & 0x07) << 4)); // channel number + + this->enable(); + + this->transfer_byte(0x01); + data_msb = this->transfer_byte(command) & 0x03; + data_lsb = this->transfer_byte(0x00); + + this->disable(); + + int data = data_msb << 8 | data_lsb; + + return data / 1024.0f; +} + +MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin) + : PollingComponent(1000), parent_(parent), pin_(pin) { + this->set_name(name); +} +void MCP3008Sensor::setup() { LOG_SENSOR("", "Setting up MCP3008 Sensor '%s'...", this); } +void MCP3008Sensor::update() { + float value_v = this->parent_->read_data_(pin_); + this->publish_state(value_v); +} + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h new file mode 100644 index 0000000000..594bdc4204 --- /dev/null +++ b/esphome/components/mcp3008/mcp3008.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp3008 { + +class MCP3008Sensor; + +class MCP3008 : public Component, + public spi::SPIDevice { // At 3.3V 2MHz is too fast 1.35MHz is about right + public: + MCP3008() = default; + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + float read_data_(uint8_t pin); + + friend class MCP3008Sensor; +}; + +class MCP3008Sensor : public PollingComponent, public sensor::Sensor { + public: + MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin); + + void setup() override; + void update() override; + + protected: + MCP3008 *parent_; + uint8_t pin_; +}; + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py new file mode 100644 index 0000000000..ec7df0429a --- /dev/null +++ b/esphome/components/mcp3008/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_NUMBER, CONF_NAME +from . import mcp3008_ns, MCP3008 + +DEPENDENCIES = ['mcp3008'] + +MCP3008Sensor = mcp3008_ns.class_('MCP3008Sensor', sensor.Sensor, cg.PollingComponent) + +CONF_MCP3008_ID = 'mcp3008_id' + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MCP3008Sensor), + cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), + cv.Required(CONF_NUMBER): cv.int_, +}).extend(cv.polling_component_schema('1s')) + + +def to_code(config): + parent = yield cg.get_variable(config[CONF_MCP3008_ID]) + var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_NAME], config[CONF_NUMBER]) + yield cg.register_component(var, config) diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 2201fc87f0..151351be4c 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -37,6 +37,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->calibrate_zero(); } protected: @@ -46,6 +47,7 @@ template class MHZ19CalibrateZeroAction : public Action { template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_enable(); } protected: @@ -55,6 +57,7 @@ template class MHZ19ABCEnableAction : public Action { template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_disable(); } protected: diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index bdcecf12cb..d0a7caee84 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import sensor, uart -from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ +from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_MOLECULE_CO2, \ UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, ICON_THERMOMETER DEPENDENCIES = ['uart'] @@ -18,7 +18,7 @@ MHZ19ABCDisableAction = mhz19_ns.class_('MHZ19ABCDisableAction', automation.Acti CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(MHZ19Component), - cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 0), cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 2f0ed0f8e2..db99334d0b 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -280,8 +280,8 @@ def mqtt_publish_json_action_to_code(config, action_id, template_arg, args): def get_default_topic_for(data, component_type, name, suffix): - whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' - sanitized_name = ''.join(x for x in name.lower().replace(' ', '_') if x in whitelist) + allowlist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' + sanitized_name = ''.join(x for x in name.lower().replace(' ', '_') if x in allowlist) return '{}/{}/{}/{}'.format(data.topic_prefix, component_type, sanitized_name, suffix) diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 6f14b0c92c..2bbebff845 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -316,6 +316,7 @@ template class MQTTPublishJsonAction : public Action { TEMPLATABLE_VALUE(bool, retain) void set_payload(std::function payload) { this->payload_ = payload; } + void play(Ts... x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 4201d41c44..9b4255060f 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -12,7 +12,7 @@ static const char *TAG = "mqtt.component"; void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { - std::string sanitized_name = sanitize_string_whitelist(App.get_name(), HOSTNAME_CHARACTER_WHITELIST); + std::string sanitized_name = sanitize_string_allowlist(App.get_name(), HOSTNAME_CHARACTER_ALLOWLIST); return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" + this->get_default_object_id_() + "/config"; } @@ -117,7 +117,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return sanitize_string_whitelist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_WHITELIST); + return sanitize_string_allowlist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_ALLOWLIST); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 2b84882e59..32bfd951e9 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -181,4 +181,4 @@ def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) # https://github.com/Makuna/NeoPixelBus/blob/master/library.json - cg.add_library('NeoPixelBus-esphome', '2.5.2') + cg.add_library('NeoPixelBus-esphome', '2.5.7') diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 4486d62e1d..a380e32bfe 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1 +1,2 @@ # Dummy package to allow components to depend on network +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 30d7519380..394de69585 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import display, uart -from esphome.const import CONF_ID, CONF_LAMBDA +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_BRIGHTNESS from . import nextion_ns DEPENDENCIES = ['uart'] @@ -12,6 +12,7 @@ NextionRef = Nextion.operator('ref') CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Nextion), + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, }).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA) @@ -20,6 +21,8 @@ def to_code(config): yield cg.register_component(var, config) yield uart.register_uart_device(var, config) + if CONF_BRIGHTNESS in config: + cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(NextionRef, 'it')], return_type=cg.void) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e594e147f4..d18d9f33d8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -9,6 +9,7 @@ static const char *TAG = "nextion"; void Nextion::setup() { this->send_command_no_ack(""); this->send_command_printf("bkcmd=3"); + this->set_backlight_brightness(static_cast(brightness_ * 100)); this->goto_page("0"); } float Nextion::get_setup_priority() const { return setup_priority::PROCESSOR; } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index bd37e241e9..a55ff747ee 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -365,6 +365,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { // (In most use cases you won't need these) void register_touch_component(NextionTouchComponent *obj) { this->touch_.push_back(obj); } void setup() override; + void set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override; void update() override; void loop() override; @@ -392,6 +393,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { std::vector touch_; optional writer_; bool wait_for_ack_{true}; + float brightness_{1.0}; }; class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 869de777d6..e6bcff045f 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_PORT, CONF_SAFE_MODE from esphome.core import CORE, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] DEPENDENCIES = ['network'] ota_ns = cg.esphome_ns.namespace('ota') diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index b406f62ee1..34cb7c3f7a 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -7,6 +7,8 @@ from esphome.const import CONF_ID, CONF_INVERTED, CONF_LEVEL, CONF_MAX_POWER, \ CONF_MIN_POWER, CONF_POWER_SUPPLY from esphome.core import CORE, coroutine + +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True BINARY_OUTPUT_SCHEMA = cv.Schema({ diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 8c8a5ab61b..51c2849702 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -33,6 +33,7 @@ template class SetLevelAction : public Action { SetLevelAction(FloatOutput *output) : output_(output) {} TEMPLATABLE_VALUE(float, level) + void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } protected: diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index e3f852b3f6..1b969c9225 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -46,9 +46,20 @@ class FloatOutput : public BinaryOutput { */ void set_min_power(float min_power); - /// Set the level of this float output, this is called from the front-end. + /** Set the level of this float output, this is called from the front-end. + * + * @param state The new state. + */ void set_level(float state); + /** Set the frequency of the output for PWM outputs. + * + * Implemented only by components which can set the output PWM frequency. + * + * @param frequence The new frequency. + */ + virtual void update_frequency(float frequency) {} + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py new file mode 100644 index 0000000000..55dfe35e34 --- /dev/null +++ b/esphome/components/packages/__init__.py @@ -0,0 +1,44 @@ +import esphome.config_validation as cv + +from esphome.const import CONF_PACKAGES + + +def _merge_package(full_old, full_new): + + def merge(old, new): + # pylint: disable=no-else-return + if isinstance(new, dict): + if not isinstance(old, dict): + return new + res = old.copy() + for k, v in new.items(): + res[k] = merge(old[k], v) if k in old else v + return res + elif isinstance(new, list): + if not isinstance(old, list): + return new + return old + new + + return new + + return merge(full_old, full_new) + + +def do_packages_pass(config: dict): + if CONF_PACKAGES not in config: + return config + packages = config[CONF_PACKAGES] + with cv.prepend_path(CONF_PACKAGES): + if not isinstance(packages, dict): + raise cv.Invalid("Packages must be a key to value mapping, got {} instead" + "".format(type(packages))) + + for package_name, package_config in packages.items(): + with cv.prepend_path(package_name): + recursive_package = package_config + if isinstance(package_config, dict): + recursive_package = do_packages_pass(package_config) + config = _merge_package(recursive_package, config) + + del config[CONF_PACKAGES] + return config diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index e47937e46a..8cc92065ec 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -17,7 +17,7 @@ CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp index ed9d1bbd43..e47d71e8af 100644 --- a/esphome/components/pcd8544/pcd_8544.cpp +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -85,14 +85,14 @@ void HOT PCD8544::display() { this->command(this->PCD8544_SETYADDR); } -void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) { return; } uint16_t pos = x + (y / 8) * this->get_width_internal(); uint8_t subpos = y % 8; - if (color) { + if (color.is_on()) { this->buffer_[pos] |= (1 << subpos); } else { this->buffer_[pos] &= ~(1 << subpos); @@ -117,8 +117,8 @@ void PCD8544::update() { this->display(); } -void PCD8544::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void PCD8544::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h index a1c247bf7b..4c590b402c 100644 --- a/esphome/components/pcd8544/pcd_8544.h +++ b/esphome/components/pcd8544/pcd_8544.h @@ -43,7 +43,7 @@ class PCD8544 : public PollingComponent, void update() override; - void fill(int color) override; + void fill(Color color) override; void setup() override { this->setup_pins_(); @@ -51,7 +51,7 @@ class PCD8544 : public PollingComponent, } protected: - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); diff --git a/esphome/components/pid/__init__.py b/esphome/components/pid/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/pid/__init__.py +++ b/esphome/components/pid/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index a3e2299296..446c614f14 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -7,6 +7,8 @@ from esphome.const import CONF_ID, CONF_SENSOR pid_ns = cg.esphome_ns.namespace('pid') PIDClimate = pid_ns.class_('PIDClimate', climate.Climate, cg.Component) PIDAutotuneAction = pid_ns.class_('PIDAutotuneAction', automation.Action) +PIDResetIntegralTermAction = pid_ns.class_('PIDResetIntegralTermAction', automation.Action) +PIDSetControlParametersAction = pid_ns.class_('PIDSetControlParametersAction', automation.Action) CONF_DEFAULT_TARGET_TEMPERATURE = 'default_target_temperature' @@ -64,6 +66,18 @@ def to_code(config): cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE])) +@automation.register_action( + 'climate.pid.reset_integral_term', + PIDResetIntegralTermAction, + automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(PIDClimate), + }) +) +def pid_reset_integral_term(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) + + @automation.register_action('climate.pid.autotune', PIDAutotuneAction, automation.maybe_simple_id({ cv.Required(CONF_ID): cv.use_id(PIDClimate), cv.Optional(CONF_NOISEBAND, default=0.25): cv.float_, @@ -77,3 +91,28 @@ def esp8266_set_frequency_to_code(config, action_id, template_arg, args): cg.add(var.set_positive_output(config[CONF_POSITIVE_OUTPUT])) cg.add(var.set_negative_output(config[CONF_NEGATIVE_OUTPUT])) yield var + + +@automation.register_action( + 'climate.pid.set_control_parameters', + PIDSetControlParametersAction, + automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(PIDClimate), + cv.Required(CONF_KP): cv.templatable(cv.float_), + cv.Optional(CONF_KI, default=0.0): cv.templatable(cv.float_), + cv.Optional(CONF_KD, default=0.0): cv.templatable(cv.float_), + }) +) +def set_control_parameters(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + kp_template_ = yield cg.templatable(config[CONF_KP], args, float) + cg.add(var.set_kp(kp_template_)) + + ki_template_ = yield cg.templatable(config[CONF_KI], args, float) + cg.add(var.set_ki(ki_template_)) + + kd_template_ = yield cg.templatable(config[CONF_KD], args, float) + cg.add(var.set_kd(kd_template_)) + yield var diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 0c777ffd8b..24fb0ec905 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -148,5 +148,7 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { }); } +void PIDClimate::reset_integral_term() { this->controller_.reset_accumulated_integral(); } + } // namespace pid } // namespace esphome diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 8f379c47b4..f11d768867 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -29,6 +29,9 @@ class PIDClimate : public climate::Climate, public Component { float get_output_value() const { return output_value_; } float get_error_value() const { return controller_.error; } + float get_kp() { return controller_.kp; } + float get_ki() { return controller_.ki; } + float get_kd() { return controller_.kd; } float get_proportional_term() const { return controller_.proportional_term; } float get_integral_term() const { return controller_.integral_term; } float get_derivative_term() const { return controller_.derivative_term; } @@ -39,6 +42,7 @@ class PIDClimate : public climate::Climate, public Component { default_target_temperature_ = default_target_temperature; } void start_autotune(std::unique_ptr &&autotune); + void reset_integral_term(); protected: /// Override control to change settings of the climate device. @@ -71,6 +75,10 @@ template class PIDAutotuneAction : public Action { public: PIDAutotuneAction(PIDClimate *parent) : parent_(parent) {} + void set_noiseband(float noiseband) { noiseband_ = noiseband; } + void set_positive_output(float positive_output) { positive_output_ = positive_output; } + void set_negative_output(float negative_output) { negative_output_ = negative_output; } + void play(Ts... x) { auto tuner = make_unique(); tuner->set_noiseband(this->noiseband_); @@ -79,10 +87,6 @@ template class PIDAutotuneAction : public Action { this->parent_->start_autotune(std::move(tuner)); } - void set_noiseband(float noiseband) { noiseband_ = noiseband; } - void set_positive_output(float positive_output) { positive_output_ = positive_output; } - void set_negative_output(float negative_output) { negative_output_ = negative_output; } - protected: float noiseband_; float positive_output_; @@ -90,5 +94,37 @@ template class PIDAutotuneAction : public Action { PIDClimate *parent_; }; +template class PIDResetIntegralTermAction : public Action { + public: + PIDResetIntegralTermAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->reset_integral_term(); } + + protected: + PIDClimate *parent_; +}; + +template class PIDSetControlParametersAction : public Action { + public: + PIDSetControlParametersAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { + auto kp = this->kp_.value(x...); + auto ki = this->ki_.value(x...); + auto kd = this->kd_.value(x...); + + this->parent_->set_kp(kp); + this->parent_->set_ki(ki); + this->parent_->set_kd(kd); + } + + protected: + TEMPLATABLE_VALUE(float, kp) + TEMPLATABLE_VALUE(float, ki) + TEMPLATABLE_VALUE(float, kd) + + PIDClimate *parent_; +}; + } // namespace pid } // namespace esphome diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 7ec7724e15..4caad8dd8b 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -40,6 +40,8 @@ struct PIDController { return proportional_term + integral_term + derivative_term; } + void reset_accumulated_integral() { accumulated_integral_ = 0; } + /// Proportional gain K_p. float kp = 0; /// Integral gain K_i. diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index cfab23d204..ff8cf15eb5 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -15,6 +15,9 @@ PID_CLIMATE_SENSOR_TYPES = { 'DERIVATIVE': PIDClimateSensorType.PID_SENSOR_TYPE_DERIVATIVE, 'HEAT': PIDClimateSensorType.PID_SENSOR_TYPE_HEAT, 'COOL': PIDClimateSensorType.PID_SENSOR_TYPE_COOL, + 'KP': PIDClimateSensorType.PID_SENSOR_TYPE_KP, + 'KI': PIDClimateSensorType.PID_SENSOR_TYPE_KI, + 'KD': PIDClimateSensorType.PID_SENSOR_TYPE_KD, } CONF_CLIMATE_ID = 'climate_id' diff --git a/esphome/components/pid/sensor/pid_climate_sensor.cpp b/esphome/components/pid/sensor/pid_climate_sensor.cpp index 6241a139f6..f60627b6ac 100644 --- a/esphome/components/pid/sensor/pid_climate_sensor.cpp +++ b/esphome/components/pid/sensor/pid_climate_sensor.cpp @@ -35,6 +35,18 @@ void PIDClimateSensor::update_from_parent_() { case PID_SENSOR_TYPE_COOL: value = clamp(-this->parent_->get_output_value(), 0.0f, 1.0f); break; + case PID_SENSOR_TYPE_KP: + value = this->parent_->get_kp(); + this->publish_state(value); + return; + case PID_SENSOR_TYPE_KI: + value = this->parent_->get_ki(); + this->publish_state(value); + return; + case PID_SENSOR_TYPE_KD: + value = this->parent_->get_kd(); + this->publish_state(value); + return; default: value = NAN; break; diff --git a/esphome/components/pid/sensor/pid_climate_sensor.h b/esphome/components/pid/sensor/pid_climate_sensor.h index 85759f1eaf..f3774610f8 100644 --- a/esphome/components/pid/sensor/pid_climate_sensor.h +++ b/esphome/components/pid/sensor/pid_climate_sensor.h @@ -14,6 +14,9 @@ enum PIDClimateSensorType { PID_SENSOR_TYPE_DERIVATIVE, PID_SENSOR_TYPE_HEAT, PID_SENSOR_TYPE_COOL, + PID_SENSOR_TYPE_KP, + PID_SENSOR_TYPE_KI, + PID_SENSOR_TYPE_KD, }; class PIDClimateSensor : public sensor::Sensor, public Component { diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index c82d35b398..cbd41d11cc 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.components import spi from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['spi'] AUTO_LOAD = ['binary_sensor'] MULTI_CONF = True @@ -17,7 +18,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ON_TAG): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532Trigger), }), -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 93000a7421..792d92a6ac 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -86,6 +86,8 @@ void PN532::setup() { this->mark_failed(); return; } + + this->turn_off_rf_(); } void PN532::update() { @@ -114,13 +116,16 @@ void PN532::loop() { if (read.size() <= 2 || read[0] != 0x4B) { // Something failed + this->turn_off_rf_(); return; } uint8_t num_targets = read[1]; - if (num_targets != 1) + if (num_targets != 1) { // no tags found or too many + this->turn_off_rf_(); return; + } // const uint8_t target_number = read[2]; // const uint16_t sens_res = uint16_t(read[3] << 8) | read[4]; @@ -150,6 +155,17 @@ void PN532::loop() { format_uid(buf, nfcid, nfcid_length); ESP_LOGD(TAG, "Found new tag '%s'", buf); } + + this->turn_off_rf_(); +} + +void PN532::turn_off_rf_() { + ESP_LOGVV(TAG, "Turning RF field OFF"); + this->pn532_write_command_check_ack_({ + 0x32, // RFConfiguration + 0x1, // RF Field + 0x0 // Off + }); } float PN532::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 49d5878265..3a734b7ba2 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -55,6 +55,8 @@ class PN532 : public PollingComponent, bool read_ack_(); + void turn_off_rf_(); + bool requested_read_{false}; std::vector binary_sensors_; std::vector triggers_; diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py index 5646ffdc0b..d502788637 100644 --- a/esphome/components/power_supply/__init__.py +++ b/esphome/components/power_supply/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_ENABLE_TIME, CONF_ID, CONF_KEEP_ON_TIME, CONF_PIN +CODEOWNERS = ['@esphome/core'] power_supply_ns = cg.esphome_ns.namespace('power_supply') PowerSupply = power_supply_ns.class_('PowerSupply', cg.Component) MULTI_CONF = True diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py new file mode 100644 index 0000000000..d015af9f78 --- /dev/null +++ b/esphome/components/prometheus/__init__.py @@ -0,0 +1,22 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +from esphome.components import web_server_base + +AUTO_LOAD = ['web_server_base'] + +prometheus_ns = cg.esphome_ns.namespace('prometheus') +PrometheusHandler = prometheus_ns.class_('PrometheusHandler', cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PrometheusHandler), + cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) + + var = cg.new_Pvariable(config[CONF_ID], paren) + yield cg.register_component(var, config) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp new file mode 100644 index 0000000000..06a0e39e2c --- /dev/null +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -0,0 +1,312 @@ +#include "prometheus_handler.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace prometheus { + +void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { + AsyncResponseStream *stream = req->beginResponseStream("text/plain"); + +#ifdef USE_SENSOR + this->sensor_type_(stream); + for (auto *obj : App.get_sensors()) + this->sensor_row_(stream, obj); +#endif + +#ifdef USE_BINARY_SENSOR + this->binary_sensor_type_(stream); + for (auto *obj : App.get_binary_sensors()) + this->binary_sensor_row_(stream, obj); +#endif + +#ifdef USE_FAN + this->fan_type_(stream); + for (auto *obj : App.get_fans()) + this->fan_row_(stream, obj); +#endif + +#ifdef USE_LIGHT + this->light_type_(stream); + for (auto *obj : App.get_lights()) + this->light_row_(stream, obj); +#endif + +#ifdef USE_COVER + this->cover_type_(stream); + for (auto *obj : App.get_covers()) + this->cover_row_(stream, obj); +#endif + +#ifdef USE_SWITCH + this->switch_type_(stream); + for (auto *obj : App.get_switches()) + this->switch_row_(stream, obj); +#endif + + req->send(stream); +} + +// Type-specific implementation +#ifdef USE_SENSOR +void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_sensor_failed GAUGE\n")); +} +void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",unit=\"")); + stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(F("\"} ")); + stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_BINARY_SENSOR +void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_binary_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n")); +} +void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { + if (obj->is_internal()) + return; + if (obj->has_state()) { + // We have a valid value, output this value + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_binary_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_FAN +void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_fan_value GAUGE\n")); + stream->print(F("#TYPE esphome_fan_failed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_speed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n")); +} +void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::FanState *obj) { + if (obj->is_internal()) + return; + stream->print(F("esphome_fan_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_fan_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + // Speed if available + if (obj->get_traits().supports_speed()) { + stream->print(F("esphome_fan_speed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->speed); + stream->print('\n'); + } + // Oscillation if available + if (obj->get_traits().supports_oscillation()) { + stream->print(F("esphome_fan_oscillation{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->oscillating); + stream->print('\n'); + } +} +#endif + +#ifdef USE_LIGHT +void PrometheusHandler::light_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_light_state GAUGE\n")); + stream->print(F("#TYPE esphome_light_color GAUGE\n")); + stream->print(F("#TYPE esphome_light_effect_active GAUGE\n")); +} +void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) { + if (obj->is_internal()) + return; + // State + stream->print(F("esphome_light_state{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->remote_values.is_on()); + stream->print(F("\n")); + // Brightness and RGBW + light::LightColorValues color = obj->current_values; + float brightness, r, g, b, w; + color.as_brightness(&brightness); + color.as_rgbw(&r, &g, &b, &w); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"brightness\"} ")); + stream->print(brightness); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"r\"} ")); + stream->print(r); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"g\"} ")); + stream->print(g); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"b\"} ")); + stream->print(b); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"w\"} ")); + stream->print(w); + stream->print(F("\n")); + // Effect + std::string effect = obj->get_effect_name(); + if (effect == "None") { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"None\"} 0\n")); + } else { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"")); + stream->print(effect.c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_COVER +void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_cover_value GAUGE\n")); + stream->print(F("#TYPE esphome_cover_failed GAUGE\n")); +} +void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->position)) { + // We have a valid value, output this value + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_cover_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->position); + stream->print('\n'); + if (obj->get_traits().get_supports_tilt()) { + stream->print(F("esphome_cover_tilt{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->tilt); + stream->print('\n'); + } + } else { + // Invalid state + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_SWITCH +void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_switch_value GAUGE\n")); + stream->print(F("#TYPE esphome_switch_failed GAUGE\n")); +} +void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { + if (obj->is_internal()) + return; + stream->print(F("esphome_switch_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_switch_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); +} +#endif + +} // namespace prometheus +} // namespace esphome diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h new file mode 100644 index 0000000000..6abd406556 --- /dev/null +++ b/esphome/components/prometheus/prometheus_handler.h @@ -0,0 +1,81 @@ +#pragma once + +#include "esphome/components/web_server_base/web_server_base.h" +#include "esphome/core/controller.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace prometheus { + +class PrometheusHandler : public AsyncWebHandler, public Component { + public: + PrometheusHandler(web_server_base::WebServerBase *base) : base_(base) {} + + bool canHandle(AsyncWebServerRequest *request) override { + if (request->method() == HTTP_GET) { + if (request->url() == "/metrics") + return true; + } + + return false; + } + + void handleRequest(AsyncWebServerRequest *req) override; + + void setup() override { + this->base_->init(); + this->base_->add_handler(this); + } + float get_setup_priority() const override { + // After WiFi + return setup_priority::WIFI - 1.0f; + } + + protected: +#ifdef USE_SENSOR + /// Return the type for prometheus + void sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj); +#endif + +#ifdef USE_BINARY_SENSOR + /// Return the type for prometheus + void binary_sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + /// Return the type for prometheus + void fan_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void fan_row_(AsyncResponseStream *stream, fan::FanState *obj); +#endif + +#ifdef USE_LIGHT + /// Return the type for prometheus + void light_type_(AsyncResponseStream *stream); + /// Return the Light Values state as prometheus data point + void light_row_(AsyncResponseStream *stream, light::LightState *obj); +#endif + +#ifdef USE_COVER + /// Return the type for prometheus + void cover_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void cover_row_(AsyncResponseStream *stream, cover::Cover *obj); +#endif + +#ifdef USE_SWITCH + /// Return the type for prometheus + void switch_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj); +#endif + + web_server_base::WebServerBase *base_; +}; + +} // namespace prometheus +} // namespace esphome diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index e2d832b019..b9deab1949 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -59,11 +59,19 @@ void PZEM004T::loop() { if (this->power_sensor_ != nullptr) this->power_sensor_->publish_state(power); ESP_LOGD(TAG, "Got Power %u W", power); + this->write_state_(READ_ENERGY); + break; + } + + case 0xA3: { // Energy Response + uint32_t energy = (uint32_t(resp[1]) << 16) | (uint32_t(resp[2]) << 8) | (uint32_t(resp[3])); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(energy); + ESP_LOGD(TAG, "Got Energy %u Wh", energy); this->write_state_(DONE); break; } - case 0xA3: // Energy Response case 0xA5: // Set Power Alarm Response case 0xB0: // Voltage Request case 0xB1: // Current Request diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h index f0208d415a..517b81eb21 100644 --- a/esphome/components/pzem004t/pzem004t.h +++ b/esphome/components/pzem004t/pzem004t.h @@ -12,6 +12,7 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void loop() override; @@ -23,12 +24,14 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; enum PZEM004TReadState { SET_ADDRESS = 0xB4, READ_VOLTAGE = 0xB0, READ_CURRENT = 0xB1, READ_POWER = 0xB2, + READ_ENERGY = 0xB3, DONE = 0x00, } read_state_{DONE}; diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index 6e3628c5ec..b54ba4887c 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ - UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + CONF_ENERGY, UNIT_VOLT, ICON_FLASH, ICON_COUNTER, UNIT_AMPERE, UNIT_WATT, \ + UNIT_WATT_HOURS DEPENDENCIES = ['uart'] @@ -15,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0), + cv.Optional(CONF_ENERGY): sensor.sensor_schema(UNIT_WATT_HOURS, ICON_COUNTER, 0) }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) @@ -35,3 +37,7 @@ def to_code(config): conf = config[CONF_POWER] sens = yield sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 05a3e7e1aa..770394faec 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -420,7 +420,7 @@ def raw_action(var, config, args): RC5Data, RC5BinarySensor, RC5Trigger, RC5Action, RC5Dumper = declare_protocol('RC5') RC5_SCHEMA = cv.Schema({ cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0x1F)), - cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x3F)), + cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x7F)), }) diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index 8a216f5348..fc40a6a874 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -23,6 +23,7 @@ DECLARE_REMOTE_PROTOCOL(JVC) template class JVCAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) + void encode(RemoteTransmitData *dst, Ts... x) override { JVCData data{}; data.data = this->data_.value(x...); diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index b810115f58..6267560443 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -26,6 +26,7 @@ template class LGAction : public RemoteTransmitterActionBasedata_.value(x...); diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index c794991eab..593a3efe17 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -25,6 +25,7 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index b13bd3e92d..eae97a8a14 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -26,6 +26,7 @@ template class PanasonicAction : public RemoteTransmitterActionB public: TEMPLATABLE_VALUE(uint16_t, address) TEMPLATABLE_VALUE(uint32_t, command) + void encode(RemoteTransmitData *dst, Ts... x) override { PanasonicData data{}; data.address = this->address_.value(x...); diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h index f93e51a033..4cac4f9f32 100644 --- a/esphome/components/remote_base/pioneer_protocol.h +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -25,6 +25,7 @@ template class PioneerAction : public RemoteTransmitterActionBas public: TEMPLATABLE_VALUE(uint16_t, rc_code_1) TEMPLATABLE_VALUE(uint16_t, rc_code_2) + void encode(RemoteTransmitData *dst, Ts... x) override { PioneerData data{}; data.rc_code_1 = this->rc_code_1_.value(x...); diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 35bff588e9..a6165492ac 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -14,10 +14,16 @@ void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { dst->set_carrier_frequency(36000); uint64_t out_data = 0; - out_data |= 0b11 << 12; + uint8_t command = data.command; + if (data.command >= 64) { + out_data |= 0b10 << 12; + command = command - 64; + } else { + out_data |= 0b11 << 12; + } out_data |= TOGGLE << 11; out_data |= data.address << 6; - out_data |= data.command; + out_data |= command; for (uint64_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { if (out_data & mask) { @@ -35,22 +41,44 @@ optional RC5Protocol::decode(RemoteReceiveData src) { .address = 0, .command = 0, }; - src.expect_space(BIT_TIME_US); - if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(BIT_TIME_US) || !src.expect_mark(BIT_TIME_US)) + int field_bit = 0; + + if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { + field_bit = 1; + } else if (src.expect_space(2 * BIT_TIME_US)) { + field_bit = 0; + } else { return {}; + } + + if (!(((src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US)) || + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) && + (((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) || + ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US)))))) { + return {}; + } uint64_t out_data = 0; - for (int bit = NBITS - 3; bit >= 0; bit--) { - if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { - out_data |= 1 << bit; - } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + for (int bit = NBITS - 4; bit >= 1; bit--) { + if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; } else { return {}; } } + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } - out.command = out_data & 0x3F; + out.command = (out_data & 0x3F) + (1 - field_bit) * 64; out.address = (out_data >> 6) & 0x1F; return out; } diff --git a/esphome/components/remote_base/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index 2e1da74d9f..589c8d42de 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -26,6 +26,7 @@ template class RC5Action : public RemoteTransmitterActionBaseaddress_.value(x...); diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 250b59e55e..916fe29c1f 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -323,6 +323,9 @@ template class RemoteTransmitterActionBase : public Actionparent_ = parent; } + TEMPLATABLE_VALUE(uint32_t, send_times); + TEMPLATABLE_VALUE(uint32_t, send_wait); + void play(Ts... x) override { auto call = this->parent_->transmit(); this->encode(call.get_data(), x...); @@ -331,12 +334,9 @@ template class RemoteTransmitterActionBase : public Action class SamsungAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) + void encode(RemoteTransmitData *dst, Ts... x) override { SamsungData data{}; data.data = this->data_.value(x...); diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index 9f0bcdf82f..aecc8ab91c 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -26,6 +26,7 @@ template class SonyAction : public RemoteTransmitterActionBasedata_.value(x...); diff --git a/esphome/components/restart/__init__.py b/esphome/components/restart/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/restart/__init__.py +++ b/esphome/components/restart/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 1fd4fbc7bd..885e5765dd 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_CODE, CONF_LOW, CONF_SY from esphome.components import uart DEPENDENCIES = ['uart'] +CODEOWNERS = ['@jesserockz'] rf_bridge_ns = cg.esphome_ns.namespace('rf_bridge') RFBridgeComponent = rf_bridge_ns.class_('RFBridgeComponent', cg.Component, uart.UARTDevice) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index f1537cdc87..42689ecf82 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -15,59 +15,74 @@ void RFBridgeComponent::ack_() { this->flush(); } -void RFBridgeComponent::decode_() { - uint8_t action = uartbuf_[0]; - RFBridgeData data{}; +bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + const uint8_t *raw = &this->rx_buffer_[0]; + + // Byte 0: Start + if (at == 0) + return byte == RF_CODE_START; + + // Byte 1: Action + if (at == 1) + return byte >= RF_CODE_ACK && byte <= RF_CODE_RFOUT; + uint8_t action = raw[1]; switch (action) { case RF_CODE_ACK: ESP_LOGD(TAG, "Action OK"); break; case RF_CODE_LEARN_KO: - this->ack_(); - ESP_LOGD(TAG, "Learn timeout"); + ESP_LOGD(TAG, "Learning timeout"); break; case RF_CODE_LEARN_OK: - ESP_LOGD(TAG, "Learn started"); case RF_CODE_RFIN: - this->ack_(); + if (at < RF_MESSAGE_SIZE + 2) + return true; - data.sync = (uartbuf_[1] << 8) | uartbuf_[2]; - data.low = (uartbuf_[3] << 8) | uartbuf_[4]; - data.high = (uartbuf_[5] << 8) | uartbuf_[6]; - data.code = (uartbuf_[7] << 16) | (uartbuf_[8] << 8) | uartbuf_[9]; + RFBridgeData data; + data.sync = (raw[2] << 8) | raw[3]; + data.low = (raw[4] << 8) | raw[5]; + data.high = (raw[6] << 8) | raw[7]; + data.code = (raw[8] << 16) | (raw[9] << 8) | raw[10]; + + if (action == RF_CODE_LEARN_OK) + ESP_LOGD(TAG, "Learning success"); ESP_LOGD(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, data.high, data.code); this->callback_.call(data); break; default: - ESP_LOGD(TAG, "Unknown action: 0x%02X", action); + ESP_LOGW(TAG, "Unknown action: 0x%02X", action); break; } - this->last_ = millis(); + + ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); + + if (byte == RF_CODE_STOP && action != RF_CODE_ACK) + this->ack_(); + + // return false to reset buffer + return false; } void RFBridgeComponent::loop() { - bool receiving = false; - if (this->last_ != 0 && millis() - this->last_ > RF_DEBOUNCE) { - this->last_ = 0; + const uint32_t now = millis(); + if (now - this->last_bridge_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_bridge_byte_ = now; } while (this->available()) { - uint8_t c = this->read(); - if (receiving) { - if (c == RF_CODE_STOP && (this->uartpos_ == 1 || this->uartpos_ == RF_MESSAGE_SIZE + 1)) { - this->decode_(); - receiving = false; - } else if (this->uartpos_ <= RF_MESSAGE_SIZE) { - this->uartbuf_[uartpos_++] = c; - } else { - receiving = false; - } - } else if (c == RF_CODE_START) { - this->uartpos_ = 0; - receiving = true; + uint8_t byte; + this->read_byte(&byte); + if (this->parse_bridge_byte_(byte)) { + ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); + this->last_bridge_byte_ = now; + } else { + this->rx_buffer_.clear(); } } } @@ -77,11 +92,17 @@ void RFBridgeComponent::send_code(RFBridgeData data) { data.code); this->write(RF_CODE_START); this->write(RF_CODE_RFOUT); - this->write(data.sync); - this->write(data.low); - this->write(data.high); - this->write(data.code); + this->write((data.sync >> 8) & 0xFF); + this->write(data.sync & 0xFF); + this->write((data.low >> 8) & 0xFF); + this->write(data.low & 0xFF); + this->write((data.high >> 8) & 0xFF); + this->write(data.high & 0xFF); + this->write((data.code >> 16) & 0xFF); + this->write((data.code >> 8) & 0xFF); + this->write(data.code & 0xFF); this->write(RF_CODE_STOP); + this->flush(); } void RFBridgeComponent::learn() { @@ -89,6 +110,7 @@ void RFBridgeComponent::learn() { this->write(RF_CODE_START); this->write(RF_CODE_LEARN); this->write(RF_CODE_STOP); + this->flush(); } void RFBridgeComponent::dump_config() { diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 86713b8a5c..7ae84a032f 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -45,10 +45,10 @@ class RFBridgeComponent : public uart::UARTDevice, public Component { protected: void ack_(); void decode_(); + bool parse_bridge_byte_(uint8_t byte); - unsigned long last_ = 0; - unsigned char uartbuf_[RF_MESSAGE_SIZE + 3] = {0}; - unsigned char uartpos_ = 0; + std::vector rx_buffer_; + uint32_t last_bridge_byte_{0}; CallbackManager callback_; }; diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index e612c80f73..1a3bf9f614 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -12,6 +12,7 @@ class RGBLightOutput : public light::LightOutput { void set_red(output::FloatOutput *red) { red_ = red; } void set_green(output::FloatOutput *green) { green_ = green; } void set_blue(output::FloatOutput *blue) { blue_ = blue; } + light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); @@ -20,7 +21,7 @@ class RGBLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue; - state->current_values_as_rgb(&red, &green, &blue); + state->current_values_as_rgb(&red, &green, &blue, false); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); diff --git a/esphome/components/rgbw/light.py b/esphome/components/rgbw/light.py index 75d6082e5a..ca31a8229d 100644 --- a/esphome/components/rgbw/light.py +++ b/esphome/components/rgbw/light.py @@ -5,6 +5,7 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, CONF_ rgbw_ns = cg.esphome_ns.namespace('rgbw') RGBWLightOutput = rgbw_ns.class_('RGBWLightOutput', light.LightOutput) +CONF_COLOR_INTERLOCK = 'color_interlock' CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWLightOutput), @@ -12,6 +13,7 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), cv.Required(CONF_WHITE): cv.use_id(output.FloatOutput), + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, }) @@ -27,3 +29,4 @@ def to_code(config): cg.add(var.set_blue(blue)) white = yield cg.get_variable(config[CONF_WHITE]) cg.add(var.set_white(white)) + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index b58c7f9d54..90a650851b 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -13,16 +13,18 @@ class RGBWLightOutput : public light::LightOutput { void set_green(output::FloatOutput *green) { green_ = green; } void set_blue(output::FloatOutput *blue) { blue_ = blue; } void set_white(output::FloatOutput *white) { white_ = white; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); + traits.set_supports_color_interlock(this->color_interlock_); traits.set_supports_rgb(true); traits.set_supports_rgb_white_value(true); return traits; } void write_state(light::LightState *state) override { float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white); + state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -34,6 +36,7 @@ class RGBWLightOutput : public light::LightOutput { output::FloatOutput *green_; output::FloatOutput *blue_; output::FloatOutput *white_; + bool color_interlock_{false}; }; } // namespace rgbw diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index 78f4bee630..1513a684ea 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -9,6 +9,7 @@ rgbww_ns = cg.esphome_ns.namespace('rgbww') RGBWWLightOutput = rgbww_ns.class_('RGBWWLightOutput', light.LightOutput) CONF_CONSTANT_BRIGHTNESS = 'constant_brightness' +CONF_COLOR_INTERLOCK = 'color_interlock' CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWWLightOutput), @@ -20,6 +21,7 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, }) @@ -42,3 +44,4 @@ def to_code(config): cg.add(var.set_warm_white(wwhite)) cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index a975331a37..152766970e 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -17,19 +17,22 @@ class RGBWWLightOutput : public light::LightOutput { void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); traits.set_supports_rgb(true); traits.set_supports_rgb_white_value(true); traits.set_supports_color_temperature(true); + traits.set_supports_color_interlock(this->color_interlock_); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; } void write_state(light::LightState *state) override { float red, green, blue, cwhite, wwhite; - state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_); + state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_, + this->color_interlock_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -46,6 +49,7 @@ class RGBWWLightOutput : public light::LightOutput { float cold_white_temperature_; float warm_white_temperature_; bool constant_brightness_; + bool color_interlock_{false}; }; } // namespace rgbww diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4220645478..f0e47dfe0a 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -74,6 +74,7 @@ template class RotaryEncoderSetValueAction : public Actionencoder_->set_value(this->value_.value(x...)); } protected: diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py new file mode 100644 index 0000000000..a276f7cb86 --- /dev/null +++ b/esphome/components/rtttl/__init__.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components.output import FloatOutput +from esphome.const import CONF_ID, CONF_OUTPUT, CONF_TRIGGER_ID + +CODEOWNERS = ['@glmnet'] +CONF_RTTTL = 'rtttl' +CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' + +rtttl_ns = cg.esphome_ns.namespace('rtttl') + +Rtttl = rtttl_ns .class_('Rtttl', cg.Component) +PlayAction = rtttl_ns.class_('PlayAction', automation.Action) +StopAction = rtttl_ns.class_('StopAction', automation.Action) +FinishedPlaybackTrigger = rtttl_ns.class_('FinishedPlaybackTrigger', + automation.Trigger.template()) +IsPlayingCondition = rtttl_ns.class_('IsPlayingCondition', automation.Condition) + +MULTI_CONF = True + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), + cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FinishedPlaybackTrigger), + }), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + out = yield cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(out)) + + for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_action('rtttl.play', PlayAction, cv.maybe_simple_value({ + cv.GenerateID(CONF_ID): cv.use_id(Rtttl), + cv.Required(CONF_RTTTL): cv.templatable(cv.string) +}, key=CONF_RTTTL)) +def rtttl_play_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_RTTTL], args, cg.std_string) + cg.add(var.set_value(template_)) + yield var + + +@automation.register_action('rtttl.stop', StopAction, cv.Schema({ + cv.GenerateID(): cv.use_id(Rtttl), +})) +def rtttl_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_condition('rtttl.is_playing', IsPlayingCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(Rtttl), +})) +def rtttl_is_playing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp new file mode 100644 index 0000000000..da50e1cbe9 --- /dev/null +++ b/esphome/components/rtttl/rtttl.cpp @@ -0,0 +1,186 @@ +#include "rtttl.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace rtttl { + +static const char* TAG = "rtttl"; + +static const uint32_t DOUBLE_NOTE_GAP_MS = 10; + +// These values can also be found as constants in the Tone library (Tone.h) +static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, + 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, + 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, + 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; + +void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } + +void Rtttl::play(std::string rtttl) { + rtttl_ = std::move(rtttl); + + default_duration_ = 4; + default_octave_ = 6; + int bpm = 63; + uint8_t num; + + // Get name + position_ = rtttl_.find(':'); + + // it's somewhat documented to be up to 10 characters but let's be a bit flexible here + if (position_ == std::string::npos || position_ > 15) { + ESP_LOGE(TAG, "Missing ':' when looking for name."); + return; + } + + auto name = this->rtttl_.substr(0, position_); + ESP_LOGD(TAG, "Playing song %s", name.c_str()); + + // get default duration + position_ = this->rtttl_.find("d=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'd='"); + return; + } + position_ += 2; + num = this->get_integer_(); + if (num > 0) + default_duration_ = num; + + // get default octave + position_ = rtttl_.find("o=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'o="); + return; + } + position_ += 2; + num = get_integer_(); + if (num >= 3 && num <= 7) + default_octave_ = num; + + // get BPM + position_ = rtttl_.find("b=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing b="); + return; + } + position_ += 2; + num = get_integer_(); + if (num != 0) + bpm = num; + + position_ = rtttl_.find(':', position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing second ':'"); + return; + } + position_++; + + // BPM usually expresses the number of quarter notes per minute + wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + + output_freq_ = 0; + last_note_ = millis(); + note_duration_ = 1; +} + +void Rtttl::loop() { + if (note_duration_ == 0 || millis() - last_note_ < note_duration_) + return; + + if (!rtttl_[position_]) { + output_->set_level(0.0); + ESP_LOGD(TAG, "Playback finished"); + this->on_finished_playback_callback_.call(); + note_duration_ = 0; + return; + } + + // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... + while (rtttl_[position_] == ',' || rtttl_[position_] == ' ') + position_++; + + // first, get note duration, if available + uint8_t num = this->get_integer_(); + + if (num) + note_duration_ = wholenote_ / num; + else + note_duration_ = wholenote_ / default_duration_; // we will need to check if we are a dotted note after + + uint8_t note; + + switch (rtttl_[position_]) { + case 'c': + note = 1; + break; + case 'd': + note = 3; + break; + case 'e': + note = 5; + break; + case 'f': + note = 6; + break; + case 'g': + note = 8; + break; + case 'a': + note = 10; + break; + case 'b': + note = 12; + break; + case 'p': + default: + note = 0; + } + position_++; + + // now, get optional '#' sharp + if (rtttl_[position_] == '#') { + note++; + position_++; + } + + // now, get optional '.' dotted note + if (rtttl_[position_] == '.') { + note_duration_ += note_duration_ / 2; + position_++; + } + + // now, get scale + uint8_t scale = get_integer_(); + if (scale == 0) + scale = default_octave_; + + // Now play the note + if (note) { + auto note_index = (scale - 4) * 12 + note; + if (note_index < 0 || note_index >= sizeof(NOTES)) { + ESP_LOGE(TAG, "Note out of valid range"); + return; + } + auto freq = NOTES[note_index]; + + if (freq == output_freq_) { + // Add small silence gap between same note + output_->set_level(0.0); + delay(DOUBLE_NOTE_GAP_MS); + note_duration_ -= DOUBLE_NOTE_GAP_MS; + } + output_freq_ = freq; + + ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_); + output_->update_frequency(freq); + output_->set_level(0.5); + } else { + ESP_LOGVV(TAG, "waiting: %dms", note_duration_); + output_->set_level(0.0); + } + + last_note_ = millis(); +} +} // namespace rtttl +} // namespace esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h new file mode 100644 index 0000000000..76d8241ddf --- /dev/null +++ b/esphome/components/rtttl/rtttl.h @@ -0,0 +1,81 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace rtttl { + +extern uint32_t global_rtttl_id; + +class Rtttl : public Component { + public: + void set_output(output::FloatOutput *output) { output_ = output; } + void play(std::string rtttl); + void stop() { + note_duration_ = 0; + output_->set_level(0.0); + } + void dump_config() override; + + bool is_playing() { return note_duration_ != 0; } + void loop() override; + + void add_on_finished_playback_callback(std::function callback) { + this->on_finished_playback_callback_.add(std::move(callback)); + } + + protected: + inline uint8_t get_integer_() { + uint8_t ret = 0; + while (isdigit(rtttl_[position_])) { + ret = (ret * 10) + (rtttl_[position_++] - '0'); + } + return ret; + } + + std::string rtttl_; + size_t position_; + uint16_t wholenote_; + uint16_t default_duration_; + uint16_t default_octave_; + uint32_t last_note_; + uint16_t note_duration_; + + uint32_t output_freq_; + output::FloatOutput *output_; + + CallbackManager on_finished_playback_callback_; +}; + +template class PlayAction : public Action { + public: + PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {} + TEMPLATABLE_VALUE(std::string, value) + + void play(Ts... x) override { this->rtttl_->play(this->value_.value(x...)); } + + protected: + Rtttl *rtttl_; +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop(); } +}; + +template class IsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_playing(); } +}; + +class FinishedPlaybackTrigger : public Trigger<> { + public: + explicit FinishedPlaybackTrigger(Rtttl *parent) { + parent->add_on_finished_playback_callback([this]() { this->trigger(); }); + } +}; + +} // namespace rtttl +} // namespace esphome diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp index 7e13140e55..897e4a2504 100644 --- a/esphome/components/ruuvi_ble/ruuvi_ble.cpp +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -50,7 +50,7 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP const float acceleration_y = (int16_t(data[8] << 8) + int16_t(data[9])) / 1000.0f; const float acceleration_z = (int16_t(data[10] << 8) + int16_t(data[11])) / 1000.0f; - const uint8_t power_info = (data[12] << 8) | data[13]; + const uint16_t power_info = (uint16_t(data[12] << 8) | data[13]); const float battery_voltage = ((power_info >> 5) + 1600.0f) / 1000.0f; const float tx_power = ((power_info & 0x1F) * 2.0f) - 40.0f; diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index b3de1b214a..ca1a254e05 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -3,7 +3,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, UNIT_PARTS_PER_MILLION, \ - CONF_HUMIDITY, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ + CONF_HUMIDITY, CONF_TEMPERATURE, ICON_MOLECULE_CO2, \ UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT, CONF_CO2 DEPENDENCIES = ['i2c'] @@ -22,7 +22,7 @@ def remove_altitude_suffix(value): CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SCD30Component), cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, - ICON_PERIODIC_TABLE_CO2, 0), + ICON_MOLECULE_CO2, 0), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 9590679f83..cdb334446a 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -2,18 +2,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_MODE +CODEOWNERS = ['@esphome/core'] script_ns = cg.esphome_ns.namespace('script') Script = script_ns.class_('Script', automation.Trigger.template()) ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) -ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action) +ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action, cg.Component) IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) +SingleScript = script_ns.class_('SingleScript', Script) +RestartScript = script_ns.class_('RestartScript', Script) +QueueingScript = script_ns.class_('QueueingScript', Script, cg.Component) +ParallelScript = script_ns.class_('ParallelScript', Script) + +CONF_SINGLE = 'single' +CONF_RESTART = 'restart' +CONF_QUEUED = 'queued' +CONF_PARALLEL = 'parallel' +CONF_MAX_RUNS = 'max_runs' + +SCRIPT_MODES = { + CONF_SINGLE: SingleScript, + CONF_RESTART: RestartScript, + CONF_QUEUED: QueueingScript, + CONF_PARALLEL: ParallelScript, +} + + +def check_max_runs(value): + if CONF_MAX_RUNS not in value: + return value + if value[CONF_MODE] not in [CONF_QUEUED, CONF_PARALLEL]: + raise cv.Invalid("The option 'max_runs' is only valid in 'queue' and 'parallel' mode.", + path=[CONF_MAX_RUNS]) + return value + + +def assign_declare_id(value): + value = value.copy() + value[CONF_ID] = cv.declare_id(SCRIPT_MODES[value[CONF_MODE]])(value[CONF_ID]) + return value + CONFIG_SCHEMA = automation.validate_automation({ - cv.Required(CONF_ID): cv.declare_id(Script), -}) + # Don't declare id as cv.declare_id yet, because the ID type + # dpeends on the mode. Will be checked later with assign_declare_id + cv.Required(CONF_ID): cv.string_strict, + cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of(*SCRIPT_MODES, lower=True), + cv.Optional(CONF_MAX_RUNS): cv.positive_int, +}, extra_validators=cv.All(check_max_runs, assign_declare_id)) def to_code(config): @@ -21,6 +59,15 @@ def to_code(config): triggers = [] for conf in config: trigger = cg.new_Pvariable(conf[CONF_ID]) + # Add a human-readable name to the script + cg.add(trigger.set_name(conf[CONF_ID].id)) + + if CONF_MAX_RUNS in conf: + cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS])) + + if conf[CONF_MODE] == CONF_QUEUED: + yield cg.register_component(trigger, conf) + triggers.append((trigger, conf)) for trigger, conf in triggers: @@ -48,7 +95,9 @@ def script_stop_action_to_code(config, action_id, template_arg, args): })) def script_wait_action_to_code(config, action_id, template_arg, args): paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(action_id, template_arg, paren) + var = yield cg.new_Pvariable(action_id, template_arg, paren) + yield cg.register_component(var, {}) + yield var @automation.register_condition('script.is_running', IsRunningCondition, automation.maybe_simple_id({ diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp new file mode 100644 index 0000000000..e99931ced6 --- /dev/null +++ b/esphome/components/script/script.cpp @@ -0,0 +1,67 @@ +#include "script.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace script { + +static const char *TAG = "script"; + +void SingleScript::execute() { + if (this->is_action_running()) { + ESP_LOGW(TAG, "Script '%s' is already running! (mode: single)", this->name_.c_str()); + return; + } + + this->trigger(); +} + +void RestartScript::execute() { + if (this->is_action_running()) { + ESP_LOGD(TAG, "Script '%s' restarting (mode: restart)", this->name_.c_str()); + this->stop_action(); + } + + this->trigger(); +} + +void QueueingScript::execute() { + if (this->is_action_running()) { + // num_runs_ is the number of *queued* instances, so total number of instances is + // num_runs_ + 1 + if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { + ESP_LOGW(TAG, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); + return; + } + + ESP_LOGD(TAG, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str()); + this->num_runs_++; + return; + } + + this->trigger(); + // Check if the trigger was immediate and we can continue right away. + this->loop(); +} + +void QueueingScript::stop() { + this->num_runs_ = 0; + Script::stop(); +} + +void QueueingScript::loop() { + if (this->num_runs_ != 0 && !this->is_action_running()) { + this->num_runs_--; + this->trigger(); + } +} + +void ParallelScript::execute() { + if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { + ESP_LOGW(TAG, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); + return; + } + this->trigger(); +} + +} // namespace script +} // namespace esphome diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 3b97327da8..64db6b80e7 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,29 +1,86 @@ #pragma once #include "esphome/core/automation.h" +#include "esphome/core/component.h" namespace esphome { namespace script { +/// The abstract base class for all script types. class Script : public Trigger<> { public: - void execute() { - bool prev = this->in_stack_; - this->in_stack_ = true; - this->trigger(); - this->in_stack_ = prev; - } - bool script_is_running() { return this->in_stack_ || this->is_running(); } + /** Execute a new instance of this script. + * + * The behavior of this function when a script is already running is defined by the subtypes + */ + virtual void execute() = 0; + /// Check if any instance of this script is currently running. + virtual bool is_running() { return this->is_action_running(); } + /// Stop all instances of this script. + virtual void stop() { this->stop_action(); } + + // Internal function to give scripts readable names. + void set_name(const std::string &name) { name_ = name; } protected: - bool in_stack_{false}; + std::string name_; +}; + +/** A script type for which only a single instance at a time is allowed. + * + * If a new instance is executed while the previous one hasn't finished yet, + * a warning is printed and the new instance is discarded. + */ +class SingleScript : public Script { + public: + void execute() override; +}; + +/** A script type that restarts scripts from the beginning when a new instance is started. + * + * If a new instance is started but another one is already running, the existing + * script is stopped and the new instance starts from the beginning. + */ +class RestartScript : public Script { + public: + void execute() override; +}; + +/** A script type that queues new instances that are created. + * + * Only one instance of the script can be active at a time. + */ +class QueueingScript : public Script, public Component { + public: + void execute() override; + void stop() override; + void loop() override; + void set_max_runs(int max_runs) { max_runs_ = max_runs; } + + protected: + int num_runs_ = 0; + int max_runs_ = 0; +}; + +/** A script type that executes new instances in parallel. + * + * If a new instance is started while previous ones haven't finished yet, + * the new one is exeucted in parallel to the other instances. + */ +class ParallelScript : public Script { + public: + void execute() override; + void set_max_runs(int max_runs) { max_runs_ = max_runs; } + + protected: + int max_runs_ = 0; }; template class ScriptExecuteAction : public Action { public: ScriptExecuteAction(Script *script) : script_(script) {} - void play(Ts... x) override { this->script_->trigger(); } + void play(Ts... x) override { this->script_->execute(); } protected: Script *script_; @@ -43,7 +100,7 @@ template class IsRunningCondition : public Condition { public: explicit IsRunningCondition(Script *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->script_is_running(); } + bool check(Ts... x) override { return this->parent_->is_running(); } protected: Script *parent_; @@ -53,41 +110,34 @@ template class ScriptWaitAction : public Action, public C public: ScriptWaitAction(Script *script) : script_(script) {} - void play(Ts... x) { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { - this->triggered_ = false; - this->play_next(x...); + this->play_next_(x...); return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (this->script_->is_running()) return; - this->triggered_ = false; - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } + void play(Ts... x) override { /* ignore - see play_complex */ + } protected: Script *script_; - bool triggered_{false}; std::tuple var_{}; }; diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 8b41a441ad..812a4aa42d 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -18,8 +18,19 @@ void SenseAirComponent::update() { } if (response[0] != 0xFE || response[1] != 0x04) { - ESP_LOGW(TAG, "Invalid preamble from SenseAir!"); + ESP_LOGW(TAG, "Invalid preamble from SenseAir! %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x", + response[0], response[1], response[2], response[3], response[4], response[5], response[6], response[7], + response[8], response[9], response[10], response[11], response[12]); + this->status_set_warning(); + while (this->available()) { + uint8_t b; + if (this->read_byte(&b)) { + ESP_LOGV(TAG, " ... %02x", b); + } else { + ESP_LOGV(TAG, " ... nothing read"); + } + } return; } diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 393bfd5182..c015871156 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart -from esphome.const import CONF_CO2, CONF_ID, ICON_PERIODIC_TABLE_CO2, UNIT_PARTS_PER_MILLION +from esphome.const import CONF_CO2, CONF_ID, ICON_MOLECULE_CO2, UNIT_PARTS_PER_MILLION DEPENDENCIES = ['uart'] @@ -10,7 +10,7 @@ SenseAirComponent = senseair_ns.class_('SenseAirComponent', cg.PollingComponent, CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SenseAirComponent), - cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 605f72a103..671bbe2b09 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -12,6 +12,7 @@ from esphome.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_B from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 079077dba0..c70fb93963 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -25,6 +25,7 @@ template class SensorPublishAction : public Action { public: SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index a37188740c..b864efc877 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -64,6 +64,7 @@ template class ServoWriteAction : public Action { public: ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) + void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } protected: @@ -73,6 +74,7 @@ template class ServoWriteAction : public Action { template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} + void play(Ts... x) override { this->servo_->detach(); } protected: diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index a52811eb34..a8963fef7b 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ - UNIT_PARTS_PER_BILLION, ICON_PERIODIC_TABLE_CO2 + UNIT_PARTS_PER_BILLION, ICON_MOLECULE_CO2 DEPENDENCIES = ['i2c'] @@ -22,7 +22,7 @@ CONF_TEMPERATURE_SOURCE = 'temperature_source' CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SGP30Component), cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, - ICON_PERIODIC_TABLE_CO2, 0), + ICON_MOLECULE_CO2, 0), cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), cv.Optional(CONF_BASELINE): cv.Schema({ cv.Required(CONF_ECO2_BASELINE): cv.hex_uint16_t, diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 8c148b8e83..1ef43c1c73 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -162,11 +162,11 @@ void SGP30Component::send_env_data_() { void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline) { uint8_t data[7]; data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; - data[1] = eco2_baseline >> 8; - data[2] = eco2_baseline & 0xFF; + data[1] = tvoc_baseline >> 8; + data[2] = tvoc_baseline & 0xFF; data[3] = sht_crc_(data[1], data[2]); - data[4] = tvoc_baseline >> 8; - data[5] = tvoc_baseline & 0xFF; + data[4] = eco2_baseline >> 8; + data[5] = eco2_baseline & 0xFF; data[6] = sht_crc_(data[4], data[5]); if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); diff --git a/esphome/components/shutdown/__init__.py b/esphome/components/shutdown/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/shutdown/__init__.py +++ b/esphome/components/shutdown/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index c64112570a..762e045598 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -5,6 +5,8 @@ from esphome.const import CONF_ID, CONF_TRIGGER_ID from esphome.components import uart DEPENDENCIES = ['uart'] +CODEOWNERS = ['@glmnet'] +MULTI_CONF = True sim800l_ns = cg.esphome_ns.namespace('sim800l') Sim800LComponent = sim800l_ns.class_('Sim800LComponent', cg.Component) @@ -16,8 +18,6 @@ Sim800LReceivedMessageTrigger = sim800l_ns.class_('Sim800LReceivedMessageTrigger # Actions Sim800LSendSmsAction = sim800l_ns.class_('Sim800LSendSmsAction', automation.Action) -MULTI_CONF = True - CONF_ON_SMS_RECEIVED = 'on_sms_received' CONF_RECIPIENT = 'recipient' CONF_MESSAGE = 'message' diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py new file mode 100644 index 0000000000..de80c38214 --- /dev/null +++ b/esphome/components/sn74hc595/__init__.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_ID, CONF_NUMBER, CONF_INVERTED, CONF_DATA_PIN, CONF_CLOCK_PIN + +DEPENDENCIES = [] +MULTI_CONF = True + +sn74hc595_ns = cg.esphome_ns.namespace('sn74hc595') + +SN74HC595Component = sn74hc595_ns.class_('SN74HC595Component', cg.Component) +SN74HC595GPIOPin = sn74hc595_ns.class_('SN74HC595GPIOPin', cg.GPIOPin) + +CONF_SN74HC595 = 'sn74hc595' +CONF_LATCH_PIN = 'latch_pin' +CONF_OE_PIN = 'oe_pin' +CONF_SR_COUNT = 'sr_count' +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(SN74HC595Component), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_LATCH_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_SR_COUNT, default=1): cv.int_range(1, 4) +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + data_pin = yield cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data_pin)) + clock_pin = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock_pin)) + latch_pin = yield cg.gpio_pin_expression(config[CONF_LATCH_PIN]) + cg.add(var.set_latch_pin(latch_pin)) + oe_pin = yield cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe_pin)) + cg.add(var.set_sr_count(config[CONF_SR_COUNT])) + + +SN74HC595_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +SN74HC595_INPUT_PIN_SCHEMA = cv.Schema({}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC595, + (SN74HC595_OUTPUT_PIN_SCHEMA, SN74HC595_INPUT_PIN_SCHEMA)) +def sn74hc595_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_SN74HC595]) + yield SN74HC595GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_INVERTED]) diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp new file mode 100644 index 0000000000..d82c5d5036 --- /dev/null +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -0,0 +1,77 @@ +#include "sn74hc595.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sn74hc595 { + +static const char *TAG = "sn74hc595"; + +void SN74HC595Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SN74HC595..."); + + if (this->have_oe_pin_) { // disable output + this->oe_pin_->pin_mode(OUTPUT); + this->oe_pin_->digital_write(true); + } + + // initialize output pins + this->clock_pin_->pin_mode(OUTPUT); + this->data_pin_->pin_mode(OUTPUT); + this->latch_pin_->pin_mode(OUTPUT); + this->clock_pin_->digital_write(LOW); + this->data_pin_->digital_write(LOW); + this->latch_pin_->digital_write(LOW); + + // send state to shift register + this->write_gpio_(); +} + +void SN74HC595Component::dump_config() { ESP_LOGCONFIG(TAG, "SN74HC595:"); } + +bool SN74HC595Component::digital_read_(uint8_t pin) { return this->output_bits_ >> pin; } + +void SN74HC595Component::digital_write_(uint8_t pin, bool value) { + uint32_t mask = 1UL << pin; + this->output_bits_ &= ~mask; + if (value) + this->output_bits_ |= mask; + this->write_gpio_(); +} + +bool SN74HC595Component::write_gpio_() { + for (int i = this->sr_count_ - 1; i >= 0; i--) { + uint8_t data = (uint8_t)(this->output_bits_ >> (8 * i) & 0xff); + for (int j = 0; j < 8; j++) { + this->data_pin_->digital_write(data & (1 << (7 - j))); + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); + } + } + + // pulse latch to activate new values + this->latch_pin_->digital_write(true); + this->latch_pin_->digital_write(false); + + // enable output if configured + if (this->have_oe_pin_) { + this->oe_pin_->digital_write(false); + } + + return true; +} + +float SN74HC595Component::get_setup_priority() const { return setup_priority::IO; } + +void SN74HC595GPIOPin::setup() {} + +bool SN74HC595GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } + +void SN74HC595GPIOPin::digital_write(bool value) { + this->parent_->digital_write_(this->pin_, value != this->inverted_); +} + +SN74HC595GPIOPin::SN74HC595GPIOPin(SN74HC595Component *parent, uint8_t pin, bool inverted) + : GPIOPin(pin, OUTPUT, inverted), parent_(parent) {} + +} // namespace sn74hc595 +} // namespace esphome diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h new file mode 100644 index 0000000000..d6f9a68bc8 --- /dev/null +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" + +namespace esphome { +namespace sn74hc595 { + +class SN74HC595Component : public Component { + public: + SN74HC595Component() = default; + + void setup() override; + float get_setup_priority() const override; + void dump_config() override; + + void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } + void set_clock_pin(GPIOPin *pin) { clock_pin_ = pin; } + void set_latch_pin(GPIOPin *pin) { latch_pin_ = pin; } + void set_oe_pin(GPIOPin *pin) { + oe_pin_ = pin; + have_oe_pin_ = true; + } + void set_sr_count(uint8_t count) { sr_count_ = count; } + + protected: + friend class SN74HC595GPIOPin; + bool digital_read_(uint8_t pin); + void digital_write_(uint8_t pin, bool value); + bool write_gpio_(); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + GPIOPin *latch_pin_; + GPIOPin *oe_pin_; + uint8_t sr_count_; + bool have_oe_pin_{false}; + uint32_t output_bits_{0x00}; +}; + +/// Helper class to expose a SC74HC595 pin as an internal output GPIO pin. +class SN74HC595GPIOPin : public GPIOPin { + public: + SN74HC595GPIOPin(SN74HC595Component *parent, uint8_t pin, bool inverted = false); + + void setup() override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + SN74HC595Component *parent_; +}; + +} // namespace sn74hc595 +} // namespace esphome diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index c10a3e5ac3..253f3ba2c1 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -33,10 +33,6 @@ void SNTPComponent::setup() { sntp_setservername(2, strdup(this->server_3_.c_str())); } -#ifdef ARDUINO_ARCH_ESP8266 - // let localtime/gmtime handle timezones, not sntp - sntp_set_timezone(0); -#endif sntp_init(); } void SNTPComponent::dump_config() { diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 798600075e..f6afcced0c 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,6 +1,7 @@ from esphome.components import time as time_ import esphome.config_validation as cv import esphome.codegen as cg +from esphome.core import CORE from esphome.const import CONF_ID, CONF_SERVERS @@ -27,3 +28,7 @@ def to_code(config): yield cg.register_component(var, config) yield time_.register_time(var, config) + + if CORE.is_esp8266 and len(servers) > 1: + # We need LwIP features enabled to get 3 SNTP servers (not just one) + cg.add_build_flag('-DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY') diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 65ee5960f0..420c957d87 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,7 +1,7 @@ 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, \ +from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_DIRECTION_OUTPUT, \ CONF_OUTPUT_ID, CONF_SPEED, CONF_LOW, CONF_MEDIUM, CONF_HIGH from .. import speed_ns @@ -11,6 +11,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_SPEED, default={}): cv.Schema({ cv.Optional(CONF_LOW, default=0.33): cv.percentage, cv.Optional(CONF_MEDIUM, default=0.66): cv.percentage, @@ -30,3 +31,7 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3bfbc1fc3c..45117d64c3 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -11,9 +11,12 @@ void SpeedFan::dump_config() { if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void SpeedFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, true); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -46,6 +49,16 @@ void SpeedFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } } float SpeedFan::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 74910bda94..cce9d07544 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -16,6 +16,7 @@ class SpeedFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } void set_speeds(float low, float medium, float high) { this->low_speed_ = low; this->medium_speed_ = medium; @@ -26,6 +27,7 @@ class SpeedFan : public Component { fan::FanState *fan_; output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; float low_speed_{}; float medium_speed_{}; float high_speed_{}; diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 69899d1e84..e7f8bc378d 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_CLK_PIN, CONF_ID, CONF_MISO_PIN, CONF_MOSI_PIN, C CONF_CS_PIN from esphome.core import coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] spi_ns = cg.esphome_ns.namespace('spi') SPIComponent = spi_ns.class_('SPIComponent', cg.Component) SPIDevice = spi_ns.class_('SPIDevice') @@ -34,15 +35,25 @@ def to_code(config): cg.add(var.set_mosi(mosi)) -SPI_DEVICE_SCHEMA = cv.Schema({ - cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), - cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, -}) +def spi_device_schema(cs_pin_required=True): + """Create a schema for an SPI device. + :param cs_pin_required: If true, make the CS_PIN required in the config. + :return: The SPI device schema, `extend` this in your config schema. + """ + schema = { + cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + } + if cs_pin_required: + schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema + else: + schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema + return cv.Schema(schema) @coroutine def register_spi_device(var, config): parent = yield cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) - pin = yield cg.gpio_pin_expression(config[CONF_CS_PIN]) - cg.add(var.set_cs_pin(pin)) + if CONF_CS_PIN in config: + pin = yield cg.gpio_pin_expression(config[CONF_CS_PIN]) + cg.add(var.set_cs_pin(pin)) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index bf2a18955a..ea6f8d68f6 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -12,9 +12,11 @@ void ICACHE_RAM_ATTR HOT SPIComponent::disable() { if (this->hw_spi_ != nullptr) { this->hw_spi_->endTransaction(); } - ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); - this->active_cs_->digital_write(true); - this->active_cs_ = nullptr; + if (this->active_cs_) { + ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); + this->active_cs_->digital_write(true); + this->active_cs_ = nullptr; + } } void SPIComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI bus..."); diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index ccef6192f3..ba726f8052 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -127,7 +127,9 @@ class SPIComponent : public Component { template void enable(GPIOPin *cs) { - SPIComponent::debug_enable(cs->get_pin()); + if (cs != nullptr) { + SPIComponent::debug_enable(cs->get_pin()); + } if (this->hw_spi_ != nullptr) { uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE); @@ -138,8 +140,10 @@ class SPIComponent : public Component { this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL; } - this->active_cs_ = cs; - this->active_cs_->digital_write(false); + if (cs != nullptr) { + this->active_cs_ = cs; + this->active_cs_->digital_write(false); + } } void disable(); @@ -174,8 +178,10 @@ class SPIDevice { void set_cs_pin(GPIOPin *cs) { cs_ = cs; } void spi_setup() { - this->cs_->setup(); - this->cs_->digital_write(true); + if (this->cs_) { + this->cs_->setup(); + this->cs_->digital_write(true); + } } void enable() { this->parent_->template enable(this->cs_); } diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 047ddddcac..a8b2a2a7bb 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -41,7 +41,7 @@ def setup_ssd1036(var, config): reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) if CONF_BRIGHTNESS in config: - cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index d60f7dc985..1537d3b526 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -5,7 +5,11 @@ namespace esphome { namespace ssd1306_base { -static const char *TAG = "sd1306"; +static const char *TAG = "ssd1306"; + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; +static const uint8_t SSD1306_MAX_CONTRAST = 255; static const uint8_t SSD1306_COMMAND_DISPLAY_OFF = 0xAE; static const uint8_t SSD1306_COMMAND_DISPLAY_ON = 0xAF; @@ -69,27 +73,6 @@ void SSD1306::setup() { break; } - this->command(SSD1306_COMMAND_SET_CONTRAST); - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - this->command(0x8F); - break; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1306_MODEL_64_48: - case SH1106_MODEL_64_48: - this->command(int(255 * (this->brightness_))); - break; - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - if (this->external_vcc_) - this->command(0x10); - else - this->command(0xAF); - break; - } - this->command(SSD1306_COMMAND_SET_PRE_CHARGE); if (this->external_vcc_) this->command(0x22); @@ -104,7 +87,12 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_DEACTIVATE_SCROLL); - this->command(SSD1306_COMMAND_DISPLAY_ON); + set_brightness(this->brightness_); + + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + + this->turn_on(); } void SSD1306::display() { if (this->is_sh1106_()) { @@ -140,6 +128,22 @@ void SSD1306::update() { this->do_update_(); this->display(); } +void SSD1306::set_brightness(float brightness) { + // validation + this->brightness_ = clamp(brightness, 0, 1); + // now write the new brightness level to the display + this->command(SSD1306_COMMAND_SET_CONTRAST); + this->command(int(SSD1306_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1306::is_on() { return this->is_on_; } +void SSD1306::turn_on() { + this->command(SSD1306_COMMAND_DISPLAY_ON); + this->is_on_ = true; +} +void SSD1306::turn_off() { + this->command(SSD1306_COMMAND_DISPLAY_OFF); + this->is_on_ = false; +} int SSD1306::get_height_internal() { switch (this->model_) { case SSD1306_MODEL_128_32: @@ -178,21 +182,20 @@ int SSD1306::get_width_internal() { size_t SSD1306::get_buffer_length_() { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; } - -void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; uint16_t pos = x + (y / 8) * this->get_width_internal(); uint8_t subpos = y & 0x07; - if (color) { + if (color.is_on()) { this->buffer_[pos] |= (1 << subpos); } else { this->buffer_[pos] &= ~(1 << subpos); } } -void SSD1306::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void SSD1306::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 8adf3c1b87..0fe09709e7 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -29,10 +29,13 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1306Model model) { this->model_ = model; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } - void set_brightness(float brightness) { this->brightness_ = brightness; } - + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } - void fill(int color) override; + void fill(Color color) override; protected: virtual void command(uint8_t value) = 0; @@ -41,7 +44,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { bool is_sh1106_() const; - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; @@ -51,6 +54,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { SSD1306Model model_{SSD1306_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; + bool is_on_{false}; float brightness_{1.0}; }; diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index 65f0df1c51..19882af6c4 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -13,7 +13,7 @@ SPISSD1306 = ssd1306_spi.class_('SPISSD1306', ssd1306_base.SSD1306, spi.SPIDevic CONFIG_SCHEMA = cv.All(ssd1306_base.SSD1306_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SPISSD1306), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 69e11ec0d1..6cb0dafe54 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import display -from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.const import CONF_BRIGHTNESS, CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, \ + CONF_RESET_PIN from esphome.core import coroutine ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base') @@ -14,6 +15,7 @@ MODELS = { 'SSD1325_128X64': SSD1325Model.SSD1325_MODEL_128_64, 'SSD1325_96X16': SSD1325Model.SSD1325_MODEL_96_16, 'SSD1325_64X48': SSD1325Model.SSD1325_MODEL_64_48, + 'SSD1327_128X128': SSD1325Model.SSD1327_MODEL_128_128, } SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") @@ -21,12 +23,13 @@ SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_MODEL): SSD1325_MODEL, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, }).extend(cv.polling_component_schema('1s')) @coroutine -def setup_ssd1036(var, config): +def setup_ssd1325(var, config): yield cg.register_component(var, config) yield display.register_display(var, config) @@ -34,6 +37,8 @@ def setup_ssd1036(var, config): if CONF_RESET_PIN in config: reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index 3079e19cc8..dfb1ef00ee 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -8,7 +8,11 @@ namespace ssd1325_base { static const char *TAG = "ssd1325"; static const uint8_t BLACK = 0; -static const uint8_t WHITE = 1; +static const uint8_t WHITE = 15; +static const uint8_t SSD1325_MAX_CONTRAST = 127; +static const uint8_t SSD1325_COLORMASK = 0x0f; +static const uint8_t SSD1325_COLORSHIFT = 4; +static const uint8_t SSD1325_PIXELSPERBYTE = 2; static const uint8_t SSD1325_SETCOLADDR = 0x15; static const uint8_t SSD1325_SETROWADDR = 0x75; @@ -33,6 +37,7 @@ static const uint8_t SSD1325_SETROWPERIOD = 0xB2; static const uint8_t SSD1325_SETCLOCK = 0xB3; static const uint8_t SSD1325_SETPRECHARGECOMP = 0xB4; static const uint8_t SSD1325_SETGRAYTABLE = 0xB8; +static const uint8_t SSD1325_SETDEFAULTGRAYTABLE = 0xB9; static const uint8_t SSD1325_SETPRECHARGEVOLTAGE = 0xBC; static const uint8_t SSD1325_SETVCOMLEVEL = 0xBE; static const uint8_t SSD1325_SETVSL = 0xBF; @@ -44,31 +49,57 @@ static const uint8_t SSD1325_COPY = 0x25; void SSD1325::setup() { this->init_internal_(this->get_buffer_length_()); - this->command(SSD1325_DISPLAYOFF); /* display off */ - this->command(SSD1325_SETCLOCK); /* set osc division */ - this->command(0xF1); /* 145 */ - this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */ - this->command(0x3f); /* duty = 1/64 */ - this->command(SSD1325_SETOFFSET); /* set display offset --- */ - this->command(0x4C); /* 76 */ - this->command(SSD1325_SETSTARTLINE); /*set start line */ - this->command(0x00); /* ------ */ - this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/ + this->command(SSD1325_DISPLAYOFF); // display off + this->command(SSD1325_SETCLOCK); // set osc division + this->command(0xF1); // 145 + this->command(SSD1325_SETMULTIPLEX); // multiplex ratio + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x7f); // duty = height - 1 + else + this->command(0x3f); // duty = 1/64 + this->command(SSD1325_SETOFFSET); // set display offset + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x00); // 0 + else + this->command(0x4C); // 76 + this->command(SSD1325_SETSTARTLINE); // set start line + this->command(0x00); // ... + this->command(SSD1325_MASTERCONFIG); // Set Master Config DC/DC Converter this->command(0x02); - this->command(SSD1325_SETREMAP); /* set segment remap------ */ - this->command(0x56); - this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */ + this->command(SSD1325_SETREMAP); // set segment remapping + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x53); // COM bottom-up, split odd/even, enable column and nibble remapping + else + this->command(0x50); // COM bottom-up, split odd/even + this->command(SSD1325_SETCURRENT + 0x2); // Set Full Current Range this->command(SSD1325_SETGRAYTABLE); - this->command(0x01); - this->command(0x11); - this->command(0x22); - this->command(0x32); - this->command(0x43); - this->command(0x54); - this->command(0x65); - this->command(0x76); - this->command(SSD1325_SETCONTRAST); /* set contrast current */ - this->command(0x7F); // max! + // gamma ~2.2 + if (this->model_ == SSD1327_MODEL_128_128) { + this->command(0); + this->command(1); + this->command(2); + this->command(3); + this->command(6); + this->command(8); + this->command(12); + this->command(16); + this->command(20); + this->command(26); + this->command(32); + this->command(39); + this->command(46); + this->command(54); + this->command(63); + } else { + this->command(0x01); + this->command(0x11); + this->command(0x22); + this->command(0x32); + this->command(0x43); + this->command(0x54); + this->command(0x65); + this->command(0x76); + } this->command(SSD1325_SETROWPERIOD); this->command(0x51); this->command(SSD1325_SETPHASELEN); @@ -78,19 +109,25 @@ void SSD1325::setup() { this->command(SSD1325_SETPRECHARGECOMPENABLE); this->command(0x28); this->command(SSD1325_SETVCOMLEVEL); // Set High Voltage Level of COM Pin - this->command(0x1C); //? - this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin + this->command(0x1C); + this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin this->command(0x0D | 0x02); - this->command(SSD1325_NORMALDISPLAY); /* set display mode */ - this->command(SSD1325_DISPLAYON); /* display ON */ + this->command(SSD1325_NORMALDISPLAY); // set display mode + set_brightness(this->brightness_); + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1325::display() { - this->command(SSD1325_SETCOLADDR); /* set column address */ - this->command(0x00); /* set column start address */ - this->command(0x3F); /* set column end address */ - this->command(SSD1325_SETROWADDR); /* set row address */ - this->command(0x00); /* set row start address */ - this->command(0x3F); /* set row end address */ + this->command(SSD1325_SETCOLADDR); // set column address + this->command(0x00); // set column start address + this->command(0x3F); // set column end address + this->command(SSD1325_SETROWADDR); // set row address + this->command(0x00); // set row start address + if (this->model_ == SSD1327_MODEL_128_128) + this->command(127); // set last row + else + this->command(63); // set last row this->write_display_data(); } @@ -98,6 +135,27 @@ void SSD1325::update() { this->do_update_(); this->display(); } +void SSD1325::set_brightness(float brightness) { + // validation + if (brightness > 1) + this->brightness_ = 1.0; + else if (brightness < 0) + this->brightness_ = 0; + else + this->brightness_ = brightness; + // now write the new brightness level to the display + this->command(SSD1325_SETCONTRAST); + this->command(int(SSD1325_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1325::is_on() { return this->is_on_; } +void SSD1325::turn_on() { + this->command(SSD1325_DISPLAYON); + this->is_on_ = true; +} +void SSD1325::turn_off() { + this->command(SSD1325_DISPLAYOFF); + this->is_on_ = false; +} int SSD1325::get_height_internal() { switch (this->model_) { case SSD1325_MODEL_128_32: @@ -108,6 +166,8 @@ int SSD1325::get_height_internal() { return 16; case SSD1325_MODEL_64_48: return 48; + case SSD1327_MODEL_128_128: + return 128; default: return 0; } @@ -116,6 +176,7 @@ int SSD1325::get_width_internal() { switch (this->model_) { case SSD1325_MODEL_128_32: case SSD1325_MODEL_128_64: + case SSD1327_MODEL_128_128: return 128; case SSD1325_MODEL_96_16: return 96; @@ -126,23 +187,25 @@ int SSD1325::get_width_internal() { } } size_t SSD1325::get_buffer_length_() { - return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SSD1325_PIXELSPERBYTE; } - -void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; - - uint16_t pos = x + (y / 8) * this->get_width_internal(); - uint8_t subpos = y % 8; - if (color) { - this->buffer_[pos] |= (1 << subpos); - } else { - this->buffer_[pos] &= ~(1 << subpos); - } + uint32_t color4 = color.to_grayscale4(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x / SSD1325_PIXELSPERBYTE) + (y * this->get_width_internal() / SSD1325_PIXELSPERBYTE); + uint8_t shift = (x % SSD1325_PIXELSPERBYTE) * SSD1325_COLORSHIFT; + // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary + color4 = (color4 & SSD1325_COLORMASK) << shift; + // first mask off the nibble we must change... + this->buffer_[pos] &= (~SSD1325_COLORMASK >> shift); + // ...then lay the new nibble back on top. done! + this->buffer_[pos] |= color4; } -void SSD1325::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void SSD1325::fill(Color color) { + const uint32_t color4 = color.to_grayscale4(); + uint8_t fill = (color4 & SSD1325_COLORMASK) | ((color4 & SSD1325_COLORMASK) << SSD1325_COLORSHIFT); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } @@ -168,6 +231,8 @@ const char *SSD1325::model_str_() { return "SSD1325 96x16"; case SSD1325_MODEL_64_48: return "SSD1325 64x48"; + case SSD1327_MODEL_128_128: + return "SSD1327 128x128"; default: return "Unknown"; } diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index e227f68f86..a06ba69a59 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -12,6 +12,7 @@ enum SSD1325Model { SSD1325_MODEL_128_64, SSD1325_MODEL_96_16, SSD1325_MODEL_64_48, + SSD1327_MODEL_128_128, }; class SSD1325 : public PollingComponent, public display::DisplayBuffer { @@ -25,16 +26,21 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1325Model model) { this->model_ = model; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } - void fill(int color) override; + void fill(Color color) override; protected: virtual void command(uint8_t value) = 0; virtual void write_display_data() = 0; void init_reset_(); - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; @@ -44,6 +50,8 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { SSD1325Model model_{SSD1325_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; + bool is_on_{false}; + float brightness_{1.0}; }; } // namespace ssd1325_base diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index 4615d45393..2d8e91c3df 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -13,13 +13,13 @@ SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevic CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SPISSD1325), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(cs_pin_required=False)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield ssd1325_base.setup_ssd1036(var, config) + yield ssd1325_base.setup_ssd1325(var, config) yield spi.register_spi_device(var, config) dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index 399700f1dd..9df05439ca 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -11,7 +11,8 @@ void SPISSD1325::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI SSD1325..."); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT - this->cs_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT this->init_reset_(); delay(500); // NOLINT @@ -20,43 +21,38 @@ void SPISSD1325::setup() { void SPISSD1325::dump_config() { LOG_DISPLAY("", "SPI SSD1325", this); ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - LOG_PIN(" CS Pin: ", this->cs_); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); LOG_UPDATE_INTERVAL(this); } void SPISSD1325::command(uint8_t value) { - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->dc_pin_->digital_write(false); delay(1); this->enable(); - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); this->write_byte(value); - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->disable(); } void HOT SPISSD1325::write_display_data() { - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->dc_pin_->digital_write(true); - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); delay(1); this->enable(); - for (uint16_t x = 0; x < this->get_width_internal(); x += 2) { - for (uint16_t y = 0; y < this->get_height_internal(); y += 8) { // we write 8 pixels at once - uint8_t left8 = this->buffer_[y * 16 + x]; - uint8_t right8 = this->buffer_[y * 16 + x + 1]; - for (uint8_t p = 0; p < 8; p++) { - uint8_t d = 0; - if (left8 & (1 << p)) - d |= 0xF0; - if (right8 & (1 << p)) - d |= 0x0F; - this->write_byte(d); - } - } - } - this->cs_->digital_write(true); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); this->disable(); } diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py new file mode 100644 index 0000000000..198f81668e --- /dev/null +++ b/esphome/components/ssd1351_base/__init__.py @@ -0,0 +1,40 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.core import coroutine + +ssd1351_base_ns = cg.esphome_ns.namespace('ssd1351_base') +SSD1351 = ssd1351_base_ns.class_('SSD1351', cg.PollingComponent, display.DisplayBuffer) +SSD1351Model = ssd1351_base_ns.enum('SSD1351Model') + +MODELS = { + 'SSD1351_128X96': SSD1351Model.SSD1351_MODEL_128_96, + 'SSD1351_128X128': SSD1351Model.SSD1351_MODEL_128_128, +} + +SSD1351_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1351_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): SSD1351_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1351(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp new file mode 100644 index 0000000000..fded8e3482 --- /dev/null +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -0,0 +1,193 @@ +#include "ssd1351_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1351_base { + +static const char *TAG = "ssd1351"; + +static const uint16_t BLACK = 0; +static const uint16_t WHITE = 0xffff; +static const uint16_t SSD1351_COLORMASK = 0xffff; +static const uint8_t SSD1351_MAX_CONTRAST = 15; +static const uint8_t SSD1351_BYTESPERPIXEL = 2; +// SSD1351 commands +static const uint8_t SSD1351_SETCOLUMN = 0x15; +static const uint8_t SSD1351_SETROW = 0x75; +static const uint8_t SSD1351_SETREMAP = 0xA0; +static const uint8_t SSD1351_STARTLINE = 0xA1; +static const uint8_t SSD1351_DISPLAYOFFSET = 0xA2; +static const uint8_t SSD1351_DISPLAYOFF = 0xAE; +static const uint8_t SSD1351_DISPLAYON = 0xAF; +static const uint8_t SSD1351_PRECHARGE = 0xB1; +static const uint8_t SSD1351_CLOCKDIV = 0xB3; +static const uint8_t SSD1351_PRECHARGELEVEL = 0xBB; +static const uint8_t SSD1351_VCOMH = 0xBE; +// display controls +static const uint8_t SSD1351_DISPLAYALLOFF = 0xA4; +static const uint8_t SSD1351_DISPLAYALLON = 0xA5; +static const uint8_t SSD1351_NORMALDISPLAY = 0xA6; +static const uint8_t SSD1351_INVERTDISPLAY = 0xA7; +// contrast controls +static const uint8_t SSD1351_CONTRASTABC = 0xC1; +static const uint8_t SSD1351_CONTRASTMASTER = 0xC7; +// memory functions +static const uint8_t SSD1351_WRITERAM = 0x5C; +static const uint8_t SSD1351_READRAM = 0x5D; +// other functions +static const uint8_t SSD1351_FUNCTIONSELECT = 0xAB; +static const uint8_t SSD1351_DISPLAYENHANCE = 0xB2; +static const uint8_t SSD1351_SETVSL = 0xB4; +static const uint8_t SSD1351_SETGPIO = 0xB5; +static const uint8_t SSD1351_PRECHARGE2 = 0xB6; +static const uint8_t SSD1351_SETGRAY = 0xB8; +static const uint8_t SSD1351_USELUT = 0xB9; +static const uint8_t SSD1351_MUXRATIO = 0xCA; +static const uint8_t SSD1351_COMMANDLOCK = 0xFD; +static const uint8_t SSD1351_HORIZSCROLL = 0x96; +static const uint8_t SSD1351_STOPSCROLL = 0x9E; +static const uint8_t SSD1351_STARTSCROLL = 0x9F; + +void SSD1351::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1351_COMMANDLOCK); + this->data(0x12); + this->command(SSD1351_COMMANDLOCK); + this->data(0xB1); + this->command(SSD1351_DISPLAYOFF); + this->command(SSD1351_CLOCKDIV); + this->data(0xF1); // 7:4 = Oscillator Freq, 3:0 = CLK Div Ratio (A[3:0]+1 = 1..16) + this->command(SSD1351_MUXRATIO); + this->data(127); + this->command(SSD1351_DISPLAYOFFSET); + this->data(0x00); + this->command(SSD1351_SETGPIO); + this->data(0x00); + this->command(SSD1351_FUNCTIONSELECT); + this->data(0x01); // internal (diode drop) + this->command(SSD1351_PRECHARGE); + this->data(0x32); + this->command(SSD1351_VCOMH); + this->data(0x05); + this->command(SSD1351_NORMALDISPLAY); + this->command(SSD1351_SETVSL); + this->data(0xA0); + this->data(0xB5); + this->data(0x55); + this->command(SSD1351_PRECHARGE2); + this->data(0x01); + this->command(SSD1351_SETREMAP); + this->data(0x34); + this->command(SSD1351_STARTLINE); + this->data(0x00); + this->command(SSD1351_CONTRASTABC); + this->data(0xC8); + this->data(0x80); + this->data(0xC8); + set_brightness(this->brightness_); + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON +} +void SSD1351::display() { + this->command(SSD1351_SETCOLUMN); // set column address + this->data(0x00); // set column start address + this->data(0x7F); // set column end address + this->command(SSD1351_SETROW); // set row address + this->data(0x00); // set row start address + this->data(0x7F); // set last row + this->command(SSD1351_WRITERAM); + this->write_display_data(); +} +void SSD1351::update() { + this->do_update_(); + this->display(); +} +void SSD1351::set_brightness(float brightness) { + // validation + if (brightness > 1) + this->brightness_ = 1.0; + else if (brightness < 0) + this->brightness_ = 0; + else + this->brightness_ = brightness; + // now write the new brightness level to the display + this->command(SSD1351_CONTRASTMASTER); + this->data(int(SSD1351_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1351::is_on() { return this->is_on_; } +void SSD1351::turn_on() { + this->command(SSD1351_DISPLAYON); + this->is_on_ = true; +} +void SSD1351::turn_off() { + this->command(SSD1351_DISPLAYOFF); + this->is_on_ = false; +} +int SSD1351::get_height_internal() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + return 96; + case SSD1351_MODEL_128_128: + return 128; + default: + return 0; + } +} +int SSD1351::get_width_internal() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + case SSD1351_MODEL_128_128: + return 128; + default: + return 0; + } +} +size_t SSD1351::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * size_t(SSD1351_BYTESPERPIXEL); +} +void HOT SSD1351::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + const uint32_t color565 = color.to_rgb_565(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x + y * this->get_width_internal()) * SSD1351_BYTESPERPIXEL; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} +void SSD1351::fill(Color color) { + const uint32_t color565 = color.to_rgb_565(); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + if (i & 1) { + this->buffer_[i] = color565 & 0xff; + } else { + this->buffer_[i] = (color565 >> 8) & 0xff; + } +} +void SSD1351::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1351::model_str_() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + return "SSD1351 128x96"; + case SSD1351_MODEL_128_128: + return "SSD1351 128x128"; + default: + return "Unknown"; + } +} + +} // namespace ssd1351_base +} // namespace esphome diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h new file mode 100644 index 0000000000..2730f798b5 --- /dev/null +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1351_base { + +enum SSD1351Model { + SSD1351_MODEL_128_96 = 0, + SSD1351_MODEL_128_128, +}; + +class SSD1351 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1351Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void data(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + const char *model_str_(); + + SSD1351Model model_{SSD1351_MODEL_128_96}; + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1351_base +} // namespace esphome diff --git a/esphome/components/ssd1351_spi/__init__.py b/esphome/components/ssd1351_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py new file mode 100644 index 0000000000..16b0d4387a --- /dev/null +++ b/esphome/components/ssd1351_spi/display.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1351_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +AUTO_LOAD = ['ssd1351_base'] +DEPENDENCIES = ['spi'] + +ssd1351_spi = cg.esphome_ns.namespace('ssd1351_spi') +SPISSD1351 = ssd1351_spi.class_('SPISSD1351', ssd1351_base.SSD1351, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1351_base.SSD1351_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1351), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1351_base.setup_ssd1351(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.cpp b/esphome/components/ssd1351_spi/ssd1351_spi.cpp new file mode 100644 index 0000000000..2839ef7a8e --- /dev/null +++ b/esphome/components/ssd1351_spi/ssd1351_spi.cpp @@ -0,0 +1,72 @@ +#include "ssd1351_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1351_spi { + +static const char *TAG = "ssd1351_spi"; + +void SPISSD1351::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1351..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1351::setup(); +} +void SPISSD1351::dump_config() { + LOG_DISPLAY("", "SPI SSD1351", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1351::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void SPISSD1351::data(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1351::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1351_spi +} // namespace esphome diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.h b/esphome/components/ssd1351_spi/ssd1351_spi.h new file mode 100644 index 0000000000..b8f3310f5c --- /dev/null +++ b/esphome/components/ssd1351_spi/ssd1351_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1351_base/ssd1351_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1351_spi { + +class SPISSD1351 : public ssd1351_base::SSD1351, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + void data(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1351_spi +} // namespace esphome diff --git a/esphome/components/st7789v/__init__.py b/esphome/components/st7789v/__init__.py new file mode 100644 index 0000000000..dc85fa6b76 --- /dev/null +++ b/esphome/components/st7789v/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +st7789v_ns = cg.esphome_ns.namespace('st7789v') diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py new file mode 100644 index 0000000000..36e5acaa72 --- /dev/null +++ b/esphome/components/st7789v/display.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, spi +from esphome.const import CONF_BACKLIGHT_PIN, CONF_BRIGHTNESS, CONF_CS_PIN, CONF_DC_PIN, CONF_ID, \ + CONF_LAMBDA, CONF_RESET_PIN +from . import st7789v_ns + +DEPENDENCIES = ['spi'] + +ST7789V = st7789v_ns.class_('ST7789V', cg.PollingComponent, spi.SPIDevice, + display.DisplayBuffer) +ST7789VRef = ST7789V.operator('ref') + +CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ST7789V), + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, +}).extend(cv.polling_component_schema('5s')).extend(spi.spi_device_schema()) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + bl = yield cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) + cg.add(var.set_backlight_pin(bl)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) + + yield display.register_display(var, config) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp new file mode 100644 index 0000000000..284f2342fc --- /dev/null +++ b/esphome/components/st7789v/st7789v.cpp @@ -0,0 +1,274 @@ +#include "st7789v.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7789v { + +static const char *TAG = "st7789v"; + +void ST7789V::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI ST7789V..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + + this->init_reset_(); + + this->write_command_(ST7789_SLPOUT); // Sleep out + delay(120); // NOLINT + + this->write_command_(ST7789_NORON); // Normal display mode on + + // *** display and color format setting *** + this->write_command_(ST7789_MADCTL); + this->write_data_(ST7789_MADCTL_COLOR_ORDER); + + // JLX240 display datasheet + this->write_command_(0xB6); + this->write_data_(0x0A); + this->write_data_(0x82); + + this->write_command_(ST7789_COLMOD); + this->write_data_(0x55); + delay(10); + + // *** ST7789V Frame rate setting *** + this->write_command_(ST7789_PORCTRL); + this->write_data_(0x0c); + this->write_data_(0x0c); + this->write_data_(0x00); + this->write_data_(0x33); + this->write_data_(0x33); + + this->write_command_(ST7789_GCTRL); // Voltages: VGH / VGL + this->write_data_(0x35); + + // *** ST7789V Power setting *** + this->write_command_(ST7789_VCOMS); + this->write_data_(0x28); // JLX240 display datasheet + + this->write_command_(ST7789_LCMCTRL); + this->write_data_(0x0C); + + this->write_command_(ST7789_VDVVRHEN); + this->write_data_(0x01); + this->write_data_(0xFF); + + this->write_command_(ST7789_VRHS); // voltage VRHS + this->write_data_(0x10); + + this->write_command_(ST7789_VDVS); + this->write_data_(0x20); + + this->write_command_(ST7789_FRCTRL2); + this->write_data_(0x0f); + + this->write_command_(ST7789_PWCTRL1); + this->write_data_(0xa4); + this->write_data_(0xa1); + + // *** ST7789V gamma setting *** + this->write_command_(ST7789_PVGAMCTRL); + this->write_data_(0xd0); + this->write_data_(0x00); + this->write_data_(0x02); + this->write_data_(0x07); + this->write_data_(0x0a); + this->write_data_(0x28); + this->write_data_(0x32); + this->write_data_(0x44); + this->write_data_(0x42); + this->write_data_(0x06); + this->write_data_(0x0e); + this->write_data_(0x12); + this->write_data_(0x14); + this->write_data_(0x17); + + this->write_command_(ST7789_NVGAMCTRL); + this->write_data_(0xd0); + this->write_data_(0x00); + this->write_data_(0x02); + this->write_data_(0x07); + this->write_data_(0x0a); + this->write_data_(0x28); + this->write_data_(0x31); + this->write_data_(0x54); + this->write_data_(0x47); + this->write_data_(0x0e); + this->write_data_(0x1c); + this->write_data_(0x17); + this->write_data_(0x1b); + this->write_data_(0x1e); + + this->write_command_(ST7789_INVON); + + // Clear display - ensures we do not see garbage at power-on + this->draw_filled_rect_(0, 0, 239, 319, 0x0000); + + delay(120); // NOLINT + + this->write_command_(ST7789_DISPON); // Display on + delay(120); // NOLINT + + backlight_(true); + + this->init_internal_(this->get_buffer_length_()); + memset(this->buffer_, 0x00, this->get_buffer_length_()); +} + +void ST7789V::dump_config() { + LOG_DISPLAY("", "SPI ST7789V", this); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" B/L Pin: ", this->backlight_pin_); + LOG_UPDATE_INTERVAL(this); +} + +float ST7789V::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void ST7789V::update() { + this->do_update_(); + this->write_display_data(); +} + +void ST7789V::loop() {} + +void ST7789V::write_display_data() { + uint16_t x1 = 52; // _offsetx + uint16_t x2 = 186; // _offsetx + uint16_t y1 = 40; // _offsety + uint16_t y2 = 279; // _offsety + + this->enable(); + + // set column(x) address + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_CASET); + this->dc_pin_->digital_write(true); + this->write_addr_(x1, x2); + // set page(y) address + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RASET); + this->dc_pin_->digital_write(true); + this->write_addr_(y1, y2); + // write display memory + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RAMWR); + this->dc_pin_->digital_write(true); + + this->write_array(this->buffer_, this->get_buffer_length_()); + + this->disable(); +} + +void ST7789V::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +void ST7789V::backlight_(bool onoff) { + if (this->backlight_pin_ != nullptr) { + this->backlight_pin_->setup(); + this->backlight_pin_->digital_write(onoff); + } +} + +void ST7789V::write_command_(uint8_t value) { + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + this->disable(); +} + +void ST7789V::write_data_(uint8_t value) { + this->dc_pin_->digital_write(true); + this->enable(); + this->write_byte(value); + this->disable(); +} + +void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) { + static uint8_t BYTE[4]; + BYTE[0] = (addr1 >> 8) & 0xFF; + BYTE[1] = addr1 & 0xFF; + BYTE[2] = (addr2 >> 8) & 0xFF; + BYTE[3] = addr2 & 0xFF; + + this->dc_pin_->digital_write(true); + this->write_array(BYTE, 4); +} + +void ST7789V::write_color_(uint16_t color, uint16_t size) { + static uint8_t BYTE[1024]; + int index = 0; + for (int i = 0; i < size; i++) { + BYTE[index++] = (color >> 8) & 0xFF; + BYTE[index++] = color & 0xFF; + } + + this->dc_pin_->digital_write(true); + return write_array(BYTE, size * 2); +} + +int ST7789V::get_height_internal() { + return 240; // 320; +} + +int ST7789V::get_width_internal() { + return 135; // 240; +} + +size_t ST7789V::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * 2; +} + +// Draw a filled rectangle +// x1: Start X coordinate +// y1: Start Y coordinate +// x2: End X coordinate +// y2: End Y coordinate +// color: color +void ST7789V::draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { + // ESP_LOGD(TAG,"offset(x)=%d offset(y)=%d",dev->_offsetx,dev->_offsety); + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_CASET); // set column(x) address + this->dc_pin_->digital_write(true); + this->write_addr_(x1, x2); + + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RASET); // set Page(y) address + this->dc_pin_->digital_write(true); + this->write_addr_(y1, y2); + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RAMWR); // begin a write to memory + this->dc_pin_->digital_write(true); + for (int i = x1; i <= x2; i++) { + uint16_t size = y2 - y1 + 1; + this->write_color_(color, size); + } + this->disable(); +} + +void HOT ST7789V::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + auto color565 = color.to_rgb_565(); + + uint16_t pos = (x + y * this->get_width_internal()) * 2; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} + +} // namespace st7789v +} // namespace esphome diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h new file mode 100644 index 0000000000..0e17e65fd7 --- /dev/null +++ b/esphome/components/st7789v/st7789v.h @@ -0,0 +1,151 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7789v { + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; + +static const uint8_t ST7789_NOP = 0x00; // No Operation +static const uint8_t ST7789_SWRESET = 0x01; // Software Reset +static const uint8_t ST7789_RDDID = 0x04; // Read Display ID +static const uint8_t ST7789_RDDST = 0x09; // Read Display Status +static const uint8_t ST7789_RDDPM = 0x0A; // Read Display Power Mode +static const uint8_t ST7789_RDDMADCTL = 0x0B; // Read Display MADCTL +static const uint8_t ST7789_RDDCOLMOD = 0x0C; // Read Display Pixel Format +static const uint8_t ST7789_RDDIM = 0x0D; // Read Display Image Mode +static const uint8_t ST7789_RDDSM = 0x0E; // Read Display Signal Mod +static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Resul +static const uint8_t ST7789_SLPIN = 0x10; // Sleep in +static const uint8_t ST7789_SLPOUT = 0x11; // Sleep Out +static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode O +static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode O +static const uint8_t ST7789_INVOFF = 0x20; // Display Inversion Off +static const uint8_t ST7789_INVON = 0x21; // Display Inversion O +static const uint8_t ST7789_GAMSET = 0x26; // Gamma Set +static const uint8_t ST7789_DISPOFF = 0x28; // Display Off +static const uint8_t ST7789_DISPON = 0x29; // Display On +static const uint8_t ST7789_CASET = 0x2A; // Column Address Set +static const uint8_t ST7789_RASET = 0x2B; // Row Address Set +static const uint8_t ST7789_RAMWR = 0x2C; // Memory Write +static const uint8_t ST7789_RAMRD = 0x2E; // Memory Read +static const uint8_t ST7789_PTLAR = 0x30; // Partial Area +static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definitio +static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line OFF +static const uint8_t ST7789_TEON = 0x35; // Tearing Effect Line On +static const uint8_t ST7789_MADCTL = 0x36; // Memory Data Access Control +static const uint8_t ST7789_VSCSAD = 0x37; // Vertical Scroll Start Address of RAM +static const uint8_t ST7789_IDMOFF = 0x38; // Idle Mode Off +static const uint8_t ST7789_IDMON = 0x39; // Idle mode on +static const uint8_t ST7789_COLMOD = 0x3A; // Interface Pixel Format +static const uint8_t ST7789_WRMEMC = 0x3C; // Write Memory Continue +static const uint8_t ST7789_RDMEMC = 0x3E; // Read Memory Continue +static const uint8_t ST7789_STE = 0x44; // Set Tear Scanline +static const uint8_t ST7789_GSCAN = 0x45; // Get Scanlin +static const uint8_t ST7789_WRDISBV = 0x51; // Write Display Brightness +static const uint8_t ST7789_RDDISBV = 0x52; // Read Display Brightness Value +static const uint8_t ST7789_WRCTRLD = 0x53; // Write CTRL Display +static const uint8_t ST7789_RDCTRLD = 0x54; // Read CTRL Value Display +static const uint8_t ST7789_WRCACE = 0x55; // Write Content Adaptive Brightness Control and Color Enhancement +static const uint8_t ST7789_RDCABC = 0x56; // Read Content Adaptive Brightness Control +static const uint8_t ST7789_WRCABCMB = 0x5E; // Write CABC Minimum Brightnes +static const uint8_t ST7789_RDCABCMB = 0x5F; // Read CABC Minimum Brightnes +static const uint8_t ST7789_RDABCSDR = 0x68; // Read Automatic Brightness Control Self-Diagnostic Result +static const uint8_t ST7789_RDID1 = 0xDA; // Read ID1 +static const uint8_t ST7789_RDID2 = 0xDB; // Read ID2 +static const uint8_t ST7789_RDID3 = 0xDC; // Read ID3 +static const uint8_t ST7789_RAMCTRL = 0xB0; // RAM Control +static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Contro +static const uint8_t ST7789_PORCTRL = 0xB2; // Porch Setting +static const uint8_t ST7789_FRCTRL1 = 0xB3; // Frame Rate Control 1 (In partial mode/ idle colors) +static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Contro +static const uint8_t ST7789_GCTRL = 0xB7; // Gate Contro +static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustmen +static const uint8_t ST7789_DGMEN = 0xBA; // Digital Gamma Enable +static const uint8_t ST7789_VCOMS = 0xBB; // VCOMS Setting +static const uint8_t ST7789_LCMCTRL = 0xC0; // LCM Control +static const uint8_t ST7789_IDSET = 0xC1; // ID Code Settin +static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enabl +static const uint8_t ST7789_VRHS = 0xC3; // VRH Set +static const uint8_t ST7789_VDVS = 0xC4; // VDV Set +static const uint8_t ST7789_VCMOFSET = 0xC5; // VCOMS Offset Set +static const uint8_t ST7789_FRCTRL2 = 0xC6; // Frame Rate Control in Normal Mode +static const uint8_t ST7789_CABCCTRL = 0xC7; // CABC Control +static const uint8_t ST7789_REGSEL1 = 0xC8; // Register Value Selection 1 +static const uint8_t ST7789_REGSEL2 = 0xCA; // Register Value Selection +static const uint8_t ST7789_PWMFRSEL = 0xCC; // PWM Frequency Selection +static const uint8_t ST7789_PWCTRL1 = 0xD0; // Power Control 1 +static const uint8_t ST7789_VAPVANEN = 0xD2; // Enable VAP/VAN signal output +static const uint8_t ST7789_CMD2EN = 0xDF; // Command 2 Enable +static const uint8_t ST7789_PVGAMCTRL = 0xE0; // Positive Voltage Gamma Control +static const uint8_t ST7789_NVGAMCTRL = 0xE1; // Negative Voltage Gamma Control +static const uint8_t ST7789_DGMLUTR = 0xE2; // Digital Gamma Look-up Table for Red +static const uint8_t ST7789_DGMLUTB = 0xE3; // Digital Gamma Look-up Table for Blue +static const uint8_t ST7789_GATECTRL = 0xE4; // Gate Control +static const uint8_t ST7789_SPI2EN = 0xE7; // SPI2 Enable +static const uint8_t ST7789_PWCTRL2 = 0xE8; // Power Control 2 +static const uint8_t ST7789_EQCTRL = 0xE9; // Equalize time control +static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Contro +static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enabl +static const uint8_t ST7789_NVMSET = 0xFC; // NVM Setting +static const uint8_t ST7789_PROMACT = 0xFE; // Program action + +// Flags for ST7789_MADCTL +static const uint8_t ST7789_MADCTL_MY = 0x80; +static const uint8_t ST7789_MADCTL_MX = 0x40; +static const uint8_t ST7789_MADCTL_MV = 0x20; +static const uint8_t ST7789_MADCTL_ML = 0x10; +static const uint8_t ST7789_MADCTL_RGB = 0x00; +static const uint8_t ST7789_MADCTL_BGR = 0x08; +static const uint8_t ST7789_MADCTL_MH = 0x04; +static const uint8_t ST7789_MADCTL_SS = 0x02; +static const uint8_t ST7789_MADCTL_GS = 0x01; + +static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; + +class ST7789V : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void loop() override; + + void write_display_data(); + + protected: + GPIOPin *dc_pin_; + GPIOPin *reset_pin_{nullptr}; + GPIOPin *backlight_pin_{nullptr}; + + void init_reset_(); + void backlight_(bool onoff); + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_addr_(uint16_t addr1, uint16_t addr2); + void write_color_(uint16_t color, uint16_t size); + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + + void draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; +}; + +} // namespace st7789v +} // namespace esphome diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 292a7bf299..9c5d444b2c 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -3,11 +3,11 @@ import re import esphome.config_validation as cv from esphome import core +from esphome.const import CONF_SUBSTITUTIONS +CODEOWNERS = ['@esphome/core'] _LOGGER = logging.getLogger(__name__) -CONF_SUBSTITUTIONS = 'substitutions' - VALID_SUBSTITUTIONS_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' \ '0123456789_' @@ -101,11 +101,15 @@ def _substitute_item(substitutions, item, path): return None -def do_substitution_pass(config): - if CONF_SUBSTITUTIONS not in config: +def do_substitution_pass(config, command_line_substitutions): + if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return substitutions = config[CONF_SUBSTITUTIONS] + if substitutions is None: + substitutions = command_line_substitutions + elif command_line_substitutions: + substitutions = {**substitutions, **command_line_substitutions} with cv.prepend_path('substitutions'): if not isinstance(substitutions, dict): raise cv.Invalid("Substitutions must be a key to value mapping, got {}" diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index e4d2023a8e..a92442ea56 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.components import time from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID +CODEOWNERS = ['@OttoWinter'] sun_ns = cg.esphome_ns.namespace('sun') Sun = sun_ns.class_('Sun') diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 3870631e13..7378b2a140 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -7,6 +7,7 @@ from esphome.const import CONF_ICON, CONF_ID, CONF_INTERNAL, CONF_INVERTED, CONF CONF_ON_TURN_ON, CONF_TRIGGER_ID, CONF_MQTT_ID, CONF_NAME from esphome.core import CORE, coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True switch_ns = cg.esphome_ns.namespace('switch_') diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 90bdabf0f4..579daf4d24 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -73,6 +73,7 @@ template class SwitchPublishAction : public Action { public: SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) + void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 2806a1cac2..0d6ffbb9b8 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -144,7 +144,7 @@ void SX1509Component::clock_(byte osc_source, byte osc_pin_function, byte osc_fr osc_source = (osc_source & 0b11) << 5; // 2-bit value, bits 6:5 osc_pin_function = (osc_pin_function & 1) << 4; // 1-bit value bit 4 osc_freq_out = (osc_freq_out & 0b1111); // 4-bit value, bits 3:0 - byte reg_clock = osc_source | osc_pin_function | osc_freq_out; + uint8_t reg_clock = osc_source | osc_pin_function | osc_freq_out; this->write_byte(REG_CLOCK, reg_clock); osc_divider = constrain(osc_divider, 1, 7); diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 3c94f4a243..11ebdc7be8 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -4,6 +4,7 @@ from esphome.components import climate_ir from esphome.const import CONF_ID AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] tcl112_ns = cg.esphome_ns.namespace('tcl112') Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate_ir.ClimateIR) diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 887f282007..147f76af7d 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -120,7 +120,7 @@ void TemplateCover::set_has_position(bool has_position) { this->has_position_ = void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5868b30996..d9f95e203c 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -19,7 +19,7 @@ void TemplateSwitch::loop() { } void TemplateSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); + this->prev_trigger_->stop_action(); } if (state) { diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 496efb1cc3..6810d10b13 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -30,6 +30,7 @@ template class TextSensorPublishAction : public Action { public: TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/thermostat/__init__.py b/esphome/components/thermostat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py new file mode 100644 index 0000000000..c9cb81194c --- /dev/null +++ b/esphome/components/thermostat/climate.py @@ -0,0 +1,254 @@ +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_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, CONF_COOL_MODE, \ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, \ + CONF_DRY_MODE, CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, \ + CONF_FAN_MODE_LOW_ACTION, CONF_FAN_MODE_MEDIUM_ACTION, CONF_FAN_MODE_HIGH_ACTION, \ + CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, \ + CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE, CONF_HEAT_ACTION, CONF_HEAT_MODE, CONF_HYSTERESIS, \ + CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, CONF_SWING_BOTH_ACTION, \ + CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION + +thermostat_ns = cg.esphome_ns.namespace('thermostat') +ThermostatClimate = thermostat_ns.class_('ThermostatClimate', climate.Climate, cg.Component) +ThermostatClimateTargetTempConfig = thermostat_ns.struct('ThermostatClimateTargetTempConfig') + + +def validate_thermostat(config): + # verify corresponding climate action action exists for any defined climate mode action + if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE)) + if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE)) + if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_MODE)) + if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE)) + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and (CONF_COOL_ACTION in config + or CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and (CONF_COOL_ACTION not in config + and CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined in away configuration when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined in away configuration when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and (CONF_COOL_ACTION not in config and + CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined in away configuration with no {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined in away configuration with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + return config + + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ThermostatClimate), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_AUTO_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_LOW_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MEDIUM_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_HIGH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MIDDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_FOCUS_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_HORIZONTAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, + cv.Optional(CONF_AWAY_CONFIG): cv.Schema({ + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + }), +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_DRY_ACTION, + CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION), + validate_thermostat) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config + two_points_available = CONF_HEAT_ACTION in config and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + + if two_points_available is True: + cg.add(var.set_supports_two_points(True)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_normal_config(normal_config)) + + yield automation.build_automation(var.get_idle_action_trigger(), [], + config[CONF_IDLE_ACTION]) + + if auto_mode_available is True: + cg.add(var.set_supports_auto(True)) + else: + cg.add(var.set_supports_auto(False)) + + if CONF_COOL_ACTION in config: + yield automation.build_automation(var.get_cool_action_trigger(), [], + config[CONF_COOL_ACTION]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_ACTION in config: + yield automation.build_automation(var.get_dry_action_trigger(), [], + config[CONF_DRY_ACTION]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_ACTION in config: + yield automation.build_automation(var.get_fan_only_action_trigger(), [], + config[CONF_FAN_ONLY_ACTION]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_ACTION in config: + yield automation.build_automation(var.get_heat_action_trigger(), [], + config[CONF_HEAT_ACTION]) + cg.add(var.set_supports_heat(True)) + if CONF_AUTO_MODE in config: + yield automation.build_automation(var.get_auto_mode_trigger(), [], + config[CONF_AUTO_MODE]) + if CONF_COOL_MODE in config: + yield automation.build_automation(var.get_cool_mode_trigger(), [], + config[CONF_COOL_MODE]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_MODE in config: + yield automation.build_automation(var.get_dry_mode_trigger(), [], + config[CONF_DRY_MODE]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_MODE in config: + yield automation.build_automation(var.get_fan_only_mode_trigger(), [], + config[CONF_FAN_ONLY_MODE]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_MODE in config: + yield automation.build_automation(var.get_heat_mode_trigger(), [], + config[CONF_HEAT_MODE]) + cg.add(var.set_supports_heat(True)) + if CONF_OFF_MODE in config: + yield automation.build_automation(var.get_off_mode_trigger(), [], + config[CONF_OFF_MODE]) + if CONF_FAN_MODE_ON_ACTION in config: + yield automation.build_automation(var.get_fan_mode_on_trigger(), [], + config[CONF_FAN_MODE_ON_ACTION]) + cg.add(var.set_supports_fan_mode_on(True)) + if CONF_FAN_MODE_OFF_ACTION in config: + yield automation.build_automation(var.get_fan_mode_off_trigger(), [], + config[CONF_FAN_MODE_OFF_ACTION]) + cg.add(var.set_supports_fan_mode_off(True)) + if CONF_FAN_MODE_AUTO_ACTION in config: + yield automation.build_automation(var.get_fan_mode_auto_trigger(), [], + config[CONF_FAN_MODE_AUTO_ACTION]) + cg.add(var.set_supports_fan_mode_auto(True)) + if CONF_FAN_MODE_LOW_ACTION in config: + yield automation.build_automation(var.get_fan_mode_low_trigger(), [], + config[CONF_FAN_MODE_LOW_ACTION]) + cg.add(var.set_supports_fan_mode_low(True)) + if CONF_FAN_MODE_MEDIUM_ACTION in config: + yield automation.build_automation(var.get_fan_mode_medium_trigger(), [], + config[CONF_FAN_MODE_MEDIUM_ACTION]) + cg.add(var.set_supports_fan_mode_medium(True)) + if CONF_FAN_MODE_HIGH_ACTION in config: + yield automation.build_automation(var.get_fan_mode_high_trigger(), [], + config[CONF_FAN_MODE_HIGH_ACTION]) + cg.add(var.set_supports_fan_mode_high(True)) + if CONF_FAN_MODE_MIDDLE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_middle_trigger(), [], + config[CONF_FAN_MODE_MIDDLE_ACTION]) + cg.add(var.set_supports_fan_mode_middle(True)) + if CONF_FAN_MODE_FOCUS_ACTION in config: + yield automation.build_automation(var.get_fan_mode_focus_trigger(), [], + config[CONF_FAN_MODE_FOCUS_ACTION]) + cg.add(var.set_supports_fan_mode_focus(True)) + if CONF_FAN_MODE_DIFFUSE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_diffuse_trigger(), [], + config[CONF_FAN_MODE_DIFFUSE_ACTION]) + cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_SWING_BOTH_ACTION in config: + yield automation.build_automation(var.get_swing_mode_both_trigger(), [], + config[CONF_SWING_BOTH_ACTION]) + cg.add(var.set_supports_swing_mode_both(True)) + if CONF_SWING_HORIZONTAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_horizontal_trigger(), [], + config[CONF_SWING_HORIZONTAL_ACTION]) + cg.add(var.set_supports_swing_mode_horizontal(True)) + if CONF_SWING_OFF_ACTION in config: + yield automation.build_automation(var.get_swing_mode_off_trigger(), [], + config[CONF_SWING_OFF_ACTION]) + cg.add(var.set_supports_swing_mode_off(True)) + if CONF_SWING_VERTICAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_vertical_trigger(), [], + config[CONF_SWING_VERTICAL_ACTION]) + cg.add(var.set_supports_swing_mode_vertical(True)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + + if two_points_available is True: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_away_config(away_config)) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp new file mode 100644 index 0000000000..64a7c1b05d --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -0,0 +1,569 @@ +#include "thermostat_climate.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace thermostat { + +static const char *TAG = "thermostat.climate"; + +void ThermostatClimate::setup() { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // required action may have changed, recompute, refresh + this->switch_to_action_(compute_action_()); + // current temperature and possibly action changed, so publish the new state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + // restore all climate data, if possible + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } else { + // restore from defaults, change_away handles temps for us + this->mode = climate::CLIMATE_MODE_AUTO; + this->change_away_(false); + } + // refresh the climate action based on the restored settings + this->switch_to_action_(compute_action_()); + this->setup_complete_ = true; + this->publish_state(); +} +float ThermostatClimate::hysteresis() { return this->hysteresis_; } +void ThermostatClimate::refresh() { + this->switch_to_mode_(this->mode); + this->switch_to_action_(compute_action_()); + this->switch_to_fan_mode_(this->fan_mode); + this->switch_to_swing_mode_(this->swing_mode); + this->publish_state(); +} +void ThermostatClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + 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()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_away_(*call.get_away()); + } else { + this->away = *call.get_away(); + } + } + // set point validation + if (this->supports_two_points_) { + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + if (this->target_temperature_high < this->target_temperature_low) + this->target_temperature_high = this->target_temperature_low; + } else { + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } + // make any changes happen + refresh(); +} +climate::ClimateTraits ThermostatClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_auto_mode(this->supports_auto_); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); + traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); + traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); + traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); + traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); + traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); + traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); + traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); + traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); + traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); + traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); + traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); + traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); + traits.set_supports_two_point_target_temperature(this->supports_two_points_); + traits.set_supports_away(this->supports_away_); + traits.set_supports_action(true); + return traits; +} +climate::ClimateAction ThermostatClimate::compute_action_() { + climate::ClimateAction target_action = this->action; + if (this->supports_two_points_) { + if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || + isnan(this->target_temperature_high) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature_low - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } + } else { + if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_COOL: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + case climate::CLIMATE_MODE_HEAT: + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } + } + // do not switch to an action that isn't enabled per the active climate mode + if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) + target_action = climate::CLIMATE_ACTION_IDLE; + if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) + target_action = climate::CLIMATE_ACTION_IDLE; + + return target_action; +} +void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->action) && this->setup_complete_) + // already in target mode + return; + + if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || + (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && + this->setup_complete_) { + // switching from OFF to IDLE or vice-versa + // these only have visual difference. OFF means user manually disabled, + // IDLE means it's in auto mode but value is in target range. + this->action = action; + return; + } + + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop_action(); + this->prev_action_trigger_ = nullptr; + } + Trigger<> *trig = this->idle_action_trigger_; + switch (action) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + // trig = this->idle_action_trigger_; + break; + case climate::CLIMATE_ACTION_COOLING: + trig = this->cool_action_trigger_; + break; + case climate::CLIMATE_ACTION_HEATING: + trig = this->heat_action_trigger_; + break; + case climate::CLIMATE_ACTION_FAN: + trig = this->fan_only_action_trigger_; + break; + case climate::CLIMATE_ACTION_DRYING: + trig = this->dry_action_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + action = climate::CLIMATE_ACTION_OFF; + // trig = this->idle_action_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->action = action; + this->prev_action_trigger_ = trig; +} +void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop_action(); + this->prev_fan_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->fan_mode_auto_trigger_; + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + trig = this->fan_mode_on_trigger_; + break; + case climate::CLIMATE_FAN_OFF: + trig = this->fan_mode_off_trigger_; + break; + case climate::CLIMATE_FAN_AUTO: + // trig = this->fan_mode_auto_trigger_; + break; + case climate::CLIMATE_FAN_LOW: + trig = this->fan_mode_low_trigger_; + break; + case climate::CLIMATE_FAN_MEDIUM: + trig = this->fan_mode_medium_trigger_; + break; + case climate::CLIMATE_FAN_HIGH: + trig = this->fan_mode_high_trigger_; + break; + case climate::CLIMATE_FAN_MIDDLE: + trig = this->fan_mode_middle_trigger_; + break; + case climate::CLIMATE_FAN_FOCUS: + trig = this->fan_mode_focus_trigger_; + break; + case climate::CLIMATE_FAN_DIFFUSE: + trig = this->fan_mode_diffuse_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + fan_mode = climate::CLIMATE_FAN_AUTO; + // trig = this->fan_mode_auto_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->fan_mode = fan_mode; + this->prev_fan_mode_ = fan_mode; + this->prev_fan_mode_trigger_ = trig; +} +void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((mode == this->prev_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_mode_trigger_ != nullptr) { + this->prev_mode_trigger_->stop_action(); + this->prev_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->auto_mode_trigger_; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + trig = this->off_mode_trigger_; + break; + case climate::CLIMATE_MODE_AUTO: + // trig = this->auto_mode_trigger_; + break; + case climate::CLIMATE_MODE_COOL: + trig = this->cool_mode_trigger_; + break; + case climate::CLIMATE_MODE_HEAT: + trig = this->heat_mode_trigger_; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + trig = this->fan_only_mode_trigger_; + break; + case climate::CLIMATE_MODE_DRY: + trig = this->dry_mode_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + mode = climate::CLIMATE_MODE_AUTO; + // trig = this->auto_mode_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->mode = mode; + this->prev_mode_ = mode; + this->prev_mode_trigger_ = trig; +} +void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_swing_mode_trigger_ != nullptr) { + this->prev_swing_mode_trigger_->stop_action(); + this->prev_swing_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->swing_mode_off_trigger_; + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + trig = this->swing_mode_both_trigger_; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + trig = this->swing_mode_horizontal_trigger_; + break; + case climate::CLIMATE_SWING_OFF: + // trig = this->swing_mode_off_trigger_; + break; + case climate::CLIMATE_SWING_VERTICAL: + trig = this->swing_mode_vertical_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + swing_mode = climate::CLIMATE_SWING_OFF; + // trig = this->swing_mode_off_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->swing_mode = swing_mode; + this->prev_swing_mode_ = swing_mode; + this->prev_swing_mode_trigger_ = trig; +} +void ThermostatClimate::change_away_(bool away) { + if (!away) { + if (this->supports_two_points_) { + this->target_temperature_low = this->normal_config_.default_temperature_low; + this->target_temperature_high = this->normal_config_.default_temperature_high; + } else + this->target_temperature = this->normal_config_.default_temperature; + } else { + if (this->supports_two_points_) { + this->target_temperature_low = this->away_config_.default_temperature_low; + this->target_temperature_high = this->away_config_.default_temperature_high; + } else + this->target_temperature = this->away_config_.default_temperature; + } + this->away = away; +} +void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { + this->normal_config_ = normal_config; +} +void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { + this->supports_away_ = true; + this->away_config_ = away_config; +} +ThermostatClimate::ThermostatClimate() + : cool_action_trigger_(new Trigger<>()), + cool_mode_trigger_(new Trigger<>()), + dry_action_trigger_(new Trigger<>()), + dry_mode_trigger_(new Trigger<>()), + heat_action_trigger_(new Trigger<>()), + heat_mode_trigger_(new Trigger<>()), + auto_mode_trigger_(new Trigger<>()), + idle_action_trigger_(new Trigger<>()), + off_mode_trigger_(new Trigger<>()), + fan_only_action_trigger_(new Trigger<>()), + fan_only_mode_trigger_(new Trigger<>()), + fan_mode_on_trigger_(new Trigger<>()), + fan_mode_off_trigger_(new Trigger<>()), + fan_mode_auto_trigger_(new Trigger<>()), + fan_mode_low_trigger_(new Trigger<>()), + fan_mode_medium_trigger_(new Trigger<>()), + fan_mode_high_trigger_(new Trigger<>()), + fan_mode_middle_trigger_(new Trigger<>()), + fan_mode_focus_trigger_(new Trigger<>()), + fan_mode_diffuse_trigger_(new Trigger<>()), + swing_mode_both_trigger_(new Trigger<>()), + swing_mode_off_trigger_(new Trigger<>()), + swing_mode_horizontal_trigger_(new Trigger<>()), + swing_mode_vertical_trigger_(new Trigger<>()) {} +void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } +void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } +void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } +void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } +void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } +void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } +void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { + this->supports_fan_mode_on_ = supports_fan_mode_on; +} +void ThermostatClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void ThermostatClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void ThermostatClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void ThermostatClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void ThermostatClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void ThermostatClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void ThermostatClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void ThermostatClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void ThermostatClimate::set_supports_two_points(bool supports_two_points) { + this->supports_two_points_ = supports_two_points; +} +Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } +Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } +Trigger<> *ThermostatClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } +Trigger<> *ThermostatClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } +Trigger<> *ThermostatClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } +Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } +Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } +void ThermostatClimate::dump_config() { + LOG_CLIMATE("", "Thermostat", this); + if (this->supports_heat_) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + else + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + else + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); + } + ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); + ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE AUTO: %s", YESNO(this->supports_fan_mode_auto_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE LOW: %s", YESNO(this->supports_fan_mode_low_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MEDIUM: %s", YESNO(this->supports_fan_mode_medium_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE HIGH: %s", YESNO(this->supports_fan_mode_high_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); + ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); + ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); + if (this->supports_away_) { + if (this->supports_heat_) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", + this->away_config_.default_temperature_low); + else + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature); + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", + this->away_config_.default_temperature_high); + else + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature); + } + } +} + +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) + : default_temperature(default_temperature) {} +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature_low, + float default_temperature_high) + : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h new file mode 100644 index 0000000000..86a1007efa --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.h @@ -0,0 +1,271 @@ +#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 thermostat { + +struct ThermostatClimateTargetTempConfig { + public: + ThermostatClimateTargetTempConfig(); + ThermostatClimateTargetTempConfig(float default_temperature); + ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + + float default_temperature{NAN}; + float default_temperature_low{NAN}; + float default_temperature_high{NAN}; + float hysteresis{NAN}; +}; + +class ThermostatClimate : public climate::Climate, public Component { + public: + ThermostatClimate(); + void setup() override; + void dump_config() override; + + void set_hysteresis(float hysteresis); + void set_sensor(sensor::Sensor *sensor); + void set_supports_auto(bool supports_auto); + void set_supports_cool(bool supports_cool); + void set_supports_dry(bool supports_dry); + void set_supports_fan_only(bool supports_fan_only); + void set_supports_heat(bool supports_heat); + void set_supports_fan_mode_on(bool supports_fan_mode_on); + void set_supports_fan_mode_off(bool supports_fan_mode_off); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_two_points(bool supports_two_points); + + void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config); + void set_away_config(const ThermostatClimateTargetTempConfig &away_config); + + Trigger<> *get_cool_action_trigger() const; + Trigger<> *get_dry_action_trigger() const; + Trigger<> *get_fan_only_action_trigger() const; + Trigger<> *get_heat_action_trigger() const; + Trigger<> *get_idle_action_trigger() const; + Trigger<> *get_auto_mode_trigger() const; + Trigger<> *get_cool_mode_trigger() const; + Trigger<> *get_dry_mode_trigger() const; + Trigger<> *get_fan_only_mode_trigger() const; + Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_off_mode_trigger() const; + Trigger<> *get_fan_mode_on_trigger() const; + Trigger<> *get_fan_mode_off_trigger() const; + Trigger<> *get_fan_mode_auto_trigger() const; + Trigger<> *get_fan_mode_low_trigger() const; + Trigger<> *get_fan_mode_medium_trigger() const; + Trigger<> *get_fan_mode_high_trigger() const; + Trigger<> *get_fan_mode_middle_trigger() const; + Trigger<> *get_fan_mode_focus_trigger() const; + Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_swing_mode_both_trigger() const; + Trigger<> *get_swing_mode_horizontal_trigger() const; + Trigger<> *get_swing_mode_off_trigger() const; + Trigger<> *get_swing_mode_vertical_trigger() const; + /// Get current hysteresis value + float hysteresis(); + /// Call triggers based on updated climate states (modes/actions) + void refresh(); + + 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 required action of this climate controller. + climate::ClimateAction compute_action_(); + + /// Switch the climate device to the given climate action. + void switch_to_action_(climate::ClimateAction action); + + /// Switch the climate device to the given climate fan mode. + void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); + + /// Switch the climate device to the given climate mode. + void switch_to_mode_(climate::ClimateMode mode); + + /// Switch the climate device to the given climate swing mode. + void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_{nullptr}; + + /// Whether the controller supports auto/cooling/drying/fanning/heating. + /// + /// A false value for any given attribute means that the controller has no such action + /// (for example a thermostat, where only heating and not-heating is possible). + bool supports_auto_{false}; + bool supports_cool_{false}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + bool supports_heat_{false}; + + /// Whether the controller supports turning on or off just the fan. + /// + /// A false value for either attribute means that the controller has no fan on/off action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + + /// Whether the controller supports fan auto mode. + /// + /// A false value for this attribute means that the controller has no fan-auto action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_auto_{false}; + + /// Whether the controller supports various fan speeds and/or positions. + /// + /// A false value for any given attribute means that the controller has no such fan action. + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + + /// Whether the controller supports various swing modes. + /// + /// A false value for any given attribute means that the controller has no such swing mode. + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_horizontal_{false}; + bool supports_swing_mode_vertical_{false}; + + /// Whether the controller supports two set points + /// + /// A false value means that the controller has no such support. + bool supports_two_points_{false}; + + /// Whether the controller supports an "away" mode + /// + /// A false value means that the controller has no such mode. + bool supports_away_{false}; + + /// The trigger to call when the controller should switch to cooling action/mode. + /// + /// A null value for this attribute means that the controller has no cooling action + /// For example electric heat, where only heating (power on) and not-heating + /// (power off) is possible. + Trigger<> *cool_action_trigger_{nullptr}; + Trigger<> *cool_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to dry (dehumidification) mode. + /// + /// In dry mode, the controller is assumed to have both heating and cooling disabled, + /// although the system may use its cooling mechanism to achieve drying. + Trigger<> *dry_action_trigger_{nullptr}; + Trigger<> *dry_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to heating action/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_action_trigger_{nullptr}; + Trigger<> *heat_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to auto mode. + /// + /// In auto mode, the controller will enable heating/cooling as necessary and switch + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *auto_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to idle action/off mode. + /// + /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. + Trigger<> *idle_action_trigger_{nullptr}; + Trigger<> *off_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to fan-only action/mode. + /// + /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. + /// The system should activate the fan only. + Trigger<> *fan_only_action_trigger_{nullptr}; + Trigger<> *fan_only_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch on the fan. + Trigger<> *fan_mode_on_trigger_{nullptr}; + + /// The trigger to call when the controller should switch off the fan. + Trigger<> *fan_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "auto" mode. + Trigger<> *fan_mode_auto_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "low" speed. + Trigger<> *fan_mode_low_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "medium" speed. + Trigger<> *fan_mode_medium_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "high" speed. + Trigger<> *fan_mode_high_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "middle" position. + Trigger<> *fan_mode_middle_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "focus" position. + Trigger<> *fan_mode_focus_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "diffuse" position. + Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "both". + Trigger<> *swing_mode_both_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "off". + Trigger<> *swing_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "horizontal". + Trigger<> *swing_mode_horizontal_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "vertical". + Trigger<> *swing_mode_vertical_trigger_{nullptr}; + + /// A reference to the trigger that was previously active. + /// + /// This is so that the previous trigger can be stopped before enabling a new one + /// for each climate category (mode, action, fan_mode, swing_mode). + Trigger<> *prev_action_trigger_{nullptr}; + Trigger<> *prev_fan_mode_trigger_{nullptr}; + Trigger<> *prev_mode_trigger_{nullptr}; + Trigger<> *prev_swing_mode_trigger_{nullptr}; + + /// Store previously-known states + /// + /// These are used to determine when a trigger/action needs to be called + climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + + /// Temperature data for normal/home and away modes + ThermostatClimateTargetTempConfig normal_config_{}; + ThermostatClimateTargetTempConfig away_config_{}; + + /// Hysteresis value used for computing climate actions + float hysteresis_{0}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; +}; + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 6283392103..49ed53c47e 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -17,6 +17,7 @@ from esphome.core import coroutine, coroutine_with_priority _LOGGER = logging.getLogger(__name__) +CODEOWNERS = ['@OttoWinter'] IS_PLATFORM_COMPONENT = True time_ns = cg.esphome_ns.namespace('time') diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index cb66dc3ce6..cdcfcb14ad 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -4,6 +4,7 @@ #ifdef ARDUINO_ARCH_ESP8266 #include "sys/time.h" #endif +#include "errno.h" namespace esphome { namespace time { @@ -20,8 +21,18 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; + ESP_LOGVV(TAG, "Got epoch %u", epoch); timezone tz = {0, 0}; - settimeofday(&timev, &tz); + int ret = settimeofday(&timev, &tz); + if (ret == EINVAL) { + // Some ESP8266 frameworks abort when timezone parameter is not NULL + // while ESP32 expects it not to be NULL + ret = settimeofday(&timev, nullptr); + } + + if (ret != 0) { + ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); + } auto time = this->now(); char buf[128]; diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 6d1de144f5..1aa3c2471a 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -78,7 +78,7 @@ void TimeBasedCover::control(const CoverCall &call) { } void TimeBasedCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py index dcc4b82719..c2692e30de 100644 --- a/esphome/components/tm1637/display.py +++ b/esphome/components/tm1637/display.py @@ -4,6 +4,8 @@ from esphome import pins from esphome.components import display from esphome.const import CONF_CLK_PIN, CONF_DIO_PIN, CONF_ID, CONF_LAMBDA, CONF_INTENSITY +CODEOWNERS = ['@glmnet'] + tm1637_ns = cg.esphome_ns.namespace('tm1637') TM1637Display = tm1637_ns.class_('TM1637Display', cg.PollingComponent) TM1637DisplayRef = TM1637Display.operator('ref') diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index d9b9a870ff..833e3caecd 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -232,7 +232,7 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char* str) { uint8_t pos = start_pos; for (; *str != '\0'; str++) { uint8_t data = TM1637_UNKNOWN_CHAR; - if (*str >= ' ' && *str <= '}') + if (*str >= ' ' && *str <= '~') data = pgm_read_byte(&TM1637_ASCII_TO_RAW[*str - ' ']); if (data == TM1637_UNKNOWN_CHAR) { diff --git a/esphome/components/toshiba/__init__.py b/esphome/components/toshiba/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py new file mode 100644 index 0000000000..ea7efbf2f5 --- /dev/null +++ b/esphome/components/toshiba/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +toshiba_ns = cg.esphome_ns.namespace('toshiba') +ToshibaClimate = toshiba_ns.class_('ToshibaClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ToshibaClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp new file mode 100644 index 0000000000..33e2831dd3 --- /dev/null +++ b/esphome/components/toshiba/toshiba.cpp @@ -0,0 +1,204 @@ +#include "toshiba.h" + +namespace esphome { +namespace toshiba { + +const uint16_t TOSHIBA_HEADER_MARK = 4380; +const uint16_t TOSHIBA_HEADER_SPACE = 4370; +const uint16_t TOSHIBA_GAP_SPACE = 5480; +const uint16_t TOSHIBA_BIT_MARK = 540; +const uint16_t TOSHIBA_ZERO_SPACE = 540; +const uint16_t TOSHIBA_ONE_SPACE = 1620; + +const uint8_t TOSHIBA_COMMAND_DEFAULT = 0x01; +const uint8_t TOSHIBA_COMMAND_TIMER = 0x02; +const uint8_t TOSHIBA_COMMAND_POWER = 0x08; +const uint8_t TOSHIBA_COMMAND_MOTION = 0x02; + +const uint8_t TOSHIBA_MODE_AUTO = 0x00; +const uint8_t TOSHIBA_MODE_COOL = 0x01; +const uint8_t TOSHIBA_MODE_DRY = 0x02; +const uint8_t TOSHIBA_MODE_HEAT = 0x03; +const uint8_t TOSHIBA_MODE_FAN_ONLY = 0x04; +const uint8_t TOSHIBA_MODE_OFF = 0x07; + +const uint8_t TOSHIBA_FAN_SPEED_AUTO = 0x00; +const uint8_t TOSHIBA_FAN_SPEED_QUIET = 0x20; +const uint8_t TOSHIBA_FAN_SPEED_1 = 0x40; +const uint8_t TOSHIBA_FAN_SPEED_2 = 0x60; +const uint8_t TOSHIBA_FAN_SPEED_3 = 0x80; +const uint8_t TOSHIBA_FAN_SPEED_4 = 0xa0; +const uint8_t TOSHIBA_FAN_SPEED_5 = 0xc0; + +const uint8_t TOSHIBA_POWER_HIGH = 0x01; +const uint8_t TOSHIBA_POWER_ECO = 0x03; + +const uint8_t TOSHIBA_MOTION_SWING = 0x04; +const uint8_t TOSHIBA_MOTION_FIX = 0x00; + +static const char *TAG = "toshiba.climate"; + +void ToshibaClimate::transmit_state() { + uint8_t message[16] = {0}; + uint8_t message_length = 9; + + /* Header */ + message[0] = 0xf2; + message[1] = 0x0d; + + /* Message length */ + message[2] = message_length - 6; + + /* First checksum */ + message[3] = message[0] ^ message[1] ^ message[2]; + + /* Command */ + message[4] = TOSHIBA_COMMAND_DEFAULT; + + /* Temperature */ + uint8_t temperature = static_cast(this->target_temperature); + if (temperature < 17) { + temperature = 17; + } + if (temperature > 30) { + temperature = 30; + } + message[5] = (temperature - 17) << 4; + + /* Mode and fan */ + uint8_t mode; + switch (this->mode) { + case climate::CLIMATE_MODE_OFF: + mode = TOSHIBA_MODE_OFF; + break; + + case climate::CLIMATE_MODE_HEAT: + mode = TOSHIBA_MODE_HEAT; + break; + + case climate::CLIMATE_MODE_COOL: + mode = TOSHIBA_MODE_COOL; + break; + + case climate::CLIMATE_MODE_AUTO: + default: + mode = TOSHIBA_MODE_AUTO; + } + + message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO; + + /* Zero */ + message[7] = 0x00; + + /* If timers bit in the command is set, two extra bytes are added here */ + + /* If power bit is set in the command, one extra byte is added here */ + + /* The last byte is the xor of all bytes from [4] */ + for (uint8_t i = 4; i < 8; i++) { + message[8] ^= message[i]; + } + + /* Transmit */ + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(38000); + + for (uint8_t copy = 0; copy < 2; copy++) { + data->mark(TOSHIBA_HEADER_MARK); + data->space(TOSHIBA_HEADER_SPACE); + + for (uint8_t byte = 0; byte < message_length; byte++) { + for (uint8_t bit = 0; bit < 8; bit++) { + data->mark(TOSHIBA_BIT_MARK); + if (message[byte] & (1 << (7 - bit))) { + data->space(TOSHIBA_ONE_SPACE); + } else { + data->space(TOSHIBA_ZERO_SPACE); + } + } + } + + data->mark(TOSHIBA_BIT_MARK); + data->space(TOSHIBA_GAP_SPACE); + } + + transmit.perform(); +} + +bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t message[16] = {0}; + uint8_t message_length = 4; + + /* Validate header */ + if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { + return false; + } + + /* Decode bytes */ + for (uint8_t byte = 0; byte < message_length; byte++) { + for (uint8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) { + message[byte] |= 1 << (7 - bit); + } else if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) { + /* Bit is already clear */ + } else { + return false; + } + } + + /* Update length */ + if (byte == 3) { + /* Validate the first checksum before trusting the length field */ + if ((message[0] ^ message[1] ^ message[2]) != message[3]) { + return false; + } + message_length = message[2] + 6; + } + } + + /* Validate the second checksum before trusting any more of the message */ + uint8_t checksum = 0; + for (uint8_t i = 4; i < message_length - 1; i++) { + checksum ^= message[i]; + } + + if (checksum != message[message_length - 1]) { + return false; + } + + /* Check if this is a short swing/fix message */ + if (message[4] & TOSHIBA_COMMAND_MOTION) { + /* Not supported yet */ + return false; + } + + /* Get the mode. */ + switch (message[6] & 0x0f) { + case TOSHIBA_MODE_OFF: + this->mode = climate::CLIMATE_MODE_OFF; + break; + + case TOSHIBA_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + + case TOSHIBA_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + + case TOSHIBA_MODE_AUTO: + default: + /* Note: Dry and Fan-only modes are reported as Auto, as they are not supported yet */ + this->mode = climate::CLIMATE_MODE_AUTO; + } + + /* Get the target temperature */ + this->target_temperature = (message[5] >> 4) + 17; + + this->publish_state(); + return true; +} + +} /* namespace toshiba */ +} /* namespace esphome */ diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h new file mode 100644 index 0000000000..3ab0dcdcdb --- /dev/null +++ b/esphome/components/toshiba/toshiba.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace toshiba { + +const float TOSHIBA_TEMP_MIN = 17.0; +const float TOSHIBA_TEMP_MAX = 30.0; + +class ToshibaClimate : public climate_ir::ClimateIR { + public: + ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_TEMP_MIN, TOSHIBA_TEMP_MAX, 1.0f) {} + + protected: + void transmit_state() override; + bool on_receive(remote_base::RemoteReceiveData data) override; +}; + +} /* namespace toshiba */ +} /* namespace esphome */ diff --git a/esphome/components/tuya/binary_sensor/__init__.py b/esphome/components/tuya/binary_sensor/__init__.py new file mode 100644 index 0000000000..b63638b4cc --- /dev/null +++ b/esphome/components/tuya/binary_sensor/__init__.py @@ -0,0 +1,29 @@ +from esphome.components import binary_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] + +CONF_SENSOR_DATAPOINT = "sensor_datapoint" + +TuyaBinarySensor = tuya_ns.class_('TuyaBinarySensor', binary_sensor.BinarySensor, cg.Component) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaBinarySensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp new file mode 100644 index 0000000000..ad3d18efae --- /dev/null +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp @@ -0,0 +1,22 @@ +#include "esphome/core/log.h" +#include "tuya_binary_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.binary_sensor"; + +void TuyaBinarySensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported binary sensor is: %s", ONOFF(datapoint.value_bool)); + }); +} + +void TuyaBinarySensor::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Binary Sensor:"); + ESP_LOGCONFIG(TAG, " Binary Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h new file mode 100644 index 0000000000..1eeeb40477 --- /dev/null +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaBinarySensor : public binary_sensor::BinarySensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py new file mode 100644 index 0000000000..878c5aefe8 --- /dev/null +++ b/esphome/components/tuya/climate/__init__.py @@ -0,0 +1,42 @@ +from esphome.components import climate +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] + +CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" +CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" +# CONF_ECO_MODE_DATAPOINT = "eco_mode_datapoint" + +TuyaClimate = tuya_ns.class_('TuyaClimate', climate.Climate, cg.Component) + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaClimate), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t, + # cv.Optional(CONF_ECO_MODE_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( + CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_TARGET_TEMPERATURE_DATAPOINT in config: + cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT])) + if CONF_CURRENT_TEMPERATURE_DATAPOINT in config: + cg.add(var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT])) + # if CONF_ECO_MODE_DATAPOINT in config: + # cg.add(var.set_eco_mode_id(config[CONF_ECO_MODE_DATAPOINT])) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp new file mode 100644 index 0000000000..cfd5acccbd --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -0,0 +1,154 @@ +#include "esphome/core/log.h" +#include "tuya_climate.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.climate"; + +void TuyaClimate::setup() { + if (this->switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + if (datapoint.value_bool) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + }); + } + if (this->target_temperature_id_.has_value()) { + this->parent_->register_listener(*this->target_temperature_id_, [this](TuyaDatapoint datapoint) { + this->target_temperature = datapoint.value_int; + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported target temperature is: %d", datapoint.value_int); + }); + } + if (this->current_temperature_id_.has_value()) { + this->parent_->register_listener(*this->current_temperature_id_, [this](TuyaDatapoint datapoint) { + this->current_temperature = datapoint.value_int; + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported current temperature is: %d", datapoint.value_int); + }); + } + // if (this->eco_mode_id_.has_value()) { + // this->parent_->register_listener(*this->eco_mode_id_, [this](TuyaDatapoint datapoint) { + // this->eco_mode = datapoint.value_bool; + // this->compute_state_(); + // this->publish_state(); + // ESP_LOGD(TAG, "MCU reported eco mode of: %s", ONOFF(datapoint.value_bool)); + // }); + // } +} + +void TuyaClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) { + this->mode = *call.get_mode(); + + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = this->mode != climate::CLIMATE_MODE_OFF; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting switch: %s", ONOFF(datapoint.value_bool)); + } + 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_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + + TuyaDatapoint datapoint{}; + datapoint.id = *this->target_temperature_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = (int) this->target_temperature; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting target temperature: %d", datapoint.value_int); + } + // if (call.get_eco_mode().has_value()) { + // this->eco_mode = *call.get_eco_mode(); + + // TuyaDatapoint datapoint{}; + // datapoint.id = *this->eco_mode_id_; + // datapoint.type = TuyaDatapointType::BOOLEAN; + // datapoint.value_bool = this->eco_mode; + // this->parent_->set_datapoint_value(datapoint); + // ESP_LOGD(TAG, "Setting eco mode: %s", ONOFF(datapoint.value_bool)); + // } + + this->compute_state_(); + this->publish_state(); +} + +climate::ClimateTraits TuyaClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); + traits.set_supports_heat_mode(true); + // traits.set_supports_eco_mode(this->eco_mode_id_.has_value()); + traits.set_supports_action(true); + return traits; +} + +void TuyaClimate::dump_config() { + LOG_CLIMATE("", "Tuya Climate", this); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->target_temperature_id_.has_value()) + ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); + if (this->current_temperature_id_.has_value()) + ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + // if (this->eco_mode_id_.has_value()) + // ESP_LOGCONFIG(TAG, " Eco Mode has datapoint ID %u", *this->mode_id_); +} + +void TuyaClimate::compute_state_() { + if (isnan(this->current_temperature) || isnan(this->target_temperature)) { + // if any control parameters are nan, go to OFF action (not IDLE!) + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + const bool too_cold = this->current_temperature < this->target_temperature - 1; + const bool too_hot = this->current_temperature > this->target_temperature + 1; + const bool on_target = this->current_temperature == this->target_temperature; + + climate::ClimateAction target_action; + if (too_cold) { + // too cold -> show as heating if possible, else idle + if (this->traits().supports_mode(climate::CLIMATE_MODE_HEAT)) { + target_action = climate::CLIMATE_ACTION_HEATING; + } else { + target_action = climate::CLIMATE_ACTION_IDLE; + } + } else if (too_hot) { + // too hot -> show as cooling if possible, else idle + if (this->traits().supports_mode(climate::CLIMATE_MODE_COOL)) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else { + target_action = climate::CLIMATE_ACTION_IDLE; + } + } else if (on_target) { + target_action = climate::CLIMATE_ACTION_IDLE; + } else { + target_action = this->action; + } + this->switch_to_action_(target_action); +} + +void TuyaClimate::switch_to_action_(climate::ClimateAction action) { + // For now this just sets the current action but could include triggers later + this->action = action; +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h new file mode 100644 index 0000000000..a073b0996c --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/climate/climate.h" + +namespace esphome { +namespace tuya { + +class TuyaClimate : public climate::Climate, public Component { + public: + void setup() override; + void dump_config() override; + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_target_temperature_id(uint8_t target_temperature_id) { + this->target_temperature_id_ = target_temperature_id; + } + void set_current_temperature_id(uint8_t current_temperature_id) { + this->current_temperature_id_ = current_temperature_id; + } + // void set_eco_mode_id(uint8_t eco_mode_id) { this->eco_mode_id_ = eco_mode_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// 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_action_(climate::ClimateAction action); + + Tuya *parent_; + optional switch_id_{}; + optional target_temperature_id_{}; + optional current_temperature_id_{}; + // optional eco_mode_id_{}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index 8b4a0fa25f..e8492fd71b 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,13 +1,12 @@ from esphome.components import fan import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_OUTPUT_ID +from esphome.const import CONF_OUTPUT_ID, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] CONF_SPEED_DATAPOINT = "speed_datapoint" -CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint" TuyaFan = tuya_ns.class_('TuyaFan', cg.Component) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index b9fe2c0829..9850fa65ed 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -7,7 +7,7 @@ namespace tuya { static const char *TAG = "tuya.fan"; void TuyaFan::setup() { - auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value()); + auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false); this->fan_->set_traits(traits); if (this->speed_id_.has_value()) { diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index adaeb52531..d014f8a763 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -2,13 +2,12 @@ from esphome.components import light import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \ - CONF_DEFAULT_TRANSITION_LENGTH + CONF_DEFAULT_TRANSITION_LENGTH, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] CONF_DIMMER_DATAPOINT = "dimmer_datapoint" -CONF_SWITCH_DATAPOINT = "switch_datapoint" TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) diff --git a/esphome/components/tuya/sensor/__init__.py b/esphome/components/tuya/sensor/__init__.py new file mode 100644 index 0000000000..b3260bfe0b --- /dev/null +++ b/esphome/components/tuya/sensor/__init__.py @@ -0,0 +1,29 @@ +from esphome.components import sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] + +CONF_SENSOR_DATAPOINT = "sensor_datapoint" + +TuyaSensor = tuya_ns.class_('TuyaSensor', sensor.Sensor, cg.Component) + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaSensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/sensor/tuya_sensor.cpp b/esphome/components/tuya/sensor/tuya_sensor.cpp new file mode 100644 index 0000000000..b8e2aa97b7 --- /dev/null +++ b/esphome/components/tuya/sensor/tuya_sensor.cpp @@ -0,0 +1,33 @@ +#include "esphome/core/log.h" +#include "tuya_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.sensor"; + +void TuyaSensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) { + if (datapoint.type == TuyaDatapointType::BOOLEAN) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported sensor is: %s", ONOFF(datapoint.value_bool)); + } else if (datapoint.type == TuyaDatapointType::INTEGER) { + this->publish_state(datapoint.value_int); + ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_int); + } else if (datapoint.type == TuyaDatapointType::ENUM) { + this->publish_state(datapoint.value_enum); + ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_enum); + } else if (datapoint.type == TuyaDatapointType::BITMASK) { + this->publish_state(datapoint.value_bitmask); + ESP_LOGD(TAG, "MCU reported sensor is: %x", datapoint.value_bitmask); + } + }); +} + +void TuyaSensor::dump_config() { + LOG_SENSOR("", "Tuya Sensor", this); + ESP_LOGCONFIG(TAG, " Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/sensor/tuya_sensor.h b/esphome/components/tuya/sensor/tuya_sensor.h new file mode 100644 index 0000000000..8fd7cd1770 --- /dev/null +++ b/esphome/components/tuya/sensor/tuya_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaSensor : public sensor::Sensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/switch/__init__.py b/esphome/components/tuya/switch/__init__.py new file mode 100644 index 0000000000..f68bbbcdb6 --- /dev/null +++ b/esphome/components/tuya/switch/__init__.py @@ -0,0 +1,27 @@ +from esphome.components import switch +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] + +TuyaSwitch = tuya_ns.class_('TuyaSwitch', switch.Switch, cg.Component) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaSwitch), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SWITCH_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield switch.register_switch(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) diff --git a/esphome/components/tuya/switch/tuya_switch.cpp b/esphome/components/tuya/switch/tuya_switch.cpp new file mode 100644 index 0000000000..8f7c7f170d --- /dev/null +++ b/esphome/components/tuya/switch/tuya_switch.cpp @@ -0,0 +1,33 @@ +#include "esphome/core/log.h" +#include "tuya_switch.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.switch"; + +void TuyaSwitch::setup() { + this->parent_->register_listener(this->switch_id_, [this](TuyaDatapoint datapoint) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + }); +} + +void TuyaSwitch::write_state(bool state) { + TuyaDatapoint datapoint{}; + datapoint.id = this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = state; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting switch: %s", ONOFF(state)); + + this->publish_state(state); +} + +void TuyaSwitch::dump_config() { + LOG_SWITCH("", "Tuya Switch", this); + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", this->switch_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/switch/tuya_switch.h b/esphome/components/tuya/switch/tuya_switch.h new file mode 100644 index 0000000000..89e6264e5c --- /dev/null +++ b/esphome/components/tuya/switch/tuya_switch.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace tuya { + +class TuyaSwitch : public switch_::Switch, public Component { + public: + void setup() override; + void dump_config() override; + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + void write_state(bool state) override; + + Tuya *parent_; + uint8_t switch_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index b21df81d1e..644babbdec 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -22,8 +22,8 @@ void Tuya::loop() { void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); if (this->init_state_ != TuyaInitState::INIT_DONE) { - ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", // NOLINT - this->init_state_); + ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", + static_cast(this->init_state_)); ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device."); return; } diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 06208dc621..b83aedad9f 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,9 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation -from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, CONF_DATA +from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, \ + CONF_DATA, CONF_RX_BUFFER_SIZE from esphome.core import CORE, coroutine +CODEOWNERS = ['@esphome/core'] uart_ns = cg.esphome_ns.namespace('uart') UARTComponent = uart_ns.class_('UARTComponent', cg.Component) UARTDevice = uart_ns.class_('UARTDevice') @@ -44,6 +46,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.output_pin, cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum(UART_PARITY_OPTIONS, upper=True) @@ -61,6 +64,7 @@ def to_code(config): cg.add(var.set_tx_pin(config[CONF_TX_PIN])) if CONF_RX_PIN in config: cg.add(var.set_rx_pin(config[CONF_RX_PIN])) + cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index cf2d00c929..1cfdb38a90 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -43,7 +43,8 @@ void UARTComponent::check_logger_conflict_() { #endif } -void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, uint8_t nr_bits) { +void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, + uint8_t data_bits) { if (this->parent_->baud_rate_ != baud_rate) { ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, this->parent_->baud_rate_); @@ -52,9 +53,9 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UART ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits, this->parent_->stop_bits_); } - if (this->parent_->nr_bits_ != nr_bits) { - ESP_LOGE(TAG, " Invalid number of data bits: Integration requested %u data bits but you have %u!", nr_bits, - this->parent_->nr_bits_); + if (this->parent_->data_bits_ != data_bits) { + ESP_LOGE(TAG, " Invalid number of data bits: Integration requested %u data bits but you have %u!", data_bits, + this->parent_->data_bits_); } if (this->parent_->parity_ != parity) { ESP_LOGE(TAG, " Invalid parity: Integration requested parity %s but you have %s!", parity_to_str(parity), diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 5528da1d5f..7430a4ee05 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -18,8 +18,8 @@ const char *parity_to_str(UARTParityOptions parity); #ifdef ARDUINO_ARCH_ESP8266 class ESP8266SoftwareSerial { public: - void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t nr_bits, - UARTParityOptions parity); + void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, + UARTParityOptions parity, size_t rx_buffer_size); uint8_t read_byte(); uint8_t peek_byte(); @@ -30,8 +30,6 @@ class ESP8266SoftwareSerial { int available(); - void begin(); - void end(); GPIOPin *gpio_tx_pin_{nullptr}; GPIOPin *gpio_rx_pin_{nullptr}; @@ -44,11 +42,11 @@ class ESP8266SoftwareSerial { uint32_t bit_time_{0}; uint8_t *rx_buffer_{nullptr}; - size_t rx_buffer_size_{512}; + size_t rx_buffer_size_; volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; uint8_t stop_bits_; - uint8_t nr_bits_; + uint8_t data_bits_; UARTParityOptions parity_; ISRInternalGPIOPin *tx_pin_{nullptr}; ISRInternalGPIOPin *rx_pin_{nullptr}; @@ -71,8 +69,6 @@ class UARTComponent : public Component, public Stream { void write_array(const std::vector &data) { this->write_array(&data[0], data.size()); } void write_str(const char *str); - void end(); - void begin(); bool peek_byte(uint8_t *data); @@ -93,8 +89,9 @@ class UARTComponent : public Component, public Stream { void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; } void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } + void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } - void set_data_bits(uint8_t nr_bits) { this->nr_bits_ = nr_bits; } + void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } void set_parity(UARTParityOptions parity) { this->parity_ = parity; } protected: @@ -108,9 +105,10 @@ class UARTComponent : public Component, public Stream { #endif optional tx_pin_; optional rx_pin_; + size_t rx_buffer_size_; uint32_t baud_rate_; uint8_t stop_bits_; - uint8_t nr_bits_; + uint8_t data_bits_; UARTParityOptions parity_; }; @@ -154,12 +152,10 @@ class UARTDevice : public Stream { size_t write(uint8_t data) override { return this->parent_->write(data); } int read() override { return this->parent_->read(); } int peek() override { return this->parent_->peek(); } - void end() { this->parent_->end(); } - void begin() { this->parent_->begin(); } /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1, - UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t nr_bits = 8); + UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8); protected: UARTComponent *parent_{nullptr}; diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_esp32.cpp index cb6ac843d1..f7af85cf7b 100644 --- a/esphome/components/uart/uart_esp32.cpp +++ b/esphome/components/uart/uart_esp32.cpp @@ -43,7 +43,7 @@ uint32_t UARTComponent::get_config() { else if (this->parity_ == UART_CONFIG_PARITY_ODD) config |= UART_PARITY_ODD | UART_PARITY_EN; - switch (this->nr_bits_) { + switch (this->data_bits_) { case 5: config |= UART_NB_BIT_5; break; @@ -81,6 +81,7 @@ void UARTComponent::setup() { int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } void UARTComponent::dump_config() { @@ -90,9 +91,10 @@ void UARTComponent::dump_config() { } if (this->rx_pin_.has_value()) { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); + ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); - ESP_LOGCONFIG(TAG, " Bits: %u", this->nr_bits_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); ESP_LOGCONFIG(TAG, " Parity: %s", parity_to_str(this->parity_)); ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); this->check_logger_conflict_(); @@ -112,8 +114,6 @@ void UARTComponent::write_str(const char *str) { this->hw_serial_->write(str); ESP_LOGVV(TAG, " Wrote \"%s\"", str); } -void UARTComponent::end() { this->hw_serial_->end(); } -void UARTComponent::begin() { this->hw_serial_->begin(this->baud_rate_, get_config()); } bool UARTComponent::read_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; @@ -159,4 +159,4 @@ void UARTComponent::flush() { } // namespace uart } // namespace esphome -#endif // ESP32 +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp index 59a08677b5..edb530f537 100644 --- a/esphome/components/uart/uart_esp8266.cpp +++ b/esphome/components/uart/uart_esp8266.cpp @@ -19,7 +19,7 @@ uint32_t UARTComponent::get_config() { else if (this->parity_ == UART_CONFIG_PARITY_ODD) config |= UART_PARITY_ODD; - switch (this->nr_bits_) { + switch (this->data_bits_) { case 5: config |= UART_NB_BIT_5; break; @@ -52,18 +52,22 @@ void UARTComponent::setup() { if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->swap(); } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { this->hw_serial_ = &Serial1; this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } else { this->sw_serial_ = new ESP8266SoftwareSerial(); int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_, this->nr_bits_, this->parity_); + this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, + this->rx_buffer_size_); } } @@ -74,9 +78,10 @@ void UARTComponent::dump_config() { } if (this->rx_pin_.has_value()) { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); + ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); - ESP_LOGCONFIG(TAG, " Bits: %u", this->nr_bits_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); ESP_LOGCONFIG(TAG, " Parity: %s", parity_to_str(this->parity_)); ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); if (this->hw_serial_ != nullptr) { @@ -116,18 +121,6 @@ void UARTComponent::write_str(const char *str) { } ESP_LOGVV(TAG, " Wrote \"%s\"", str); } -void UARTComponent::end() { - if (this->hw_serial_ != nullptr) - this->hw_serial_->end(); - else if (this->sw_serial_ != nullptr) - this->sw_serial_->end(); -} -void UARTComponent::begin() { - if (this->hw_serial_ != nullptr) - this->hw_serial_->begin(this->baud_rate_, static_cast(get_config())); - else if (this->sw_serial_ != nullptr) - this->sw_serial_->begin(); -} bool UARTComponent::read_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; @@ -193,27 +186,12 @@ void UARTComponent::flush() { this->sw_serial_->flush(); } } -void ESP8266SoftwareSerial::end() { - /* Because of this bug: https://github.com/esp8266/Arduino/issues/6049 - * detach_interrupt can't called. - * So simply reset rx_in_pos and rx_out_pos even if it's totally racy with - * the interrupt. - */ - // this->gpio_rx_pin_->detach_interrupt(); - this->rx_in_pos_ = 0; - this->rx_out_pos_ = 0; -} -void ESP8266SoftwareSerial::begin() { - /* attach_interrupt() is also not safe because gpio_intr() may - * endup with arg == nullptr. - */ - // this->gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); -} -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t nr_bits, - UARTParityOptions parity) { +void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, + uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size) { this->bit_time_ = F_CPU / baud_rate; + this->rx_buffer_size_ = rx_buffer_size; this->stop_bits_ = stop_bits; - this->nr_bits_ = nr_bits; + this->data_bits_ = data_bits; this->parity_ = parity; if (tx_pin != -1) { auto pin = GPIOPin(tx_pin, OUTPUT); @@ -236,7 +214,7 @@ void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg const uint32_t start = ESP.getCycleCount(); uint8_t rec = 0; // Manually unroll the loop - for (int i = 0; i < arg->nr_bits_; i++) + for (int i = 0; i < arg->data_bits_; i++) rec |= arg->read_bit_(&wait, start) << i; /* If parity is enabled, just read it and ignore it. */ @@ -276,7 +254,7 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { const uint32_t start = ESP.getCycleCount(); // Start bit this->write_bit_(false, &wait, start); - for (int i = 0; i < this->nr_bits_; i++) { + for (int i = 0; i < this->data_bits_; i++) { bool bit = data & (1 << i); this->write_bit_(bit, &wait, start); if (need_parity_bit) @@ -327,4 +305,4 @@ int ESP8266SoftwareSerial::available() { } // namespace uart } // namespace esphome -#endif // ESP8266 +#endif // ARDUINO_ARCH_ESP8266 diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index f8130f7d1f..c573c21863 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -17,7 +17,8 @@ void UltrasonicSensorComponent::update() { delayMicroseconds(this->pulse_time_us_); this->trigger_pin_->digital_write(false); - uint32_t time = pulseIn(this->echo_pin_->get_pin(), uint8_t(!this->echo_pin_->is_inverted()), this->timeout_us_); + uint32_t time = pulseIn( // NOLINT + this->echo_pin_->get_pin(), uint8_t(!this->echo_pin_->is_inverted()), this->timeout_us_); ESP_LOGV(TAG, "Echo took %uµs", time); diff --git a/esphome/components/version/__init__.py b/esphome/components/version/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/version/__init__.py +++ b/esphome/components/version/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index 21044bb89f..01cf8ba30b 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -1,14 +1,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ID, CONF_ICON, ICON_NEW_BOX +from esphome.const import CONF_ID, CONF_ICON, ICON_NEW_BOX, CONF_HIDE_TIMESTAMP version_ns = cg.esphome_ns.namespace('version') VersionTextSensor = version_ns.class_('VersionTextSensor', text_sensor.TextSensor, cg.Component) CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(VersionTextSensor), - cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon + cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon, + cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean }).extend(cv.COMPONENT_SCHEMA) @@ -16,3 +17,4 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield text_sensor.register_text_sensor(var, config) yield cg.register_component(var, config) + cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP])) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 6aedfdedcd..fa8e6a9d01 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -8,9 +8,15 @@ namespace version { static const char *TAG = "version.text_sensor"; -void VersionTextSensor::setup() { this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); } +void VersionTextSensor::setup() { + if (this->hide_timestamp_) { + this->publish_state(ESPHOME_VERSION); + } else { + this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); + } +} float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } - +void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } std::string VersionTextSensor::unique_id() { return get_mac_address() + "-version"; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index cc798939ef..9355e78442 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -8,10 +8,14 @@ namespace version { class VersionTextSensor : public text_sensor::TextSensor, public Component { public: + void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; float get_setup_priority() const override; std::string unique_id() override; + + protected: + bool hide_timestamp_{false}; }; } // namespace version diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index 6740d53e13..209016fe40 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -10,15 +10,20 @@ VL53L0XSensor = vl53l0x_ns.class_('VL53L0XSensor', sensor.Sensor, cg.PollingComp i2c.I2CDevice) CONF_SIGNAL_RATE_LIMIT = 'signal_rate_limit' +CONF_LONG_RANGE = 'long_range' + CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({ cv.GenerateID(): cv.declare_id(VL53L0XSensor), cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( - min=0.0, max=512.0, min_included=False, max_included=False) + min=0.0, max=512.0, min_included=False, max_included=False), + cv.Optional(CONF_LONG_RANGE, default=False): cv.boolean, }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29)) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) + cg.add(var.set_signal_rate_limit(config[CONF_SIGNAL_RATE_LIMIT])) + cg.add(var.set_long_range(config[CONF_LONG_RANGE])) yield sensor.register_sensor(var, config) yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 231bed99ac..8ce822352f 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -33,7 +33,8 @@ void VL53L0XSensor::setup() { reg(0xFF) = 0x00; reg(0x80) = 0x00; reg(0x60) |= 0x12; - + if (this->long_range_) + this->signal_rate_limit_ = 0.1; auto rate_value = static_cast(signal_rate_limit_ * 128); write_byte_16(0x44, rate_value); @@ -104,7 +105,11 @@ void VL53L0XSensor::setup() { reg(0x48) = 0x00; reg(0x30) = 0x20; reg(0xFF) = 0x00; - reg(0x30) = 0x09; + if (this->long_range_) { + reg(0x30) = 0x07; // WAS 0x09 + } else { + reg(0x30) = 0x09; + } reg(0x54) = 0x00; reg(0x31) = 0x04; reg(0x32) = 0x03; @@ -116,7 +121,11 @@ void VL53L0XSensor::setup() { reg(0x51) = 0x00; reg(0x52) = 0x96; reg(0x56) = 0x08; - reg(0x57) = 0x30; + if (this->long_range_) { + reg(0x57) = 0x50; // was 0x30 + } else { + reg(0x57) = 0x30; + } reg(0x61) = 0x00; reg(0x62) = 0x00; reg(0x64) = 0x00; @@ -153,7 +162,11 @@ void VL53L0XSensor::setup() { reg(0x44) = 0x00; reg(0x45) = 0x20; reg(0x47) = 0x08; - reg(0x48) = 0x28; + if (this->long_range_) { + reg(0x48) = 0x48; // was 0x28 + } else { + reg(0x48) = 0x28; + } reg(0x67) = 0x00; reg(0x70) = 0x04; reg(0x71) = 0x01; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 1825383cee..4939a9806c 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -29,6 +29,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void loop() override; void set_signal_rate_limit(float signal_rate_limit) { signal_rate_limit_ = signal_rate_limit; } + void set_long_range(bool long_range) { long_range_ = long_range; } protected: uint32_t get_measurement_timing_budget_() { @@ -247,6 +248,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c } float signal_rate_limit_; + bool long_range_; uint32_t measurement_timing_budget_us_; bool initiated_read_{false}; bool waiting_for_interrupt_{false}; diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index a654e55981..da2e30de00 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -16,6 +16,7 @@ WaveshareEPaper2P9InB = waveshare_epaper_ns.class_('WaveshareEPaper2P9InB', Wave WaveshareEPaper4P2In = waveshare_epaper_ns.class_('WaveshareEPaper4P2In', WaveshareEPaper) WaveshareEPaper5P8In = waveshare_epaper_ns.class_('WaveshareEPaper5P8In', WaveshareEPaper) WaveshareEPaper7P5In = waveshare_epaper_ns.class_('WaveshareEPaper7P5In', WaveshareEPaper) +WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_('WaveshareEPaper7P5InV2', WaveshareEPaper) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeAModel') WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeBModel') @@ -31,6 +32,7 @@ MODELS = { '4.20in': ('b', WaveshareEPaper4P2In), '5.83in': ('b', WaveshareEPaper5P8In), '7.50in': ('b', WaveshareEPaper7P5In), + '7.50inv2': ('b', WaveshareEPaper7P5InV2), } @@ -50,7 +52,7 @@ CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()), validate_full_update_every_only_type_a, cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index c145fb361c..d57b814bb2 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -115,20 +115,20 @@ void WaveshareEPaper::update() { this->do_update_(); this->display(); } -void WaveshareEPaper::fill(int color) { +void WaveshareEPaper::fill(Color color) { // flip logic - const uint8_t fill = color ? 0x00 : 0xFF; + const uint8_t fill = color.is_on() ? 0x00 : 0xFF; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } -void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; const uint32_t pos = (x + y * this->get_width_internal()) / 8u; const uint8_t subpos = x & 0x07; // flip logic - if (!color) + if (!color.is_on()) this->buffer_[pos] |= 0x80 >> subpos; else this->buffer_[pos] &= ~(0x80 >> subpos); @@ -751,63 +751,51 @@ void WaveshareEPaper5P8In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } - void WaveshareEPaper7P5In::initialize() { // COMMAND POWER SETTING this->command(0x01); this->data(0x37); this->data(0x00); - // COMMAND PANEL SETTING this->command(0x00); this->data(0xCF); this->data(0x0B); - // COMMAND BOOSTER SOFT START this->command(0x06); this->data(0xC7); this->data(0xCC); this->data(0x28); - // COMMAND POWER ON this->command(0x04); this->wait_until_idle_(); delay(10); - // COMMAND PLL CONTROL this->command(0x30); this->data(0x3C); - // COMMAND TEMPERATURE SENSOR CALIBRATION this->command(0x41); this->data(0x00); - // COMMAND VCOM AND DATA INTERVAL SETTING this->command(0x50); this->data(0x77); - // COMMAND TCON SETTING this->command(0x60); this->data(0x22); - // COMMAND RESOLUTION SETTING this->command(0x61); this->data(0x02); this->data(0x80); this->data(0x01); this->data(0x80); - // COMMAND VCM DC SETTING REGISTER this->command(0x82); this->data(0x1E); - this->command(0xE5); this->data(0x03); } void HOT WaveshareEPaper7P5In::display() { // COMMAND DATA START TRANSMISSION 1 this->command(0x10); - this->start_data_(); for (size_t i = 0; i < this->get_buffer_length_(); i++) { uint8_t temp1 = this->buffer_[i]; @@ -817,7 +805,6 @@ void HOT WaveshareEPaper7P5In::display() { temp2 = 0x03; else temp2 = 0x00; - temp2 <<= 4; temp1 <<= 1; j++; @@ -828,11 +815,9 @@ void HOT WaveshareEPaper7P5In::display() { temp1 <<= 1; this->write_byte(temp2); } - App.feed_wdt(); } this->end_data_(); - // COMMAND DISPLAY REFRESH this->command(0x12); } @@ -846,6 +831,62 @@ void WaveshareEPaper7P5In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper7P5InV2::initialize() { + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x07); + this->data(0x07); + this->data(0x3f); + this->data(0x3f); + this->command(0x04); + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x1F); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x03); + this->data(0x20); + this->data(0x01); + this->data(0xE0); + // COMMAND ...? + this->command(0x15); + this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x07); + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); +} +void HOT WaveshareEPaper7P5InV2::display() { + uint32_t buf_len = this->get_buffer_length_(); + // COMMAND DATA START TRANSMISSION NEW DATA + this->command(0x13); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(~(this->buffer_[i])); + } + + // COMMAND DISPLAY REFRESH + this->command(0x12); + delay(100); // NOLINT + this->wait_until_idle_(); +} + +int WaveshareEPaper7P5InV2::get_width_internal() { return 800; } +int WaveshareEPaper7P5InV2::get_height_internal() { return 480; } +void WaveshareEPaper7P5InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5inV2"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 1ed7475350..01b162fd35 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -26,7 +26,7 @@ class WaveshareEPaper : public PollingComponent, void update() override; - void fill(int color) override; + void fill(Color color) override; void setup() override { this->setup_pins_(); @@ -36,7 +36,7 @@ class WaveshareEPaper : public PollingComponent, void on_safe_shutdown() override; protected: - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; bool wait_until_idle_(); @@ -105,6 +105,7 @@ enum WaveshareEPaperTypeBModel { WAVESHARE_EPAPER_2_7_IN = 0, WAVESHARE_EPAPER_4_2_IN, WAVESHARE_EPAPER_7_5_IN, + WAVESHARE_EPAPER_7_5_INV2, }; class WaveshareEPaper2P7In : public WaveshareEPaper { @@ -236,5 +237,28 @@ class WaveshareEPaper7P5In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P5InV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1f6cd10666..48a47080b2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -139,7 +139,7 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); std::string title = App.get_name() + " Web Server"; - stream->print(F("")); + stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><title>")); stream->print(title.c_str()); stream->print(F("")); #ifdef WEBSERVER_CSS_INCLUDE diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index d2faaf7162..05f4a4a4c6 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.const import CONF_ID from esphome.core import coroutine_with_priority, CORE +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['network'] AUTO_LOAD = ['async_tcp'] @@ -23,4 +24,4 @@ def to_code(config): if CORE.is_esp32: cg.add_library('FS', None) # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json - cg.add_library('ESPAsyncWebServer-esphome', '1.2.6') + cg.add_library('ESPAsyncWebServer-esphome', '1.2.7') diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py index 1083b86618..1fd62b411a 100644 --- a/esphome/components/whirlpool/climate.py +++ b/esphome/components/whirlpool/climate.py @@ -4,6 +4,7 @@ from esphome.components import climate_ir from esphome.const import CONF_ID, CONF_MODEL AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] whirlpool_ns = cg.esphome_ns.namespace('whirlpool') WhirlpoolClimate = whirlpool_ns.class_('WhirlpoolClimate', climate_ir.ClimateIR) diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 0956f816ce..d8db3b7353 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -41,11 +41,11 @@ void WhirlpoolClimate::transmit_state() { remote_state[18] = 0x08; auto powered_on = this->mode != climate::CLIMATE_MODE_OFF; - if (powered_on != this->powered_on_assumed_) { + if (powered_on != this->powered_on_assumed) { // Set power toggle command remote_state[2] = 4; remote_state[15] = 1; - this->powered_on_assumed_ = powered_on; + this->powered_on_assumed = powered_on; } switch (this->mode) { case climate::CLIMATE_MODE_AUTO: @@ -215,14 +215,14 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { if (powered_on) { this->mode = climate::CLIMATE_MODE_OFF; - this->powered_on_assumed_ = false; + this->powered_on_assumed = false; } else { - this->powered_on_assumed_ = true; + this->powered_on_assumed = true; } } // Set received mode - if (powered_on_assumed_) { + if (powered_on_assumed) { auto mode = remote_state[3] & 0x7; ESP_LOGV(TAG, "Mode: %02X", mode); switch (mode) { diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h index 44116b340c..7f31894df9 100644 --- a/esphome/components/whirlpool/whirlpool.h +++ b/esphome/components/whirlpool/whirlpool.h @@ -28,7 +28,7 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { void setup() override { climate_ir::ClimateIR::setup(); - this->powered_on_assumed_ = this->mode != climate::CLIMATE_MODE_OFF; + this->powered_on_assumed = this->mode != climate::CLIMATE_MODE_OFF; } /// Override control to change settings of the climate device. @@ -39,15 +39,15 @@ class WhirlpoolClimate : public climate_ir::ClimateIR { void set_model(Model model) { this->model_ = model; } + // used to track when to send the power toggle command + bool powered_on_assumed; + protected: /// Transmit via IR the state of this climate controller. void transmit_state() override; /// Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; - // used to track when to send the power toggle command - bool powered_on_assumed_; - bool send_swing_cmd_{false}; Model model_; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index d3c7e51603..7e7ab468ff 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,12 +5,16 @@ from esphome.automation import Condition from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ - CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY + CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY, CONF_IDENTITY, CONF_CERTIFICATE_AUTHORITY, \ + CONF_CERTIFICATE, CONF_KEY, CONF_USERNAME, CONF_EAP from esphome.core import CORE, HexInt, coroutine_with_priority +from . import wpa2_eap + AUTO_LOAD = ['network'] wifi_ns = cg.esphome_ns.namespace('wifi') +EAPAuth = wifi_ns.struct('EAPAuth') IPAddress = cg.global_ns.class_('IPAddress') ManualIP = wifi_ns.struct('ManualIP') WiFiComponent = wifi_ns.class_('WiFiComponent', cg.Component) @@ -56,6 +60,17 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, }) +EAP_AUTH_SCHEMA = cv.All(cv.only_on_esp32, cv.Schema({ + cv.Optional(CONF_IDENTITY): cv.string_strict, + cv.Optional(CONF_USERNAME): cv.string_strict, + cv.Optional(CONF_PASSWORD): cv.string_strict, + cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate, + cv.Inclusive(CONF_CERTIFICATE, 'certificate_and_key'): wpa2_eap.validate_certificate, + # Only validate as file first because we need the password to load it + # Actual validation happens in validate_eap. + cv.Inclusive(CONF_KEY, 'certificate_and_key'): cv.file_, +}), wpa2_eap.validate_eap, cv.has_at_least_one_key(CONF_IDENTITY, CONF_CERTIFICATE)) + WIFI_NETWORK_BASE = cv.Schema({ cv.GenerateID(): cv.declare_id(WiFiAP), cv.Optional(CONF_SSID): cv.ssid, @@ -73,6 +88,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, + cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, }) @@ -81,9 +97,13 @@ def validate(config): raise cv.Invalid("Cannot have WiFi password without SSID!") if CONF_SSID in config: + # Automatically move single network to 'networks' section + config = config.copy() network = {CONF_SSID: config.pop(CONF_SSID)} if CONF_PASSWORD in config: network[CONF_PASSWORD] = config.pop(CONF_PASSWORD) + if CONF_EAP in config: + network[CONF_EAP] = config.pop(CONF_EAP) if CONF_NETWORKS in config: raise cv.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please " "copy your network into the 'networks:' key") @@ -118,6 +138,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): WIFI_NETWORK_AP, cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, @@ -133,6 +154,29 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ }), validate) +def eap_auth(config): + if config is None: + return None + ca_cert = "" + if CONF_CERTIFICATE_AUTHORITY in config: + ca_cert = wpa2_eap.read_relative_config_path(config[CONF_CERTIFICATE_AUTHORITY]) + client_cert = "" + if CONF_CERTIFICATE in config: + client_cert = wpa2_eap.read_relative_config_path(config[CONF_CERTIFICATE]) + key = "" + if CONF_KEY in config: + key = wpa2_eap.read_relative_config_path(config[CONF_KEY]) + return cg.StructInitializer( + EAPAuth, + ('identity', config.get(CONF_IDENTITY, "")), + ('username', config.get(CONF_USERNAME, "")), + ('password', config.get(CONF_PASSWORD, "")), + ('ca_cert', ca_cert), + ('client_cert', client_cert), + ('client_key', key), + ) + + def safe_ip(ip): if ip is None: return IPAddress(0, 0, 0, 0) @@ -158,6 +202,9 @@ def wifi_network(config, static_ip): cg.add(ap.set_ssid(config[CONF_SSID])) if CONF_PASSWORD in config: cg.add(ap.set_password(config[CONF_PASSWORD])) + if CONF_EAP in config: + cg.add(ap.set_eap(eap_auth(config[CONF_EAP]))) + cg.add_define('ESPHOME_WIFI_WPA2_EAP') if CONF_BSSID in config: cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) if CONF_HIDDEN in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 40f12a8adc..df80c5b109 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -98,6 +98,7 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_STA_CONNECTED: { if (!this->is_connected()) { ESP_LOGW(TAG, "WiFi Connection lost... Reconnecting..."); + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); } else { this->status_clear_warning(); @@ -200,7 +201,26 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } else { ESP_LOGV(TAG, " BSSID: Not Set"); } - ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); + +#ifdef ESPHOME_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:"); + EAPAuth eap_config = ap.get_eap().value(); + ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); + ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); + bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); + bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); + bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key); + ESP_LOGV(TAG, " CA Cert: %s", ca_cert_present ? "present" : "not present"); + ESP_LOGV(TAG, " Client Cert: %s", client_cert_present ? "present" : "not present"); + ESP_LOGV(TAG, " Client Key: %s", client_key_present ? "present" : "not present"); + } else { +#endif + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); +#ifdef ESPHOME_WIFI_WPA2_EAP + } +#endif if (ap.get_channel().has_value()) { ESP_LOGV(TAG, " Channel: %u", *ap.get_channel()); } else { @@ -399,9 +419,17 @@ void WiFiComponent::check_scanning_finished() { connect_params.set_channel(scan_res.get_channel()); connect_params.set_bssid(scan_res.get_bssid()); } - // set manual IP+password (if any) + // copy manual IP (if set) connect_params.set_manual_ip(config.get_manual_ip()); + +#ifdef ESPHOME_WIFI_WPA2_EAP + // copy EAP parameters (if set) + connect_params.set_eap(config.get_eap()); +#endif + + // copy password (if set) connect_params.set_password(config.get_password()); + break; } @@ -540,12 +568,18 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } +#ifdef ESPHOME_WIFI_WPA2_EAP +void WiFiAP::set_eap(optional eap_auth) { this->eap_ = eap_auth; } +#endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } const std::string &WiFiAP::get_password() const { return this->password_; } +#ifdef ESPHOME_WIFI_WPA2_EAP +const optional &WiFiAP::get_eap() const { return this->eap_; } +#endif const optional &WiFiAP::get_channel() const { return this->channel_; } const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } bool WiFiAP::get_hidden() const { return this->hidden_; } @@ -569,9 +603,21 @@ bool WiFiScanResult::matches(const WiFiAP &config) { // If BSSID configured, only match for correct BSSIDs if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_) return false; - // If PW given, only match for networks with auth (and vice versa) + +#ifdef ESPHOME_WIFI_WPA2_EAP + // BSSID requires auth but no PSK or EAP credentials given + if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value())) + return false; + + // BSSID does not require auth, but PSK or EAP credentials given + if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value())) + return false; +#else + // If PSK given, only match for networks with auth (and vice versa) if (config.get_password().empty() == this->with_auth_) return false; +#endif + // If channel configured, only match networks on that channel. if (config.get_channel().has_value() && *config.get_channel() != this->channel_) { return false; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d04e1c2ce0..536d914a36 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -57,6 +57,18 @@ struct ManualIP { IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; +#ifdef ESPHOME_WIFI_WPA2_EAP +struct EAPAuth { + std::string identity; // required for all auth types + std::string username; + std::string password; + const char *ca_cert; // optionally verify authentication server + // used for EAP-TLS + const char *client_cert; + const char *client_key; +}; +#endif // ESPHOME_WIFI_WPA2_EAP + using bssid_t = std::array; class WiFiAP { @@ -65,6 +77,9 @@ class WiFiAP { void set_bssid(bssid_t bssid); void set_bssid(optional bssid); void set_password(const std::string &password); +#ifdef ESPHOME_WIFI_WPA2_EAP + void set_eap(optional eap_auth); +#endif // ESPHOME_WIFI_WPA2_EAP void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); @@ -72,6 +87,9 @@ class WiFiAP { const std::string &get_ssid() const; const optional &get_bssid() const; const std::string &get_password() const; +#ifdef ESPHOME_WIFI_WPA2_EAP + const optional &get_eap() const; +#endif // ESPHOME_WIFI_WPA2_EAP const optional &get_channel() const; float get_priority() const { return priority_; } const optional &get_manual_ip() const; @@ -81,6 +99,9 @@ class WiFiAP { std::string ssid_; optional bssid_; std::string password_; +#ifdef ESPHOME_WIFI_WPA2_EAP + optional eap_; +#endif // ESPHOME_WIFI_WPA2_EAP optional channel_; float priority_{0}; optional manual_ip_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index e345ab1671..09b8433a0e 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -6,6 +6,9 @@ #include #include +#ifdef ESPHOME_WIFI_WPA2_EAP +#include +#endif #include "lwip/err.h" #include "lwip/dns.h" @@ -187,6 +190,53 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { return false; } + // setup enterprise authentication if required +#ifdef ESPHOME_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. + EAPAuth eap = ap.get_eap().value(); + err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err); + } + int ca_cert_len = strlen(eap.ca_cert); + int client_cert_len = strlen(eap.client_cert); + int client_key_len = strlen(eap.client_key); + if (ca_cert_len) { + err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err); + } + } + // workout what type of EAP this is + // validation is not required as the config tool has already validated it + if (client_cert_len && client_key_len) { + // if we have certs, this must be EAP-TLS + err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, + (uint8_t *) eap.client_key, client_key_len + 1, + (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err); + } + } else { + // in the absence of certs, assume this is username/password based + err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err); + } + err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); + } + } + esp_wpa2_config_t wpa2_config = WPA2_CONFIG_INIT_DEFAULT(); + err = esp_wifi_sta_wpa2_ent_enable(&wpa2_config); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); + } + } +#endif // ESPHOME_WIFI_WPA2_EAP + this->wifi_apply_hostname_(); err = esp_wifi_connect(); @@ -341,6 +391,18 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i auto it = info.auth_change; ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); + // Mitigate CVE-2020-12638 + // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors + if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { + ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting..."); + // we can't call retry_connect() from this context, so disconnect immediately + // and notify main thread with error_from_callback_ + err_t err = esp_wifi_disconnect(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err)); + } + this->error_from_callback_ = true; + } break; } case SYSTEM_EVENT_STA_GOT_IP: { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index deee578b4c..efffff0abc 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -220,6 +220,7 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { + // Only allow auth modes with at least WPA conf.threshold.authmode = AUTH_WPA_PSK; } conf.threshold.rssi = -127; @@ -399,6 +400,15 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { auto it = event->event_info.auth_change; ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); + // Mitigate CVE-2020-12638 + // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors + if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) { + ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting..."); + // we can't call retry_connect() from this context, so disconnect immediately + // and notify main thread with error_from_callback_ + wifi_station_disconnect(); + global_wifi_component->error_from_callback_ = true; + } break; } case EVENT_STAMODE_GOT_IP: { diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py new file mode 100644 index 0000000000..8bf50598b0 --- /dev/null +++ b/esphome/components/wifi/wpa2_eap.py @@ -0,0 +1,136 @@ +"""Module for all WPA2 Utilities. + +The cryptography package is loaded lazily in the functions +so that it doesn't crash if it's not installed. +""" +import logging +from pathlib import Path + +from esphome.core import CORE +import esphome.config_validation as cv +from esphome.const import CONF_USERNAME, CONF_IDENTITY, CONF_PASSWORD, CONF_CERTIFICATE, \ + CONF_KEY + + +_LOGGER = logging.getLogger(__name__) + + +def validate_cryptography_installed(): + try: + import cryptography + except ImportError: + raise cv.Invalid("This settings requires the cryptography python package. " + "Please install it with `pip install cryptography`") + + if cryptography.__version__[0] < '2': + raise cv.Invalid("Please update your python cryptography installation to least 2.x " + "(pip install -U cryptography)") + + +def wrapped_load_pem_x509_certificate(value): + validate_cryptography_installed() + + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + + return x509.load_pem_x509_certificate(value.encode('UTF-8'), default_backend()) + + +def wrapped_load_pem_private_key(value, password): + validate_cryptography_installed() + + from cryptography.hazmat.primitives.serialization import load_pem_private_key + from cryptography.hazmat.backends import default_backend + + if password: + password = password.encode("UTF-8") + return load_pem_private_key(value.encode('UTF-8'), password, default_backend()) + + +def read_relative_config_path(value): + return Path(CORE.relative_config_path(value)).read_text() + + +def _validate_load_certificate(value): + value = cv.file_(value) + try: + contents = read_relative_config_path(value) + return wrapped_load_pem_x509_certificate(contents) + except ValueError as err: + raise cv.Invalid(f"Invalid certificate: {err}") + + +def validate_certificate(value): + _validate_load_certificate(value) + # Validation result should be the path, not the loaded certificate + return value + + +def _validate_load_private_key(key, cert_pw): + key = cv.file_(key) + try: + contents = read_relative_config_path(key) + return wrapped_load_pem_private_key(contents, cert_pw) + except ValueError as e: + raise cv.Invalid(f"There was an error with the EAP 'password:' provided for 'key' {e}") + except TypeError as e: + raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}") + + +def _check_private_key_cert_match(key, cert): + from cryptography.hazmat.primitives.asymmetric import rsa, ec + + def check_match_a(): + return key.public_key().public_numbers() == cert.public_key().public_numbers() + + def check_match_b(): + return key.public_key() == cert + + private_key_types = { + rsa.RSAPrivateKey: check_match_a, + ec.EllipticCurvePrivateKey: check_match_a, + } + + try: + # pylint: disable=no-name-in-module + from cryptography.hazmat.primitives.asymmetric import ed448, ed25519 + + private_key_types.update({ + ed448.Ed448PrivateKey: check_match_b, + ed25519.Ed25519PrivateKey: check_match_b, + }) + except ImportError: + # ed448, ed25519 not supported + pass + + key_type = next((kt for kt in private_key_types if isinstance(key, kt)), None) + if key_type is None: + _LOGGER.warning( + "Unrecognised EAP 'certificate:' 'key:' pair format: %s. Proceed with caution!", + type(key) + ) + elif not private_key_types[key_type](): + raise cv.Invalid("The provided EAP 'key' is not valid for the 'certificate'.") + + +def validate_eap(value): + if CONF_USERNAME in value: + if CONF_IDENTITY not in value: + _LOGGER.info("EAP 'identity:' is not set, assuming username.") + value = value.copy() + value[CONF_IDENTITY] = value[CONF_USERNAME] + if CONF_PASSWORD not in value: + raise cv.Invalid("You cannot use the EAP 'username:' option without a 'password:'. " + "Please provide the 'password:' key") + + if CONF_CERTIFICATE in value or CONF_KEY in value: + # Check the key is valid and for this certificate, just to check the user hasn't pasted + # the wrong thing. I write this after I spent a while debugging that exact issue. + # This may require a password to decrypt to key, so we should verify that at the same time. + cert_pw = value.get(CONF_PASSWORD) + key = _validate_load_private_key(value[CONF_KEY], cert_pw) + + cert = _validate_load_certificate(value[CONF_CERTIFICATE]) + _check_private_key_cert_match(key, cert) + + return value diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py new file mode 100644 index 0000000000..1a248e530f --- /dev/null +++ b/esphome/components/wled/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_PORT + +wled_ns = cg.esphome_ns.namespace('wled') +WLEDLightEffect = wled_ns.class_('WLEDLightEffect', AddressableLightEffect) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect('wled', WLEDLightEffect, "WLED", { + cv.Optional(CONF_PORT, default=21324): cv.port, +}) +def wled_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_port(config[CONF_PORT])) + + yield effect diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp new file mode 100644 index 0000000000..e06a23aacf --- /dev/null +++ b/esphome/components/wled/wled_light_effect.cpp @@ -0,0 +1,239 @@ +#include "wled_light_effect.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace wled { + +// Description of protocols: +// https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control +enum Protocol { WLED_NOTIFIER = 0, WARLS = 1, DRGB = 2, DRGBW = 3, DNRGB = 4 }; + +const int DEFAULT_BLANK_TIME = 1000; + +static const char *TAG = "wled_light_effect"; + +WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void WLEDLightEffect::start() { + AddressableLightEffect::start(); + + blank_at_ = 0; +} + +void WLEDLightEffect::stop() { + AddressableLightEffect::stop(); + + if (udp_) { + udp_->stop(); + udp_.reset(); + } +} + +void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(light::ESPColor::BLACK); + } +} + +void WLEDLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + // Init UDP lazily + if (!udp_) { + udp_.reset(new WiFiUDP()); + + if (!udp_->begin(port_)) { + ESP_LOGW(TAG, "Cannot bind WLEDLightEffect to %d.", port_); + return; + } + } + + std::vector payload; + while (uint16_t packet_size = udp_->parsePacket()) { + payload.resize(packet_size); + + if (!udp_->read(&payload[0], payload.size())) { + continue; + } + + if (!this->parse_frame_(it, &payload[0], payload.size())) { + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=0x%02X).", payload.size(), payload[0]); + continue; + } + } + + // FIXME: Use roll-over safe arithmetic + if (blank_at_ < millis()) { + blank_all_leds_(it); + blank_at_ = millis() + DEFAULT_BLANK_TIME; + } +} + +bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // At minimum frame needs to have: + // 1b - protocol + // 1b - timeout + if (size < 2) { + return false; + } + + uint8_t protocol = payload[0]; + uint8_t timeout = payload[1]; + + payload += 2; + size -= 2; + + switch (protocol) { + case WLED_NOTIFIER: + if (!parse_notifier_frame_(it, payload, size)) + return false; + break; + + case WARLS: + if (!parse_warls_frame_(it, payload, size)) + return false; + break; + + case DRGB: + if (!parse_drgb_frame_(it, payload, size)) + return false; + break; + + case DRGBW: + if (!parse_drgbw_frame_(it, payload, size)) + return false; + break; + + case DNRGB: + if (!parse_dnrgb_frame_(it, payload, size)) + return false; + break; + + default: + return false; + } + + if (timeout == UINT8_MAX) { + blank_at_ = UINT32_MAX; + } else if (timeout > 0) { + blank_at_ = millis() + timeout * 1000; + } else { + blank_at_ = millis() + DEFAULT_BLANK_TIME; + } + + return true; +} + +bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // Packet needs to be empty + return size == 0; +} + +bool WLEDLightEffect::parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: index, r, g, b + if ((size % 4) != 0) { + return false; + } + + auto count = size / 4; + auto max_leds = it.size(); + + for (; count > 0; count--, payload += 4) { + uint8_t led = payload[0]; + uint8_t r = payload[1]; + uint8_t g = payload[2]; + uint8_t b = payload[3]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: r, g, b + if ((size % 3) != 0) { + return false; + } + + auto count = size / 3; + auto max_leds = it.size(); + + for (uint16_t led = 0; led < count; ++led, payload += 3) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: r, g, b, w + if ((size % 4) != 0) { + return false; + } + + auto count = size / 4; + auto max_leds = it.size(); + + for (uint16_t led = 0; led < count; ++led, payload += 4) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + uint8_t w = payload[3]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b, w)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // offset: high, low + if (size < 2) { + return false; + } + + uint16_t led = (uint16_t(payload[0]) << 8) + payload[1]; + payload += 2; + size -= 2; + + // packet: r, g, b + if ((size % 3) != 0) { + return false; + } + + auto count = size / 3; + auto max_leds = it.size(); + + for (; count > 0; count--, payload += 3, led++) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +} // namespace wled +} // namespace esphome diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h new file mode 100644 index 0000000000..f1d27b06c7 --- /dev/null +++ b/esphome/components/wled/wled_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" + +#include +#include + +class UDP; + +namespace esphome { +namespace wled { + +class WLEDLightEffect : public light::AddressableLightEffect { + public: + WLEDLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + void set_port(uint16_t port) { this->port_ = port; } + + protected: + void blank_all_leds_(light::AddressableLight &it); + bool parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + + protected: + uint16_t port_{0}; + std::unique_ptr udp_; + uint32_t blank_at_{0}; + uint32_t dropped_{0}; +}; + +} // namespace wled +} // namespace esphome diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 030ee73d4b..2ca27bf51a 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -1,187 +1,322 @@ #include "xiaomi_ble.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #ifdef ARDUINO_ARCH_ESP32 +#include +#include "mbedtls/ccm.h" + namespace esphome { namespace xiaomi_ble { static const char *TAG = "xiaomi_ble"; -bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result) { - switch (data_type) { - case 0x0D: { // temperature+humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % - if (data_length != 4) - return false; - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); - result.temperature = temperature / 10.0f; - result.humidity = humidity / 10.0f; - return true; - } - case 0x0A: { // battery, 1 byte, 8-bit unsigned integer, 1 % - if (data_length != 1) - return false; - result.battery_level = data[0]; - return true; - } - case 0x06: { // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % - if (data_length != 2) - return false; - const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.humidity = humidity / 10.0f; - return true; - } - case 0x04: { // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C - if (data_length != 2) - return false; - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.temperature = temperature / 10.0f; - return true; - } - case 0x09: { // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm - if (data_length != 2) - return false; - const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.conductivity = conductivity; - return true; - } - case 0x07: { // illuminance, 3 bytes, 24-bit unsigned integer (LE), 1 lx - if (data_length != 3) - return false; - const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); - result.illuminance = illuminance; - return true; - } - case 0x08: { // soil moisture, 1 byte, 8-bit unsigned integer, 1 % - if (data_length != 1) - return false; - result.moisture = data[0]; - return true; - } - default: - return false; - } -} -bool parse_xiaomi_service_data(XiaomiParseResult &result, const esp32_ble_tracker::ServiceData &service_data) { - if (!service_data.uuid.contains(0x95, 0xFE)) { - // ESP_LOGVV(TAG, "Xiaomi no service data UUID magic bytes"); +bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) { + result.has_encryption = (message[0] & 0x08) ? true : false; // update encryption status + if (result.has_encryption) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): payload is encrypted, stop reading message."); return false; } - const auto raw = service_data.data; - - if (raw.size() < 14) { - // ESP_LOGVV(TAG, "Xiaomi service data too short!"); - return false; - } - - bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; - bool is_hhccjcy01 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; - bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04; - bool is_cgg1 = ((raw[1] & 0x30) == 0x30 || (raw[1] & 0x20) == 0x20) && raw[2] == 0x47 && raw[3] == 0x03; - - if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) { - // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); - return false; - } - - result.type = XiaomiParseResult::TYPE_HHCCJCY01; - if (is_lywsdcgq) { - result.type = XiaomiParseResult::TYPE_LYWSDCGQ; - } else if (is_lywsd02) { - result.type = XiaomiParseResult::TYPE_LYWSD02; - } else if (is_cgg1) { - result.type = XiaomiParseResult::TYPE_CGG1; - } - - uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12; - // Data point specs // Byte 0: type // Byte 1: fixed 0x10 // Byte 2: length // Byte 3..3+len-1: data point value - const uint8_t *raw_data = &raw[raw_offset]; - uint8_t data_offset = 0; - uint8_t data_length = raw.size() - raw_offset; - bool success = false; + const uint8_t *raw = message.data() + result.raw_offset; + const uint8_t *data = raw + 3; + const uint8_t data_length = raw[2]; - while (true) { - if (data_length < 4) - // at least 4 bytes required - // type, fixed 0x10, length, 1 byte value - break; - - const uint8_t datapoint_type = raw_data[data_offset + 0]; - const uint8_t datapoint_length = raw_data[data_offset + 2]; - - if (data_length < 3 + datapoint_length) - // 3 fixed bytes plus value length - break; - - const uint8_t *datapoint_data = &raw_data[data_offset + 3]; - - if (parse_xiaomi_data_byte(datapoint_type, datapoint_data, datapoint_length, result)) - success = true; - - data_length -= data_offset + 3 + datapoint_length; - data_offset += 3 + datapoint_length; - } - - return success; -} -optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { - XiaomiParseResult result; - bool success = false; - for (auto &service_data : device.get_service_datas()) { - if (parse_xiaomi_service_data(result, service_data)) - success = true; - } - if (!success) - return {}; - return result; -} - -bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - auto res = parse_xiaomi(device); - if (!res.has_value()) + if ((data_length < 1) || (data_length > 4)) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", data_length); return false; - - const char *name = "HHCCJCY01"; - if (res->type == XiaomiParseResult::TYPE_LYWSDCGQ) { - name = "LYWSDCGQ"; - } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) { - name = "LYWSD02"; - } else if (res->type == XiaomiParseResult::TYPE_CGG1) { - name = "CGG1"; } - ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str()); - - if (res->temperature.has_value()) { - ESP_LOGD(TAG, " Temperature: %.1f°C", *res->temperature); + // motion detection, 1 byte, 8-bit unsigned integer + if ((raw[0] == 0x03) && (data_length == 1)) { + result.has_motion = (data[0]) ? true : false; } - if (res->humidity.has_value()) { - ESP_LOGD(TAG, " Humidity: %.1f%%", *res->humidity); + // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C + else if ((raw[0] == 0x04) && (data_length == 2)) { + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.temperature = temperature / 10.0f; } - if (res->battery_level.has_value()) { - ESP_LOGD(TAG, " Battery Level: %.0f%%", *res->battery_level); + // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % + else if ((raw[0] == 0x06) && (data_length == 2)) { + const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.humidity = humidity / 10.0f; } - if (res->conductivity.has_value()) { - ESP_LOGD(TAG, " Conductivity: %.0fµS/cm", *res->conductivity); + // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx + else if (((raw[0] == 0x07) || (raw[0] == 0x0F)) && (data_length == 3)) { + const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + result.illuminance = illuminance; + result.is_light = (illuminance == 100) ? true : false; + if (raw[0] == 0x0F) + result.has_motion = true; } - if (res->illuminance.has_value()) { - ESP_LOGD(TAG, " Illuminance: %.0flx", *res->illuminance); + // soil moisture, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x08) && (data_length == 1)) { + result.moisture = data[0]; } - if (res->moisture.has_value()) { - ESP_LOGD(TAG, " Moisture: %.0f%%", *res->moisture); + // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm + else if ((raw[0] == 0x09) && (data_length == 2)) { + const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.conductivity = conductivity; + } + // battery, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x0A) && (data_length == 1)) { + result.battery_level = data[0]; + } + // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % + else if ((raw[0] == 0x0D) && (data_length == 4)) { + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + result.temperature = temperature / 10.0f; + result.humidity = humidity / 10.0f; + } + // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 + else if ((raw[0] == 0x10) && (data_length == 2)) { + const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.formaldehyde = formaldehyde / 100.0f; + } + // on/off state, 1 byte, 8-bit unsigned integer + else if ((raw[0] == 0x12) && (data_length == 1)) { + result.is_active = (data[0]) ? true : false; + } + // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x13) && (data_length == 1)) { + result.tablet = data[0]; + } + // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min + else if ((raw[0] == 0x17) && (data_length == 4)) { + const uint32_t idle_time = + uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16) | (uint32_t(data[2]) << 24); + result.idle_time = idle_time / 60.0f; + result.has_motion = (idle_time) ? false : true; + } else { + return false; } return true; } +optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) { + XiaomiParseResult result; + if (!service_data.uuid.contains(0x95, 0xFE)) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes."); + return {}; + } + + auto raw = service_data.data; + result.has_data = (raw[0] & 0x40) ? true : false; + result.has_capability = (raw[0] & 0x20) ? true : false; + result.has_encryption = (raw[0] & 0x08) ? true : false; + + if (!result.has_data) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): service data has no DATA flag."); + return {}; + } + + static uint8_t last_frame_count = 0; + if (last_frame_count == raw[4]) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); + result.is_duplicate = true; + return {}; + } + last_frame_count = raw[4]; + result.is_duplicate = false; + result.raw_offset = result.has_capability ? 12 : 11; + + if ((raw[2] == 0x98) && (raw[3] == 0x00)) { // MiFlora + result.type = XiaomiParseResult::TYPE_HHCCJCY01; + result.name = "HHCCJCY01"; + } else if ((raw[2] == 0xaa) && (raw[3] == 0x01)) { // round body, segment LCD + result.type = XiaomiParseResult::TYPE_LYWSDCGQ; + result.name = "LYWSDCGQ"; + } else if ((raw[2] == 0x5d) && (raw[3] == 0x01)) { // FlowerPot, RoPot + result.type = XiaomiParseResult::TYPE_HHCCPOT002; + result.name = "HHCCPOT002"; + } else if ((raw[2] == 0xdf) && (raw[3] == 0x02)) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display + result.type = XiaomiParseResult::TYPE_JQJCY01YM; + result.name = "JQJCY01YM"; + } else if ((raw[2] == 0xdd) && (raw[3] == 0x03)) { // Philips/Xiaomi BLE nightlight + result.type = XiaomiParseResult::TYPE_MUE4094RT; + result.name = "MUE4094RT"; + result.raw_offset -= 6; + } else if ((raw[2] == 0x47) && (raw[3] == 0x03)) { // round body, e-ink display + result.type = XiaomiParseResult::TYPE_CGG1; + result.name = "CGG1"; + } else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) { // VegTrug Grow Care Garden + result.type = XiaomiParseResult::TYPE_GCLS002; + result.name = "GCLS002"; + } else if ((raw[2] == 0x5b) && (raw[3] == 0x04)) { // rectangular body, e-ink display + result.type = XiaomiParseResult::TYPE_LYWSD02; + result.name = "LYWSD02"; + } else if ((raw[2] == 0x0a) && (raw[3] == 0x04)) { // Mosquito Repellent Smart Version + result.type = XiaomiParseResult::TYPE_WX08ZM; + result.name = "WX08ZM"; + } else if ((raw[2] == 0x76) && (raw[3] == 0x05)) { // Cleargrass (Qingping) alarm clock, segment LCD + result.type = XiaomiParseResult::TYPE_CGD1; + result.name = "CGD1"; + } else if ((raw[2] == 0x5b) && (raw[3] == 0x05)) { // small square body, segment LCD, encrypted + result.type = XiaomiParseResult::TYPE_LYWSD03MMC; + result.name = "LYWSD03MMC"; + } else if ((raw[2] == 0xf6) && (raw[3] == 0x07)) { // Xiaomi-Yeelight BLE nightlight + result.type = XiaomiParseResult::TYPE_MJYD02YLA; + result.name = "MJYD02YLA"; + if (raw.size() == 19) + result.raw_offset -= 6; + } else { + ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); + return {}; + } + + return result; +} + +bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) { + if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) { + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size()); + ESP_LOGVV(TAG, " Packet : %s", hexencode(raw.data(), raw.size()).c_str()); + return false; + } + + uint8_t mac_reverse[6] = {0}; + mac_reverse[5] = (uint8_t)(address >> 40); + mac_reverse[4] = (uint8_t)(address >> 32); + mac_reverse[3] = (uint8_t)(address >> 24); + mac_reverse[2] = (uint8_t)(address >> 16); + mac_reverse[1] = (uint8_t)(address >> 8); + mac_reverse[0] = (uint8_t)(address >> 0); + + XiaomiAESVector vector{.key = {0}, + .plaintext = {0}, + .ciphertext = {0}, + .authdata = {0x11}, + .iv = {0}, + .tag = {0}, + .keysize = 16, + .authsize = 1, + .datasize = 0, + .tagsize = 4, + .ivsize = 12}; + + vector.datasize = (raw.size() == 19) ? raw.size() - 12 : raw.size() - 18; + int cipher_pos = (raw.size() == 19) ? 5 : 11; + + const uint8_t *v = raw.data(); + + memcpy(vector.key, bindkey, vector.keysize); + memcpy(vector.ciphertext, v + cipher_pos, vector.datasize); + memcpy(vector.tag, v + raw.size() - vector.tagsize, vector.tagsize); + memcpy(vector.iv, mac_reverse, 6); // MAC address reverse + memcpy(vector.iv + 6, v + 2, 3); // sensor type (2) + packet id (1) + memcpy(vector.iv + 9, v + raw.size() - 7, 3); // payload counter + + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + + int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, vector.key, vector.keysize * 8); + if (ret) { + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): mbedtls_ccm_setkey() failed."); + mbedtls_ccm_free(&ctx); + return false; + } + + ret = mbedtls_ccm_auth_decrypt(&ctx, vector.datasize, vector.iv, vector.ivsize, vector.authdata, vector.authsize, + vector.ciphertext, vector.plaintext, vector.tag, vector.tagsize); + if (ret) { + uint8_t mac_address[6] = {0}; + memcpy(mac_address, mac_reverse + 5, 1); + memcpy(mac_address + 1, mac_reverse + 4, 1); + memcpy(mac_address + 2, mac_reverse + 3, 1); + memcpy(mac_address + 3, mac_reverse + 2, 1); + memcpy(mac_address + 4, mac_reverse + 1, 1); + memcpy(mac_address + 5, mac_reverse, 1); + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); + ESP_LOGVV(TAG, " MAC address : %s", hexencode(mac_address, 6).c_str()); + ESP_LOGVV(TAG, " Packet : %s", hexencode(raw.data(), raw.size()).c_str()); + ESP_LOGVV(TAG, " Key : %s", hexencode(vector.key, vector.keysize).c_str()); + ESP_LOGVV(TAG, " Iv : %s", hexencode(vector.iv, vector.ivsize).c_str()); + ESP_LOGVV(TAG, " Cipher : %s", hexencode(vector.ciphertext, vector.datasize).c_str()); + ESP_LOGVV(TAG, " Tag : %s", hexencode(vector.tag, vector.tagsize).c_str()); + mbedtls_ccm_free(&ctx); + return false; + } + + // replace encrypted payload with plaintext + uint8_t *p = vector.plaintext; + for (std::vector::iterator it = raw.begin() + cipher_pos; it != raw.begin() + cipher_pos + vector.datasize; + ++it) { + *it = *(p++); + } + + // clear encrypted flag + raw[0] &= ~0x08; + + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed."); + ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", hexencode(raw.data() + cipher_pos, vector.datasize).c_str(), + static_cast(raw[4])); + + mbedtls_ccm_free(&ctx); + return true; +} + +bool report_xiaomi_results(const optional &result, const std::string &address) { + if (!result.has_value()) { + ESP_LOGVV(TAG, "report_xiaomi_results(): no results available."); + return false; + } + + ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address.c_str()); + + if (result->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.1f°C", *result->temperature); + } + if (result->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.1f%%", *result->humidity); + } + if (result->battery_level.has_value()) { + ESP_LOGD(TAG, " Battery Level: %.0f%%", *result->battery_level); + } + if (result->conductivity.has_value()) { + ESP_LOGD(TAG, " Conductivity: %.0fµS/cm", *result->conductivity); + } + if (result->illuminance.has_value()) { + ESP_LOGD(TAG, " Illuminance: %.0flx", *result->illuminance); + } + if (result->moisture.has_value()) { + ESP_LOGD(TAG, " Moisture: %.0f%%", *result->moisture); + } + if (result->tablet.has_value()) { + ESP_LOGD(TAG, " Mosquito tablet: %.0f%%", *result->tablet); + } + if (result->is_active.has_value()) { + ESP_LOGD(TAG, " Repellent: %s", (*result->is_active) ? "on" : "off"); + } + if (result->has_motion.has_value()) { + ESP_LOGD(TAG, " Motion: %s", (*result->has_motion) ? "yes" : "no"); + } + if (result->is_light.has_value()) { + ESP_LOGD(TAG, " Light: %s", (*result->is_light) ? "on" : "off"); + } + + return true; +} + +bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + // Previously the message was parsed twice per packet, once by XiaomiListener::parse_device() + // and then again by the respective device class's parse_device() function. Parsing the header + // here and then for each device seems to be unneccessary and complicates the duplicate packet filtering. + // Hence I disabled the call to parse_xiaomi_header() here and the message parsing is done entirely + // in the respecive device instance. The XiaomiListener class is defined in __init__.py and I was not + // able to remove it entirely. + + return false; // with true it's not showing device scans +} + } // namespace xiaomi_ble } // namespace esphome diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 824ea80edf..daa71787a5 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -9,18 +9,58 @@ namespace esphome { namespace xiaomi_ble { struct XiaomiParseResult { - enum { TYPE_LYWSDCGQ, TYPE_HHCCJCY01, TYPE_LYWSD02, TYPE_CGG1 } type; + enum { + TYPE_HHCCJCY01, + TYPE_GCLS002, + TYPE_HHCCPOT002, + TYPE_LYWSDCGQ, + TYPE_LYWSD02, + TYPE_CGG1, + TYPE_LYWSD03MMC, + TYPE_CGD1, + TYPE_JQJCY01YM, + TYPE_MUE4094RT, + TYPE_WX08ZM, + TYPE_MJYD02YLA + } type; + std::string name; optional temperature; optional humidity; - optional battery_level; + optional moisture; optional conductivity; optional illuminance; - optional moisture; + optional formaldehyde; + optional battery_level; + optional tablet; + optional idle_time; + optional is_active; + optional has_motion; + optional is_light; + bool has_data; // 0x40 + bool has_capability; // 0x20 + bool has_encryption; // 0x08 + bool is_duplicate; + int raw_offset; }; -bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result); +struct XiaomiAESVector { + uint8_t key[16]; + uint8_t plaintext[16]; + uint8_t ciphertext[16]; + uint8_t authdata[16]; + uint8_t iv[16]; + uint8_t tag[16]; + size_t keysize; + size_t authsize; + size_t datasize; + size_t tagsize; + size_t ivsize; +}; -optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device); +bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); +optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); +bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); +bool report_xiaomi_results(const optional &result, const std::string &address); class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener { public: diff --git a/esphome/components/xiaomi_cgd1/__init__.py b/esphome/components/xiaomi_cgd1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py new file mode 100644 index 0000000000..401f6de7d2 --- /dev/null +++ b/esphome/components/xiaomi_cgd1/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_BINDKEY + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_cgd1_ns = cg.esphome_ns.namespace('xiaomi_cgd1') +XiaomiCGD1 = xiaomi_cgd1_ns.class_('XiaomiCGD1', esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiCGD1), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp new file mode 100644 index 0000000000..d701e8ee6d --- /dev/null +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp @@ -0,0 +1,77 @@ +#include "xiaomi_cgd1.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cgd1 { + +static const char *TAG = "xiaomi_cgd1"; + +void XiaomiCGD1::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi CGD1"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiCGD1::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_cgd1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h new file mode 100644 index 0000000000..b9e05f857c --- /dev/null +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cgd1 { + +class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_cgd1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index 6cc14f5a8e..a7c94fafad 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -15,6 +15,48 @@ void XiaomiCGG1::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_level_); } +bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_cgg1 } // namespace esphome diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 7f73011275..57f883405c 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -14,22 +14,7 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_gcls002/__init__.py b/esphome/components/xiaomi_gcls002/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_gcls002/sensor.py b/esphome/components/xiaomi_gcls002/sensor.py new file mode 100644 index 0000000000..1822977c38 --- /dev/null +++ b/esphome/components/xiaomi_gcls002/sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ + CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ + UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_gcls002_ns = cg.esphome_ns.namespace('xiaomi_gcls002') +XiaomiGCLS002 = xiaomi_gcls002_ns.class_('XiaomiGCLS002', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiGCLS002), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp new file mode 100644 index 0000000000..24156f98ac --- /dev/null +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp @@ -0,0 +1,66 @@ +#include "xiaomi_gcls002.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_gcls002 { + +static const char *TAG = "xiaomi_gcls002"; + +void XiaomiGCLS002::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi GCLS002"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); +} + +bool XiaomiGCLS002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_gcls002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h new file mode 100644 index 0000000000..d800e2837d --- /dev/null +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_gcls002 { + +class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; +}; + +} // namespace xiaomi_gcls002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py index 495446ba11..0b0349d7e4 100644 --- a/esphome/components/xiaomi_hhccjcy01/sensor.py +++ b/esphome/components/xiaomi_hhccjcy01/sensor.py @@ -1,8 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ +from esphome.const import CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER @@ -21,7 +21,6 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), }).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) @@ -44,6 +43,3 @@ def to_code(config): if CONF_CONDUCTIVITY in config: sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) cg.add(var.set_conductivity(sens)) - if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp index 8c8152c54c..fd099f7aa5 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp @@ -14,7 +14,50 @@ void XiaomiHHCCJCY01::dump_config() { LOG_SENSOR(" ", "Moisture", this->moisture_); LOG_SENSOR(" ", "Conductivity", this->conductivity_); LOG_SENSOR(" ", "Illuminance", this->illuminance_); - LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiHHCCJCY01::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + success = true; + } + + if (!success) { + return false; + } + + return true; } } // namespace xiaomi_hhccjcy01 diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index c1b8511bb8..e72bf98161 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -14,26 +14,7 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->moisture.has_value() && this->moisture_ != nullptr) - this->moisture_->publish_state(*res->moisture); - if (res->conductivity.has_value() && this->conductivity_ != nullptr) - this->conductivity_->publish_state(*res->conductivity); - if (res->illuminance.has_value() && this->illuminance_ != nullptr) - this->illuminance_->publish_state(*res->illuminance); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -41,7 +22,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } - void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } protected: uint64_t address_; @@ -49,7 +29,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL sensor::Sensor *moisture_{nullptr}; sensor::Sensor *conductivity_{nullptr}; sensor::Sensor *illuminance_{nullptr}; - sensor::Sensor *battery_level_{nullptr}; }; } // namespace xiaomi_hhccjcy01 diff --git a/esphome/components/xiaomi_hhccpot002/__init__.py b/esphome/components/xiaomi_hhccpot002/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_hhccpot002/sensor.py b/esphome/components/xiaomi_hhccpot002/sensor.py new file mode 100644 index 0000000000..33cd96252e --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/sensor.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ + CONF_MOISTURE, CONF_CONDUCTIVITY, UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_hhccpot002_ns = cg.esphome_ns.namespace('xiaomi_hhccpot002') +XiaomiHHCCPOT002 = xiaomi_hhccpot002_ns.class_('XiaomiHHCCPOT002', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiHHCCPOT002), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp new file mode 100644 index 0000000000..2b5ad3a826 --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp @@ -0,0 +1,60 @@ +#include "xiaomi_hhccpot002.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_hhccpot002 { + +static const char *TAG = "xiaomi_hhccpot002"; + +void XiaomiHHCCPOT002 ::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002"); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); +} + +bool XiaomiHHCCPOT002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_hhccpot002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h new file mode 100644 index 0000000000..1add8e27b1 --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_hhccpot002 { + +class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } + + protected: + uint64_t address_; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; +}; + +} // namespace xiaomi_hhccpot002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_jqjcy01ym/__init__.py b/esphome/components/xiaomi_jqjcy01ym/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py new file mode 100644 index 0000000000..2bd397e829 --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_HUMIDITY, UNIT_MILLIGRAMS_PER_CUBIC_METER, ICON_FLASK_OUTLINE, CONF_FORMALDEHYDE + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_jqjcy01ym_ns = cg.esphome_ns.namespace('xiaomi_jqjcy01ym') +XiaomiJQJCY01YM = xiaomi_jqjcy01ym_ns.class_('XiaomiJQJCY01YM', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiJQJCY01YM), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_FORMALDEHYDE): + sensor.sensor_schema(UNIT_MILLIGRAMS_PER_CUBIC_METER, ICON_FLASK_OUTLINE, 2), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_FORMALDEHYDE in config: + sens = yield sensor.new_sensor(config[CONF_FORMALDEHYDE]) + cg.add(var.set_formaldehyde(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp new file mode 100644 index 0000000000..3e7090509b --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp @@ -0,0 +1,66 @@ +#include "xiaomi_jqjcy01ym.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_jqjcy01ym { + +static const char *TAG = "xiaomi_jqjcy01ym"; + +void XiaomiJQJCY01YM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi JQJCY01YM"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiJQJCY01YM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->formaldehyde.has_value() && this->formaldehyde_ != nullptr) + this->formaldehyde_->publish_state(*res->formaldehyde); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_jqjcy01ym +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h new file mode 100644 index 0000000000..d750e1e97f --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_jqjcy01ym { + +class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_formaldehyde(sensor::Sensor *formaldehyde) { formaldehyde_ = formaldehyde; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *formaldehyde_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_jqjcy01ym +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp index cd77c133a5..5ecd99047e 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp @@ -14,6 +14,46 @@ void XiaomiLYWSD02::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_); } +bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_lywsd02 } // namespace esphome diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index 9b8aba1bb0..f32506eb44 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -14,20 +14,7 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_lywsd03mmc/__init__.py b/esphome/components/xiaomi_lywsd03mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py new file mode 100644 index 0000000000..9ecf3f64a9 --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_BINDKEY + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_lywsd03mmc_ns = cg.esphome_ns.namespace('xiaomi_lywsd03mmc') +XiaomiLYWSD03MMC = xiaomi_lywsd03mmc_ns.class_('XiaomiLYWSD03MMC', + esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiLYWSD03MMC), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp new file mode 100644 index 0000000000..e9cc99358b --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -0,0 +1,81 @@ +#include "xiaomi_lywsd03mmc.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsd03mmc { + +static const char *TAG = "xiaomi_lywsd03mmc"; + +void XiaomiLYWSD03MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSD03MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (res->humidity.has_value() && this->humidity_ != nullptr) { + // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 + *res->humidity = trunc(*res->humidity); + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiLYWSD03MMC::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_lywsd03mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h new file mode 100644 index 0000000000..c2828e3cd1 --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsd03mmc { + +class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_lywsd03mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp index 2dacff2876..035ac8c906 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp @@ -15,6 +15,48 @@ void XiaomiLYWSDCGQ::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_level_); } +bool XiaomiLYWSDCGQ::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_lywsdcgq } // namespace esphome diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index b6756eec61..553b5965fd 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -14,22 +14,7 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_mjyd02yla/__init__.py b/esphome/components/xiaomi_mjyd02yla/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py new file mode 100644 index 0000000000..72cf57d22c --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_BINDKEY, \ + CONF_DEVICE_CLASS, CONF_LIGHT, CONF_BATTERY_LEVEL, UNIT_PERCENT, ICON_BATTERY, \ + CONF_IDLE_TIME, UNIT_MINUTE, ICON_TIMELAPSE + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mjyd02yla_ns = cg.esphome_ns.namespace('xiaomi_mjyd02yla') +XiaomiMJYD02YLA = xiaomi_mjyd02yla_ns.class_('XiaomiMJYD02YLA', binary_sensor.BinarySensor, + cg.Component, esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiMJYD02YLA), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Optional(CONF_DEVICE_CLASS, default='motion'): binary_sensor.device_class, + cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema(UNIT_MINUTE, ICON_TIMELAPSE, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), + cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.Optional(CONF_DEVICE_CLASS, default='light'): binary_sensor.device_class, + }), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_IDLE_TIME in config: + sens = yield sensor.new_sensor(config[CONF_IDLE_TIME]) + cg.add(var.set_idle_time(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + if CONF_LIGHT in config: + sens = yield binary_sensor.new_binary_sensor(config[CONF_LIGHT]) + cg.add(var.set_light(sens)) diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp new file mode 100644 index 0000000000..aaea3606ba --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp @@ -0,0 +1,79 @@ +#include "xiaomi_mjyd02yla.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mjyd02yla { + +static const char *TAG = "xiaomi_mjyd02yla"; + +void XiaomiMJYD02YLA::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi MJYD02YL-A"); + LOG_BINARY_SENSOR(" ", "Motion", this); + LOG_BINARY_SENSOR(" ", "Light", this->is_light_); + LOG_SENSOR(" ", "Idle Time", this->idle_time_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->idle_time.has_value() && this->idle_time_ != nullptr) + this->idle_time_->publish_state(*res->idle_time); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + if (res->is_light.has_value() && this->is_light_ != nullptr) + this->is_light_->publish_state(*res->is_light); + if (res->has_motion.has_value()) + this->publish_state(*res->has_motion); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiMJYD02YLA::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_mjyd02yla +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h new file mode 100644 index 0000000000..d3fde4d6f8 --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mjyd02yla { + +class XiaomiMJYD02YLA : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_light(binary_sensor::BinarySensor *light) { is_light_ = light; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *idle_time_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + binary_sensor::BinarySensor *is_light_{nullptr}; +}; + +} // namespace xiaomi_mjyd02yla +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mue4094rt/__init__.py b/esphome/components/xiaomi_mue4094rt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_mue4094rt/binary_sensor.py b/esphome/components/xiaomi_mue4094rt/binary_sensor.py new file mode 100644 index 0000000000..946b1694c4 --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/binary_sensor.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_DEVICE_CLASS, CONF_TIMEOUT, CONF_ID + + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mue4094rt_ns = cg.esphome_ns.namespace('xiaomi_mue4094rt') +XiaomiMUE4094RT = xiaomi_mue4094rt_ns.class_('XiaomiMUE4094RT', binary_sensor.BinarySensor, + cg.Component, esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiMUE4094RT), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_DEVICE_CLASS, default='motion'): binary_sensor.device_class, + cv.Optional(CONF_TIMEOUT, default='5s'): cv.positive_time_period_milliseconds, +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_time(config[CONF_TIMEOUT])) diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp new file mode 100644 index 0000000000..45337f330e --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp @@ -0,0 +1,59 @@ +#include "xiaomi_mue4094rt.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mue4094rt { + +static const char *TAG = "xiaomi_mue4094rt"; + +void XiaomiMUE4094RT::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi MUE4094RT"); + LOG_BINARY_SENSOR(" ", "Motion", this); +} + +bool XiaomiMUE4094RT::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->has_motion.has_value()) { + this->publish_state(*res->has_motion); + this->set_timeout("motion_timeout", timeout_, [this]() { this->publish_state(false); }); + } + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_mue4094rt +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h new file mode 100644 index 0000000000..31f913ec94 --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mue4094rt { + +class XiaomiMUE4094RT : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_time(uint16_t timeout) { timeout_ = timeout; } + + protected: + uint64_t address_; + uint16_t timeout_; +}; + +} // namespace xiaomi_mue4094rt +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_wx08zm/__init__.py b/esphome/components/xiaomi_wx08zm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py new file mode 100644 index 0000000000..1d60dbf5e0 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TABLET, \ + UNIT_PERCENT, ICON_BUG, ICON_BATTERY, CONF_ID + + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_wx08zm_ns = cg.esphome_ns.namespace('xiaomi_wx08zm') +XiaomiWX08ZM = xiaomi_wx08zm_ns.class_('XiaomiWX08ZM', binary_sensor.BinarySensor, + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiWX08ZM), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TABLET): sensor.sensor_schema(UNIT_PERCENT, ICON_BUG, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TABLET in config: + sens = yield sensor.new_sensor(config[CONF_TABLET]) + cg.add(var.set_tablet(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp new file mode 100644 index 0000000000..6ecdf8f4f4 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp @@ -0,0 +1,64 @@ +#include "xiaomi_wx08zm.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_wx08zm { + +static const char *TAG = "xiaomi_wx08zm"; + +void XiaomiWX08ZM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi WX08ZM"); + LOG_BINARY_SENSOR(" ", "Mosquito Repellent", this); + LOG_SENSOR(" ", "Tablet Resource", this->tablet_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiWX08ZM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->is_active.has_value()) { + this->publish_state(*res->is_active); + } + if (res->tablet.has_value() && this->tablet_ != nullptr) + this->tablet_->publish_state(*res->tablet); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_wx08zm +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h new file mode 100644 index 0000000000..f3eba0e159 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_wx08zm { + +class XiaomiWX08ZM : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_tablet(sensor::Sensor *tablet) { tablet_ = tablet; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *tablet_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_wx08zm +} // namespace esphome + +#endif diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py index 649b80b444..df263974e8 100644 --- a/esphome/components/zyaura/sensor.py +++ b/esphome/components/zyaura/sensor.py @@ -5,7 +5,7 @@ from esphome.components import sensor from esphome.const import CONF_ID, CONF_CLOCK_PIN, CONF_DATA_PIN, \ CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, \ UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, UNIT_PERCENT, \ - ICON_PERIODIC_TABLE_CO2, ICON_THERMOMETER, ICON_WATER_PERCENT + ICON_MOLECULE_CO2, ICON_THERMOMETER, ICON_WATER_PERCENT from esphome.cpp_helpers import gpio_pin_expression zyaura_ns = cg.esphome_ns.namespace('zyaura') @@ -17,7 +17,7 @@ CONFIG_SCHEMA = cv.Schema({ pins.validate_has_interrupt), cv.Required(CONF_DATA_PIN): cv.All(pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt), - cv.Optional(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), + cv.Optional(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), 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')) diff --git a/esphome/config.py b/esphome/config.py index 8d7c622a27..85f48c64b7 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -11,9 +11,8 @@ from contextlib import contextmanager import voluptuous as vol from esphome import core, core_config, yaml_util -from esphome.components import substitutions -from esphome.components.substitutions import CONF_SUBSTITUTIONS -from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS +from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS, CONF_PACKAGES, \ + CONF_SUBSTITUTIONS from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent from esphome.util import safe_print, OrderedDict @@ -67,6 +66,10 @@ class ComponentManifest: def auto_load(self): return getattr(self.module, 'AUTO_LOAD', []) + @property + def codeowners(self) -> List[str]: + return getattr(self.module, 'CODEOWNERS', []) + def _get_flags_set(self, name, config): if not hasattr(self.module, name): return set() @@ -387,15 +390,27 @@ def recursive_check_replaceme(value): return value -def validate_config(config): +def validate_config(config, command_line_substitutions): result = Config() + # 0. Load packages + if CONF_PACKAGES in config: + from esphome.components.packages import do_packages_pass + result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) + try: + config = do_packages_pass(config) + except vol.Invalid as err: + result.update(config) + result.add_error(err) + return result + # 1. Load substitutions if CONF_SUBSTITUTIONS in config: - result[CONF_SUBSTITUTIONS] = config[CONF_SUBSTITUTIONS] + from esphome.components import substitutions + result[CONF_SUBSTITUTIONS] = {**config[CONF_SUBSTITUTIONS], **command_line_substitutions} result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) try: - substitutions.do_substitution_pass(config) + substitutions.do_substitution_pass(config, command_line_substitutions) except vol.Invalid as err: result.add_error(err) return result @@ -656,7 +671,7 @@ class InvalidYAMLError(EsphomeError): self.base_exc = base_exc -def _load_config(): +def _load_config(command_line_substitutions): try: config = yaml_util.load_yaml(CORE.config_path) except EsphomeError as e: @@ -664,7 +679,7 @@ def _load_config(): CORE.raw_config = config try: - result = validate_config(config) + result = validate_config(config, command_line_substitutions) except EsphomeError: raise except Exception: @@ -674,9 +689,9 @@ def _load_config(): return result -def load_config(): +def load_config(command_line_substitutions): try: - return _load_config() + return _load_config(command_line_substitutions) except vol.Invalid as err: raise EsphomeError(f"Error while parsing config: {err}") @@ -813,10 +828,10 @@ def strip_default_ids(config): return config -def read_config(): +def read_config(command_line_substitutions): _LOGGER.info("Reading configuration %s...", CORE.config_path) try: - res = load_config() + res = load_config(command_line_substitutions) except EsphomeError as err: _LOGGER.error("Error while reading config: %s", err) return None diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 55199e6647..3fb2b4827c 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -11,9 +11,9 @@ from string import ascii_letters, digits import voluptuous as vol from esphome import core -from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \ - CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, \ - CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \ +from esphome.const import ALLOWED_NAME_CHARS, CONF_AVAILABILITY, CONF_COMMAND_TOPIC, \ + CONF_DISCOVERY, CONF_ID, CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, \ + CONF_PAYLOAD_NOT_AVAILABLE, CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \ CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes @@ -40,8 +40,6 @@ ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid -ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' - RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword 'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', 'bool', 'break', @@ -104,7 +102,7 @@ def alphanumeric(value): raise Invalid("string value is None") value = str(value) if not value.isalnum(): - raise Invalid("string value is not alphanumeric") + raise Invalid(f"{value} is not alphanumeric") return value @@ -186,6 +184,7 @@ def ensure_list(*validators): None and empty dictionaries are converted to empty lists. """ user = All(*validators) + list_schema = Schema([user]) def validator(value): check_not_templatable(value) @@ -193,19 +192,7 @@ def ensure_list(*validators): return [] if not isinstance(value, list): return [user(value)] - ret = [] - errs = [] - for i, val in enumerate(value): - try: - with prepend_path([i]): - ret.append(user(val)) - except MultipleInvalid as err: - errs.extend(err.errors) - except Invalid as err: - errs.append(err) - if errs: - raise MultipleInvalid(errs) - return ret + return list_schema(value) return validator @@ -566,6 +553,23 @@ def mac_address(value): return core.MACAddress(*parts_int) +def bind_key(value): + value = string_strict(value) + parts = [value[i:i+2] for i in range(0, len(value), 2)] + if len(parts) != 16: + raise Invalid("Bind key must consist of 16 hexadecimal numbers") + parts_int = [] + if any(len(part) != 2 for part in parts): + raise Invalid("Bind key must be format XX") + for part in parts: + try: + parts_int.append(int(part, 16)) + except ValueError: + raise Invalid("Bind key must be hex values from 00 to FF") + + return ''.join(f'{part:02X}' for part in parts_int) + + def uuid(value): return Coerce(uuid_.UUID)(value) @@ -794,7 +798,9 @@ def mqtt_qos(value): def requires_component(comp): """Validate that this option can only be specified when the component `comp` is loaded.""" + # pylint: disable=unsupported-membership-test def validator(value): + # pylint: disable=unsupported-membership-test if comp not in CORE.raw_config: raise Invalid(f"This option requires component {comp}") return value @@ -821,9 +827,16 @@ def percentage(value): def possibly_negative_percentage(value): - has_percent_sign = isinstance(value, str) and value.endswith('%') - if has_percent_sign: - value = float(value[:-1].rstrip()) / 100.0 + has_percent_sign = False + if isinstance(value, str): + try: + if value.endswith('%'): + has_percent_sign = False + value = float(value[:-1].rstrip()) / 100.0 + else: + value = float(value) + except ValueError: + raise Invalid("invalid number") if value > 1: msg = "Percentage must not be higher than 100%." if not has_percent_sign: @@ -1101,7 +1114,7 @@ def typed_schema(schemas, **kwargs): def validator(value): if not isinstance(value, dict): raise Invalid("Value must be dict") - if CONF_TYPE not in value: + if key not in value: raise Invalid("type not specified!") value = value.copy() key_v = key_validator(value.pop(key)) @@ -1151,6 +1164,7 @@ class OnlyWith(Optional): @property def default(self): + # pylint: disable=unsupported-membership-test if self._component not in CORE.raw_config: return vol.UNDEFINED return self._default diff --git a/esphome/const.py b/esphome/const.py index 31359e610d..d8a8ca9905 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,7 +1,7 @@ """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 15 +MINOR_VERSION = 16 PATCH_VERSION = '0-dev' __short_version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}' __version__ = f'{__short_version__}.{PATCH_VERSION}' @@ -10,19 +10,37 @@ ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] -ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' -ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' -ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0' -ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0' -ARDUINO_VERSION_ESP32_1_0_2 = 'espressif32@1.9.0' -ARDUINO_VERSION_ESP32_1_0_3 = 'espressif32@1.10.0' -ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0' -ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \ - '/stage' -ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.1' -ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0' -ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3' -ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0' +ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_-' +# Lookup table from ESP32 arduino framework version to latest platformio +# package with that version +# See also https://github.com/platformio/platform-espressif32/releases +ARDUINO_VERSION_ESP32 = { + 'dev': 'https://github.com/platformio/platform-espressif32.git', + '1.0.4': 'espressif32@1.12.4', + '1.0.3': 'espressif32@1.10.0', + '1.0.2': 'espressif32@1.9.0', + '1.0.1': 'espressif32@1.7.0', + '1.0.0': 'espressif32@1.5.0', +} +# See also https://github.com/platformio/platform-espressif8266/releases +ARDUINO_VERSION_ESP8266 = { + 'dev': 'https://github.com/platformio/platform-espressif8266.git', + '2.7.4': 'espressif8266@2.6.2', + '2.7.3': 'espressif8266@2.6.1', + '2.7.2': 'espressif8266@2.6.0', + '2.7.1': 'espressif8266@2.5.3', + '2.7.0': 'espressif8266@2.5.0', + '2.6.3': 'espressif8266@2.4.0', + '2.6.2': 'espressif8266@2.3.1', + '2.6.1': 'espressif8266@2.3.0', + '2.5.2': 'espressif8266@2.2.3', + '2.5.1': 'espressif8266@2.1.1', + '2.5.0': 'espressif8266@2.0.4', + '2.4.2': 'espressif8266@1.8.0', + '2.4.1': 'espressif8266@1.7.3', + '2.4.0': 'espressif8266@1.6.0', + '2.3.0': 'espressif8266@1.5.0', +} SOURCE_FILE_EXTENSIONS = {'.cpp', '.hpp', '.h', '.c', '.tcc', '.ino'} HEADER_FILE_EXTENSIONS = {'.h', '.hpp', '.tcc'} @@ -44,10 +62,12 @@ CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' CONF_AUTH = 'auth' +CONF_AUTO_MODE = 'auto_mode' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' CONF_AWAY_CONFIG = 'away_config' +CONF_BACKLIGHT_PIN = 'backlight_pin' CONF_BATTERY_LEVEL = 'battery_level' CONF_BATTERY_VOLTAGE = 'battery_voltage' CONF_BAUD_RATE = 'baud_rate' @@ -55,6 +75,7 @@ CONF_BELOW = 'below' CONF_BINARY = 'binary' CONF_BINARY_SENSOR = 'binary_sensor' CONF_BINARY_SENSORS = 'binary_sensors' +CONF_BINDKEY = 'bindkey' CONF_BIRTH_MESSAGE = 'birth_message' CONF_BIT_DEPTH = 'bit_depth' CONF_BLUE = 'blue' @@ -73,6 +94,8 @@ CONF_CALIBRATION = 'calibration' CONF_CAPACITANCE = 'capacitance' CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' CONF_CARRIER_FREQUENCY = 'carrier_frequency' +CONF_CERTIFICATE = "certificate" +CONF_CERTIFICATE_AUTHORITY = "certificate_authority" CONF_CHANGE_MODE_EVERY = 'change_mode_every' CONF_CHANNEL = 'channel' CONF_CHANNELS = 'channels' @@ -100,6 +123,7 @@ CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' +CONF_COOL_MODE = 'cool_mode' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' CONF_CS_PIN = 'cs_pin' @@ -129,6 +153,7 @@ CONF_DIMENSIONS = 'dimensions' CONF_DIO_PIN = 'dio_pin' CONF_DIR_PIN = 'dir_pin' CONF_DIRECTION = 'direction' +CONF_DIRECTION_OUTPUT = 'direction_output' CONF_DISCOVERY = 'discovery' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_DISCOVERY_RETAIN = 'discovery_retain' @@ -137,8 +162,11 @@ CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' CONF_DOMAIN = 'domain' +CONF_DRY_ACTION = 'dry_action' +CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' +CONF_EAP = 'eap' CONF_ECHO_PIN = 'echo_pin' CONF_EFFECT = 'effect' CONF_EFFECTS = 'effects' @@ -156,6 +184,17 @@ CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' CONF_FAN_MODE = 'fan_mode' +CONF_FAN_MODE_AUTO_ACTION = 'fan_mode_auto_action' +CONF_FAN_MODE_DIFFUSE_ACTION = 'fan_mode_diffuse_action' +CONF_FAN_MODE_FOCUS_ACTION = 'fan_mode_focus_action' +CONF_FAN_MODE_HIGH_ACTION = 'fan_mode_high_action' +CONF_FAN_MODE_LOW_ACTION = 'fan_mode_low_action' +CONF_FAN_MODE_MEDIUM_ACTION = 'fan_mode_medium_action' +CONF_FAN_MODE_MIDDLE_ACTION = 'fan_mode_middle_action' +CONF_FAN_MODE_OFF_ACTION = 'fan_mode_off_action' +CONF_FAN_MODE_ON_ACTION = 'fan_mode_on_action' +CONF_FAN_ONLY_ACTION = 'fan_only_action' +CONF_FAN_ONLY_MODE = 'fan_only_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -180,20 +219,25 @@ CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' CONF_HEAT_ACTION = 'heat_action' +CONF_HEAT_MODE = 'heat_mode' CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' +CONF_HIDE_TIMESTAMP = 'hide_timestamp' CONF_HIGH = 'high' CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' CONF_HOUR = 'hour' CONF_HOURS = 'hours' CONF_HUMIDITY = 'humidity' +CONF_HYSTERESIS = "hysteresis" CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' CONF_ID = 'id' +CONF_IDENTITY = 'identity' CONF_IDLE = 'idle' CONF_IDLE_ACTION = 'idle_action' CONF_IDLE_LEVEL = 'idle_level' +CONF_IDLE_TIME = 'idle_time' CONF_IF = 'if' CONF_IIR_FILTER = 'iir_filter' CONF_ILLUMINANCE = 'illuminance' @@ -217,6 +261,7 @@ CONF_JS_URL = 'js_url' CONF_JVC = 'jvc' CONF_KEEP_ON_TIME = 'keep_on_time' CONF_KEEPALIVE = 'keepalive' +CONF_KEY = 'key' CONF_LAMBDA = 'lambda' CONF_LEVEL = 'level' CONF_LG = 'lg' @@ -265,6 +310,7 @@ CONF_MODEL = 'model' CONF_MOISTURE = 'moisture' CONF_MONTHS = 'months' CONF_MOSI_PIN = 'mosi_pin' +CONF_MOTION = 'motion' CONF_MOVEMENT_COUNTER = 'movement_counter' CONF_MQTT = 'mqtt' CONF_MQTT_ID = 'mqtt_id' @@ -280,6 +326,7 @@ CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' CONF_NUMBER = 'number' +CONF_OFF_MODE = 'off_mode' CONF_OFFSET = 'offset' CONF_ON = 'on' CONF_ON_BLE_ADVERTISE = 'on_ble_advertise' @@ -318,6 +365,7 @@ CONF_OUTPUT = 'output' CONF_OUTPUT_ID = 'output_id' CONF_OUTPUTS = 'outputs' CONF_OVERSAMPLING = 'oversampling' +CONF_PACKAGES = 'packages' CONF_PAGE_ID = 'page_id' CONF_PAGES = 'pages' CONF_PANASONIC = 'panasonic' @@ -392,6 +440,7 @@ CONF_RTD_WIRES = 'rtd_wires' CONF_RUN_CYCLES = 'run_cycles' CONF_RUN_DURATION = 'run_duration' CONF_RW_PIN = 'rw_pin' +CONF_RX_BUFFER_SIZE = 'rx_buffer_size' CONF_RX_ONLY = 'rx_only' CONF_RX_PIN = 'rx_pin' CONF_SAFE_MODE = 'safe_mode' @@ -440,11 +489,18 @@ CONF_STEP_PIN = 'step_pin' CONF_STOP = 'stop' CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' +CONF_SUBSTITUTIONS = 'substitutions' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SWING_BOTH_ACTION = 'swing_both_action' +CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' CONF_SWING_MODE = 'swing_mode' +CONF_SWING_OFF_ACTION = 'swing_off_action' +CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' +CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' +CONF_TABLET = 'tablet' CONF_TAG = 'tag' CONF_TARGET = 'target' CONF_TARGET_TEMPERATURE = 'target_temperature' @@ -520,20 +576,23 @@ ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical' ICON_BATTERY = 'mdi:battery' ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' +ICON_BUG = 'mdi:bug' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' ICON_COUNTER = 'mdi:counter' ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' +ICON_FLASK_OUTLINE = 'mdi:flask-outline' ICON_FLOWER = 'mdi:flower' ICON_GAS_CYLINDER = 'mdi:gas-cylinder' ICON_GAUGE = 'mdi:gauge' ICON_LIGHTBULB = 'mdi:lightbulb' ICON_MAGNET = 'mdi:magnet' +ICON_MOLECULE_CO2 = 'mdi:molecule-co2' +ICON_MOTION_SENSOR = 'mdi:motion-sensor' ICON_NEW_BOX = 'mdi:new-box' ICON_PERCENT = 'mdi:percent' -ICON_PERIODIC_TABLE_CO2 = 'mdi:periodic-table-co2' ICON_POWER = 'mdi:power' ICON_PULSE = 'mdi:pulse' ICON_RADIATOR = 'mdi:radiator' @@ -546,7 +605,8 @@ ICON_SIGN_DIRECTION = 'mdi:sign-direction' ICON_SIGNAL = 'mdi:signal-distance-variant' ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal' ICON_THERMOMETER = 'mdi:thermometer' -ICON_TIMER = 'mdi:timer' +ICON_TIMELAPSE = 'mdi:timelapse' +ICON_TIMER = 'mdi:timer-outline' ICON_WATER_PERCENT = 'mdi:water-percent' ICON_WEATHER_SUNSET = 'mdi:weather-sunset' ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' @@ -575,6 +635,8 @@ UNIT_MICROGRAMS_PER_CUBIC_METER = 'µg/m³' UNIT_MICROMETER = 'µm' UNIT_MICROSIEMENS_PER_CENTIMETER = 'µS/cm' UNIT_MICROTESLA = 'µT' +UNIT_MILLIGRAMS_PER_CUBIC_METER = 'mg/m³' +UNIT_MINUTE = 'min' UNIT_OHM = 'Ω' UNIT_PARTS_PER_BILLION = 'ppb' UNIT_PARTS_PER_MILLION = 'ppm' diff --git a/esphome/core.py b/esphome/core.py index 7bbaf9c54c..0065b750c4 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -8,13 +8,16 @@ import os import re # pylint: disable=unused-import, wrong-import-order -from typing import Any, Dict, List, Optional, Set # noqa +from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING # noqa from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \ CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI from esphome.helpers import ensure_unique_string, is_hassio from esphome.util import OrderedDict +if TYPE_CHECKING: + from .cpp_generator import MockObj, MockObjClass, Statement + _LOGGER = logging.getLogger(__name__) @@ -24,15 +27,18 @@ class EsphomeError(Exception): class HexInt(int): def __str__(self): - if 0 <= self <= 255: - return f"0x{self:02X}" - return f"0x{self:X}" + value = self + sign = "-" if value < 0 else "" + value = abs(value) + if 0 <= value <= 255: + return f"{sign}0x{value:02X}" + return f"{sign}0x{value:X}" class IPAddress: def __init__(self, *args): if len(args) != 4: - raise ValueError("IPAddress must consist up 4 items") + raise ValueError("IPAddress must consist of 4 items") self.args = args def __str__(self): @@ -490,9 +496,9 @@ class EsphomeCore: # The board that's used (for example nodemcuv2) self.board: Optional[str] = None # The full raw configuration - self.raw_config: ConfigType = {} + self.raw_config: Optional[ConfigType] = None # The validated configuration, this is None until the config has been validated - self.config: ConfigType = {} + self.config: Optional[ConfigType] = None # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -544,6 +550,9 @@ class EsphomeCore: @property def address(self) -> Optional[str]: + if self.config is None: + raise ValueError("Config has not been loaded yet") + if 'wifi' in self.config: return self.config[CONF_WIFI][CONF_USE_ADDRESS] @@ -554,6 +563,9 @@ class EsphomeCore: @property def comment(self) -> Optional[str]: + if self.config is None: + raise ValueError("Config has not been loaded yet") + if CONF_COMMENT in self.config[CONF_ESPHOME]: return self.config[CONF_ESPHOME][CONF_COMMENT] @@ -567,6 +579,9 @@ class EsphomeCore: @property def arduino_version(self) -> str: + if self.config is None: + raise ValueError("Config has not been loaded yet") + return self.config[CONF_ESPHOME][CONF_ARDUINO_VERSION] @property diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 4ecb247ec3..5e23c6250b 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -1,6 +1,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/version.h" +#include "esphome/core/esphal.h" #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -58,6 +59,9 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); this->schedule_dump_config(); this->calculate_looping_components_(); + + // Dummy function to link some symbols into the binary. + force_link_symbols(); } void Application::loop() { uint32_t new_app_state = 0; diff --git a/esphome/core/automation.h b/esphome/core/automation.h index cbe96a749e..6d79480f0f 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -50,18 +50,22 @@ template class Automation; template class Trigger { public: + /// Inform the parent automation that the event has triggered. void trigger(Ts... x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); } void set_automation_parent(Automation *automation_parent) { this->automation_parent_ = automation_parent; } - void stop() { + + /// Stop any action connected to this trigger. + void stop_action() { if (this->automation_parent_ == nullptr) return; this->automation_parent_->stop(); } - bool is_running() { + /// Returns true if any action connected to this trigger is running. + bool is_action_running() { if (this->automation_parent_ == nullptr) return false; return this->automation_parent_->is_running(); @@ -75,45 +79,67 @@ template class ActionList; template class Action { public: - virtual void play(Ts... x) = 0; virtual void play_complex(Ts... x) { + this->num_running_++; this->play(x...); - this->play_next(x...); + this->play_next_(x...); } - void play_next(Ts... x) { - if (this->next_ != nullptr) { - this->next_->play_complex(x...); - } - } - virtual void stop() {} virtual void stop_complex() { - this->stop(); - this->stop_next(); - } - void stop_next() { - if (this->next_ != nullptr) { - this->next_->stop_complex(); + if (num_running_) { + this->stop(); + this->num_running_ = 0; } + this->stop_next_(); } - virtual bool is_running() { return this->is_running_next(); } - bool is_running_next() { - if (this->next_ == nullptr) - return false; - return this->next_->is_running(); - } + /// Check if this or any of the following actions are currently running. + virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); } - void play_next_tuple(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); + /// The total number of actions that are currently running in this plus any of + /// the following actions in the chain. + int num_running_total() { + int total = this->num_running_; + if (this->next_ != nullptr) + total += this->next_->num_running_total(); + return total; } protected: friend ActionList; + virtual void play(Ts... x) = 0; + void play_next_(Ts... x) { + if (this->num_running_ > 0) { + this->num_running_--; + if (this->next_ != nullptr) { + this->next_->play_complex(x...); + } + } + } template void play_next_tuple_(const std::tuple &tuple, seq) { - this->play_next(std::get(tuple)...); + this->play_next_(std::get(tuple)...); + } + void play_next_tuple_(const std::tuple &tuple) { + this->play_next_tuple_(tuple, typename gens::type()); + } + + virtual void stop() {} + void stop_next_() { + if (this->next_ != nullptr) { + this->next_->stop_complex(); + } + } + + bool is_running_next_() { + if (this->next_ == nullptr) + return false; + return this->next_->is_running(); } Action *next_ = nullptr; + + /// The number of instances of this sequence in the list of actions + /// that is currently being executed. + int num_running_{0}; }; template class ActionList { @@ -141,11 +167,19 @@ template class ActionList { this->actions_begin_->stop_complex(); } bool empty() const { return this->actions_begin_ == nullptr; } + + /// Check if any action in this action list is currently running. bool is_running() { if (this->actions_begin_ == nullptr) return false; return this->actions_begin_->is_running(); } + /// Return the number of actions in this action list that are currently running. + int num_running() { + if (this->actions_begin_ == nullptr) + return false; + return this->actions_begin_->num_running_total(); + } protected: template void play_tuple_(const std::tuple &tuple, seq) { this->play(std::get(tuple)...); } @@ -167,6 +201,9 @@ template class Automation { bool is_running() { return this->actions_.is_running(); } + /// Return the number of actions in the action part of this automation that are currently running. + int num_running() { return this->actions_.num_running(); } + protected: Trigger *trigger_; ActionList actions_; diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index add3df0bb5..d2656290bc 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -108,34 +108,23 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) - void stop() override { - this->cancel_timeout(""); - this->num_running_ = 0; - } - - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { - auto f = std::bind(&DelayAction::delay_end_, this, x...); + auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; this->set_timeout(this->delay_.value(x...), f); } float get_setup_priority() const override { return setup_priority::HARDWARE; } - bool is_running() override { return this->num_running_ > 0 || this->is_running_next(); } - - protected: - void delay_end_(Ts... x) { - this->num_running_--; - this->play_next(x...); + void play(Ts... x) override { /* ignore - see play_complex */ } - int num_running_{0}; + + void stop() override { this->cancel_timeout(""); } }; template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} + void play(Ts... x) override { this->f_(x...); } protected: @@ -148,41 +137,40 @@ template class IfAction : public Action { void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); + this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } void add_else(const std::vector *> &actions) { this->else_.add_actions(actions); - this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); - } - - void play(Ts... x) override { /* ignore - see play_complex */ + this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } void play_complex(Ts... x) override { + this->num_running_++; bool res = this->condition_->check(x...); if (res) { if (this->then_.empty()) { - this->play_next(x...); - } else { + this->play_next_(x...); + } else if (this->num_running_ > 0) { this->then_.play(x...); } } else { if (this->else_.empty()) { - this->play_next(x...); - } else { + this->play_next_(x...); + } else if (this->num_running_ > 0) { this->else_.play(x...); } } } + void play(Ts... x) override { /* ignore - see play_complex */ + } + void stop() override { this->then_.stop(); this->else_.stop(); } - bool is_running() override { return this->then_.is_running() || this->else_.is_running() || this->is_running_next(); } - protected: Condition *condition_; ActionList then_; @@ -196,37 +184,40 @@ template class WhileAction : public Action { void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); this->then_.add_action(new LambdaAction([this](Ts... x) { - if (this->condition_->check_tuple(this->var_)) { + if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) { // play again - this->then_.play_tuple(this->var_); + if (this->num_running_ > 0) { + this->then_.play_tuple(this->var_); + } } else { // condition false, play next - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } })); } - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Store loop parameters this->var_ = std::make_tuple(x...); // Initial condition check if (!this->condition_->check_tuple(this->var_)) { // If new condition check failed, stop loop if running this->then_.stop(); - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); return; } - this->then_.play_tuple(this->var_); + if (this->num_running_ > 0) { + this->then_.play_tuple(this->var_); + } + } + + void play(Ts... x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } - bool is_running() override { return this->then_.is_running() || this->is_running_next(); } - protected: Condition *condition_; ActionList then_; @@ -237,48 +228,44 @@ template class WaitUntilAction : public Action, public Co public: WaitUntilAction(Condition *condition) : condition_(condition) {} - void play(Ts... x) { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { - this->triggered_ = false; - this->play_next(x...); + if (this->num_running_ > 0) { + this->play_next_(x...); + } return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (!this->condition_->check_tuple(this->var_)) { return; } - this->triggered_ = false; - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } + void play(Ts... x) override { /* ignore - see play_complex */ + } protected: Condition *condition_; - bool triggered_{false}; std::tuple var_{}; }; template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} + void play(Ts... x) override { this->component_->update(); } protected: diff --git a/esphome/core/color.h b/esphome/core/color.h new file mode 100644 index 0000000000..b19340a1d3 --- /dev/null +++ b/esphome/core/color.h @@ -0,0 +1,161 @@ +#pragma once + +#include "component.h" +#include "helpers.h" + +namespace esphome { + +inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; } + +struct Color { + union { + struct { + union { + uint8_t r; + uint8_t red; + }; + union { + uint8_t g; + uint8_t green; + }; + union { + uint8_t b; + uint8_t blue; + }; + union { + uint8_t w; + uint8_t white; + }; + }; + uint8_t raw[4]; + uint32_t raw_32; + }; + inline Color() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {} // NOLINT + inline Color(float red, float green, float blue) ALWAYS_INLINE : r(uint8_t(red * 255)), + g(uint8_t(green * 255)), + b(uint8_t(blue * 255)), + w(0) {} + inline Color(float red, float green, float blue, float white) ALWAYS_INLINE : r(uint8_t(red * 255)), + g(uint8_t(green * 255)), + b(uint8_t(blue * 255)), + w(uint8_t(white * 255)) {} + inline Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), + g((colorcode >> 8) & 0xFF), + b((colorcode >> 0) & 0xFF), + w((colorcode >> 24) & 0xFF) {} + inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; } + inline Color &operator=(const Color &rhs) ALWAYS_INLINE { + this->r = rhs.r; + this->g = rhs.g; + this->b = rhs.b; + this->w = rhs.w; + return *this; + } + inline Color &operator=(uint32_t colorcode) ALWAYS_INLINE { + this->w = (colorcode >> 24) & 0xFF; + this->r = (colorcode >> 16) & 0xFF; + this->g = (colorcode >> 8) & 0xFF; + this->b = (colorcode >> 0) & 0xFF; + return *this; + } + inline uint8_t &operator[](uint8_t x) ALWAYS_INLINE { return this->raw[x]; } + inline Color operator*(uint8_t scale) const ALWAYS_INLINE { + return Color(esp_scale8(this->red, scale), esp_scale8(this->green, scale), esp_scale8(this->blue, scale), + esp_scale8(this->white, scale)); + } + inline Color &operator*=(uint8_t scale) ALWAYS_INLINE { + this->red = esp_scale8(this->red, scale); + this->green = esp_scale8(this->green, scale); + this->blue = esp_scale8(this->blue, scale); + this->white = esp_scale8(this->white, scale); + return *this; + } + inline Color operator*(const Color &scale) const ALWAYS_INLINE { + return Color(esp_scale8(this->red, scale.red), esp_scale8(this->green, scale.green), + esp_scale8(this->blue, scale.blue), esp_scale8(this->white, scale.white)); + } + inline Color &operator*=(const Color &scale) ALWAYS_INLINE { + this->red = esp_scale8(this->red, scale.red); + this->green = esp_scale8(this->green, scale.green); + this->blue = esp_scale8(this->blue, scale.blue); + this->white = esp_scale8(this->white, scale.white); + return *this; + } + inline Color operator+(const Color &add) const ALWAYS_INLINE { + Color ret; + if (uint8_t(add.r + this->r) < this->r) + ret.r = 255; + else + ret.r = this->r + add.r; + if (uint8_t(add.g + this->g) < this->g) + ret.g = 255; + else + ret.g = this->g + add.g; + if (uint8_t(add.b + this->b) < this->b) + ret.b = 255; + else + ret.b = this->b + add.b; + if (uint8_t(add.w + this->w) < this->w) + ret.w = 255; + else + ret.w = this->w + add.w; + return ret; + } + inline Color &operator+=(const Color &add) ALWAYS_INLINE { return *this = (*this) + add; } + inline Color operator+(uint8_t add) const ALWAYS_INLINE { return (*this) + Color(add, add, add, add); } + inline Color &operator+=(uint8_t add) ALWAYS_INLINE { return *this = (*this) + add; } + inline Color operator-(const Color &subtract) const ALWAYS_INLINE { + Color ret; + if (subtract.r > this->r) + ret.r = 0; + else + ret.r = this->r - subtract.r; + if (subtract.g > this->g) + ret.g = 0; + else + ret.g = this->g - subtract.g; + if (subtract.b > this->b) + ret.b = 0; + else + ret.b = this->b - subtract.b; + if (subtract.w > this->w) + ret.w = 0; + else + ret.w = this->w - subtract.w; + return ret; + } + inline Color &operator-=(const Color &subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } + inline Color operator-(uint8_t subtract) const ALWAYS_INLINE { + return (*this) - Color(subtract, subtract, subtract, subtract); + } + inline Color &operator-=(uint8_t subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } + static Color random_color() { + float r = float(random_uint32()) / float(UINT32_MAX); + float g = float(random_uint32()) / float(UINT32_MAX); + float b = float(random_uint32()) / float(UINT32_MAX); + float w = float(random_uint32()) / float(UINT32_MAX); + return Color(r, g, b, w); + } + Color fade_to_white(uint8_t amnt) { return Color(1, 1, 1, 1) - (*this * amnt); } + Color fade_to_black(uint8_t amnt) { return *this * amnt; } + Color lighten(uint8_t delta) { return *this + delta; } + Color darken(uint8_t delta) { return *this - delta; } + + uint32_t to_rgb_565() const { + uint32_t color565 = + (esp_scale8(this->red, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->blue, 31) << 0); + return color565; + } + uint32_t to_bgr_565() const { + uint32_t color565 = + (esp_scale8(this->blue, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->red, 31) << 0); + return color565; + } + uint32_t to_grayscale4() const { + uint32_t gs4 = esp_scale8(this->white, 15); + return gs4; + } +}; +static const Color COLOR_BLACK(0, 0, 0); +static const Color COLOR_WHITE(1, 1, 1); +}; // namespace esphome diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f4151a14fc..d2e9607e28 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -177,7 +177,7 @@ const std::string &Nameable::get_object_id() { return this->object_id_; } bool Nameable::is_internal() const { return this->internal_; } void Nameable::set_internal(bool internal) { this->internal_ = internal; } void Nameable::calc_object_id_() { - this->object_id_ = sanitize_string_whitelist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_WHITELIST); + this->object_id_ = sanitize_string_allowlist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_ALLOWLIST); // FNV-1 hash this->object_id_hash_ = fnv1_hash(this->object_id_); } diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index 2389a2a7f6..30a1e9c49f 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -271,6 +271,22 @@ ISRInternalGPIOPin *GPIOPin::to_isr() const { this->gpio_read_, this->gpio_mask_, this->inverted_); } +void force_link_symbols() { +#ifdef ARDUINO_ARCH_ESP8266 + // Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible + // with their settings - ESPHome uses a different settings system (that can also survive + // erases). So set magic bytes indicating all tasmota versions are supported. + // This only adds 12 bytes of binary size, which is an acceptable price to pay for easier support + // for Tasmota. + // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/settings.ino#L346-L380 + // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/i18n.h#L652-L654 + const static uint32_t TASMOTA_MAGIC_BYTES[] PROGMEM = {0x5AA55AA5, 0xFFFFFFFF, 0xA55AA55A}; + // Force link symbol by using a volatile integer (GCC attribute used does not work because of LTO) + volatile int x = 0; + x = TASMOTA_MAGIC_BYTES[x]; +#endif +} + } // namespace esphome #ifdef ARDUINO_ESP8266_RELEASE_2_3_0 @@ -288,3 +304,14 @@ void *memchr(const void *s, int c, size_t n) { } }; #endif + +#ifdef ARDUINO_ARCH_ESP8266 +extern "C" { +extern void resetPins() { // NOLINT + // Added in framework 2.7.0 + // usually this sets up all pins to be in INPUT mode + // however, not strictly needed as we set up the pins properly + // ourselves and this causes pins to toggle during reboot. +} +} +#endif diff --git a/esphome/core/esphal.h b/esphome/core/esphal.h index 453e094acc..809c7d91b5 100644 --- a/esphome/core/esphal.h +++ b/esphome/core/esphal.h @@ -116,4 +116,12 @@ class GPIOPin { template void GPIOPin::attach_interrupt(void (*func)(T *), T *arg, int mode) const { this->attach_interrupt_(reinterpret_cast(func), arg, mode); } +/** This function can be used by the HAL to force-link specific symbols + * into the generated binary without modifying the linker script. + * + * It is called by the application very early on startup and should not be used for anything + * other than forcing symbols to be linked. + */ +void force_link_symbols(); + } // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2222a1a664..78a62a5e86 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -85,16 +85,16 @@ std::string to_lowercase_underscore(std::string s) { return s; } -std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist) { +std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist) { std::string out(s); out.erase(std::remove_if(out.begin(), out.end(), - [&whitelist](const char &c) { return whitelist.find(c) == std::string::npos; }), + [&allowlist](const char &c) { return allowlist.find(c) == std::string::npos; }), out.end()); return out; } std::string sanitize_hostname(const std::string &hostname) { - std::string s = sanitize_string_whitelist(hostname, HOSTNAME_CHARACTER_WHITELIST); + std::string s = sanitize_string_allowlist(hostname, HOSTNAME_CHARACTER_ALLOWLIST); return truncate_string(s, 63); } @@ -154,7 +154,7 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } -const char *HOSTNAME_CHARACTER_WHITELIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; +const char *HOSTNAME_CHARACTER_ALLOWLIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; uint8_t crc8(uint8_t *data, uint8_t len) { uint8_t crc = 0; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index ab3d883e05..0c660bdc8e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -24,7 +24,7 @@ namespace esphome { /// The characters that are allowed in a hostname. -extern const char *HOSTNAME_CHARACTER_WHITELIST; +extern const char *HOSTNAME_CHARACTER_ALLOWLIST; /// Gets the MAC address as a string, this can be used as way to identify this ESP. std::string get_mac_address(); @@ -43,7 +43,7 @@ std::string to_string(double val); std::string to_string(long double val); optional parse_float(const std::string &str); -/// Sanitize the hostname by removing characters that are not in the whitelist and truncating it to 63 chars. +/// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars. std::string sanitize_hostname(const std::string &hostname); /// Truncate a string to a specific length @@ -121,8 +121,8 @@ std::string uint64_to_string(uint64_t num); /// Convert a uint32_t to a hex string std::string uint32_to_string(uint32_t num); -/// Sanitizes the input string with the whitelist. -std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist); +/// Sanitizes the input string with the allowlist. +std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist); uint8_t reverse_bits_8(uint8_t x); uint16_t reverse_bits_16(uint16_t x); diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 15d49c0038..9b49a4c6ba 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -53,16 +53,6 @@ int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT if (log == nullptr) return 0; - size_t len = strlen(format); - if (format[len - 1] == '\n') { - // Remove trailing newline from format - // Use locally stored - static std::string FORMAT_COPY; - FORMAT_COPY.clear(); - FORMAT_COPY.insert(0, format, len - 1); - format = FORMAT_COPY.c_str(); - } - log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args); #endif return 0; diff --git a/esphome/core_config.py b/esphome/core_config.py index d447eaaffb..f167eb8d8b 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -5,17 +5,14 @@ import re import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation, pins -from esphome.const import ARDUINO_VERSION_ESP32_DEV, ARDUINO_VERSION_ESP8266_DEV, \ - CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, \ - CONF_COMMENT, CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ +from esphome.const import CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, \ + CONF_BUILD_PATH, CONF_COMMENT, CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_TRIGGER_ID, \ - CONF_ESP8266_RESTORE_FROM_FLASH, ARDUINO_VERSION_ESP8266_2_3_0, \ - ARDUINO_VERSION_ESP8266_2_5_0, ARDUINO_VERSION_ESP8266_2_5_1, ARDUINO_VERSION_ESP8266_2_5_2, \ - ESP_PLATFORMS + CONF_ESP8266_RESTORE_FROM_FLASH, ARDUINO_VERSION_ESP8266, \ + ARDUINO_VERSION_ESP32, ESP_PLATFORMS from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed, walk_files -from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS _LOGGER = logging.getLogger(__name__) @@ -46,30 +43,17 @@ def validate_board(value): validate_platform = cv.one_of(*ESP_PLATFORMS, upper=True) PLATFORMIO_ESP8266_LUT = { - '2.6.3': 'espressif8266@2.4.0', - '2.6.2': 'espressif8266@2.3.1', - '2.6.1': 'espressif8266@2.3.0', - '2.5.2': 'espressif8266@2.2.3', - '2.5.1': 'espressif8266@2.1.0', - '2.5.0': 'espressif8266@2.0.1', - '2.4.2': 'espressif8266@1.8.0', - '2.4.1': 'espressif8266@1.7.3', - '2.4.0': 'espressif8266@1.6.0', - '2.3.0': 'espressif8266@1.5.0', - 'RECOMMENDED': 'espressif8266@2.2.3', + **ARDUINO_VERSION_ESP8266, + 'RECOMMENDED': ARDUINO_VERSION_ESP8266['2.7.4'], 'LATEST': 'espressif8266', - 'DEV': ARDUINO_VERSION_ESP8266_DEV, + 'DEV': ARDUINO_VERSION_ESP8266['dev'], } PLATFORMIO_ESP32_LUT = { - '1.0.0': 'espressif32@1.4.0', - '1.0.1': 'espressif32@1.6.0', - '1.0.2': 'espressif32@1.9.0', - '1.0.3': 'espressif32@1.10.0', - '1.0.4': 'espressif32@1.12.1', - 'RECOMMENDED': 'espressif32@1.12.1', + **ARDUINO_VERSION_ESP32, + 'RECOMMENDED': ARDUINO_VERSION_ESP32['1.0.4'], 'LATEST': 'espressif32', - 'DEV': ARDUINO_VERSION_ESP32_DEV, + 'DEV': ARDUINO_VERSION_ESP32['dev'], } @@ -205,6 +189,26 @@ def add_includes(includes): include_file(path, basename) +@coroutine_with_priority(-1000.0) +def _esp8266_add_lwip_type(): + # If any component has already set this, do not change it + if any(flag.startswith('-DPIO_FRAMEWORK_ARDUINO_LWIP2_') for flag in CORE.build_flags): + return + + # Default for platformio is LWIP2_LOW_MEMORY with: + # - MSS=536 + # - LWIP_FEATURES enabled + # - this only adds some optional features like IP incoming packet reassembly and NAPT + # see also: + # https://github.com/esp8266/Arduino/blob/master/tools/sdk/lwip2/include/lwipopts.h + + # Instead we use LWIP2_HIGHER_BANDWIDTH_LOW_FLASH with: + # - MSS=1460 + # - LWIP_FEATURES disabled (because we don't need them) + # Other projects like Tasmota & ESPEasy also use this + cg.add_build_flag('-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH') + + @coroutine_with_priority(100.0) def to_code(config): cg.add_global(cg.global_ns.namespace('esphome').using) @@ -225,22 +229,9 @@ def to_code(config): yield cg.register_component(trigger, conf) yield automation.build_automation(trigger, [], conf) - # Build flags - if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES and \ - CORE.arduino_version != ARDUINO_VERSION_ESP8266_2_3_0: - flash_size = ESP8266_FLASH_SIZES[CORE.board] - ld_scripts = ESP8266_LD_SCRIPTS[flash_size] - ld_script = None - - if CORE.arduino_version in ('espressif8266@1.8.0', 'espressif8266@1.7.3', - 'espressif8266@1.6.0'): - ld_script = ld_scripts[0] - elif CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, ARDUINO_VERSION_ESP8266_2_5_0, - ARDUINO_VERSION_ESP8266_2_5_1, ARDUINO_VERSION_ESP8266_2_5_2): - ld_script = ld_scripts[1] - - if ld_script is not None: - cg.add_build_flag(f'-Wl,-T{ld_script}') + # Set LWIP build constants for ESP8266 + if CORE.is_esp8266: + CORE.add_job(_esp8266_add_lwip_type) cg.add_build_flag('-fno-exceptions') diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 4f2d63d545..c53c68b3a3 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -26,11 +26,11 @@ import tornado.web import tornado.websocket from esphome import const, util -from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path -from esphome.util import shlex_quote +from esphome.util import shlex_quote, get_serial_ports +from .util import password_hash # pylint: disable=unused-import, wrong-import-order from typing import Optional # noqa @@ -43,7 +43,7 @@ _LOGGER = logging.getLogger(__name__) class DashboardSettings: def __init__(self): self.config_dir = '' - self.password_digest = '' + self.password_hash = '' self.username = '' self.using_password = False self.on_hassio = False @@ -56,7 +56,7 @@ class DashboardSettings: self.username = args.username or os.getenv('USERNAME', '') self.using_password = bool(password) if self.using_password: - self.password_digest = hmac.new(password.encode()).digest() + self.password_hash = password_hash(password) self.config_dir = args.configuration[0] @property @@ -83,8 +83,11 @@ class DashboardSettings: if username != self.username: return False - password = hmac.new(password.encode()).digest() - return username == self.username and hmac.compare_digest(self.password_digest, password) + # Compare password in constant running time (to prevent timing attacks) + return hmac.compare_digest( + self.password_hash, + password_hash(password) + ) def rel_path(self, *args): return os.path.join(self.config_dir, *args) @@ -100,9 +103,15 @@ cookie_authenticated_yes = b'yes' def template_args(): version = const.__version__ + if 'b' in version: + docs_link = 'https://beta.esphome.io/' + elif 'dev' in version: + docs_link = 'https://next.esphome.io/' + else: + docs_link = 'https://www.esphome.io/' return { 'version': version, - 'docs_link': 'https://beta.esphome.io/' if 'b' in version else 'https://esphome.io/', + 'docs_link': docs_link, 'get_static_file_url': get_static_file_url, 'relative_url': settings.relative_url, 'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'), @@ -307,14 +316,15 @@ class SerialPortRequestHandler(BaseHandler): def get(self): ports = get_serial_ports() data = [] - for port, desc in ports: - if port == '/dev/ttyAMA0': + for port in ports: + desc = port.description + if port.path == '/dev/ttyAMA0': desc = 'UART pins on GPIO header' split_desc = desc.split(' - ') if len(split_desc) == 2 and split_desc[0] == split_desc[1]: # Some serial ports repeat their values desc = split_desc[0] - data.append({'port': port, 'desc': desc}) + data.append({'port': port.path, 'desc': desc}) data.append({'port': 'OTA', 'desc': 'Over-The-Air'}) data.sort(key=lambda x: x['port'], reverse=True) self.write(json.dumps(data)) @@ -440,7 +450,7 @@ class MainRequestHandler(BaseHandler): entries = _list_dashboard_entries() self.render("templates/index.html", entries=entries, begin=begin, - **template_args()) + **template_args(), login_enabled=settings.using_auth) def _ping_func(filename, address): diff --git a/esphome/dashboard/static/css/esphome.css b/esphome/dashboard/static/css/esphome.css index db0ac55985..116170f95d 100644 --- a/esphome/dashboard/static/css/esphome.css +++ b/esphome/dashboard/static/css/esphome.css @@ -163,6 +163,11 @@ main { color: black; } +nav { + height: auto; + line-height: normal; +} + .select-port-container { margin-top: 8px; margin-right: 10px; diff --git a/esphome/dashboard/static/js/esphome.js b/esphome/dashboard/static/js/esphome.js index f6e9979eb2..52dda446ce 100644 --- a/esphome/dashboard/static/js/esphome.js +++ b/esphome/dashboard/static/js/esphome.js @@ -5,6 +5,7 @@ $(document).ready(function () { M.AutoInit(document.body); nodeGrid(); startAceWebsocket(); + fixNavbarHeight(); }); // WebSocket URL Helper @@ -16,6 +17,17 @@ if (loc.protocol === "https:") { } const wsUrl = wsLoc.href; +/** + * Fix NavBar height + */ +const fixNavbarHeight = () => { + const fixFunc = () => { + const sel = $(".select-wrapper"); + $(".navbar-fixed").css("height", (sel.position().top + sel.outerHeight()) + "px"); + } + $(window).resize(fixFunc); + fixFunc(); +} /** * Dashboard Dynamic Grid @@ -735,6 +747,7 @@ document.querySelectorAll("[data-action='edit']").forEach((button) => { const closeButton = document.querySelector("#js-editor-modal [data-action='close']"); saveButton.setAttribute('data-filename', editorActiveFilename); uploadButton.setAttribute('data-filename', editorActiveFilename); + uploadButton.setAttribute('onClick', `saveFile("${editorActiveFilename}")`); if (editorActiveFilename === "secrets.yaml") { uploadButton.classList.add("disabled"); editorActiveSecrets = true; @@ -1002,4 +1015,3 @@ jQuery.validator.addMethod("nospaces", (value, element) => { jQuery.validator.addMethod("lowercase", (value, element) => { return value === value.toLowerCase(); }, "Name must be all lower case!"); - diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 1e546b037f..142bc2cd6f 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -31,8 +31,8 @@