mirror of
https://github.com/esphome/esphome.git
synced 2024-12-01 13:13:54 +01:00
Merge commit 'bb044a789c636ac787bdb6e53c5b20d851dd117c' into optolink
This commit is contained in:
commit
9f4e10cf55
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,2 +1,3 @@
|
|||||||
# Normalize line endings to LF in the repository
|
# Normalize line endings to LF in the repository
|
||||||
* text eol=lf
|
* text eol=lf
|
||||||
|
*.png binary
|
||||||
|
398
.github/workflows/ci.yml
vendored
398
.github/workflows/ci.yml
vendored
@ -12,60 +12,266 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEFAULT_PYTHON: "3.9"
|
||||||
|
PYUPGRADE_TARGET: "--py39-plus"
|
||||||
|
CLANG_FORMAT_VERSION: "13.0.1"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
common:
|
||||||
name: ${{ matrix.name }}
|
name: Create common environment
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v4.6.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Create Python virtual environment
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
python -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
python --version
|
||||||
|
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
yamllint:
|
||||||
|
name: yamllint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Run yamllint
|
||||||
|
uses: frenck/action-yamllint@v1.4.0
|
||||||
|
|
||||||
|
black:
|
||||||
|
name: Check black
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Run black
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
black --verbose esphome tests
|
||||||
|
- name: Suggested changes
|
||||||
|
run: script/ci-suggest-changes
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
flake8:
|
||||||
|
name: Check flake8
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Run flake8
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
flake8 esphome
|
||||||
|
- name: Suggested changes
|
||||||
|
run: script/ci-suggest-changes
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
pylint:
|
||||||
|
name: Check pylint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Run pylint
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pylint -f parseable --persistent=n esphome
|
||||||
|
- name: Suggested changes
|
||||||
|
run: script/ci-suggest-changes
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
pyupgrade:
|
||||||
|
name: Check pyupgrade
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Run pyupgrade
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f`
|
||||||
|
- name: Suggested changes
|
||||||
|
run: script/ci-suggest-changes
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
ci-custom:
|
||||||
|
name: Run script/ci-custom
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Register matcher
|
||||||
|
run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
|
||||||
|
- name: Run script/ci-custom
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
script/ci-custom.py
|
||||||
|
script/build_codeowners.py --check
|
||||||
|
|
||||||
|
pytest:
|
||||||
|
name: Run pytest
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Register matcher
|
||||||
|
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||||
|
- name: Run pytest
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pytest -vv --tb=native tests
|
||||||
|
|
||||||
|
clang-format:
|
||||||
|
name: Check clang-format
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Install clang-format
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install clang-format==${{ env.CLANG_FORMAT_VERSION }}
|
||||||
|
- name: Run clang-format
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
script/clang-format -i
|
||||||
|
git diff-index --quiet HEAD --
|
||||||
|
- name: Suggested changes
|
||||||
|
run: script/ci-suggest-changes
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
compile-tests:
|
||||||
|
name: Run YAML test ${{ matrix.file }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
- black
|
||||||
|
- ci-custom
|
||||||
|
- clang-format
|
||||||
|
- flake8
|
||||||
|
- pylint
|
||||||
|
- pytest
|
||||||
|
- pyupgrade
|
||||||
|
- yamllint
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 5
|
max-parallel: 2
|
||||||
|
matrix:
|
||||||
|
file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8]
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v3.5.2
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
uses: actions/cache/restore@v3.3.1
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
|
- name: Cache platformio
|
||||||
|
uses: actions/cache@v3.3.1
|
||||||
|
with:
|
||||||
|
path: ~/.platformio
|
||||||
|
# yamllint disable-line rule:line-length
|
||||||
|
key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }}
|
||||||
|
- name: Run esphome compile tests/test${{ matrix.file }}.yaml
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
esphome compile tests/test${{ matrix.file }}.yaml
|
||||||
|
|
||||||
|
clang-tidy:
|
||||||
|
name: ${{ matrix.name }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- common
|
||||||
|
- black
|
||||||
|
- ci-custom
|
||||||
|
- clang-format
|
||||||
|
- flake8
|
||||||
|
- pylint
|
||||||
|
- pytest
|
||||||
|
- pyupgrade
|
||||||
|
- yamllint
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 2
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- id: ci-custom
|
|
||||||
name: Run script/ci-custom
|
|
||||||
- id: lint-python
|
|
||||||
name: Run script/lint-python
|
|
||||||
- id: test
|
|
||||||
file: tests/test1.yaml
|
|
||||||
name: Test tests/test1.yaml
|
|
||||||
pio_cache_key: test1
|
|
||||||
- id: test
|
|
||||||
file: tests/test2.yaml
|
|
||||||
name: Test tests/test2.yaml
|
|
||||||
pio_cache_key: test2
|
|
||||||
- id: test
|
|
||||||
file: tests/test3.yaml
|
|
||||||
name: Test tests/test3.yaml
|
|
||||||
pio_cache_key: test3
|
|
||||||
- id: test
|
|
||||||
file: tests/test3.1.yaml
|
|
||||||
name: Test tests/test3.1.yaml
|
|
||||||
pio_cache_key: test3.1
|
|
||||||
- id: test
|
|
||||||
file: tests/test4.yaml
|
|
||||||
name: Test tests/test4.yaml
|
|
||||||
pio_cache_key: test4
|
|
||||||
- id: test
|
|
||||||
file: tests/test5.yaml
|
|
||||||
name: Test tests/test5.yaml
|
|
||||||
pio_cache_key: test5
|
|
||||||
- id: test
|
|
||||||
file: tests/test6.yaml
|
|
||||||
name: Test tests/test6.yaml
|
|
||||||
pio_cache_key: test6
|
|
||||||
- id: test
|
|
||||||
file: tests/test7.yaml
|
|
||||||
name: Test tests/test7.yaml
|
|
||||||
pio_cache_key: test7
|
|
||||||
- id: pytest
|
|
||||||
name: Run pytest
|
|
||||||
- id: clang-format
|
|
||||||
name: Run script/clang-format
|
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ESP8266
|
name: Run script/clang-tidy for ESP8266
|
||||||
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
|
||||||
@ -90,119 +296,65 @@ jobs:
|
|||||||
name: Run script/clang-tidy for ESP32 IDF
|
name: Run script/clang-tidy for ESP32 IDF
|
||||||
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
options: --environment esp32-idf-tidy --grep USE_ESP_IDF
|
||||||
pio_cache_key: tidyesp32-idf
|
pio_cache_key: tidyesp32-idf
|
||||||
- id: yamllint
|
|
||||||
name: Run yamllint
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Check out code from GitHub
|
||||||
- name: Set up Python
|
uses: actions/checkout@v3.5.2
|
||||||
uses: actions/setup-python@v4
|
- name: Restore Python virtual environment
|
||||||
id: python
|
uses: actions/cache/restore@v3.3.1
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
path: venv
|
||||||
|
|
||||||
- name: Cache virtualenv
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: .venv
|
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }}
|
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
||||||
restore-keys: |
|
# Use per check platformio cache because checks use different parts
|
||||||
venv-${{ steps.python.outputs.python-version }}-
|
|
||||||
|
|
||||||
- name: Set up virtualenv
|
|
||||||
# yamllint disable rule:line-length
|
|
||||||
run: |
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
pip install -U pip
|
|
||||||
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
|
|
||||||
pip install -e .
|
|
||||||
echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH
|
|
||||||
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> $GITHUB_ENV
|
|
||||||
# yamllint enable rule:line-length
|
|
||||||
|
|
||||||
# Use per check platformio cache because checks use different parts
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ~/.platformio
|
path: ~/.platformio
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||||
if: matrix.id == 'test' || matrix.id == 'clang-tidy'
|
|
||||||
|
|
||||||
- name: Install clang tools
|
- name: Install clang-tidy
|
||||||
run: |
|
run: sudo apt-get install clang-tidy-11
|
||||||
sudo apt-get install \
|
|
||||||
clang-format-13 \
|
|
||||||
clang-tidy-11
|
|
||||||
if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format'
|
|
||||||
|
|
||||||
- name: Register problem matchers
|
- name: Register problem matchers
|
||||||
run: |
|
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"
|
|
||||||
echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
|
||||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||||
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
|
||||||
|
|
||||||
- name: Lint Custom
|
|
||||||
run: |
|
|
||||||
script/ci-custom.py
|
|
||||||
script/build_codeowners.py --check
|
|
||||||
if: matrix.id == 'ci-custom'
|
|
||||||
|
|
||||||
- name: Lint Python
|
|
||||||
run: script/lint-python -a
|
|
||||||
if: matrix.id == 'lint-python'
|
|
||||||
|
|
||||||
- run: esphome compile ${{ matrix.file }}
|
|
||||||
if: matrix.id == 'test'
|
|
||||||
env:
|
|
||||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
|
||||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
|
||||||
|
|
||||||
- name: Run pytest
|
|
||||||
run: |
|
|
||||||
pytest -vv --tb=native tests
|
|
||||||
if: matrix.id == 'pytest'
|
|
||||||
|
|
||||||
# Also run git-diff-index so that the step is marked as failed on
|
|
||||||
# formatting errors, since clang-format doesn't do anything but
|
|
||||||
# change files if -i is passed.
|
|
||||||
- name: Run clang-format
|
|
||||||
run: |
|
|
||||||
script/clang-format -i
|
|
||||||
git diff-index --quiet HEAD --
|
|
||||||
if: matrix.id == 'clang-format'
|
|
||||||
|
|
||||||
- name: Run clang-tidy
|
- name: Run clang-tidy
|
||||||
run: |
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
script/clang-tidy --all-headers --fix ${{ matrix.options }}
|
script/clang-tidy --all-headers --fix ${{ matrix.options }}
|
||||||
if: matrix.id == 'clang-tidy'
|
|
||||||
env:
|
env:
|
||||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||||
|
|
||||||
- name: Run yamllint
|
|
||||||
if: matrix.id == 'yamllint'
|
|
||||||
uses: frenck/action-yamllint@v1.4.0
|
|
||||||
|
|
||||||
- name: Suggested changes
|
- name: Suggested changes
|
||||||
run: script/ci-suggest-changes
|
run: script/ci-suggest-changes
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python')
|
if: always()
|
||||||
|
|
||||||
ci-status:
|
ci-status:
|
||||||
name: CI Status
|
name: CI Status
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ci]
|
needs:
|
||||||
|
- common
|
||||||
|
- black
|
||||||
|
- ci-custom
|
||||||
|
- clang-format
|
||||||
|
- flake8
|
||||||
|
- pylint
|
||||||
|
- pytest
|
||||||
|
- pyupgrade
|
||||||
|
- yamllint
|
||||||
|
- compile-tests
|
||||||
|
- clang-tidy
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- name: Successful deploy
|
- name: Success
|
||||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
run: exit 0
|
run: exit 0
|
||||||
- name: Failing deploy
|
- name: Failure
|
||||||
if: ${{ contains(needs.*.result, 'failure') }}
|
if: ${{ contains(needs.*.result, 'failure') }}
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
@ -27,7 +27,7 @@ repos:
|
|||||||
- --branch=release
|
- --branch=release
|
||||||
- --branch=beta
|
- --branch=beta
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.3.2
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py39-plus]
|
args: [--py39-plus]
|
||||||
|
@ -107,6 +107,7 @@ esphome/components/hbridge/fan/* @WeekendWarrior
|
|||||||
esphome/components/hbridge/light/* @DotNetDann
|
esphome/components/hbridge/light/* @DotNetDann
|
||||||
esphome/components/heatpumpir/* @rob-deutsch
|
esphome/components/heatpumpir/* @rob-deutsch
|
||||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||||
|
esphome/components/hm3301/* @freekode
|
||||||
esphome/components/homeassistant/* @OttoWinter
|
esphome/components/homeassistant/* @OttoWinter
|
||||||
esphome/components/honeywellabp/* @RubyBailey
|
esphome/components/honeywellabp/* @RubyBailey
|
||||||
esphome/components/host/* @esphome/core
|
esphome/components/host/* @esphome/core
|
||||||
@ -221,6 +222,7 @@ esphome/components/restart/* @esphome/core
|
|||||||
esphome/components/rf_bridge/* @jesserockz
|
esphome/components/rf_bridge/* @jesserockz
|
||||||
esphome/components/rgbct/* @jesserockz
|
esphome/components/rgbct/* @jesserockz
|
||||||
esphome/components/rp2040/* @jesserockz
|
esphome/components/rp2040/* @jesserockz
|
||||||
|
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
||||||
esphome/components/rp2040_pwm/* @jesserockz
|
esphome/components/rp2040_pwm/* @jesserockz
|
||||||
esphome/components/rtttl/* @glmnet
|
esphome/components/rtttl/* @glmnet
|
||||||
esphome/components/safe_mode/* @jsuanet @paulmonigatti
|
esphome/components/safe_mode/* @jsuanet @paulmonigatti
|
||||||
@ -282,6 +284,7 @@ esphome/components/tlc5947/* @rnauber
|
|||||||
esphome/components/tm1621/* @Philippe12
|
esphome/components/tm1621/* @Philippe12
|
||||||
esphome/components/tm1637/* @glmnet
|
esphome/components/tm1637/* @glmnet
|
||||||
esphome/components/tm1638/* @skykingjwc
|
esphome/components/tm1638/* @skykingjwc
|
||||||
|
esphome/components/tm1651/* @freekode
|
||||||
esphome/components/tmp102/* @timsavage
|
esphome/components/tmp102/* @timsavage
|
||||||
esphome/components/tmp117/* @Azimath
|
esphome/components/tmp117/* @Azimath
|
||||||
esphome/components/tof10120/* @wstrzalka
|
esphome/components/tof10120/* @wstrzalka
|
||||||
|
@ -52,7 +52,7 @@ RUN \
|
|||||||
# Ubuntu python3-pip is missing wheel
|
# Ubuntu python3-pip is missing wheel
|
||||||
pip3 install --no-cache-dir \
|
pip3 install --no-cache-dir \
|
||||||
wheel==0.37.1 \
|
wheel==0.37.1 \
|
||||||
platformio==6.1.6 \
|
platformio==6.1.7 \
|
||||||
# Change some platformio settings
|
# Change some platformio settings
|
||||||
&& platformio settings set enable_telemetry No \
|
&& platformio settings set enable_telemetry No \
|
||||||
&& platformio settings set check_platformio_interval 1000000 \
|
&& platformio settings set check_platformio_interval 1000000 \
|
||||||
|
@ -18,6 +18,9 @@ from esphome.const import (
|
|||||||
CONF_LOGGER,
|
CONF_LOGGER,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_OTA,
|
CONF_OTA,
|
||||||
|
CONF_MQTT,
|
||||||
|
CONF_MDNS,
|
||||||
|
CONF_DISABLED,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
@ -42,7 +45,7 @@ from esphome.log import color, setup_log, Fore
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def choose_prompt(options):
|
def choose_prompt(options, purpose: str = None):
|
||||||
if not options:
|
if not options:
|
||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
"Found no valid options for upload/logging, please make sure relevant "
|
"Found no valid options for upload/logging, please make sure relevant "
|
||||||
@ -53,7 +56,9 @@ def choose_prompt(options):
|
|||||||
if len(options) == 1:
|
if len(options) == 1:
|
||||||
return options[0][1]
|
return options[0][1]
|
||||||
|
|
||||||
safe_print("Found multiple options, please choose one:")
|
safe_print(
|
||||||
|
f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:'
|
||||||
|
)
|
||||||
for i, (desc, _) in enumerate(options):
|
for i, (desc, _) in enumerate(options):
|
||||||
safe_print(f" [{i+1}] {desc}")
|
safe_print(f" [{i+1}] {desc}")
|
||||||
|
|
||||||
@ -72,7 +77,9 @@ def choose_prompt(options):
|
|||||||
return options[opt - 1][1]
|
return options[opt - 1][1]
|
||||||
|
|
||||||
|
|
||||||
def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api):
|
def choose_upload_log_host(
|
||||||
|
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
|
||||||
|
):
|
||||||
options = []
|
options = []
|
||||||
for port in get_serial_ports():
|
for port in get_serial_ports():
|
||||||
options.append((f"{port.path} ({port.description})", port.path))
|
options.append((f"{port.path} ({port.description})", port.path))
|
||||||
@ -80,7 +87,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api
|
|||||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||||
if default == "OTA":
|
if default == "OTA":
|
||||||
return CORE.address
|
return CORE.address
|
||||||
if show_mqtt and "mqtt" in CORE.config:
|
if show_mqtt and CONF_MQTT in CORE.config:
|
||||||
options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
|
options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
|
||||||
if default == "OTA":
|
if default == "OTA":
|
||||||
return "MQTT"
|
return "MQTT"
|
||||||
@ -88,7 +95,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api
|
|||||||
return default
|
return default
|
||||||
if check_default is not None and check_default in [opt[1] for opt in options]:
|
if check_default is not None and check_default in [opt[1] for opt in options]:
|
||||||
return check_default
|
return check_default
|
||||||
return choose_prompt(options)
|
return choose_prompt(options, purpose=purpose)
|
||||||
|
|
||||||
|
|
||||||
def get_port_type(port):
|
def get_port_type(port):
|
||||||
@ -288,19 +295,30 @@ def upload_program(config, args, host):
|
|||||||
|
|
||||||
return 1 # Unknown target platform
|
return 1 # Unknown target platform
|
||||||
|
|
||||||
from esphome import espota2
|
|
||||||
|
|
||||||
if CONF_OTA not in config:
|
if CONF_OTA not in config:
|
||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
"Cannot upload Over the Air as the config does not include the ota: "
|
"Cannot upload Over the Air as the config does not include the ota: "
|
||||||
"component"
|
"component"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from esphome import espota2
|
||||||
|
|
||||||
ota_conf = config[CONF_OTA]
|
ota_conf = config[CONF_OTA]
|
||||||
remote_port = ota_conf[CONF_PORT]
|
remote_port = ota_conf[CONF_PORT]
|
||||||
password = ota_conf.get(CONF_PASSWORD, "")
|
password = ota_conf.get(CONF_PASSWORD, "")
|
||||||
|
|
||||||
|
if (
|
||||||
|
get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]
|
||||||
|
) and CONF_MQTT in config:
|
||||||
|
from esphome import mqtt
|
||||||
|
|
||||||
|
host = mqtt.get_esphome_device_ip(
|
||||||
|
config, args.username, args.password, args.client_id
|
||||||
|
)
|
||||||
|
|
||||||
if getattr(args, "file", None) is not None:
|
if getattr(args, "file", None) is not None:
|
||||||
return espota2.run_ota(host, remote_port, password, args.file)
|
return espota2.run_ota(host, remote_port, password, args.file)
|
||||||
|
|
||||||
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
|
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
|
||||||
|
|
||||||
|
|
||||||
@ -310,6 +328,13 @@ def show_logs(config, args, port):
|
|||||||
if get_port_type(port) == "SERIAL":
|
if get_port_type(port) == "SERIAL":
|
||||||
return run_miniterm(config, port)
|
return run_miniterm(config, port)
|
||||||
if get_port_type(port) == "NETWORK" and "api" in config:
|
if get_port_type(port) == "NETWORK" and "api" in config:
|
||||||
|
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
|
||||||
|
from esphome import mqtt
|
||||||
|
|
||||||
|
port = mqtt.get_esphome_device_ip(
|
||||||
|
config, args.username, args.password, args.client_id
|
||||||
|
)
|
||||||
|
|
||||||
from esphome.components.api.client import run_logs
|
from esphome.components.api.client import run_logs
|
||||||
|
|
||||||
return run_logs(config, port)
|
return run_logs(config, port)
|
||||||
@ -374,6 +399,7 @@ def command_upload(args, config):
|
|||||||
show_ota=True,
|
show_ota=True,
|
||||||
show_mqtt=False,
|
show_mqtt=False,
|
||||||
show_api=False,
|
show_api=False,
|
||||||
|
purpose="uploading",
|
||||||
)
|
)
|
||||||
exit_code = upload_program(config, args, port)
|
exit_code = upload_program(config, args, port)
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
@ -382,6 +408,15 @@ def command_upload(args, config):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def command_discover(args, config):
|
||||||
|
if "mqtt" in config:
|
||||||
|
from esphome import mqtt
|
||||||
|
|
||||||
|
return mqtt.show_discover(config, args.username, args.password, args.client_id)
|
||||||
|
|
||||||
|
raise EsphomeError("No discover method configured (mqtt)")
|
||||||
|
|
||||||
|
|
||||||
def command_logs(args, config):
|
def command_logs(args, config):
|
||||||
port = choose_upload_log_host(
|
port = choose_upload_log_host(
|
||||||
default=args.device,
|
default=args.device,
|
||||||
@ -389,6 +424,7 @@ def command_logs(args, config):
|
|||||||
show_ota=False,
|
show_ota=False,
|
||||||
show_mqtt=True,
|
show_mqtt=True,
|
||||||
show_api=True,
|
show_api=True,
|
||||||
|
purpose="logging",
|
||||||
)
|
)
|
||||||
return show_logs(config, args, port)
|
return show_logs(config, args, port)
|
||||||
|
|
||||||
@ -407,6 +443,7 @@ def command_run(args, config):
|
|||||||
show_ota=True,
|
show_ota=True,
|
||||||
show_mqtt=False,
|
show_mqtt=False,
|
||||||
show_api=True,
|
show_api=True,
|
||||||
|
purpose="uploading",
|
||||||
)
|
)
|
||||||
exit_code = upload_program(config, args, port)
|
exit_code = upload_program(config, args, port)
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
@ -420,6 +457,7 @@ def command_run(args, config):
|
|||||||
show_ota=False,
|
show_ota=False,
|
||||||
show_mqtt=True,
|
show_mqtt=True,
|
||||||
show_api=True,
|
show_api=True,
|
||||||
|
purpose="logging",
|
||||||
)
|
)
|
||||||
return show_logs(config, args, port)
|
return show_logs(config, args, port)
|
||||||
|
|
||||||
@ -623,6 +661,7 @@ POST_CONFIG_ACTIONS = {
|
|||||||
"clean": command_clean,
|
"clean": command_clean,
|
||||||
"idedata": command_idedata,
|
"idedata": command_idedata,
|
||||||
"rename": command_rename,
|
"rename": command_rename,
|
||||||
|
"discover": command_discover,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -711,6 +750,15 @@ def parse_args(argv):
|
|||||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser_discover = subparsers.add_parser(
|
||||||
|
"discover",
|
||||||
|
help="Validate the configuration and show all discovered devices.",
|
||||||
|
parents=[mqtt_options],
|
||||||
|
)
|
||||||
|
parser_discover.add_argument(
|
||||||
|
"configuration", help="Your YAML configuration file.", nargs=1
|
||||||
|
)
|
||||||
|
|
||||||
parser_run = subparsers.add_parser(
|
parser_run = subparsers.add_parser(
|
||||||
"run",
|
"run",
|
||||||
help="Validate the configuration, create a binary, upload it, and start logs.",
|
help="Validate the configuration, create a binary, upload it, and start logs.",
|
||||||
@ -932,6 +980,8 @@ def run_esphome(argv):
|
|||||||
_LOGGER.error(e, exc_info=args.verbose)
|
_LOGGER.error(e, exc_info=args.verbose)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
safe_print(f"ESPHome {const.__version__}")
|
||||||
|
|
||||||
for conf_path in args.configuration:
|
for conf_path in args.configuration:
|
||||||
if any(os.path.basename(conf_path) == x for x in SECRETS_FILES):
|
if any(os.path.basename(conf_path) == x for x in SECRETS_FILES):
|
||||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||||
|
@ -3,6 +3,7 @@ import logging
|
|||||||
from esphome import core
|
from esphome import core
|
||||||
from esphome.components import display, font
|
from esphome.components import display, font
|
||||||
import esphome.components.image as espImage
|
import esphome.components.image as espImage
|
||||||
|
from esphome.components.image import CONF_USE_TRANSPARENCY
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE
|
from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE
|
||||||
@ -15,16 +16,42 @@ MULTI_CONF = True
|
|||||||
|
|
||||||
Animation_ = display.display_ns.class_("Animation", espImage.Image_)
|
Animation_ = display.display_ns.class_("Animation", espImage.Image_)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_cross_dependencies(config):
|
||||||
|
"""
|
||||||
|
Validate fields whose possible values depend on other fields.
|
||||||
|
For example, validate that explicitly transparent image types
|
||||||
|
have "use_transparency" set to True.
|
||||||
|
Also set the default value for those kind of dependent fields.
|
||||||
|
"""
|
||||||
|
image_type = config[CONF_TYPE]
|
||||||
|
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
|
||||||
|
# If the use_transparency option was not specified, set the default depending on the image type
|
||||||
|
if CONF_USE_TRANSPARENCY not in config:
|
||||||
|
config[CONF_USE_TRANSPARENCY] = is_transparent_type
|
||||||
|
|
||||||
|
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
|
||||||
|
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
ANIMATION_SCHEMA = cv.Schema(
|
ANIMATION_SCHEMA = cv.Schema(
|
||||||
{
|
cv.All(
|
||||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
{
|
||||||
cv.Required(CONF_FILE): cv.file_,
|
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
cv.Required(CONF_FILE): cv.file_,
|
||||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
espImage.IMAGE_TYPE, upper=True
|
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
|
||||||
),
|
espImage.IMAGE_TYPE, upper=True
|
||||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
),
|
||||||
}
|
# Not setting default here on purpose; the default depends on the image type,
|
||||||
|
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||||
|
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||||
|
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||||
|
},
|
||||||
|
validate_cross_dependencies,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
|
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
|
||||||
@ -50,16 +77,19 @@ async def to_code(config):
|
|||||||
else:
|
else:
|
||||||
if width > 500 or height > 500:
|
if width > 500 or height > 500:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"The image you requested is very big. Please consider using"
|
'The image "%s" you requested is very big. Please consider'
|
||||||
" the resize parameter."
|
" using the resize parameter.",
|
||||||
|
path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
transparent = config[CONF_USE_TRANSPARENCY]
|
||||||
|
|
||||||
if config[CONF_TYPE] == "GRAYSCALE":
|
if config[CONF_TYPE] == "GRAYSCALE":
|
||||||
data = [0 for _ in range(height * width * frames)]
|
data = [0 for _ in range(height * width * frames)]
|
||||||
pos = 0
|
pos = 0
|
||||||
for frameIndex in range(frames):
|
for frameIndex in range(frames):
|
||||||
image.seek(frameIndex)
|
image.seek(frameIndex)
|
||||||
frame = image.convert("L", dither=Image.NONE)
|
frame = image.convert("LA", dither=Image.NONE)
|
||||||
if CONF_RESIZE in config:
|
if CONF_RESIZE in config:
|
||||||
frame = frame.resize([width, height])
|
frame = frame.resize([width, height])
|
||||||
pixels = list(frame.getdata())
|
pixels = list(frame.getdata())
|
||||||
@ -67,16 +97,22 @@ async def to_code(config):
|
|||||||
raise core.EsphomeError(
|
raise core.EsphomeError(
|
||||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
|
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
|
||||||
)
|
)
|
||||||
for pix in pixels:
|
for pix, a in pixels:
|
||||||
|
if transparent:
|
||||||
|
if pix == 1:
|
||||||
|
pix = 0
|
||||||
|
if a < 0x80:
|
||||||
|
pix = 1
|
||||||
|
|
||||||
data[pos] = pix
|
data[pos] = pix
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
elif config[CONF_TYPE] == "RGB24":
|
elif config[CONF_TYPE] == "RGBA":
|
||||||
data = [0 for _ in range(height * width * 3 * frames)]
|
data = [0 for _ in range(height * width * 4 * frames)]
|
||||||
pos = 0
|
pos = 0
|
||||||
for frameIndex in range(frames):
|
for frameIndex in range(frames):
|
||||||
image.seek(frameIndex)
|
image.seek(frameIndex)
|
||||||
frame = image.convert("RGB")
|
frame = image.convert("RGBA")
|
||||||
if CONF_RESIZE in config:
|
if CONF_RESIZE in config:
|
||||||
frame = frame.resize([width, height])
|
frame = frame.resize([width, height])
|
||||||
pixels = list(frame.getdata())
|
pixels = list(frame.getdata())
|
||||||
@ -91,13 +127,15 @@ async def to_code(config):
|
|||||||
pos += 1
|
pos += 1
|
||||||
data[pos] = pix[2]
|
data[pos] = pix[2]
|
||||||
pos += 1
|
pos += 1
|
||||||
|
data[pos] = pix[3]
|
||||||
|
pos += 1
|
||||||
|
|
||||||
elif config[CONF_TYPE] == "RGB565":
|
elif config[CONF_TYPE] == "RGB24":
|
||||||
data = [0 for _ in range(height * width * 2 * frames)]
|
data = [0 for _ in range(height * width * 3 * frames)]
|
||||||
pos = 0
|
pos = 0
|
||||||
for frameIndex in range(frames):
|
for frameIndex in range(frames):
|
||||||
image.seek(frameIndex)
|
image.seek(frameIndex)
|
||||||
frame = image.convert("RGB")
|
frame = image.convert("RGBA")
|
||||||
if CONF_RESIZE in config:
|
if CONF_RESIZE in config:
|
||||||
frame = frame.resize([width, height])
|
frame = frame.resize([width, height])
|
||||||
pixels = list(frame.getdata())
|
pixels = list(frame.getdata())
|
||||||
@ -105,14 +143,50 @@ async def to_code(config):
|
|||||||
raise core.EsphomeError(
|
raise core.EsphomeError(
|
||||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
|
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
|
||||||
)
|
)
|
||||||
for pix in pixels:
|
for r, g, b, a in pixels:
|
||||||
R = pix[0] >> 3
|
if transparent:
|
||||||
G = pix[1] >> 2
|
if r == 0 and g == 0 and b == 1:
|
||||||
B = pix[2] >> 3
|
b = 0
|
||||||
|
if a < 0x80:
|
||||||
|
r = 0
|
||||||
|
g = 0
|
||||||
|
b = 1
|
||||||
|
|
||||||
|
data[pos] = r
|
||||||
|
pos += 1
|
||||||
|
data[pos] = g
|
||||||
|
pos += 1
|
||||||
|
data[pos] = b
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
|
||||||
|
data = [0 for _ in range(height * width * 2 * frames)]
|
||||||
|
pos = 0
|
||||||
|
for frameIndex in range(frames):
|
||||||
|
image.seek(frameIndex)
|
||||||
|
frame = image.convert("RGBA")
|
||||||
|
if CONF_RESIZE in config:
|
||||||
|
frame = frame.resize([width, height])
|
||||||
|
pixels = list(frame.getdata())
|
||||||
|
if len(pixels) != height * width:
|
||||||
|
raise core.EsphomeError(
|
||||||
|
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
|
||||||
|
)
|
||||||
|
for r, g, b, a in pixels:
|
||||||
|
R = r >> 3
|
||||||
|
G = g >> 2
|
||||||
|
B = b >> 3
|
||||||
rgb = (R << 11) | (G << 5) | B
|
rgb = (R << 11) | (G << 5) | B
|
||||||
|
|
||||||
|
if transparent:
|
||||||
|
if rgb == 0x0020:
|
||||||
|
rgb = 0
|
||||||
|
if a < 0x80:
|
||||||
|
rgb = 0x0020
|
||||||
|
|
||||||
data[pos] = rgb >> 8
|
data[pos] = rgb >> 8
|
||||||
pos += 1
|
pos += 1
|
||||||
data[pos] = rgb & 255
|
data[pos] = rgb & 0xFF
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
|
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
|
||||||
@ -120,19 +194,31 @@ async def to_code(config):
|
|||||||
data = [0 for _ in range((height * width8 // 8) * frames)]
|
data = [0 for _ in range((height * width8 // 8) * frames)]
|
||||||
for frameIndex in range(frames):
|
for frameIndex in range(frames):
|
||||||
image.seek(frameIndex)
|
image.seek(frameIndex)
|
||||||
|
if transparent:
|
||||||
|
alpha = image.split()[-1]
|
||||||
|
has_alpha = alpha.getextrema()[0] < 0xFF
|
||||||
frame = image.convert("1", dither=Image.NONE)
|
frame = image.convert("1", dither=Image.NONE)
|
||||||
if CONF_RESIZE in config:
|
if CONF_RESIZE in config:
|
||||||
frame = frame.resize([width, height])
|
frame = frame.resize([width, height])
|
||||||
for y in range(height):
|
if transparent:
|
||||||
for x in range(width):
|
alpha = alpha.resize([width, height])
|
||||||
if frame.getpixel((x, y)):
|
for x, y in [(i, j) for i in range(width) for j in range(height)]:
|
||||||
|
if transparent and has_alpha:
|
||||||
|
if not alpha.getpixel((x, y)):
|
||||||
continue
|
continue
|
||||||
pos = x + y * width8 + (height * width8 * frameIndex)
|
elif frame.getpixel((x, y)):
|
||||||
data[pos // 8] |= 0x80 >> (pos % 8)
|
continue
|
||||||
|
|
||||||
|
pos = x + y * width8 + (height * width8 * frameIndex)
|
||||||
|
data[pos // 8] |= 0x80 >> (pos % 8)
|
||||||
|
else:
|
||||||
|
raise core.EsphomeError(
|
||||||
|
f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}."
|
||||||
|
)
|
||||||
|
|
||||||
rhs = [HexInt(x) for x in data]
|
rhs = [HexInt(x) for x in data]
|
||||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||||
cg.new_Pvariable(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID],
|
config[CONF_ID],
|
||||||
prog_arr,
|
prog_arr,
|
||||||
width,
|
width,
|
||||||
@ -140,3 +226,4 @@ async def to_code(config):
|
|||||||
frames,
|
frames,
|
||||||
espImage.IMAGE_TYPE[config[CONF_TYPE]],
|
espImage.IMAGE_TYPE[config[CONF_TYPE]],
|
||||||
)
|
)
|
||||||
|
cg.add(var.set_transparency(transparent))
|
||||||
|
@ -140,8 +140,9 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
|
|||||||
/** Stop the cover.
|
/** Stop the cover.
|
||||||
*
|
*
|
||||||
* This is a legacy method and may be removed later, please use `.make_call()` instead.
|
* This is a legacy method and may be removed later, please use `.make_call()` instead.
|
||||||
|
* As per solution from issue #2885 the call should include perform()
|
||||||
*/
|
*/
|
||||||
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9")
|
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
void add_on_state_callback(std::function<void()> &&f);
|
void add_on_state_callback(std::function<void()> &&f);
|
||||||
|
@ -12,7 +12,7 @@ namespace display {
|
|||||||
|
|
||||||
static const char *const TAG = "display";
|
static const char *const TAG = "display";
|
||||||
|
|
||||||
const Color COLOR_OFF(0, 0, 0, 0);
|
const Color COLOR_OFF(0, 0, 0, 255);
|
||||||
const Color COLOR_ON(255, 255, 255, 255);
|
const Color COLOR_ON(255, 255, 255, 255);
|
||||||
|
|
||||||
void Rect::expand(int16_t horizontal, int16_t vertical) {
|
void Rect::expand(int16_t horizontal, int16_t vertical) {
|
||||||
@ -307,40 +307,58 @@ void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign al
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) {
|
void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) {
|
||||||
|
bool transparent = image->has_transparency();
|
||||||
|
|
||||||
switch (image->get_type()) {
|
switch (image->get_type()) {
|
||||||
case IMAGE_TYPE_BINARY:
|
case IMAGE_TYPE_BINARY: {
|
||||||
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
||||||
for (int img_y = 0; img_y < image->get_height(); img_y++) {
|
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);
|
if (image->get_pixel(img_x, img_y)) {
|
||||||
|
this->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||||
|
} else if (!transparent) {
|
||||||
|
this->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case IMAGE_TYPE_GRAYSCALE:
|
case IMAGE_TYPE_GRAYSCALE:
|
||||||
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
||||||
for (int img_y = 0; img_y < image->get_height(); img_y++) {
|
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));
|
auto color = image->get_grayscale_pixel(img_x, img_y);
|
||||||
}
|
if (color.w >= 0x80) {
|
||||||
}
|
this->draw_pixel_at(x + img_x, y + img_y, color);
|
||||||
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;
|
|
||||||
case IMAGE_TYPE_TRANSPARENT_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++) {
|
|
||||||
if (image->get_pixel(img_x, img_y))
|
|
||||||
this->draw_pixel_at(x + img_x, y + img_y, color_on);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case IMAGE_TYPE_RGB565:
|
case IMAGE_TYPE_RGB565:
|
||||||
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
||||||
for (int img_y = 0; img_y < image->get_height(); img_y++) {
|
for (int img_y = 0; img_y < image->get_height(); img_y++) {
|
||||||
this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y));
|
auto color = image->get_rgb565_pixel(img_x, img_y);
|
||||||
|
if (color.w >= 0x80) {
|
||||||
|
this->draw_pixel_at(x + img_x, y + img_y, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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++) {
|
||||||
|
auto color = image->get_color_pixel(img_x, img_y);
|
||||||
|
if (color.w >= 0x80) {
|
||||||
|
this->draw_pixel_at(x + img_x, y + img_y, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IMAGE_TYPE_RGBA:
|
||||||
|
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
||||||
|
for (int img_y = 0; img_y < image->get_height(); img_y++) {
|
||||||
|
auto color = image->get_rgba_pixel(img_x, img_y);
|
||||||
|
if (color.w >= 0x80) {
|
||||||
|
this->draw_pixel_at(x + img_x, y + img_y, color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -629,14 +647,27 @@ bool Image::get_pixel(int x, int y) const {
|
|||||||
const uint32_t pos = x + y * width_8;
|
const uint32_t pos = x + y * width_8;
|
||||||
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||||
}
|
}
|
||||||
|
Color Image::get_rgba_pixel(int x, int y) const {
|
||||||
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
|
return Color::BLACK;
|
||||||
|
const uint32_t pos = (x + y * this->width_) * 4;
|
||||||
|
return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
||||||
|
progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3));
|
||||||
|
}
|
||||||
Color Image::get_color_pixel(int x, int y) const {
|
Color Image::get_color_pixel(int x, int y) const {
|
||||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
return Color::BLACK;
|
return Color::BLACK;
|
||||||
const uint32_t pos = (x + y * this->width_) * 3;
|
const uint32_t pos = (x + y * this->width_) * 3;
|
||||||
const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) |
|
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
||||||
(progmem_read_byte(this->data_start_ + pos + 1) << 8) |
|
progmem_read_byte(this->data_start_ + pos + 2));
|
||||||
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
|
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
|
||||||
return Color(color32);
|
// (0, 0, 1) has been defined as transparent color for non-alpha images.
|
||||||
|
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
|
||||||
|
color.w = 0;
|
||||||
|
} else {
|
||||||
|
color.w = 0xFF;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
}
|
}
|
||||||
Color Image::get_rgb565_pixel(int x, int y) const {
|
Color Image::get_rgb565_pixel(int x, int y) const {
|
||||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
@ -647,14 +678,22 @@ Color Image::get_rgb565_pixel(int x, int y) const {
|
|||||||
auto r = (rgb565 & 0xF800) >> 11;
|
auto r = (rgb565 & 0xF800) >> 11;
|
||||||
auto g = (rgb565 & 0x07E0) >> 5;
|
auto g = (rgb565 & 0x07E0) >> 5;
|
||||||
auto b = rgb565 & 0x001F;
|
auto b = rgb565 & 0x001F;
|
||||||
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
|
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
|
||||||
|
if (rgb565 == 0x0020 && transparent_) {
|
||||||
|
// darkest green has been defined as transparent color for transparent RGB565 images.
|
||||||
|
color.w = 0;
|
||||||
|
} else {
|
||||||
|
color.w = 0xFF;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
}
|
}
|
||||||
Color Image::get_grayscale_pixel(int x, int y) const {
|
Color Image::get_grayscale_pixel(int x, int y) const {
|
||||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
return Color::BLACK;
|
return Color::BLACK;
|
||||||
const uint32_t pos = (x + y * this->width_);
|
const uint32_t pos = (x + y * this->width_);
|
||||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||||
return Color(gray | gray << 8 | gray << 16 | gray << 24);
|
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF;
|
||||||
|
return Color(gray, gray, gray, alpha);
|
||||||
}
|
}
|
||||||
int Image::get_width() const { return this->width_; }
|
int Image::get_width() const { return this->width_; }
|
||||||
int Image::get_height() const { return this->height_; }
|
int Image::get_height() const { return this->height_; }
|
||||||
@ -673,6 +712,16 @@ bool Animation::get_pixel(int x, int y) const {
|
|||||||
const uint32_t pos = x + y * width_8 + frame_index;
|
const uint32_t pos = x + y * width_8 + frame_index;
|
||||||
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||||
}
|
}
|
||||||
|
Color Animation::get_rgba_pixel(int x, int y) const {
|
||||||
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
|
return Color::BLACK;
|
||||||
|
const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_;
|
||||||
|
if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
|
||||||
|
return Color::BLACK;
|
||||||
|
const uint32_t pos = (x + y * this->width_ + frame_index) * 4;
|
||||||
|
return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
||||||
|
progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3));
|
||||||
|
}
|
||||||
Color Animation::get_color_pixel(int x, int y) const {
|
Color Animation::get_color_pixel(int x, int y) const {
|
||||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
return Color::BLACK;
|
return Color::BLACK;
|
||||||
@ -680,10 +729,16 @@ Color Animation::get_color_pixel(int x, int y) const {
|
|||||||
if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
|
if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
|
||||||
return Color::BLACK;
|
return Color::BLACK;
|
||||||
const uint32_t pos = (x + y * this->width_ + frame_index) * 3;
|
const uint32_t pos = (x + y * this->width_ + frame_index) * 3;
|
||||||
const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) |
|
Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1),
|
||||||
(progmem_read_byte(this->data_start_ + pos + 1) << 8) |
|
progmem_read_byte(this->data_start_ + pos + 2));
|
||||||
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
|
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
|
||||||
return Color(color32);
|
// (0, 0, 1) has been defined as transparent color for non-alpha images.
|
||||||
|
// putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if)
|
||||||
|
color.w = 0;
|
||||||
|
} else {
|
||||||
|
color.w = 0xFF;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
}
|
}
|
||||||
Color Animation::get_rgb565_pixel(int x, int y) const {
|
Color Animation::get_rgb565_pixel(int x, int y) const {
|
||||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
@ -697,7 +752,14 @@ Color Animation::get_rgb565_pixel(int x, int y) const {
|
|||||||
auto r = (rgb565 & 0xF800) >> 11;
|
auto r = (rgb565 & 0xF800) >> 11;
|
||||||
auto g = (rgb565 & 0x07E0) >> 5;
|
auto g = (rgb565 & 0x07E0) >> 5;
|
||||||
auto b = rgb565 & 0x001F;
|
auto b = rgb565 & 0x001F;
|
||||||
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
|
Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
|
||||||
|
if (rgb565 == 0x0020 && transparent_) {
|
||||||
|
// darkest green has been defined as transparent color for transparent RGB565 images.
|
||||||
|
color.w = 0;
|
||||||
|
} else {
|
||||||
|
color.w = 0xFF;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
}
|
}
|
||||||
Color Animation::get_grayscale_pixel(int x, int y) const {
|
Color Animation::get_grayscale_pixel(int x, int y) const {
|
||||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||||
@ -707,7 +769,8 @@ Color Animation::get_grayscale_pixel(int x, int y) const {
|
|||||||
return Color::BLACK;
|
return Color::BLACK;
|
||||||
const uint32_t pos = (x + y * this->width_ + frame_index);
|
const uint32_t pos = (x + y * this->width_ + frame_index);
|
||||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||||
return Color(gray | gray << 8 | gray << 16 | gray << 24);
|
uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF;
|
||||||
|
return Color(gray, gray, gray, alpha);
|
||||||
}
|
}
|
||||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
|
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
|
||||||
: Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {}
|
: Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {}
|
||||||
|
@ -82,8 +82,8 @@ enum ImageType {
|
|||||||
IMAGE_TYPE_BINARY = 0,
|
IMAGE_TYPE_BINARY = 0,
|
||||||
IMAGE_TYPE_GRAYSCALE = 1,
|
IMAGE_TYPE_GRAYSCALE = 1,
|
||||||
IMAGE_TYPE_RGB24 = 2,
|
IMAGE_TYPE_RGB24 = 2,
|
||||||
IMAGE_TYPE_TRANSPARENT_BINARY = 3,
|
IMAGE_TYPE_RGB565 = 3,
|
||||||
IMAGE_TYPE_RGB565 = 4,
|
IMAGE_TYPE_RGBA = 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum DisplayType {
|
enum DisplayType {
|
||||||
@ -540,6 +540,7 @@ class Image {
|
|||||||
Image(const uint8_t *data_start, int width, int height, ImageType type);
|
Image(const uint8_t *data_start, int width, int height, ImageType type);
|
||||||
virtual bool get_pixel(int x, int y) const;
|
virtual bool get_pixel(int x, int y) const;
|
||||||
virtual Color get_color_pixel(int x, int y) const;
|
virtual Color get_color_pixel(int x, int y) const;
|
||||||
|
virtual Color get_rgba_pixel(int x, int y) const;
|
||||||
virtual Color get_rgb565_pixel(int x, int y) const;
|
virtual Color get_rgb565_pixel(int x, int y) const;
|
||||||
virtual Color get_grayscale_pixel(int x, int y) const;
|
virtual Color get_grayscale_pixel(int x, int y) const;
|
||||||
int get_width() const;
|
int get_width() const;
|
||||||
@ -548,11 +549,15 @@ class Image {
|
|||||||
|
|
||||||
virtual int get_current_frame() const;
|
virtual int get_current_frame() const;
|
||||||
|
|
||||||
|
void set_transparency(bool transparent) { transparent_ = transparent; }
|
||||||
|
bool has_transparency() const { return transparent_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int width_;
|
int width_;
|
||||||
int height_;
|
int height_;
|
||||||
ImageType type_;
|
ImageType type_;
|
||||||
const uint8_t *data_start_;
|
const uint8_t *data_start_;
|
||||||
|
bool transparent_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Animation : public Image {
|
class Animation : public Image {
|
||||||
@ -560,6 +565,7 @@ class Animation : public Image {
|
|||||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
|
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
|
||||||
bool get_pixel(int x, int y) const override;
|
bool get_pixel(int x, int y) const override;
|
||||||
Color get_color_pixel(int x, int y) const override;
|
Color get_color_pixel(int x, int y) const override;
|
||||||
|
Color get_rgba_pixel(int x, int y) const override;
|
||||||
Color get_rgb565_pixel(int x, int y) const override;
|
Color get_rgb565_pixel(int x, int y) const override;
|
||||||
Color get_grayscale_pixel(int x, int y) const override;
|
Color get_grayscale_pixel(int x, int y) const override;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ from esphome.components.light.types import AddressableLightEffect
|
|||||||
from esphome.components.light.effects import register_addressable_effect
|
from esphome.components.light.effects import register_addressable_effect
|
||||||
from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS
|
from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS
|
||||||
|
|
||||||
|
AUTO_LOAD = ["socket"]
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
|
|
||||||
e131_ns = cg.esphome_ns.namespace("e131")
|
e131_ns = cg.esphome_ns.namespace("e131")
|
||||||
@ -23,16 +24,11 @@ CHANNELS = {
|
|||||||
CONF_UNIVERSE = "universe"
|
CONF_UNIVERSE = "universe"
|
||||||
CONF_E131_ID = "e131_id"
|
CONF_E131_ID = "e131_id"
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
cv.Schema(
|
{
|
||||||
{
|
cv.GenerateID(): cv.declare_id(E131Component),
|
||||||
cv.GenerateID(): cv.declare_id(E131Component),
|
cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True),
|
||||||
cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(
|
}
|
||||||
*METHODS, upper=True
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
cv.only_with_arduino,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,18 +1,7 @@
|
|||||||
#ifdef USE_ARDUINO
|
|
||||||
|
|
||||||
#include "e131.h"
|
#include "e131.h"
|
||||||
#include "e131_addressable_light_effect.h"
|
#include "e131_addressable_light_effect.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
|
||||||
#include <WiFi.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_ESP8266
|
|
||||||
#include <ESP8266WiFi.h>
|
|
||||||
#include <WiFiUdp.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace e131 {
|
namespace e131 {
|
||||||
|
|
||||||
@ -22,17 +11,41 @@ static const int PORT = 5568;
|
|||||||
E131Component::E131Component() {}
|
E131Component::E131Component() {}
|
||||||
|
|
||||||
E131Component::~E131Component() {
|
E131Component::~E131Component() {
|
||||||
if (udp_) {
|
if (this->socket_) {
|
||||||
udp_->stop();
|
this->socket_->close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void E131Component::setup() {
|
void E131Component::setup() {
|
||||||
udp_ = make_unique<WiFiUDP>();
|
this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
|
||||||
|
|
||||||
if (!udp_->begin(PORT)) {
|
int enable = 1;
|
||||||
ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT);
|
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
|
||||||
mark_failed();
|
if (err != 0) {
|
||||||
|
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
|
||||||
|
// we can still continue
|
||||||
|
}
|
||||||
|
err = this->socket_->setblocking(false);
|
||||||
|
if (err != 0) {
|
||||||
|
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sockaddr_storage server;
|
||||||
|
|
||||||
|
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), PORT);
|
||||||
|
if (sl == 0) {
|
||||||
|
ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno);
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
server.ss_family = AF_INET;
|
||||||
|
|
||||||
|
err = this->socket_->bind((struct sockaddr *) &server, sizeof(server));
|
||||||
|
if (err != 0) {
|
||||||
|
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
|
||||||
|
this->mark_failed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,22 +56,22 @@ void E131Component::loop() {
|
|||||||
std::vector<uint8_t> payload;
|
std::vector<uint8_t> payload;
|
||||||
E131Packet packet;
|
E131Packet packet;
|
||||||
int universe = 0;
|
int universe = 0;
|
||||||
|
uint8_t buf[1460];
|
||||||
|
|
||||||
while (uint16_t packet_size = udp_->parsePacket()) {
|
ssize_t len = this->socket_->read(buf, sizeof(buf));
|
||||||
payload.resize(packet_size);
|
if (len == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.resize(len);
|
||||||
|
memmove(&payload[0], buf, len);
|
||||||
|
|
||||||
if (!udp_->read(&payload[0], payload.size())) {
|
if (!this->packet_(payload, universe, packet)) {
|
||||||
continue;
|
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!packet_(payload, universe, packet)) {
|
if (!this->process_(universe, packet)) {
|
||||||
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
|
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process_(universe, packet)) {
|
|
||||||
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,5 +119,3 @@ bool E131Component::process_(int universe, const E131Packet &packet) {
|
|||||||
|
|
||||||
} // namespace e131
|
} // namespace e131
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ARDUINO
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#include "esphome/components/socket/socket.h"
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
@ -9,8 +8,6 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
class UDP;
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace e131 {
|
namespace e131 {
|
||||||
|
|
||||||
@ -47,7 +44,7 @@ class E131Component : public esphome::Component {
|
|||||||
void leave_(int universe);
|
void leave_(int universe);
|
||||||
|
|
||||||
E131ListenMethod listen_method_{E131_MULTICAST};
|
E131ListenMethod listen_method_{E131_MULTICAST};
|
||||||
std::unique_ptr<UDP> udp_;
|
std::unique_ptr<socket::Socket> socket_;
|
||||||
std::set<E131AddressableLightEffect *> light_effects_;
|
std::set<E131AddressableLightEffect *> light_effects_;
|
||||||
std::map<int, int> universe_consumers_;
|
std::map<int, int> universe_consumers_;
|
||||||
std::map<int, E131Packet> universe_packets_;
|
std::map<int, E131Packet> universe_packets_;
|
||||||
@ -55,5 +52,3 @@ class E131Component : public esphome::Component {
|
|||||||
|
|
||||||
} // namespace e131
|
} // namespace e131
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ARDUINO
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
#ifdef USE_ARDUINO
|
|
||||||
|
|
||||||
#include "e131.h"
|
|
||||||
#include "e131_addressable_light_effect.h"
|
#include "e131_addressable_light_effect.h"
|
||||||
|
#include "e131.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@ -92,5 +90,3 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
|
|||||||
|
|
||||||
} // namespace e131
|
} // namespace e131
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ARDUINO
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/components/light/addressable_light_effect.h"
|
#include "esphome/components/light/addressable_light_effect.h"
|
||||||
|
|
||||||
@ -44,5 +42,3 @@ class E131AddressableLightEffect : public light::AddressableLightEffect {
|
|||||||
|
|
||||||
} // namespace e131
|
} // namespace e131
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ARDUINO
|
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
#ifdef USE_ARDUINO
|
#include <cstring>
|
||||||
|
|
||||||
#include "e131.h"
|
#include "e131.h"
|
||||||
|
#include "esphome/components/network/ip_address.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
#include "esphome/components/network/ip_address.h"
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include <lwip/init.h>
|
|
||||||
#include <lwip/ip_addr.h>
|
|
||||||
#include <lwip/ip4_addr.h>
|
|
||||||
#include <lwip/igmp.h>
|
#include <lwip/igmp.h>
|
||||||
|
#include <lwip/init.h>
|
||||||
|
#include <lwip/ip4_addr.h>
|
||||||
|
#include <lwip/ip_addr.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace e131 {
|
namespace e131 {
|
||||||
@ -62,7 +60,7 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *)
|
|||||||
bool E131Component::join_igmp_groups_() {
|
bool E131Component::join_igmp_groups_() {
|
||||||
if (listen_method_ != E131_MULTICAST)
|
if (listen_method_ != E131_MULTICAST)
|
||||||
return false;
|
return false;
|
||||||
if (!udp_)
|
if (this->socket_ == nullptr)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
for (auto universe : universe_consumers_) {
|
for (auto universe : universe_consumers_) {
|
||||||
@ -140,5 +138,3 @@ bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E13
|
|||||||
|
|
||||||
} // namespace e131
|
} // namespace e131
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ARDUINO
|
|
||||||
|
@ -34,7 +34,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
|
|||||||
light::LightTraits get_traits() override {
|
light::LightTraits get_traits() override {
|
||||||
auto traits = light::LightTraits();
|
auto traits = light::LightTraits();
|
||||||
if (this->is_rgbw_) {
|
if (this->is_rgbw_) {
|
||||||
traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::RGB_WHITE});
|
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
|
||||||
} else {
|
} else {
|
||||||
traits.set_supported_color_modes({light::ColorMode::RGB});
|
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ["i2c"]
|
DEPENDENCIES = ["i2c"]
|
||||||
|
CODEOWNERS = ["@freekode"]
|
||||||
|
|
||||||
hm3301_ns = cg.esphome_ns.namespace("hm3301")
|
hm3301_ns = cg.esphome_ns.namespace("hm3301")
|
||||||
HM3301Component = hm3301_ns.class_(
|
HM3301Component = hm3301_ns.class_(
|
||||||
|
@ -7,6 +7,30 @@ namespace i2c {
|
|||||||
|
|
||||||
static const char *const TAG = "i2c";
|
static const char *const TAG = "i2c";
|
||||||
|
|
||||||
|
ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) {
|
||||||
|
ErrorCode err = this->write(&a_register, 1, stop);
|
||||||
|
if (err != ERROR_OK)
|
||||||
|
return err;
|
||||||
|
return bus_->read(address_, data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) {
|
||||||
|
WriteBuffer buffers[2];
|
||||||
|
buffers[0].data = &a_register;
|
||||||
|
buffers[0].len = 1;
|
||||||
|
buffers[1].data = data;
|
||||||
|
buffers[1].len = len;
|
||||||
|
return bus_->writev(address_, buffers, 2, stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) {
|
||||||
|
if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK)
|
||||||
|
return false;
|
||||||
|
for (size_t i = 0; i < len; i++)
|
||||||
|
data[i] = i2ctohs(data[i]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) {
|
bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) {
|
||||||
// we have to copy in order to be able to change byte order
|
// we have to copy in order to be able to change byte order
|
||||||
std::unique_ptr<uint16_t[]> temp{new uint16_t[len]};
|
std::unique_ptr<uint16_t[]> temp{new uint16_t[len]};
|
||||||
|
@ -46,22 +46,10 @@ class I2CDevice {
|
|||||||
I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
|
I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
|
||||||
|
|
||||||
ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
|
ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
|
||||||
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) {
|
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true);
|
||||||
ErrorCode err = this->write(&a_register, 1, stop);
|
|
||||||
if (err != ERROR_OK)
|
|
||||||
return err;
|
|
||||||
return this->read(data, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
|
ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
|
||||||
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true) {
|
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true);
|
||||||
WriteBuffer buffers[2];
|
|
||||||
buffers[0].data = &a_register;
|
|
||||||
buffers[0].len = 1;
|
|
||||||
buffers[1].data = data;
|
|
||||||
buffers[1].len = len;
|
|
||||||
return bus_->writev(address_, buffers, 2, stop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compat APIs
|
// Compat APIs
|
||||||
|
|
||||||
@ -85,13 +73,7 @@ class I2CDevice {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) {
|
bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len);
|
||||||
if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK)
|
|
||||||
return false;
|
|
||||||
for (size_t i = 0; i < len; i++)
|
|
||||||
data[i] = i2ctohs(data[i]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) {
|
bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) {
|
||||||
return read_register(a_register, data, 1, stop) == ERROR_OK;
|
return read_register(a_register, data, 1, stop) == ERROR_OK;
|
||||||
|
@ -18,6 +18,7 @@ MULTI_CONF = True
|
|||||||
|
|
||||||
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
|
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
|
||||||
CONF_I2S_DIN_PIN = "i2s_din_pin"
|
CONF_I2S_DIN_PIN = "i2s_din_pin"
|
||||||
|
CONF_I2S_MCLK_PIN = "i2s_mclk_pin"
|
||||||
CONF_I2S_BCLK_PIN = "i2s_bclk_pin"
|
CONF_I2S_BCLK_PIN = "i2s_bclk_pin"
|
||||||
CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
|
CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
cv.GenerateID(): cv.declare_id(I2SAudioComponent),
|
cv.GenerateID(): cv.declare_id(I2SAudioComponent),
|
||||||
cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number,
|
cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number,
|
||||||
cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number,
|
cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number,
|
||||||
|
cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,3 +71,5 @@ async def to_code(config):
|
|||||||
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
|
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
|
||||||
if CONF_I2S_BCLK_PIN in config:
|
if CONF_I2S_BCLK_PIN in config:
|
||||||
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
|
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
|
||||||
|
if CONF_I2S_MCLK_PIN in config:
|
||||||
|
cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN]))
|
||||||
|
@ -21,7 +21,7 @@ class I2SAudioComponent : public Component {
|
|||||||
|
|
||||||
i2s_pin_config_t get_pin_config() const {
|
i2s_pin_config_t get_pin_config() const {
|
||||||
return {
|
return {
|
||||||
.mck_io_num = I2S_PIN_NO_CHANGE,
|
.mck_io_num = this->mclk_pin_,
|
||||||
.bck_io_num = this->bclk_pin_,
|
.bck_io_num = this->bclk_pin_,
|
||||||
.ws_io_num = this->lrclk_pin_,
|
.ws_io_num = this->lrclk_pin_,
|
||||||
.data_out_num = I2S_PIN_NO_CHANGE,
|
.data_out_num = I2S_PIN_NO_CHANGE,
|
||||||
@ -29,6 +29,7 @@ class I2SAudioComponent : public Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_mclk_pin(int pin) { this->mclk_pin_ = pin; }
|
||||||
void set_bclk_pin(int pin) { this->bclk_pin_ = pin; }
|
void set_bclk_pin(int pin) { this->bclk_pin_ = pin; }
|
||||||
void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; }
|
void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; }
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ class I2SAudioComponent : public Component {
|
|||||||
I2SAudioIn *audio_in_{nullptr};
|
I2SAudioIn *audio_in_{nullptr};
|
||||||
I2SAudioOut *audio_out_{nullptr};
|
I2SAudioOut *audio_out_{nullptr};
|
||||||
|
|
||||||
|
int mclk_pin_{I2S_PIN_NO_CHANGE};
|
||||||
int bclk_pin_{I2S_PIN_NO_CHANGE};
|
int bclk_pin_{I2S_PIN_NO_CHANGE};
|
||||||
int lrclk_pin_;
|
int lrclk_pin_;
|
||||||
i2s_port_t port_{};
|
i2s_port_t port_{};
|
||||||
|
@ -133,7 +133,7 @@ void I2SAudioMediaPlayer::play_() {
|
|||||||
|
|
||||||
void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; }
|
void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; }
|
||||||
void I2SAudioMediaPlayer::start_() {
|
void I2SAudioMediaPlayer::start_() {
|
||||||
if (this->parent_->try_lock()) {
|
if (!this->parent_->try_lock()) {
|
||||||
return; // Waiting for another i2s to return lock
|
return; // Waiting for another i2s to return lock
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +156,7 @@ void I2SAudioMediaPlayer::start_() {
|
|||||||
#if SOC_I2S_SUPPORTS_DAC
|
#if SOC_I2S_SUPPORTS_DAC
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
this->i2s_state_ = I2S_STATE_RUNNING;
|
this->i2s_state_ = I2S_STATE_RUNNING;
|
||||||
this->high_freq_.start();
|
this->high_freq_.start();
|
||||||
this->audio_->setVolume(remap<uint8_t, float>(this->volume, 0.0f, 1.0f, 0, 21));
|
this->audio_->setVolume(remap<uint8_t, float>(this->volume, 0.0f, 1.0f, 0, 21));
|
||||||
@ -218,6 +219,12 @@ void I2SAudioMediaPlayer::dump_config() {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
#endif
|
||||||
|
ESP_LOGCONFIG(TAG, " External DAC channels: %d", this->external_dac_channels_);
|
||||||
|
ESP_LOGCONFIG(TAG, " I2S DOUT Pin: %d", this->dout_pin_);
|
||||||
|
LOG_PIN(" Mute Pin: ", this->mute_pin_);
|
||||||
|
#if SOC_I2S_SUPPORTS_DAC
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import esphome.config_validation as cv
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
from esphome.const import CONF_ID, CONF_NUMBER
|
from esphome.const import CONF_CHANNEL, CONF_ID, CONF_NUMBER
|
||||||
from esphome.components import microphone, esp32
|
from esphome.components import microphone, esp32
|
||||||
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
|
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
|
||||||
|
|
||||||
@ -25,6 +25,12 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
|
|||||||
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
|
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
|
||||||
)
|
)
|
||||||
|
|
||||||
|
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
|
||||||
|
CHANNELS = {
|
||||||
|
"left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT,
|
||||||
|
"right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32]
|
INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32]
|
||||||
PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
|
PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
|
||||||
|
|
||||||
@ -47,6 +53,7 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
|
|||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
|
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
|
||||||
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
|
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
|
||||||
|
cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
@ -86,4 +93,6 @@ async def to_code(config):
|
|||||||
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
|
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
|
||||||
cg.add(var.set_pdm(config[CONF_PDM]))
|
cg.add(var.set_pdm(config[CONF_PDM]))
|
||||||
|
|
||||||
|
cg.add(var.set_channel(CHANNELS[config[CONF_CHANNEL]]))
|
||||||
|
|
||||||
await microphone.register_microphone(var, config)
|
await microphone.register_microphone(var, config)
|
||||||
|
@ -49,7 +49,7 @@ void I2SAudioMicrophone::start_() {
|
|||||||
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
|
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
|
||||||
.sample_rate = 16000,
|
.sample_rate = 16000,
|
||||||
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
|
||||||
.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
|
.channel_format = this->channel_,
|
||||||
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
|
||||||
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
|
||||||
.dma_buf_count = 4,
|
.dma_buf_count = 4,
|
||||||
|
@ -28,6 +28,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void start_();
|
void start_();
|
||||||
void stop_();
|
void stop_();
|
||||||
@ -40,6 +42,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
|||||||
#endif
|
#endif
|
||||||
bool pdm_{false};
|
bool pdm_{false};
|
||||||
std::vector<uint8_t> buffer_;
|
std::vector<uint8_t> buffer_;
|
||||||
|
i2s_channel_fmt_t channel_;
|
||||||
|
|
||||||
HighFrequencyLoopRequester high_freq_;
|
HighFrequencyLoopRequester high_freq_;
|
||||||
};
|
};
|
||||||
|
@ -116,8 +116,8 @@ static const uint8_t PROGMEM INITCMD_ILI9486[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const uint8_t PROGMEM INITCMD_ILI9488[] = {
|
static const uint8_t PROGMEM INITCMD_ILI9488[] = {
|
||||||
ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F,
|
ILI9XXX_GMCTRP1,15, 0x0f, 0x24, 0x1c, 0x0a, 0x0f, 0x08, 0x43, 0x88, 0x32, 0x0f, 0x10, 0x06, 0x0f, 0x07, 0x00,
|
||||||
ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F,
|
ILI9XXX_GMCTRN1,15, 0x0F, 0x38, 0x30, 0x09, 0x0f, 0x0f, 0x4e, 0x77, 0x3c, 0x07, 0x10, 0x05, 0x23, 0x1b, 0x00,
|
||||||
|
|
||||||
ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2
|
ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2
|
||||||
ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL
|
ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL
|
||||||
|
@ -22,26 +22,55 @@ MULTI_CONF = True
|
|||||||
ImageType = display.display_ns.enum("ImageType")
|
ImageType = display.display_ns.enum("ImageType")
|
||||||
IMAGE_TYPE = {
|
IMAGE_TYPE = {
|
||||||
"BINARY": ImageType.IMAGE_TYPE_BINARY,
|
"BINARY": ImageType.IMAGE_TYPE_BINARY,
|
||||||
|
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
|
||||||
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
|
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
|
||||||
"RGB24": ImageType.IMAGE_TYPE_RGB24,
|
|
||||||
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
|
|
||||||
"RGB565": ImageType.IMAGE_TYPE_RGB565,
|
"RGB565": ImageType.IMAGE_TYPE_RGB565,
|
||||||
"TRANSPARENT_IMAGE": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
|
"RGB24": ImageType.IMAGE_TYPE_RGB24,
|
||||||
|
"RGBA": ImageType.IMAGE_TYPE_RGBA,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CONF_USE_TRANSPARENCY = "use_transparency"
|
||||||
|
|
||||||
Image_ = display.display_ns.class_("Image")
|
Image_ = display.display_ns.class_("Image")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_cross_dependencies(config):
|
||||||
|
"""
|
||||||
|
Validate fields whose possible values depend on other fields.
|
||||||
|
For example, validate that explicitly transparent image types
|
||||||
|
have "use_transparency" set to True.
|
||||||
|
Also set the default value for those kind of dependent fields.
|
||||||
|
"""
|
||||||
|
image_type = config[CONF_TYPE]
|
||||||
|
is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
|
||||||
|
|
||||||
|
# If the use_transparency option was not specified, set the default depending on the image type
|
||||||
|
if CONF_USE_TRANSPARENCY not in config:
|
||||||
|
config[CONF_USE_TRANSPARENCY] = is_transparent_type
|
||||||
|
|
||||||
|
if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
|
||||||
|
raise cv.Invalid(f"Image type {image_type} must always be transparent.")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
IMAGE_SCHEMA = cv.Schema(
|
IMAGE_SCHEMA = cv.Schema(
|
||||||
{
|
cv.All(
|
||||||
cv.Required(CONF_ID): cv.declare_id(Image_),
|
{
|
||||||
cv.Required(CONF_FILE): cv.file_,
|
cv.Required(CONF_ID): cv.declare_id(Image_),
|
||||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
cv.Required(CONF_FILE): cv.file_,
|
||||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
|
||||||
"NONE", "FLOYDSTEINBERG", upper=True
|
# Not setting default here on purpose; the default depends on the image type,
|
||||||
),
|
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||||
}
|
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
||||||
|
"NONE", "FLOYDSTEINBERG", upper=True
|
||||||
|
),
|
||||||
|
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||||
|
},
|
||||||
|
validate_cross_dependencies,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
|
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
|
||||||
@ -64,72 +93,113 @@ async def to_code(config):
|
|||||||
else:
|
else:
|
||||||
if width > 500 or height > 500:
|
if width > 500 or height > 500:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"The image you requested is very big. Please consider using"
|
'The image "%s" you requested is very big. Please consider'
|
||||||
" the resize parameter."
|
" using the resize parameter.",
|
||||||
|
path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
transparent = config[CONF_USE_TRANSPARENCY]
|
||||||
|
|
||||||
dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG
|
dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG
|
||||||
if config[CONF_TYPE] == "GRAYSCALE":
|
if config[CONF_TYPE] == "GRAYSCALE":
|
||||||
image = image.convert("L", dither=dither)
|
image = image.convert("LA", dither=dither)
|
||||||
pixels = list(image.getdata())
|
pixels = list(image.getdata())
|
||||||
data = [0 for _ in range(height * width)]
|
data = [0 for _ in range(height * width)]
|
||||||
pos = 0
|
pos = 0
|
||||||
for pix in pixels:
|
for g, a in pixels:
|
||||||
data[pos] = pix
|
if transparent:
|
||||||
|
if g == 1:
|
||||||
|
g = 0
|
||||||
|
if a < 0x80:
|
||||||
|
g = 1
|
||||||
|
|
||||||
|
data[pos] = g
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
elif config[CONF_TYPE] == "RGBA":
|
||||||
|
image = image.convert("RGBA")
|
||||||
|
pixels = list(image.getdata())
|
||||||
|
data = [0 for _ in range(height * width * 4)]
|
||||||
|
pos = 0
|
||||||
|
for r, g, b, a in pixels:
|
||||||
|
data[pos] = r
|
||||||
|
pos += 1
|
||||||
|
data[pos] = g
|
||||||
|
pos += 1
|
||||||
|
data[pos] = b
|
||||||
|
pos += 1
|
||||||
|
data[pos] = a
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
elif config[CONF_TYPE] == "RGB24":
|
elif config[CONF_TYPE] == "RGB24":
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGBA")
|
||||||
pixels = list(image.getdata())
|
pixels = list(image.getdata())
|
||||||
data = [0 for _ in range(height * width * 3)]
|
data = [0 for _ in range(height * width * 3)]
|
||||||
pos = 0
|
pos = 0
|
||||||
for pix in pixels:
|
for r, g, b, a in pixels:
|
||||||
data[pos] = pix[0]
|
if transparent:
|
||||||
|
if r == 0 and g == 0 and b == 1:
|
||||||
|
b = 0
|
||||||
|
if a < 0x80:
|
||||||
|
r = 0
|
||||||
|
g = 0
|
||||||
|
b = 1
|
||||||
|
|
||||||
|
data[pos] = r
|
||||||
pos += 1
|
pos += 1
|
||||||
data[pos] = pix[1]
|
data[pos] = g
|
||||||
pos += 1
|
pos += 1
|
||||||
data[pos] = pix[2]
|
data[pos] = b
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
elif config[CONF_TYPE] == "RGB565":
|
elif config[CONF_TYPE] in ["RGB565"]:
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGBA")
|
||||||
pixels = list(image.getdata())
|
pixels = list(image.getdata())
|
||||||
data = [0 for _ in range(height * width * 3)]
|
data = [0 for _ in range(height * width * 2)]
|
||||||
pos = 0
|
pos = 0
|
||||||
for pix in pixels:
|
for r, g, b, a in pixels:
|
||||||
R = pix[0] >> 3
|
R = r >> 3
|
||||||
G = pix[1] >> 2
|
G = g >> 2
|
||||||
B = pix[2] >> 3
|
B = b >> 3
|
||||||
rgb = (R << 11) | (G << 5) | B
|
rgb = (R << 11) | (G << 5) | B
|
||||||
|
|
||||||
|
if transparent:
|
||||||
|
if rgb == 0x0020:
|
||||||
|
rgb = 0
|
||||||
|
if a < 0x80:
|
||||||
|
rgb = 0x0020
|
||||||
|
|
||||||
data[pos] = rgb >> 8
|
data[pos] = rgb >> 8
|
||||||
pos += 1
|
pos += 1
|
||||||
data[pos] = rgb & 255
|
data[pos] = rgb & 0xFF
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
elif (config[CONF_TYPE] == "BINARY") or (config[CONF_TYPE] == "TRANSPARENT_BINARY"):
|
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
|
||||||
|
if transparent:
|
||||||
|
alpha = image.split()[-1]
|
||||||
|
has_alpha = alpha.getextrema()[0] < 0xFF
|
||||||
|
_LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha)
|
||||||
image = image.convert("1", dither=dither)
|
image = image.convert("1", dither=dither)
|
||||||
width8 = ((width + 7) // 8) * 8
|
width8 = ((width + 7) // 8) * 8
|
||||||
data = [0 for _ in range(height * width8 // 8)]
|
data = [0 for _ in range(height * width8 // 8)]
|
||||||
for y in range(height):
|
for y in range(height):
|
||||||
for x in range(width):
|
for x in range(width):
|
||||||
if image.getpixel((x, y)):
|
if transparent and has_alpha:
|
||||||
continue
|
a = alpha.getpixel((x, y))
|
||||||
pos = x + y * width8
|
if not a:
|
||||||
data[pos // 8] |= 0x80 >> (pos % 8)
|
continue
|
||||||
|
elif image.getpixel((x, y)):
|
||||||
elif config[CONF_TYPE] == "TRANSPARENT_IMAGE":
|
|
||||||
image = image.convert("RGBA")
|
|
||||||
width8 = ((width + 7) // 8) * 8
|
|
||||||
data = [0 for _ in range(height * width8 // 8)]
|
|
||||||
for y in range(height):
|
|
||||||
for x in range(width):
|
|
||||||
if not image.getpixel((x, y))[3]:
|
|
||||||
continue
|
continue
|
||||||
pos = x + y * width8
|
pos = x + y * width8
|
||||||
data[pos // 8] |= 0x80 >> (pos % 8)
|
data[pos // 8] |= 0x80 >> (pos % 8)
|
||||||
|
else:
|
||||||
|
raise core.EsphomeError(
|
||||||
|
f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}."
|
||||||
|
)
|
||||||
|
|
||||||
rhs = [HexInt(x) for x in data]
|
rhs = [HexInt(x) for x in data]
|
||||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||||
cg.new_Pvariable(
|
var = cg.new_Pvariable(
|
||||||
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
|
config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
|
||||||
)
|
)
|
||||||
|
cg.add(var.set_transparency(transparent))
|
||||||
|
@ -33,6 +33,10 @@ void InternalTemperatureSensor::update() {
|
|||||||
temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT();
|
temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT();
|
||||||
temp_sensor_set_config(tsens);
|
temp_sensor_set_config(tsens);
|
||||||
temp_sensor_start();
|
temp_sensor_start();
|
||||||
|
#if defined(USE_ESP32_VARIANT_ESP32S3) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 3))
|
||||||
|
#error \
|
||||||
|
"ESP32-S3 internal temperature sensor requires ESP IDF V4.4.3 or higher. See https://github.com/esphome/issues/issues/4271"
|
||||||
|
#endif
|
||||||
esp_err_t result = temp_sensor_read_celsius(&temperature);
|
esp_err_t result = temp_sensor_read_celsius(&temperature);
|
||||||
temp_sensor_stop();
|
temp_sensor_stop();
|
||||||
success = (result == ESP_OK);
|
success = (result == ESP_OK);
|
||||||
|
@ -1,18 +1,45 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.components import sensor
|
from esphome.components import sensor
|
||||||
|
from esphome.components.esp32 import get_esp32_variant
|
||||||
|
from esphome.components.esp32.const import (
|
||||||
|
VARIANT_ESP32S3,
|
||||||
|
)
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
UNIT_CELSIUS,
|
UNIT_CELSIUS,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||||
|
KEY_CORE,
|
||||||
|
KEY_FRAMEWORK_VERSION,
|
||||||
)
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature")
|
internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature")
|
||||||
InternalTemperatureSensor = internal_temperature_ns.class_(
|
InternalTemperatureSensor = internal_temperature_ns.class_(
|
||||||
"InternalTemperatureSensor", sensor.Sensor, cg.PollingComponent
|
"InternalTemperatureSensor", sensor.Sensor, cg.PollingComponent
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config(config):
|
||||||
|
if CORE.is_esp32:
|
||||||
|
variant = get_esp32_variant()
|
||||||
|
if variant == VARIANT_ESP32S3:
|
||||||
|
if CORE.using_arduino and CORE.data[KEY_CORE][
|
||||||
|
KEY_FRAMEWORK_VERSION
|
||||||
|
] < cv.Version(2, 0, 6):
|
||||||
|
raise cv.Invalid(
|
||||||
|
"ESP32-S3 Internal Temperature Sensor requires framework version 2.0.6 or higher. See <https://github.com/esphome/issues/issues/4271>."
|
||||||
|
)
|
||||||
|
if CORE.using_esp_idf and CORE.data[KEY_CORE][
|
||||||
|
KEY_FRAMEWORK_VERSION
|
||||||
|
] < cv.Version(4, 4, 3):
|
||||||
|
raise cv.Invalid(
|
||||||
|
"ESP32-S3 Internal Temperature Sensor requires framework version 4.4.3 or higher. See <https://github.com/esphome/issues/issues/4271>."
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
sensor.sensor_schema(
|
sensor.sensor_schema(
|
||||||
InternalTemperatureSensor,
|
InternalTemperatureSensor,
|
||||||
@ -23,6 +50,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||||
).extend(cv.polling_component_schema("60s")),
|
).extend(cv.polling_component_schema("60s")),
|
||||||
cv.only_on(["esp32", "rp2040"]),
|
cv.only_on(["esp32", "rp2040"]),
|
||||||
|
validate_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ from esphome.const import (
|
|||||||
CONF_SERVICE,
|
CONF_SERVICE,
|
||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
KEY_FRAMEWORK_VERSION,
|
KEY_FRAMEWORK_VERSION,
|
||||||
|
CONF_DISABLED,
|
||||||
)
|
)
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
@ -39,7 +40,6 @@ SERVICE_SCHEMA = cv.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
CONF_DISABLED = "disabled"
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
|
@ -506,12 +506,12 @@ void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueTy
|
|||||||
case SensorValueType::U_DWORD:
|
case SensorValueType::U_DWORD:
|
||||||
case SensorValueType::S_DWORD:
|
case SensorValueType::S_DWORD:
|
||||||
case SensorValueType::FP32:
|
case SensorValueType::FP32:
|
||||||
case SensorValueType::FP32_R:
|
|
||||||
data.push_back((value & 0xFFFF0000) >> 16);
|
data.push_back((value & 0xFFFF0000) >> 16);
|
||||||
data.push_back(value & 0xFFFF);
|
data.push_back(value & 0xFFFF);
|
||||||
break;
|
break;
|
||||||
case SensorValueType::U_DWORD_R:
|
case SensorValueType::U_DWORD_R:
|
||||||
case SensorValueType::S_DWORD_R:
|
case SensorValueType::S_DWORD_R:
|
||||||
|
case SensorValueType::FP32_R:
|
||||||
data.push_back(value & 0xFFFF);
|
data.push_back(value & 0xFFFF);
|
||||||
data.push_back((value & 0xFFFF0000) >> 16);
|
data.push_back((value & 0xFFFF0000) >> 16);
|
||||||
break;
|
break;
|
||||||
|
@ -44,6 +44,9 @@ CONF_SUPPORTED_TANKS_MAP = {
|
|||||||
"20LB_V": (38, 254), # empty/full readings for 20lb US tank
|
"20LB_V": (38, 254), # empty/full readings for 20lb US tank
|
||||||
"30LB_V": (38, 381),
|
"30LB_V": (38, 381),
|
||||||
"40LB_V": (38, 508),
|
"40LB_V": (38, 508),
|
||||||
|
"EUROPE_6KG": (38, 336),
|
||||||
|
"EUROPE_11KG": (38, 366),
|
||||||
|
"EUROPE_14KG": (38, 467),
|
||||||
}
|
}
|
||||||
|
|
||||||
CODEOWNERS = ["@spbrogan"]
|
CODEOWNERS = ["@spbrogan"]
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/version.h"
|
||||||
#ifdef USE_LOGGER
|
#ifdef USE_LOGGER
|
||||||
#include "esphome/components/logger/logger.h"
|
#include "esphome/components/logger/logger.h"
|
||||||
#endif
|
#endif
|
||||||
@ -14,6 +15,13 @@
|
|||||||
#include "lwip/err.h"
|
#include "lwip/err.h"
|
||||||
#include "mqtt_component.h"
|
#include "mqtt_component.h"
|
||||||
|
|
||||||
|
#ifdef USE_API
|
||||||
|
#include "esphome/components/api/api_server.h"
|
||||||
|
#endif
|
||||||
|
#ifdef USE_DASHBOARD_IMPORT
|
||||||
|
#include "esphome/components/dashboard_import/dashboard_import.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace mqtt {
|
namespace mqtt {
|
||||||
|
|
||||||
@ -58,9 +66,63 @@ void MQTTClientComponent::setup() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
this->subscribe(
|
||||||
|
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
|
||||||
|
2);
|
||||||
|
|
||||||
|
std::string topic = "esphome/ping/";
|
||||||
|
topic.append(App.get_name());
|
||||||
|
this->subscribe(
|
||||||
|
topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
|
||||||
|
|
||||||
this->last_connected_ = millis();
|
this->last_connected_ = millis();
|
||||||
this->start_dnslookup_();
|
this->start_dnslookup_();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MQTTClientComponent::send_device_info_() {
|
||||||
|
if (!this->is_connected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string topic = "esphome/discover/";
|
||||||
|
topic.append(App.get_name());
|
||||||
|
this->publish_json(
|
||||||
|
topic,
|
||||||
|
[](JsonObject root) {
|
||||||
|
auto ip = network::get_ip_address();
|
||||||
|
root["ip"] = ip.str();
|
||||||
|
root["name"] = App.get_name();
|
||||||
|
#ifdef USE_API
|
||||||
|
root["port"] = api::global_api_server->get_port();
|
||||||
|
#endif
|
||||||
|
root["version"] = ESPHOME_VERSION;
|
||||||
|
root["mac"] = get_mac_address();
|
||||||
|
|
||||||
|
#ifdef USE_ESP8266
|
||||||
|
root["platform"] = "ESP8266";
|
||||||
|
#endif
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
root["platform"] = "ESP32";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
root["board"] = ESPHOME_BOARD;
|
||||||
|
#if defined(USE_WIFI)
|
||||||
|
root["network"] = "wifi";
|
||||||
|
#elif defined(USE_ETHERNET)
|
||||||
|
root["network"] = "ethernet";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef ESPHOME_PROJECT_NAME
|
||||||
|
root["project_name"] = ESPHOME_PROJECT_NAME;
|
||||||
|
root["project_version"] = ESPHOME_PROJECT_VERSION;
|
||||||
|
#endif // ESPHOME_PROJECT_NAME
|
||||||
|
|
||||||
|
#ifdef USE_DASHBOARD_IMPORT
|
||||||
|
root["package_import_url"] = dashboard_import::get_package_import_url();
|
||||||
|
#endif
|
||||||
|
},
|
||||||
|
2, this->discovery_info_.retain);
|
||||||
|
}
|
||||||
|
|
||||||
void MQTTClientComponent::dump_config() {
|
void MQTTClientComponent::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "MQTT:");
|
ESP_LOGCONFIG(TAG, "MQTT:");
|
||||||
ESP_LOGCONFIG(TAG, " Server Address: %s:%u (%s)", this->credentials_.address.c_str(), this->credentials_.port,
|
ESP_LOGCONFIG(TAG, " Server Address: %s:%u (%s)", this->credentials_.address.c_str(), this->credentials_.port,
|
||||||
@ -226,6 +288,7 @@ void MQTTClientComponent::check_connected() {
|
|||||||
delay(100); // NOLINT
|
delay(100); // NOLINT
|
||||||
|
|
||||||
this->resubscribe_subscriptions_();
|
this->resubscribe_subscriptions_();
|
||||||
|
this->send_device_info_();
|
||||||
|
|
||||||
for (MQTTComponent *component : this->children_)
|
for (MQTTComponent *component : this->children_)
|
||||||
component->schedule_resend_state();
|
component->schedule_resend_state();
|
||||||
|
@ -251,6 +251,8 @@ class MQTTClientComponent : public Component {
|
|||||||
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
|
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void send_device_info_();
|
||||||
|
|
||||||
/// Reconnect to the MQTT broker if not already connected.
|
/// Reconnect to the MQTT broker if not already connected.
|
||||||
void start_connect_();
|
void start_connect_();
|
||||||
void start_dnslookup_();
|
void start_dnslookup_();
|
||||||
|
@ -21,7 +21,7 @@ void PsramComponent::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
|
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0)
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0)
|
||||||
if (available) {
|
if (available) {
|
||||||
ESP_LOGCONFIG(TAG, " Size: %d MB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024 / 1024);
|
ESP_LOGCONFIG(TAG, " Size: %d KB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from string import ascii_letters, digits
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
@ -12,9 +15,11 @@ from esphome.const import (
|
|||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority, EsphomeError
|
||||||
|
from esphome.helpers import mkdir_p, write_file
|
||||||
|
import esphome.platformio_api as api
|
||||||
|
|
||||||
from .const import KEY_BOARD, KEY_RP2040, rp2040_ns
|
from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns
|
||||||
|
|
||||||
# force import gpio to register pin schema
|
# force import gpio to register pin schema
|
||||||
from .gpio import rp2040_pin_to_code # noqa
|
from .gpio import rp2040_pin_to_code # noqa
|
||||||
@ -33,6 +38,8 @@ def set_core_data(config):
|
|||||||
)
|
)
|
||||||
CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD]
|
CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD]
|
||||||
|
|
||||||
|
CORE.data[KEY_RP2040][KEY_PIO_FILES] = {}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
@ -148,7 +155,10 @@ async def to_code(config):
|
|||||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||||
cg.add_platformio_option(
|
cg.add_platformio_option(
|
||||||
"platform_packages",
|
"platform_packages",
|
||||||
[f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}"],
|
[
|
||||||
|
f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}",
|
||||||
|
"earlephilhower/tool-pioasm-rp2040-earlephilhower",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
cg.add_platformio_option("board_build.core", "earlephilhower")
|
cg.add_platformio_option("board_build.core", "earlephilhower")
|
||||||
@ -159,3 +169,53 @@ async def to_code(config):
|
|||||||
"USE_ARDUINO_VERSION_CODE",
|
"USE_ARDUINO_VERSION_CODE",
|
||||||
cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"),
|
cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_pio_file(component: str, key: str, data: str):
|
||||||
|
try:
|
||||||
|
cv.validate_id_name(key)
|
||||||
|
except cv.Invalid as e:
|
||||||
|
raise EsphomeError(
|
||||||
|
f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/issues"
|
||||||
|
) from e
|
||||||
|
CORE.data[KEY_RP2040][KEY_PIO_FILES][key] = data
|
||||||
|
|
||||||
|
|
||||||
|
def generate_pio_files() -> bool:
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
shutil.rmtree(CORE.relative_build_path("src/pio"), ignore_errors=True)
|
||||||
|
|
||||||
|
includes: list[str] = []
|
||||||
|
files = CORE.data[KEY_RP2040][KEY_PIO_FILES]
|
||||||
|
if not files:
|
||||||
|
return False
|
||||||
|
for key, data in files.items():
|
||||||
|
pio_path = CORE.relative_build_path(f"src/pio/{key}.pio")
|
||||||
|
mkdir_p(os.path.dirname(pio_path))
|
||||||
|
write_file(pio_path, data)
|
||||||
|
_LOGGER.info("Assembling PIO assembly code")
|
||||||
|
retval = api.run_platformio_cli(
|
||||||
|
"pkg",
|
||||||
|
"exec",
|
||||||
|
"--package",
|
||||||
|
"earlephilhower/tool-pioasm-rp2040-earlephilhower",
|
||||||
|
"--",
|
||||||
|
"pioasm",
|
||||||
|
pio_path,
|
||||||
|
pio_path + ".h",
|
||||||
|
)
|
||||||
|
includes.append(f"pio/{key}.pio.h")
|
||||||
|
if retval != 0:
|
||||||
|
raise EsphomeError("PIO assembly failed")
|
||||||
|
|
||||||
|
write_file(
|
||||||
|
CORE.relative_build_path("src/pio_includes.h"),
|
||||||
|
"#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]),
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Called by writer.py
|
||||||
|
def copy_files() -> bool:
|
||||||
|
return generate_pio_files()
|
||||||
|
@ -2,5 +2,6 @@ import esphome.codegen as cg
|
|||||||
|
|
||||||
KEY_BOARD = "board"
|
KEY_BOARD = "board"
|
||||||
KEY_RP2040 = "rp2040"
|
KEY_RP2040 = "rp2040"
|
||||||
|
KEY_PIO_FILES = "pio_files"
|
||||||
|
|
||||||
rp2040_ns = cg.esphome_ns.namespace("rp2040")
|
rp2040_ns = cg.esphome_ns.namespace("rp2040")
|
||||||
|
1
esphome/components/rp2040_pio_led_strip/__init__.py
Normal file
1
esphome/components/rp2040_pio_led_strip/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@Papa-DMan"]
|
139
esphome/components/rp2040_pio_led_strip/led_strip.cpp
Normal file
139
esphome/components/rp2040_pio_led_strip/led_strip.cpp
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
#include "led_strip.h"
|
||||||
|
|
||||||
|
#ifdef USE_RP2040
|
||||||
|
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <hardware/clocks.h>
|
||||||
|
#include <hardware/pio.h>
|
||||||
|
#include <pico/stdlib.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace rp2040_pio_led_strip {
|
||||||
|
|
||||||
|
static const char *TAG = "rp2040_pio_led_strip";
|
||||||
|
|
||||||
|
void RP2040PIOLEDStripLightOutput::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip...");
|
||||||
|
|
||||||
|
size_t buffer_size = this->get_buffer_size_();
|
||||||
|
|
||||||
|
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||||
|
this->buf_ = allocator.allocate(buffer_size);
|
||||||
|
if (this->buf_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate buffer of size %u", buffer_size);
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->effect_data_ = allocator.allocate(this->num_leds_);
|
||||||
|
if (this->effect_data_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to allocate effect data of size %u", this->num_leds_);
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select PIO instance to use (0 or 1)
|
||||||
|
this->pio_ = pio0;
|
||||||
|
if (this->pio_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to claim PIO instance");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the assembled program into the PIO and get its location in the PIO's instruction memory
|
||||||
|
uint offset = pio_add_program(this->pio_, this->program_);
|
||||||
|
|
||||||
|
// Configure the state machine's PIO, and start it
|
||||||
|
this->sm_ = pio_claim_unused_sm(this->pio_, true);
|
||||||
|
if (this->sm_ < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to claim PIO state machine");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) {
|
||||||
|
ESP_LOGVV(TAG, "Writing state...");
|
||||||
|
|
||||||
|
if (this->is_failed()) {
|
||||||
|
ESP_LOGW(TAG, "Light is in failed state, not writing state.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->buf_ == nullptr) {
|
||||||
|
ESP_LOGW(TAG, "Buffer is null, not writing state.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// assemble bits in buffer to 32 bit words with ex for GBR: 0bGGGGGGGGRRRRRRRRBBBBBBBB00000000
|
||||||
|
for (int i = 0; i < this->num_leds_; i++) {
|
||||||
|
uint8_t c1 = this->buf_[(i * 3) + 0];
|
||||||
|
uint8_t c2 = this->buf_[(i * 3) + 1];
|
||||||
|
uint8_t c3 = this->buf_[(i * 3) + 2];
|
||||||
|
uint8_t w = this->is_rgbw_ ? this->buf_[(i * 4) + 3] : 0;
|
||||||
|
uint32_t color = encode_uint32(c1, c2, c3, w);
|
||||||
|
pio_sm_put_blocking(this->pio_, this->sm_, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
light::ESPColorView RP2040PIOLEDStripLightOutput::get_view_internal(int32_t index) const {
|
||||||
|
int32_t r = 0, g = 0, b = 0, w = 0;
|
||||||
|
switch (this->rgb_order_) {
|
||||||
|
case ORDER_RGB:
|
||||||
|
r = 0;
|
||||||
|
g = 1;
|
||||||
|
b = 2;
|
||||||
|
break;
|
||||||
|
case ORDER_RBG:
|
||||||
|
r = 0;
|
||||||
|
g = 2;
|
||||||
|
b = 1;
|
||||||
|
break;
|
||||||
|
case ORDER_GRB:
|
||||||
|
r = 1;
|
||||||
|
g = 0;
|
||||||
|
b = 2;
|
||||||
|
break;
|
||||||
|
case ORDER_GBR:
|
||||||
|
r = 2;
|
||||||
|
g = 0;
|
||||||
|
b = 1;
|
||||||
|
break;
|
||||||
|
case ORDER_BGR:
|
||||||
|
r = 2;
|
||||||
|
g = 1;
|
||||||
|
b = 0;
|
||||||
|
break;
|
||||||
|
case ORDER_BRG:
|
||||||
|
r = 1;
|
||||||
|
g = 2;
|
||||||
|
b = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t multiplier = this->is_rgbw_ ? 4 : 3;
|
||||||
|
return {this->buf_ + (index * multiplier) + r,
|
||||||
|
this->buf_ + (index * multiplier) + g,
|
||||||
|
this->buf_ + (index * multiplier) + b,
|
||||||
|
this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr,
|
||||||
|
&this->effect_data_[index],
|
||||||
|
&this->correction_};
|
||||||
|
}
|
||||||
|
|
||||||
|
void RP2040PIOLEDStripLightOutput::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "RP2040 PIO LED Strip Light Output:");
|
||||||
|
ESP_LOGCONFIG(TAG, " Pin: GPIO%d", this->pin_);
|
||||||
|
ESP_LOGCONFIG(TAG, " Number of LEDs: %d", this->num_leds_);
|
||||||
|
ESP_LOGCONFIG(TAG, " RGBW: %s", YESNO(this->is_rgbw_));
|
||||||
|
ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order_to_string(this->rgb_order_));
|
||||||
|
ESP_LOGCONFIG(TAG, " Max Refresh Rate: %f Hz", this->max_refresh_rate_);
|
||||||
|
}
|
||||||
|
|
||||||
|
float RP2040PIOLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
|
|
||||||
|
} // namespace rp2040_pio_led_strip
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
108
esphome/components/rp2040_pio_led_strip/led_strip.h
Normal file
108
esphome/components/rp2040_pio_led_strip/led_strip.h
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_RP2040
|
||||||
|
|
||||||
|
#include "esphome/core/color.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
#include "esphome/components/light/addressable_light.h"
|
||||||
|
#include "esphome/components/light/light_output.h"
|
||||||
|
|
||||||
|
#include <hardware/pio.h>
|
||||||
|
#include <hardware/structs/pio.h>
|
||||||
|
#include <pico/stdio.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace rp2040_pio_led_strip {
|
||||||
|
|
||||||
|
enum RGBOrder : uint8_t {
|
||||||
|
ORDER_RGB,
|
||||||
|
ORDER_RBG,
|
||||||
|
ORDER_GRB,
|
||||||
|
ORDER_GBR,
|
||||||
|
ORDER_BGR,
|
||||||
|
ORDER_BRG,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const char *rgb_order_to_string(RGBOrder order) {
|
||||||
|
switch (order) {
|
||||||
|
case ORDER_RGB:
|
||||||
|
return "RGB";
|
||||||
|
case ORDER_RBG:
|
||||||
|
return "RBG";
|
||||||
|
case ORDER_GRB:
|
||||||
|
return "GRB";
|
||||||
|
case ORDER_GBR:
|
||||||
|
return "GBR";
|
||||||
|
case ORDER_BGR:
|
||||||
|
return "BGR";
|
||||||
|
case ORDER_BRG:
|
||||||
|
return "BRG";
|
||||||
|
default:
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using init_fn = void (*)(PIO pio, uint sm, uint offset, uint pin, float freq);
|
||||||
|
|
||||||
|
class RP2040PIOLEDStripLightOutput : public light::AddressableLight {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void write_state(light::LightState *state) override;
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
|
int32_t size() const override { return this->num_leds_; }
|
||||||
|
light::LightTraits get_traits() override {
|
||||||
|
auto traits = light::LightTraits();
|
||||||
|
this->is_rgbw_ ? traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE})
|
||||||
|
: traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
void set_pin(uint8_t pin) { this->pin_ = pin; }
|
||||||
|
void set_num_leds(uint32_t num_leds) { this->num_leds_ = num_leds; }
|
||||||
|
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
|
||||||
|
|
||||||
|
void set_max_refresh_rate(float interval_us) { this->max_refresh_rate_ = interval_us; }
|
||||||
|
|
||||||
|
void set_pio(int pio_num) { pio_num ? this->pio_ = pio1 : this->pio_ = pio0; }
|
||||||
|
void set_program(const pio_program_t *program) { this->program_ = program; }
|
||||||
|
void set_init_function(init_fn init) { this->init_ = init; }
|
||||||
|
|
||||||
|
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
|
||||||
|
void clear_effect_data() override {
|
||||||
|
for (int i = 0; i < this->size(); i++) {
|
||||||
|
this->effect_data_[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
light::ESPColorView get_view_internal(int32_t index) const override;
|
||||||
|
|
||||||
|
size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); }
|
||||||
|
|
||||||
|
uint8_t *buf_{nullptr};
|
||||||
|
uint8_t *effect_data_{nullptr};
|
||||||
|
|
||||||
|
uint8_t pin_;
|
||||||
|
uint32_t num_leds_;
|
||||||
|
bool is_rgbw_;
|
||||||
|
|
||||||
|
pio_hw_t *pio_;
|
||||||
|
uint sm_;
|
||||||
|
|
||||||
|
RGBOrder rgb_order_{ORDER_RGB};
|
||||||
|
|
||||||
|
uint32_t last_refresh_{0};
|
||||||
|
float max_refresh_rate_;
|
||||||
|
|
||||||
|
const pio_program_t *program_;
|
||||||
|
init_fn init_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace rp2040_pio_led_strip
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_RP2040
|
267
esphome/components/rp2040_pio_led_strip/light.py
Normal file
267
esphome/components/rp2040_pio_led_strip/light.py
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from esphome import pins
|
||||||
|
from esphome.components import light, rp2040
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_CHIPSET,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_NUM_LEDS,
|
||||||
|
CONF_OUTPUT_ID,
|
||||||
|
CONF_PIN,
|
||||||
|
CONF_RGB_ORDER,
|
||||||
|
)
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from esphome.util import _LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
def get_nops(timing):
|
||||||
|
"""
|
||||||
|
Calculate the number of NOP instructions required to wait for a given amount of time.
|
||||||
|
"""
|
||||||
|
time_remaining = timing
|
||||||
|
nops = []
|
||||||
|
if time_remaining < 32:
|
||||||
|
nops.append(time_remaining - 1)
|
||||||
|
return nops
|
||||||
|
nops.append(31)
|
||||||
|
time_remaining -= 32
|
||||||
|
while time_remaining > 0:
|
||||||
|
if time_remaining >= 32:
|
||||||
|
nops.append("nop [31]")
|
||||||
|
time_remaining -= 32
|
||||||
|
else:
|
||||||
|
nops.append("nop [" + str(time_remaining) + " - 1 ]")
|
||||||
|
time_remaining = 0
|
||||||
|
return nops
|
||||||
|
|
||||||
|
|
||||||
|
def generate_assembly_code(id, rgbw, t0h, t0l, t1h, t1l):
|
||||||
|
"""
|
||||||
|
Generate assembly code with the given timing values.
|
||||||
|
"""
|
||||||
|
nops_t0h = get_nops(t0h)
|
||||||
|
nops_t0l = get_nops(t0l)
|
||||||
|
nops_t1h = get_nops(t1h)
|
||||||
|
nops_t1l = get_nops(t1l)
|
||||||
|
|
||||||
|
t0h = nops_t0h.pop(0)
|
||||||
|
t0l = nops_t0l.pop(0)
|
||||||
|
t1h = nops_t1h.pop(0)
|
||||||
|
t1l = nops_t1l.pop(0)
|
||||||
|
|
||||||
|
nops_t0h = "\n".join(" " * 4 + nop for nop in nops_t0h)
|
||||||
|
nops_t0l = "\n".join(" " * 4 + nop for nop in nops_t0l)
|
||||||
|
nops_t1h = "\n".join(" " * 4 + nop for nop in nops_t1h)
|
||||||
|
nops_t1l = "\n".join(" " * 4 + nop for nop in nops_t1l)
|
||||||
|
|
||||||
|
const_csdk_code = f"""
|
||||||
|
% c-sdk {{
|
||||||
|
#include "hardware/clocks.h"
|
||||||
|
|
||||||
|
static inline void rp2040_pio_led_strip_driver_{id}_init(PIO pio, uint sm, uint offset, uint pin, float freq) {{
|
||||||
|
pio_gpio_init(pio, pin);
|
||||||
|
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
|
||||||
|
|
||||||
|
pio_sm_config c = rp2040_pio_led_strip_{id}_program_get_default_config(offset);
|
||||||
|
sm_config_set_set_pins(&c, pin, 1);
|
||||||
|
sm_config_set_out_shift(&c, false, true, {32 if rgbw else 24});
|
||||||
|
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
|
||||||
|
|
||||||
|
int cycles_per_bit = 69;
|
||||||
|
float div = 2.409;
|
||||||
|
sm_config_set_clkdiv(&c, div);
|
||||||
|
|
||||||
|
|
||||||
|
pio_sm_init(pio, sm, offset, &c);
|
||||||
|
pio_sm_set_enabled(pio, sm, true);
|
||||||
|
}}
|
||||||
|
%}}"""
|
||||||
|
|
||||||
|
assembly_template = f""".program rp2040_pio_led_strip_{id}
|
||||||
|
|
||||||
|
.wrap_target
|
||||||
|
awaiting_data:
|
||||||
|
; Wait for data in FIFO queue
|
||||||
|
pull block ; this will block until there is data in the FIFO queue and then it will pull it into the shift register
|
||||||
|
set y, {31 if rgbw else 23} ; set y to the number of bits to write counting 0, (23 if RGB, 31 if RGBW)
|
||||||
|
|
||||||
|
mainloop:
|
||||||
|
; go through each bit in the shift register and jump to the appropriate label
|
||||||
|
; depending on the value of the bit
|
||||||
|
|
||||||
|
out x, 1
|
||||||
|
jmp !x, writezero
|
||||||
|
jmp writeone
|
||||||
|
|
||||||
|
writezero:
|
||||||
|
; Write T0H and T0L bits to the output pin
|
||||||
|
set pins, 1 [{t0h}]
|
||||||
|
{nops_t0h}
|
||||||
|
set pins, 0 [{t0l}]
|
||||||
|
{nops_t0l}
|
||||||
|
jmp y--, mainloop
|
||||||
|
jmp awaiting_data
|
||||||
|
|
||||||
|
writeone:
|
||||||
|
; Write T1H and T1L bits to the output pin
|
||||||
|
set pins, 1 [{t1h}]
|
||||||
|
{nops_t1h}
|
||||||
|
set pins, 0 [{t1l}]
|
||||||
|
{nops_t1l}
|
||||||
|
jmp y--, mainloop
|
||||||
|
jmp awaiting_data
|
||||||
|
|
||||||
|
.wrap"""
|
||||||
|
|
||||||
|
return assembly_template + const_csdk_code
|
||||||
|
|
||||||
|
|
||||||
|
def time_to_cycles(time_us):
|
||||||
|
cycles_per_us = 57.5
|
||||||
|
cycles = round(float(time_us) * cycles_per_us)
|
||||||
|
return cycles
|
||||||
|
|
||||||
|
|
||||||
|
CONF_PIO = "pio"
|
||||||
|
|
||||||
|
CODEOWNERS = ["@Papa-DMan"]
|
||||||
|
DEPENDENCIES = ["rp2040"]
|
||||||
|
|
||||||
|
rp2040_pio_led_strip_ns = cg.esphome_ns.namespace("rp2040_pio_led_strip")
|
||||||
|
RP2040PIOLEDStripLightOutput = rp2040_pio_led_strip_ns.class_(
|
||||||
|
"RP2040PIOLEDStripLightOutput", light.AddressableLight
|
||||||
|
)
|
||||||
|
|
||||||
|
RGBOrder = rp2040_pio_led_strip_ns.enum("RGBOrder")
|
||||||
|
|
||||||
|
Chipsets = rp2040_pio_led_strip_ns.enum("Chipset")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LEDStripTimings:
|
||||||
|
T0H: int
|
||||||
|
T0L: int
|
||||||
|
T1H: int
|
||||||
|
T1L: int
|
||||||
|
|
||||||
|
|
||||||
|
RGB_ORDERS = {
|
||||||
|
"RGB": RGBOrder.ORDER_RGB,
|
||||||
|
"RBG": RGBOrder.ORDER_RBG,
|
||||||
|
"GRB": RGBOrder.ORDER_GRB,
|
||||||
|
"GBR": RGBOrder.ORDER_GBR,
|
||||||
|
"BGR": RGBOrder.ORDER_BGR,
|
||||||
|
"BRG": RGBOrder.ORDER_BRG,
|
||||||
|
}
|
||||||
|
|
||||||
|
CHIPSETS = {
|
||||||
|
"WS2812": LEDStripTimings(20, 43, 41, 31),
|
||||||
|
"WS2812B": LEDStripTimings(23, 46, 46, 23),
|
||||||
|
"SK6812": LEDStripTimings(17, 52, 31, 31),
|
||||||
|
"SM16703": LEDStripTimings(17, 52, 52, 17),
|
||||||
|
}
|
||||||
|
|
||||||
|
CONF_IS_RGBW = "is_rgbw"
|
||||||
|
CONF_BIT0_HIGH = "bit0_high"
|
||||||
|
CONF_BIT0_LOW = "bit0_low"
|
||||||
|
CONF_BIT1_HIGH = "bit1_high"
|
||||||
|
CONF_BIT1_LOW = "bit1_low"
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_timing(value):
|
||||||
|
# if doesn't end with us, raise error
|
||||||
|
if not value.endswith("us"):
|
||||||
|
raise cv.Invalid("Timing must be in microseconds (us)")
|
||||||
|
value = float(value[:-2])
|
||||||
|
nops = get_nops(value)
|
||||||
|
nops.pop(0)
|
||||||
|
if len(nops) > 3:
|
||||||
|
raise cv.Invalid("Timing is too long, please try again.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RP2040PIOLEDStripLightOutput),
|
||||||
|
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
|
||||||
|
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
|
||||||
|
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
|
||||||
|
cv.Required(CONF_PIO): cv.one_of(0, 1, int=True),
|
||||||
|
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
|
||||||
|
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
|
||||||
|
cv.Inclusive(
|
||||||
|
CONF_BIT0_HIGH,
|
||||||
|
"custom",
|
||||||
|
): _validate_timing,
|
||||||
|
cv.Inclusive(
|
||||||
|
CONF_BIT0_LOW,
|
||||||
|
"custom",
|
||||||
|
): _validate_timing,
|
||||||
|
cv.Inclusive(
|
||||||
|
CONF_BIT1_HIGH,
|
||||||
|
"custom",
|
||||||
|
): _validate_timing,
|
||||||
|
cv.Inclusive(
|
||||||
|
CONF_BIT1_LOW,
|
||||||
|
"custom",
|
||||||
|
): _validate_timing,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||||
|
id = config[CONF_ID].id
|
||||||
|
await light.register_light(var, config)
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
|
||||||
|
cg.add(var.set_pin(config[CONF_PIN]))
|
||||||
|
|
||||||
|
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
|
||||||
|
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
|
||||||
|
|
||||||
|
cg.add(var.set_pio(config[CONF_PIO]))
|
||||||
|
cg.add(var.set_program(cg.RawExpression(f"&rp2040_pio_led_strip_{id}_program")))
|
||||||
|
cg.add(
|
||||||
|
var.set_init_function(
|
||||||
|
cg.RawExpression(f"rp2040_pio_led_strip_driver_{id}_init")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
key = f"led_strip_{id}"
|
||||||
|
|
||||||
|
if CONF_CHIPSET in config:
|
||||||
|
_LOGGER.info("Generating PIO assembly code")
|
||||||
|
rp2040.add_pio_file(
|
||||||
|
__name__,
|
||||||
|
key,
|
||||||
|
generate_assembly_code(
|
||||||
|
id,
|
||||||
|
config[CONF_IS_RGBW],
|
||||||
|
CHIPSETS[config[CONF_CHIPSET]].T0H,
|
||||||
|
CHIPSETS[config[CONF_CHIPSET]].T0L,
|
||||||
|
CHIPSETS[config[CONF_CHIPSET]].T1H,
|
||||||
|
CHIPSETS[config[CONF_CHIPSET]].T1L,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_LOGGER.info("Generating custom PIO assembly code")
|
||||||
|
rp2040.add_pio_file(
|
||||||
|
__name__,
|
||||||
|
key,
|
||||||
|
generate_assembly_code(
|
||||||
|
id,
|
||||||
|
config[CONF_IS_RGBW],
|
||||||
|
time_to_cycles(config[CONF_BIT0_HIGH]),
|
||||||
|
time_to_cycles(config[CONF_BIT0_LOW]),
|
||||||
|
time_to_cycles(config[CONF_BIT1_HIGH]),
|
||||||
|
time_to_cycles(config[CONF_BIT1_LOW]),
|
||||||
|
),
|
||||||
|
)
|
@ -599,15 +599,6 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
cg.add(var.set_controller_auto_adv_switch(sw_aa_var))
|
cg.add(var.set_controller_auto_adv_switch(sw_aa_var))
|
||||||
|
|
||||||
if CONF_QUEUE_ENABLE_SWITCH in sprinkler_controller:
|
|
||||||
sw_qen_var = await switch.new_switch(
|
|
||||||
sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH]
|
|
||||||
)
|
|
||||||
await cg.register_component(
|
|
||||||
sw_qen_var, sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH]
|
|
||||||
)
|
|
||||||
cg.add(var.set_controller_queue_enable_switch(sw_qen_var))
|
|
||||||
|
|
||||||
if CONF_REVERSE_SWITCH in sprinkler_controller:
|
if CONF_REVERSE_SWITCH in sprinkler_controller:
|
||||||
sw_rev_var = await switch.new_switch(
|
sw_rev_var = await switch.new_switch(
|
||||||
sprinkler_controller[CONF_REVERSE_SWITCH]
|
sprinkler_controller[CONF_REVERSE_SWITCH]
|
||||||
@ -617,78 +608,83 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
cg.add(var.set_controller_reverse_switch(sw_rev_var))
|
cg.add(var.set_controller_reverse_switch(sw_rev_var))
|
||||||
|
|
||||||
if CONF_STANDBY_SWITCH in sprinkler_controller:
|
if CONF_STANDBY_SWITCH in sprinkler_controller:
|
||||||
sw_stb_var = await switch.new_switch(
|
sw_stb_var = await switch.new_switch(
|
||||||
sprinkler_controller[CONF_STANDBY_SWITCH]
|
sprinkler_controller[CONF_STANDBY_SWITCH]
|
||||||
)
|
)
|
||||||
await cg.register_component(
|
await cg.register_component(
|
||||||
sw_stb_var, sprinkler_controller[CONF_STANDBY_SWITCH]
|
sw_stb_var, sprinkler_controller[CONF_STANDBY_SWITCH]
|
||||||
)
|
)
|
||||||
cg.add(var.set_controller_standby_switch(sw_stb_var))
|
cg.add(var.set_controller_standby_switch(sw_stb_var))
|
||||||
|
|
||||||
if CONF_MULTIPLIER_NUMBER in sprinkler_controller:
|
if CONF_QUEUE_ENABLE_SWITCH in sprinkler_controller:
|
||||||
num_mult_var = await number.new_number(
|
sw_qen_var = await switch.new_switch(
|
||||||
sprinkler_controller[CONF_MULTIPLIER_NUMBER],
|
sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH]
|
||||||
min_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][
|
)
|
||||||
CONF_MIN_VALUE
|
await cg.register_component(
|
||||||
],
|
sw_qen_var, sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH]
|
||||||
max_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][
|
)
|
||||||
CONF_MAX_VALUE
|
cg.add(var.set_controller_queue_enable_switch(sw_qen_var))
|
||||||
],
|
|
||||||
step=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_STEP],
|
if CONF_MULTIPLIER_NUMBER in sprinkler_controller:
|
||||||
|
num_mult_var = await number.new_number(
|
||||||
|
sprinkler_controller[CONF_MULTIPLIER_NUMBER],
|
||||||
|
min_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_MIN_VALUE],
|
||||||
|
max_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_MAX_VALUE],
|
||||||
|
step=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_STEP],
|
||||||
|
)
|
||||||
|
await cg.register_component(
|
||||||
|
num_mult_var, sprinkler_controller[CONF_MULTIPLIER_NUMBER]
|
||||||
|
)
|
||||||
|
cg.add(
|
||||||
|
num_mult_var.set_initial_value(
|
||||||
|
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_INITIAL_VALUE]
|
||||||
)
|
)
|
||||||
await cg.register_component(
|
)
|
||||||
num_mult_var, sprinkler_controller[CONF_MULTIPLIER_NUMBER]
|
cg.add(
|
||||||
|
num_mult_var.set_restore_value(
|
||||||
|
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_RESTORE_VALUE]
|
||||||
)
|
)
|
||||||
cg.add(
|
)
|
||||||
num_mult_var.set_initial_value(
|
|
||||||
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_INITIAL_VALUE]
|
if CONF_SET_ACTION in sprinkler_controller[CONF_MULTIPLIER_NUMBER]:
|
||||||
)
|
await automation.build_automation(
|
||||||
)
|
num_mult_var.get_set_trigger(),
|
||||||
cg.add(
|
[(float, "x")],
|
||||||
num_mult_var.set_restore_value(
|
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_SET_ACTION],
|
||||||
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_RESTORE_VALUE]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_SET_ACTION in sprinkler_controller[CONF_MULTIPLIER_NUMBER]:
|
cg.add(var.set_controller_multiplier_number(num_mult_var))
|
||||||
await automation.build_automation(
|
|
||||||
num_mult_var.get_set_trigger(),
|
|
||||||
[(float, "x")],
|
|
||||||
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_SET_ACTION],
|
|
||||||
)
|
|
||||||
|
|
||||||
cg.add(var.set_controller_multiplier_number(num_mult_var))
|
if CONF_REPEAT_NUMBER in sprinkler_controller:
|
||||||
|
num_repeat_var = await number.new_number(
|
||||||
|
sprinkler_controller[CONF_REPEAT_NUMBER],
|
||||||
|
min_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MIN_VALUE],
|
||||||
|
max_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MAX_VALUE],
|
||||||
|
step=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_STEP],
|
||||||
|
)
|
||||||
|
await cg.register_component(
|
||||||
|
num_repeat_var, sprinkler_controller[CONF_REPEAT_NUMBER]
|
||||||
|
)
|
||||||
|
cg.add(
|
||||||
|
num_repeat_var.set_initial_value(
|
||||||
|
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_INITIAL_VALUE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cg.add(
|
||||||
|
num_repeat_var.set_restore_value(
|
||||||
|
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_RESTORE_VALUE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if CONF_REPEAT_NUMBER in sprinkler_controller:
|
if CONF_SET_ACTION in sprinkler_controller[CONF_REPEAT_NUMBER]:
|
||||||
num_repeat_var = await number.new_number(
|
await automation.build_automation(
|
||||||
sprinkler_controller[CONF_REPEAT_NUMBER],
|
num_repeat_var.get_set_trigger(),
|
||||||
min_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MIN_VALUE],
|
[(float, "x")],
|
||||||
max_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MAX_VALUE],
|
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_SET_ACTION],
|
||||||
step=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_STEP],
|
|
||||||
)
|
|
||||||
await cg.register_component(
|
|
||||||
num_repeat_var, sprinkler_controller[CONF_REPEAT_NUMBER]
|
|
||||||
)
|
|
||||||
cg.add(
|
|
||||||
num_repeat_var.set_initial_value(
|
|
||||||
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_INITIAL_VALUE]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
cg.add(
|
|
||||||
num_repeat_var.set_restore_value(
|
|
||||||
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_RESTORE_VALUE]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_SET_ACTION in sprinkler_controller[CONF_REPEAT_NUMBER]:
|
cg.add(var.set_controller_repeat_number(num_repeat_var))
|
||||||
await automation.build_automation(
|
|
||||||
num_repeat_var.get_set_trigger(),
|
|
||||||
[(float, "x")],
|
|
||||||
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_SET_ACTION],
|
|
||||||
)
|
|
||||||
|
|
||||||
cg.add(var.set_controller_repeat_number(num_repeat_var))
|
|
||||||
|
|
||||||
for valve in sprinkler_controller[CONF_VALVES]:
|
for valve in sprinkler_controller[CONF_VALVES]:
|
||||||
sw_valve_var = await switch.new_switch(valve[CONF_VALVE_SWITCH])
|
sw_valve_var = await switch.new_switch(valve[CONF_VALVE_SWITCH])
|
||||||
|
@ -147,22 +147,22 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler
|
|||||||
: controller_(controller), valve_(valve) {}
|
: controller_(controller), valve_(valve) {}
|
||||||
|
|
||||||
void SprinklerValveOperator::loop() {
|
void SprinklerValveOperator::loop() {
|
||||||
if (millis() >= this->pinned_millis_) { // dummy check
|
if (millis() >= this->start_millis_) { // dummy check
|
||||||
switch (this->state_) {
|
switch (this->state_) {
|
||||||
case STARTING:
|
case STARTING:
|
||||||
if (millis() > (this->pinned_millis_ + this->start_delay_)) {
|
if (millis() > (this->start_millis_ + this->start_delay_)) {
|
||||||
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
|
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ACTIVE:
|
case ACTIVE:
|
||||||
if (millis() > (this->pinned_millis_ + this->start_delay_ + this->run_duration_)) {
|
if (millis() > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
|
||||||
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
|
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STOPPING:
|
case STOPPING:
|
||||||
if (millis() > (this->pinned_millis_ + this->stop_delay_)) {
|
if (millis() > (this->stop_millis_ + this->stop_delay_)) {
|
||||||
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
|
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -183,11 +183,12 @@ void SprinklerValveOperator::set_controller(Sprinkler *controller) {
|
|||||||
|
|
||||||
void SprinklerValveOperator::set_valve(SprinklerValve *valve) {
|
void SprinklerValveOperator::set_valve(SprinklerValve *valve) {
|
||||||
if (valve != nullptr) {
|
if (valve != nullptr) {
|
||||||
this->state_ = IDLE; // reset state
|
this->state_ = IDLE; // reset state
|
||||||
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
|
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
|
||||||
this->pinned_millis_ = 0; // reset because (new) valve has not been started yet
|
this->start_millis_ = 0; // reset because (new) valve has not been started yet
|
||||||
this->kill_(); // ensure everything is off before we let go!
|
this->stop_millis_ = 0; // reset because (new) valve has not been started yet
|
||||||
this->valve_ = valve; // finally, set the pointer to the new valve
|
this->kill_(); // ensure everything is off before we let go!
|
||||||
|
this->valve_ = valve; // finally, set the pointer to the new valve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +222,8 @@ void SprinklerValveOperator::start() {
|
|||||||
} else {
|
} else {
|
||||||
this->run_(); // there is no start_delay_, so just start the pump and valve
|
this->run_(); // there is no start_delay_, so just start the pump and valve
|
||||||
}
|
}
|
||||||
this->pinned_millis_ = millis(); // save the time the start request was made
|
this->stop_millis_ = 0;
|
||||||
|
this->start_millis_ = millis(); // save the time the start request was made
|
||||||
}
|
}
|
||||||
|
|
||||||
void SprinklerValveOperator::stop() {
|
void SprinklerValveOperator::stop() {
|
||||||
@ -238,19 +240,33 @@ void SprinklerValveOperator::stop() {
|
|||||||
if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use...
|
if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use...
|
||||||
this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time
|
this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time
|
||||||
}
|
}
|
||||||
this->pinned_millis_ = millis(); // save the time the stop request was made
|
|
||||||
} else {
|
} else {
|
||||||
this->kill_(); // there is no stop_delay_, so just stop the pump and valve
|
this->kill_(); // there is no stop_delay_, so just stop the pump and valve
|
||||||
}
|
}
|
||||||
|
this->stop_millis_ = millis(); // save the time the stop request was made
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_; }
|
uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; }
|
||||||
|
|
||||||
uint32_t SprinklerValveOperator::time_remaining() {
|
uint32_t SprinklerValveOperator::time_remaining() {
|
||||||
if ((this->state_ == STARTING) || (this->state_ == ACTIVE)) {
|
if (this->start_millis_ == 0) {
|
||||||
return (this->pinned_millis_ + this->start_delay_ + this->run_duration_ - millis()) / 1000;
|
return this->run_duration(); // hasn't been started yet
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
|
if (this->stop_millis_) {
|
||||||
|
if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) {
|
||||||
|
return 0; // valve was active for more than its configured duration, so we are done
|
||||||
|
} else {
|
||||||
|
// we're stopped; return time remaining
|
||||||
|
return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_;
|
||||||
|
if (completed_millis > millis()) {
|
||||||
|
return (completed_millis - millis()) / 1000; // running now
|
||||||
|
}
|
||||||
|
return 0; // run completed
|
||||||
}
|
}
|
||||||
|
|
||||||
SprinklerState SprinklerValveOperator::state() { return this->state_; }
|
SprinklerState SprinklerValveOperator::state() { return this->state_; }
|
||||||
@ -386,6 +402,9 @@ void Sprinkler::loop() {
|
|||||||
for (auto &vo : this->valve_op_) {
|
for (auto &vo : this->valve_op_) {
|
||||||
vo.loop();
|
vo.loop();
|
||||||
}
|
}
|
||||||
|
if (this->prev_req_.has_request() && this->prev_req_.valve_operator()->state() == IDLE) {
|
||||||
|
this->prev_req_.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw) {
|
void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw) {
|
||||||
@ -732,7 +751,7 @@ bool Sprinkler::auto_advance() {
|
|||||||
if (this->auto_adv_sw_ != nullptr) {
|
if (this->auto_adv_sw_ != nullptr) {
|
||||||
return this->auto_adv_sw_->state;
|
return this->auto_adv_sw_->state;
|
||||||
}
|
}
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
float Sprinkler::multiplier() {
|
float Sprinkler::multiplier() {
|
||||||
@ -972,7 +991,14 @@ optional<SprinklerValveRunRequestOrigin> Sprinkler::active_valve_request_is_from
|
|||||||
return nullopt;
|
return nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional<size_t> Sprinkler::active_valve() { return this->active_req_.valve_as_opt(); }
|
optional<size_t> Sprinkler::active_valve() {
|
||||||
|
if (!this->valve_overlap_ && this->prev_req_.has_request() &&
|
||||||
|
(this->prev_req_.valve_operator()->state() == STARTING || this->prev_req_.valve_operator()->state() == ACTIVE)) {
|
||||||
|
return this->prev_req_.valve_as_opt();
|
||||||
|
}
|
||||||
|
return this->active_req_.valve_as_opt();
|
||||||
|
}
|
||||||
|
|
||||||
optional<size_t> Sprinkler::paused_valve() { return this->paused_valve_; }
|
optional<size_t> Sprinkler::paused_valve() { return this->paused_valve_; }
|
||||||
|
|
||||||
optional<size_t> Sprinkler::queued_valve() {
|
optional<size_t> Sprinkler::queued_valve() {
|
||||||
@ -1097,22 +1123,35 @@ uint32_t Sprinkler::total_cycle_time_enabled_valves() {
|
|||||||
|
|
||||||
uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() {
|
uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() {
|
||||||
uint32_t total_time_remaining = 0;
|
uint32_t total_time_remaining = 0;
|
||||||
uint32_t valve_count = 0;
|
uint32_t enabled_valve_count = 0;
|
||||||
|
uint32_t incomplete_valve_count = 0;
|
||||||
|
|
||||||
for (size_t valve = 0; valve < this->number_of_valves(); valve++) {
|
for (size_t valve = 0; valve < this->number_of_valves(); valve++) {
|
||||||
if (this->valve_is_enabled_(valve) && !this->valve_cycle_complete_(valve)) {
|
if (this->valve_is_enabled_(valve)) {
|
||||||
if (!this->active_valve().has_value() || (valve != this->active_valve().value())) {
|
enabled_valve_count++;
|
||||||
total_time_remaining += this->valve_run_duration_adjusted(valve);
|
if (!this->valve_cycle_complete_(valve)) {
|
||||||
valve_count++;
|
if (!this->active_valve().has_value() || (valve != this->active_valve().value())) {
|
||||||
|
total_time_remaining += this->valve_run_duration_adjusted(valve);
|
||||||
|
incomplete_valve_count++;
|
||||||
|
} else {
|
||||||
|
// to get here, there must be an active valve and this valve must be equal to 'valve'
|
||||||
|
if (this->active_req_.valve_operator() == nullptr) { // no SVO has been assigned yet so it hasn't started
|
||||||
|
total_time_remaining += this->valve_run_duration_adjusted(valve);
|
||||||
|
incomplete_valve_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valve_count) {
|
if (incomplete_valve_count >= enabled_valve_count) {
|
||||||
|
incomplete_valve_count--;
|
||||||
|
}
|
||||||
|
if (incomplete_valve_count) {
|
||||||
if (this->valve_overlap_) {
|
if (this->valve_overlap_) {
|
||||||
total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1);
|
total_time_remaining -= this->switching_delay_.value_or(0) * incomplete_valve_count;
|
||||||
} else {
|
} else {
|
||||||
total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1);
|
total_time_remaining += this->switching_delay_.value_or(0) * incomplete_valve_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1149,31 +1188,32 @@ optional<uint32_t> Sprinkler::time_remaining_active_valve() {
|
|||||||
return this->active_req_.valve_operator()->time_remaining();
|
return this->active_req_.valve_operator()->time_remaining();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto &vo : this->valve_op_) { // ...else return the value from the first non-IDLE SprinklerValveOperator
|
if (this->prev_req_.has_request()) { // try to return the value based on prev_req_...
|
||||||
if (vo.state() != IDLE) {
|
if (this->prev_req_.valve_operator() != nullptr) {
|
||||||
return vo.time_remaining();
|
return this->prev_req_.valve_operator()->time_remaining();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nullopt;
|
return nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional<uint32_t> Sprinkler::time_remaining_current_operation() {
|
optional<uint32_t> Sprinkler::time_remaining_current_operation() {
|
||||||
auto total_time_remaining = this->time_remaining_active_valve();
|
if (!this->time_remaining_active_valve().has_value() && this->state_ == IDLE) {
|
||||||
|
return nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
if (total_time_remaining.has_value()) {
|
auto total_time_remaining = this->time_remaining_active_valve().value_or(0);
|
||||||
if (this->auto_advance()) {
|
if (this->auto_advance()) {
|
||||||
total_time_remaining = total_time_remaining.value() + this->total_cycle_time_enabled_incomplete_valves();
|
total_time_remaining += this->total_cycle_time_enabled_incomplete_valves();
|
||||||
total_time_remaining =
|
if (this->repeat().value_or(0) > 0) {
|
||||||
total_time_remaining.value() +
|
total_time_remaining +=
|
||||||
(this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0)));
|
(this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->queue_enabled()) {
|
|
||||||
total_time_remaining = total_time_remaining.value() + this->total_queue_time();
|
|
||||||
}
|
|
||||||
return total_time_remaining;
|
|
||||||
}
|
}
|
||||||
return nullopt;
|
|
||||||
|
if (this->queue_enabled()) {
|
||||||
|
total_time_remaining += this->total_queue_time();
|
||||||
|
}
|
||||||
|
return total_time_remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Sprinkler::any_controller_is_active() {
|
bool Sprinkler::any_controller_is_active() {
|
||||||
@ -1305,6 +1345,12 @@ optional<size_t> Sprinkler::next_valve_number_in_cycle_(const optional<size_t> f
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Sprinkler::load_next_valve_run_request_(const optional<size_t> first_valve) {
|
void Sprinkler::load_next_valve_run_request_(const optional<size_t> first_valve) {
|
||||||
|
if (this->active_req_.has_request()) {
|
||||||
|
this->prev_req_ = this->active_req_;
|
||||||
|
} else {
|
||||||
|
this->prev_req_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
if (this->next_req_.has_request()) {
|
if (this->next_req_.has_request()) {
|
||||||
if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on
|
if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on
|
||||||
this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve()));
|
this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve()));
|
||||||
|
@ -170,7 +170,8 @@ class SprinklerValveOperator {
|
|||||||
uint32_t start_delay_{0};
|
uint32_t start_delay_{0};
|
||||||
uint32_t stop_delay_{0};
|
uint32_t stop_delay_{0};
|
||||||
uint32_t run_duration_{0};
|
uint32_t run_duration_{0};
|
||||||
uint64_t pinned_millis_{0};
|
uint64_t start_millis_{0};
|
||||||
|
uint64_t stop_millis_{0};
|
||||||
Sprinkler *controller_{nullptr};
|
Sprinkler *controller_{nullptr};
|
||||||
SprinklerValve *valve_{nullptr};
|
SprinklerValve *valve_{nullptr};
|
||||||
SprinklerState state_{IDLE};
|
SprinklerState state_{IDLE};
|
||||||
@ -538,15 +539,18 @@ class Sprinkler : public Component {
|
|||||||
/// The valve run request that is currently active
|
/// The valve run request that is currently active
|
||||||
SprinklerValveRunRequest active_req_;
|
SprinklerValveRunRequest active_req_;
|
||||||
|
|
||||||
|
/// The next run request for the controller to consume after active_req_ is complete
|
||||||
|
SprinklerValveRunRequest next_req_;
|
||||||
|
|
||||||
|
/// The previous run request the controller processed
|
||||||
|
SprinklerValveRunRequest prev_req_;
|
||||||
|
|
||||||
/// The number of the manually selected valve currently selected
|
/// The number of the manually selected valve currently selected
|
||||||
optional<size_t> manual_valve_;
|
optional<size_t> manual_valve_;
|
||||||
|
|
||||||
/// The number of the valve to resume from (if paused)
|
/// The number of the valve to resume from (if paused)
|
||||||
optional<size_t> paused_valve_;
|
optional<size_t> paused_valve_;
|
||||||
|
|
||||||
/// The next run request for the controller to consume after active_req_ is complete
|
|
||||||
SprinklerValveRunRequest next_req_;
|
|
||||||
|
|
||||||
/// Set the number of times to repeat a full cycle
|
/// Set the number of times to repeat a full cycle
|
||||||
optional<uint32_t> target_repeats_;
|
optional<uint32_t> target_repeats_;
|
||||||
|
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
|
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome import core
|
from esphome import core
|
||||||
from esphome.const import CONF_SUBSTITUTIONS
|
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
|
||||||
from esphome.yaml_util import ESPHomeDataBase, make_data_base
|
from esphome.yaml_util import ESPHomeDataBase, make_data_base
|
||||||
from esphome.config_helpers import merge_config
|
from esphome.config_helpers import merge_config
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
|
||||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_substitution_key(value):
|
def validate_substitution_key(value):
|
||||||
value = cv.string(value)
|
value = cv.string(value)
|
||||||
@ -42,12 +37,6 @@ async def to_code(config):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=consider-using-f-string
|
|
||||||
VARIABLE_PROG = re.compile(
|
|
||||||
"\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _expand_substitutions(substitutions, value, path, ignore_missing):
|
def _expand_substitutions(substitutions, value, path, ignore_missing):
|
||||||
if "$" not in value:
|
if "$" not in value:
|
||||||
return value
|
return value
|
||||||
@ -56,7 +45,7 @@ def _expand_substitutions(substitutions, value, path, ignore_missing):
|
|||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
while True:
|
while True:
|
||||||
m = VARIABLE_PROG.search(value, i)
|
m = cv.VARIABLE_PROG.search(value, i)
|
||||||
if not m:
|
if not m:
|
||||||
# Nothing more to match. Done
|
# Nothing more to match. Done
|
||||||
break
|
break
|
||||||
|
@ -10,6 +10,8 @@ from esphome.const import (
|
|||||||
CONF_BRIGHTNESS,
|
CONF_BRIGHTNESS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@freekode"]
|
||||||
|
|
||||||
tm1651_ns = cg.esphome_ns.namespace("tm1651")
|
tm1651_ns = cg.esphome_ns.namespace("tm1651")
|
||||||
TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component)
|
TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component)
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import esphome.config_validation as cv
|
|||||||
from esphome import pins
|
from esphome import pins
|
||||||
from esphome.const import CONF_ID, CONF_SDO_PIN, CONF_SCL_PIN
|
from esphome.const import CONF_ID, CONF_SDO_PIN, CONF_SCL_PIN
|
||||||
|
|
||||||
DEPENDENCIES = ["i2c"]
|
|
||||||
AUTO_LOAD = ["binary_sensor"]
|
AUTO_LOAD = ["binary_sensor"]
|
||||||
|
|
||||||
CONF_TTP229_ID = "ttp229_id"
|
CONF_TTP229_ID = "ttp229_id"
|
||||||
|
@ -102,6 +102,16 @@ void Wiegand::loop() {
|
|||||||
uint8_t key = KEYS[value];
|
uint8_t key = KEYS[value];
|
||||||
this->send_key_(key);
|
this->send_key_(key);
|
||||||
}
|
}
|
||||||
|
} else if (count == 8) {
|
||||||
|
if ((value ^ 0xf0) >> 4 == (value & 0xf)) {
|
||||||
|
value &= 0xf;
|
||||||
|
for (auto *trigger : this->key_triggers_)
|
||||||
|
trigger->trigger(value);
|
||||||
|
if (value < 12) {
|
||||||
|
uint8_t key = KEYS[value];
|
||||||
|
this->send_key_(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "received unknown %d-bit value: %llx", count, value);
|
ESP_LOGD(TAG, "received unknown %d-bit value: %llx", count, value);
|
||||||
}
|
}
|
||||||
|
@ -183,6 +183,11 @@ network::IPAddress WiFiComponent::get_ip_address() {
|
|||||||
return this->wifi_soft_ap_ip();
|
return this->wifi_soft_ap_ip();
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
network::IPAddress WiFiComponent::get_dns_address(int num) {
|
||||||
|
if (this->has_sta())
|
||||||
|
return this->wifi_dns_ip_(num);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
std::string WiFiComponent::get_use_address() const {
|
std::string WiFiComponent::get_use_address() const {
|
||||||
if (this->use_address_.empty()) {
|
if (this->use_address_.empty()) {
|
||||||
return App.get_name() + ".local";
|
return App.get_name() + ".local";
|
||||||
|
@ -240,6 +240,7 @@ class WiFiComponent : public Component {
|
|||||||
void set_rrm(bool rrm);
|
void set_rrm(bool rrm);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
network::IPAddress get_dns_address(int num);
|
||||||
network::IPAddress get_ip_address();
|
network::IPAddress get_ip_address();
|
||||||
std::string get_use_address() const;
|
std::string get_use_address() const;
|
||||||
void set_use_address(const std::string &use_address);
|
void set_use_address(const std::string &use_address);
|
||||||
|
@ -7,6 +7,7 @@ from esphome.const import (
|
|||||||
CONF_SCAN_RESULTS,
|
CONF_SCAN_RESULTS,
|
||||||
CONF_SSID,
|
CONF_SSID,
|
||||||
CONF_MAC_ADDRESS,
|
CONF_MAC_ADDRESS,
|
||||||
|
CONF_DNS_ADDRESS,
|
||||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,6 +29,9 @@ BSSIDWiFiInfo = wifi_info_ns.class_(
|
|||||||
MacAddressWifiInfo = wifi_info_ns.class_(
|
MacAddressWifiInfo = wifi_info_ns.class_(
|
||||||
"MacAddressWifiInfo", text_sensor.TextSensor, cg.Component
|
"MacAddressWifiInfo", text_sensor.TextSensor, cg.Component
|
||||||
)
|
)
|
||||||
|
DNSAddressWifiInfo = wifi_info_ns.class_(
|
||||||
|
"DNSAddressWifiInfo", text_sensor.TextSensor, cg.PollingComponent
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
@ -46,6 +50,9 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema(
|
cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema(
|
||||||
MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema(
|
||||||
|
DNSAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||||
|
).extend(cv.polling_component_schema("1s")),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -63,3 +70,4 @@ async def to_code(config):
|
|||||||
await setup_conf(config, CONF_BSSID)
|
await setup_conf(config, CONF_BSSID)
|
||||||
await setup_conf(config, CONF_MAC_ADDRESS)
|
await setup_conf(config, CONF_MAC_ADDRESS)
|
||||||
await setup_conf(config, CONF_SCAN_RESULTS)
|
await setup_conf(config, CONF_SCAN_RESULTS)
|
||||||
|
await setup_conf(config, CONF_DNS_ADDRESS)
|
||||||
|
@ -11,6 +11,7 @@ void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Scan Res
|
|||||||
void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
|
void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
|
||||||
void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
|
void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
|
||||||
void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); }
|
void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); }
|
||||||
|
void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo DNS Address", this); }
|
||||||
|
|
||||||
} // namespace wifi_info
|
} // namespace wifi_info
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -24,6 +24,32 @@ class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSenso
|
|||||||
network::IPAddress last_ip_;
|
network::IPAddress last_ip_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor {
|
||||||
|
public:
|
||||||
|
void update() override {
|
||||||
|
std::string dns_results;
|
||||||
|
|
||||||
|
auto dns_one = wifi::global_wifi_component->get_dns_address(0);
|
||||||
|
auto dns_two = wifi::global_wifi_component->get_dns_address(1);
|
||||||
|
|
||||||
|
dns_results += "DNS1: ";
|
||||||
|
dns_results += dns_one.str();
|
||||||
|
dns_results += " DNS2: ";
|
||||||
|
dns_results += dns_two.str();
|
||||||
|
|
||||||
|
if (dns_results != this->last_results_) {
|
||||||
|
this->last_results_ = dns_results;
|
||||||
|
this->publish_state(dns_results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
std::string unique_id() override { return get_mac_address() + "-wifiinfo-dns"; }
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string last_results_;
|
||||||
|
};
|
||||||
|
|
||||||
class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
|
class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
|
||||||
public:
|
public:
|
||||||
void update() override {
|
void update() override {
|
||||||
|
@ -53,6 +53,7 @@ from esphome.const import (
|
|||||||
KEY_TARGET_PLATFORM,
|
KEY_TARGET_PLATFORM,
|
||||||
TYPE_GIT,
|
TYPE_GIT,
|
||||||
TYPE_LOCAL,
|
TYPE_LOCAL,
|
||||||
|
VALID_SUBSTITUTIONS_CHARACTERS,
|
||||||
)
|
)
|
||||||
from esphome.core import (
|
from esphome.core import (
|
||||||
CORE,
|
CORE,
|
||||||
@ -79,6 +80,11 @@ from esphome.yaml_util import make_data_base
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
|
VARIABLE_PROG = re.compile(
|
||||||
|
"\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS)
|
||||||
|
)
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
Schema = _Schema
|
Schema = _Schema
|
||||||
@ -265,6 +271,14 @@ def alphanumeric(value):
|
|||||||
|
|
||||||
def valid_name(value):
|
def valid_name(value):
|
||||||
value = string_strict(value)
|
value = string_strict(value)
|
||||||
|
|
||||||
|
if CORE.vscode:
|
||||||
|
# If the value is a substitution, it can't be validated until the substitution
|
||||||
|
# is actually made.
|
||||||
|
sub_match = VARIABLE_PROG.search(value)
|
||||||
|
if sub_match:
|
||||||
|
return value
|
||||||
|
|
||||||
for c in value:
|
for c in value:
|
||||||
if c not in ALLOWED_NAME_CHARS:
|
if c not in ALLOWED_NAME_CHARS:
|
||||||
raise Invalid(
|
raise Invalid(
|
||||||
@ -447,6 +461,14 @@ def validate_id_name(value):
|
|||||||
raise Invalid(
|
raise Invalid(
|
||||||
"Dashes are not supported in IDs, please use underscores instead."
|
"Dashes are not supported in IDs, please use underscores instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if CORE.vscode:
|
||||||
|
# If the value is a substitution, it can't be validated until the substitution
|
||||||
|
# is actually made
|
||||||
|
sub_match = VARIABLE_PROG.match(value)
|
||||||
|
if sub_match:
|
||||||
|
return value
|
||||||
|
|
||||||
valid_chars = f"{ascii_letters + digits}_"
|
valid_chars = f"{ascii_letters + digits}_"
|
||||||
for char in value:
|
for char in value:
|
||||||
if char not in valid_chars:
|
if char not in valid_chars:
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
__version__ = "2023.6.0-dev"
|
__version__ = "2023.6.0-dev"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
|
||||||
|
)
|
||||||
|
|
||||||
PLATFORM_ESP32 = "esp32"
|
PLATFORM_ESP32 = "esp32"
|
||||||
PLATFORM_ESP8266 = "esp8266"
|
PLATFORM_ESP8266 = "esp8266"
|
||||||
@ -180,6 +183,7 @@ CONF_DIR_PIN = "dir_pin"
|
|||||||
CONF_DIRECTION = "direction"
|
CONF_DIRECTION = "direction"
|
||||||
CONF_DIRECTION_OUTPUT = "direction_output"
|
CONF_DIRECTION_OUTPUT = "direction_output"
|
||||||
CONF_DISABLE_CRC = "disable_crc"
|
CONF_DISABLE_CRC = "disable_crc"
|
||||||
|
CONF_DISABLED = "disabled"
|
||||||
CONF_DISABLED_BY_DEFAULT = "disabled_by_default"
|
CONF_DISABLED_BY_DEFAULT = "disabled_by_default"
|
||||||
CONF_DISCONNECT_DELAY = "disconnect_delay"
|
CONF_DISCONNECT_DELAY = "disconnect_delay"
|
||||||
CONF_DISCOVERY = "discovery"
|
CONF_DISCOVERY = "discovery"
|
||||||
@ -190,6 +194,7 @@ CONF_DISCOVERY_UNIQUE_ID_GENERATOR = "discovery_unique_id_generator"
|
|||||||
CONF_DISTANCE = "distance"
|
CONF_DISTANCE = "distance"
|
||||||
CONF_DITHER = "dither"
|
CONF_DITHER = "dither"
|
||||||
CONF_DIV_RATIO = "div_ratio"
|
CONF_DIV_RATIO = "div_ratio"
|
||||||
|
CONF_DNS_ADDRESS = "dns_address"
|
||||||
CONF_DNS1 = "dns1"
|
CONF_DNS1 = "dns1"
|
||||||
CONF_DNS2 = "dns2"
|
CONF_DNS2 = "dns2"
|
||||||
CONF_DOMAIN = "domain"
|
CONF_DOMAIN = "domain"
|
||||||
@ -391,6 +396,7 @@ CONF_MAX_SPEED = "max_speed"
|
|||||||
CONF_MAX_TEMPERATURE = "max_temperature"
|
CONF_MAX_TEMPERATURE = "max_temperature"
|
||||||
CONF_MAX_VALUE = "max_value"
|
CONF_MAX_VALUE = "max_value"
|
||||||
CONF_MAX_VOLTAGE = "max_voltage"
|
CONF_MAX_VOLTAGE = "max_voltage"
|
||||||
|
CONF_MDNS = "mdns"
|
||||||
CONF_MEASUREMENT_DURATION = "measurement_duration"
|
CONF_MEASUREMENT_DURATION = "measurement_duration"
|
||||||
CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number"
|
CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number"
|
||||||
CONF_MEDIUM = "medium"
|
CONF_MEDIUM = "medium"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import binascii
|
||||||
import codecs
|
import codecs
|
||||||
import collections
|
import collections
|
||||||
import functools
|
import functools
|
||||||
@ -76,6 +77,10 @@ class DashboardSettings:
|
|||||||
def status_use_ping(self):
|
def status_use_ping(self):
|
||||||
return get_bool_env("ESPHOME_DASHBOARD_USE_PING")
|
return get_bool_env("ESPHOME_DASHBOARD_USE_PING")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status_use_mqtt(self):
|
||||||
|
return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def using_ha_addon_auth(self):
|
def using_ha_addon_auth(self):
|
||||||
if not self.on_ha_addon:
|
if not self.on_ha_addon:
|
||||||
@ -583,6 +588,12 @@ class DashboardEntry:
|
|||||||
return None
|
return None
|
||||||
return self.storage.address
|
return self.storage.address
|
||||||
|
|
||||||
|
@property
|
||||||
|
def no_mdns(self):
|
||||||
|
if self.storage is None:
|
||||||
|
return None
|
||||||
|
return self.storage.no_mdns
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def web_port(self):
|
def web_port(self):
|
||||||
if self.storage is None:
|
if self.storage is None:
|
||||||
@ -775,9 +786,12 @@ class MDNSStatusThread(threading.Thread):
|
|||||||
stat.start()
|
stat.start()
|
||||||
while not STOP_EVENT.is_set():
|
while not STOP_EVENT.is_set():
|
||||||
entries = _list_dashboard_entries()
|
entries = _list_dashboard_entries()
|
||||||
stat.request_query(
|
hosts = {}
|
||||||
{entry.filename: f"{entry.name}.local." for entry in entries}
|
for entry in entries:
|
||||||
)
|
if entry.no_mdns is not True:
|
||||||
|
hosts[entry.filename] = f"{entry.name}.local."
|
||||||
|
|
||||||
|
stat.request_query(hosts)
|
||||||
IMPORT_RESULT = imports.import_state
|
IMPORT_RESULT = imports.import_state
|
||||||
|
|
||||||
PING_REQUEST.wait()
|
PING_REQUEST.wait()
|
||||||
@ -801,6 +815,9 @@ class PingStatusThread(threading.Thread):
|
|||||||
entries = _list_dashboard_entries()
|
entries = _list_dashboard_entries()
|
||||||
queue = collections.deque()
|
queue = collections.deque()
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
|
if entry.no_mdns is True:
|
||||||
|
continue
|
||||||
|
|
||||||
if entry.address is None:
|
if entry.address is None:
|
||||||
PING_RESULT[entry.filename] = None
|
PING_RESULT[entry.filename] = None
|
||||||
continue
|
continue
|
||||||
@ -832,10 +849,67 @@ class PingStatusThread(threading.Thread):
|
|||||||
PING_REQUEST.clear()
|
PING_REQUEST.clear()
|
||||||
|
|
||||||
|
|
||||||
|
class MqttStatusThread(threading.Thread):
|
||||||
|
def run(self):
|
||||||
|
from esphome import mqtt
|
||||||
|
|
||||||
|
entries = _list_dashboard_entries()
|
||||||
|
|
||||||
|
config = mqtt.config_from_env()
|
||||||
|
topic = "esphome/discover/#"
|
||||||
|
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
nonlocal entries
|
||||||
|
|
||||||
|
payload = msg.payload.decode(errors="backslashreplace")
|
||||||
|
if len(payload) > 0:
|
||||||
|
data = json.loads(payload)
|
||||||
|
if "name" not in data:
|
||||||
|
return
|
||||||
|
for entry in entries:
|
||||||
|
if entry.name == data["name"]:
|
||||||
|
PING_RESULT[entry.filename] = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_connect(client, userdata, flags, return_code):
|
||||||
|
client.publish("esphome/discover", None, retain=False)
|
||||||
|
|
||||||
|
mqttid = str(binascii.hexlify(os.urandom(6)).decode())
|
||||||
|
|
||||||
|
client = mqtt.prepare(
|
||||||
|
config,
|
||||||
|
[topic],
|
||||||
|
on_message,
|
||||||
|
on_connect,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
f"esphome-dashboard-{mqttid}",
|
||||||
|
)
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
|
while not STOP_EVENT.wait(2):
|
||||||
|
# update entries
|
||||||
|
entries = _list_dashboard_entries()
|
||||||
|
|
||||||
|
# will be set to true on on_message
|
||||||
|
for entry in entries:
|
||||||
|
if entry.no_mdns:
|
||||||
|
PING_RESULT[entry.filename] = False
|
||||||
|
|
||||||
|
client.publish("esphome/discover", None, retain=False)
|
||||||
|
MQTT_PING_REQUEST.wait()
|
||||||
|
MQTT_PING_REQUEST.clear()
|
||||||
|
|
||||||
|
client.disconnect()
|
||||||
|
client.loop_stop()
|
||||||
|
|
||||||
|
|
||||||
class PingRequestHandler(BaseHandler):
|
class PingRequestHandler(BaseHandler):
|
||||||
@authenticated
|
@authenticated
|
||||||
def get(self):
|
def get(self):
|
||||||
PING_REQUEST.set()
|
PING_REQUEST.set()
|
||||||
|
if settings.status_use_mqtt:
|
||||||
|
MQTT_PING_REQUEST.set()
|
||||||
self.set_header("content-type", "application/json")
|
self.set_header("content-type", "application/json")
|
||||||
self.write(json.dumps(PING_RESULT))
|
self.write(json.dumps(PING_RESULT))
|
||||||
|
|
||||||
@ -910,6 +984,7 @@ PING_RESULT: dict = {}
|
|||||||
IMPORT_RESULT = {}
|
IMPORT_RESULT = {}
|
||||||
STOP_EVENT = threading.Event()
|
STOP_EVENT = threading.Event()
|
||||||
PING_REQUEST = threading.Event()
|
PING_REQUEST = threading.Event()
|
||||||
|
MQTT_PING_REQUEST = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
class LoginHandler(BaseHandler):
|
class LoginHandler(BaseHandler):
|
||||||
@ -1197,6 +1272,11 @@ def start_web_server(args):
|
|||||||
else:
|
else:
|
||||||
status_thread = MDNSStatusThread()
|
status_thread = MDNSStatusThread()
|
||||||
status_thread.start()
|
status_thread.start()
|
||||||
|
|
||||||
|
if settings.status_use_mqtt:
|
||||||
|
status_thread_mqtt = MqttStatusThread()
|
||||||
|
status_thread_mqtt.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tornado.ioloop.IOLoop.current().start()
|
tornado.ioloop.IOLoop.current().start()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
@ -1204,5 +1284,8 @@ def start_web_server(args):
|
|||||||
STOP_EVENT.set()
|
STOP_EVENT.set()
|
||||||
PING_REQUEST.set()
|
PING_REQUEST.set()
|
||||||
status_thread.join()
|
status_thread.join()
|
||||||
|
if settings.status_use_mqtt:
|
||||||
|
status_thread_mqtt.join()
|
||||||
|
MQTT_PING_REQUEST.set()
|
||||||
if args.socket is not None:
|
if args.socket is not None:
|
||||||
os.remove(args.socket)
|
os.remove(args.socket)
|
||||||
|
@ -147,6 +147,14 @@ def get_bool_env(var, default=False):
|
|||||||
return bool(os.getenv(var, default))
|
return bool(os.getenv(var, default))
|
||||||
|
|
||||||
|
|
||||||
|
def get_str_env(var, default=None):
|
||||||
|
return str(os.getenv(var, default))
|
||||||
|
|
||||||
|
|
||||||
|
def get_int_env(var, default=0):
|
||||||
|
return int(os.getenv(var, default))
|
||||||
|
|
||||||
|
|
||||||
def is_ha_addon():
|
def is_ha_addon():
|
||||||
return get_bool_env("ESPHOME_IS_HA_ADDON")
|
return get_bool_env("ESPHOME_IS_HA_ADDON")
|
||||||
|
|
||||||
|
128
esphome/mqtt.py
128
esphome/mqtt.py
@ -4,6 +4,7 @@ import logging
|
|||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
@ -24,15 +25,45 @@ from esphome.const import (
|
|||||||
from esphome.core import CORE, EsphomeError
|
from esphome.core import CORE, EsphomeError
|
||||||
from esphome.log import color, Fore
|
from esphome.log import color, Fore
|
||||||
from esphome.util import safe_print
|
from esphome.util import safe_print
|
||||||
|
from esphome.helpers import get_str_env, get_int_env
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def initialize(config, subscriptions, on_message, username, password, client_id):
|
def config_from_env():
|
||||||
def on_connect(client, userdata, flags, return_code):
|
config = {
|
||||||
|
CONF_MQTT: {
|
||||||
|
CONF_USERNAME: get_str_env("ESPHOME_DASHBOARD_MQTT_USERNAME"),
|
||||||
|
CONF_PASSWORD: get_str_env("ESPHOME_DASHBOARD_MQTT_PASSWORD"),
|
||||||
|
CONF_BROKER: get_str_env("ESPHOME_DASHBOARD_MQTT_BROKER"),
|
||||||
|
CONF_PORT: get_int_env("ESPHOME_DASHBOARD_MQTT_PORT", 1883),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(
|
||||||
|
config, subscriptions, on_message, on_connect, username, password, client_id
|
||||||
|
):
|
||||||
|
client = prepare(
|
||||||
|
config, subscriptions, on_message, on_connect, username, password, client_id
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
client.loop_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def prepare(
|
||||||
|
config, subscriptions, on_message, on_connect, username, password, client_id
|
||||||
|
):
|
||||||
|
def on_connect_(client, userdata, flags, return_code):
|
||||||
_LOGGER.info("Connected to MQTT broker!")
|
_LOGGER.info("Connected to MQTT broker!")
|
||||||
for topic in subscriptions:
|
for topic in subscriptions:
|
||||||
client.subscribe(topic)
|
client.subscribe(topic)
|
||||||
|
if on_connect is not None:
|
||||||
|
on_connect(client, userdata, flags, return_code)
|
||||||
|
|
||||||
def on_disconnect(client, userdata, result_code):
|
def on_disconnect(client, userdata, result_code):
|
||||||
if result_code == 0:
|
if result_code == 0:
|
||||||
@ -57,7 +88,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id)
|
|||||||
tries += 1
|
tries += 1
|
||||||
|
|
||||||
client = mqtt.Client(client_id or "")
|
client = mqtt.Client(client_id or "")
|
||||||
client.on_connect = on_connect
|
client.on_connect = on_connect_
|
||||||
client.on_message = on_message
|
client.on_message = on_message
|
||||||
client.on_disconnect = on_disconnect
|
client.on_disconnect = on_disconnect
|
||||||
if username is None:
|
if username is None:
|
||||||
@ -89,11 +120,88 @@ def initialize(config, subscriptions, on_message, username, password, client_id)
|
|||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise EsphomeError(f"Cannot connect to MQTT broker: {err}") from err
|
raise EsphomeError(f"Cannot connect to MQTT broker: {err}") from err
|
||||||
|
|
||||||
try:
|
return client
|
||||||
client.loop_forever()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
def show_discover(config, username=None, password=None, client_id=None):
|
||||||
return 0
|
topic = "esphome/discover/#"
|
||||||
|
_LOGGER.info("Starting log output from %s", topic)
|
||||||
|
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
time_ = datetime.now().time().strftime("[%H:%M:%S]")
|
||||||
|
payload = msg.payload.decode(errors="backslashreplace")
|
||||||
|
if len(payload) > 0:
|
||||||
|
message = time_ + " " + payload
|
||||||
|
safe_print(message)
|
||||||
|
|
||||||
|
def on_connect(client, userdata, flags, return_code):
|
||||||
|
_LOGGER.info("Send discover via MQTT broker")
|
||||||
|
client.publish("esphome/discover", None, retain=False)
|
||||||
|
|
||||||
|
return initialize(
|
||||||
|
config, [topic], on_message, on_connect, username, password, client_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_esphome_device_ip(
|
||||||
|
config, username=None, password=None, client_id=None, timeout=25
|
||||||
|
):
|
||||||
|
if CONF_MQTT not in config:
|
||||||
|
raise EsphomeError(
|
||||||
|
"Cannot discover IP via MQTT as the config does not include the mqtt: "
|
||||||
|
"component"
|
||||||
|
)
|
||||||
|
if CONF_ESPHOME not in config or CONF_NAME not in config[CONF_ESPHOME]:
|
||||||
|
raise EsphomeError(
|
||||||
|
"Cannot discover IP via MQTT as the config does not include the device name: "
|
||||||
|
"component"
|
||||||
|
)
|
||||||
|
|
||||||
|
dev_name = config[CONF_ESPHOME][CONF_NAME]
|
||||||
|
dev_ip = None
|
||||||
|
|
||||||
|
topic = "esphome/discover/" + dev_name
|
||||||
|
_LOGGER.info("Starting looking for IP in topic %s", topic)
|
||||||
|
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
nonlocal dev_ip
|
||||||
|
time_ = datetime.now().time().strftime("[%H:%M:%S]")
|
||||||
|
payload = msg.payload.decode(errors="backslashreplace")
|
||||||
|
if len(payload) > 0:
|
||||||
|
message = time_ + " " + payload
|
||||||
|
_LOGGER.debug(message)
|
||||||
|
|
||||||
|
data = json.loads(payload)
|
||||||
|
if "name" not in data or data["name"] != dev_name:
|
||||||
|
_LOGGER.Warn("Wrong device answer")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "ip" in data:
|
||||||
|
dev_ip = data["ip"]
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
def on_connect(client, userdata, flags, return_code):
|
||||||
|
topic = "esphome/ping/" + dev_name
|
||||||
|
_LOGGER.info("Send discover via MQTT broker topic: %s", topic)
|
||||||
|
client.publish(topic, None, retain=False)
|
||||||
|
|
||||||
|
mqtt_client = prepare(
|
||||||
|
config, [topic], on_message, on_connect, username, password, client_id
|
||||||
|
)
|
||||||
|
|
||||||
|
mqtt_client.loop_start()
|
||||||
|
while timeout > 0:
|
||||||
|
if dev_ip is not None:
|
||||||
|
break
|
||||||
|
timeout -= 0.250
|
||||||
|
time.sleep(0.250)
|
||||||
|
mqtt_client.loop_stop()
|
||||||
|
|
||||||
|
if dev_ip is None:
|
||||||
|
raise EsphomeError("Failed to find IP via MQTT")
|
||||||
|
|
||||||
|
_LOGGER.info("Found IP: %s", dev_ip)
|
||||||
|
return dev_ip
|
||||||
|
|
||||||
|
|
||||||
def show_logs(config, topic=None, username=None, password=None, client_id=None):
|
def show_logs(config, topic=None, username=None, password=None, client_id=None):
|
||||||
@ -118,7 +226,7 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None):
|
|||||||
message = time_ + payload
|
message = time_ + payload
|
||||||
safe_print(message)
|
safe_print(message)
|
||||||
|
|
||||||
return initialize(config, [topic], on_message, username, password, client_id)
|
return initialize(config, [topic], on_message, None, username, password, client_id)
|
||||||
|
|
||||||
|
|
||||||
def clear_topic(config, topic, username=None, password=None, client_id=None):
|
def clear_topic(config, topic, username=None, password=None, client_id=None):
|
||||||
@ -142,7 +250,7 @@ def clear_topic(config, topic, username=None, password=None, client_id=None):
|
|||||||
return
|
return
|
||||||
client.publish(msg.topic, None, retain=True)
|
client.publish(msg.topic, None, retain=True)
|
||||||
|
|
||||||
return initialize(config, [topic], on_message, username, password, client_id)
|
return initialize(config, [topic], on_message, None, username, password, client_id)
|
||||||
|
|
||||||
|
|
||||||
# From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py
|
# From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py
|
||||||
|
@ -10,6 +10,12 @@ from esphome import const
|
|||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.helpers import write_file_if_changed
|
from esphome.helpers import write_file_if_changed
|
||||||
|
|
||||||
|
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_MDNS,
|
||||||
|
CONF_DISABLED,
|
||||||
|
)
|
||||||
|
|
||||||
from esphome.types import CoreType
|
from esphome.types import CoreType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -46,6 +52,7 @@ class StorageJSON:
|
|||||||
build_path,
|
build_path,
|
||||||
firmware_bin_path,
|
firmware_bin_path,
|
||||||
loaded_integrations,
|
loaded_integrations,
|
||||||
|
no_mdns,
|
||||||
):
|
):
|
||||||
# Version of the storage JSON schema
|
# Version of the storage JSON schema
|
||||||
assert storage_version is None or isinstance(storage_version, int)
|
assert storage_version is None or isinstance(storage_version, int)
|
||||||
@ -75,6 +82,8 @@ class StorageJSON:
|
|||||||
# A list of strings of names of loaded integrations
|
# A list of strings of names of loaded integrations
|
||||||
self.loaded_integrations: list[str] = loaded_integrations
|
self.loaded_integrations: list[str] = loaded_integrations
|
||||||
self.loaded_integrations.sort()
|
self.loaded_integrations.sort()
|
||||||
|
# Is mDNS disabled
|
||||||
|
self.no_mdns = no_mdns
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
return {
|
return {
|
||||||
@ -90,6 +99,7 @@ class StorageJSON:
|
|||||||
"build_path": self.build_path,
|
"build_path": self.build_path,
|
||||||
"firmware_bin_path": self.firmware_bin_path,
|
"firmware_bin_path": self.firmware_bin_path,
|
||||||
"loaded_integrations": self.loaded_integrations,
|
"loaded_integrations": self.loaded_integrations,
|
||||||
|
"no_mdns": self.no_mdns,
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
@ -120,6 +130,11 @@ class StorageJSON:
|
|||||||
build_path=esph.build_path,
|
build_path=esph.build_path,
|
||||||
firmware_bin_path=esph.firmware_bin,
|
firmware_bin_path=esph.firmware_bin,
|
||||||
loaded_integrations=list(esph.loaded_integrations),
|
loaded_integrations=list(esph.loaded_integrations),
|
||||||
|
no_mdns=(
|
||||||
|
CONF_MDNS in esph.config
|
||||||
|
and CONF_DISABLED in esph.config[CONF_MDNS]
|
||||||
|
and esph.config[CONF_MDNS][CONF_DISABLED] is True
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -139,6 +154,7 @@ class StorageJSON:
|
|||||||
build_path=None,
|
build_path=None,
|
||||||
firmware_bin_path=None,
|
firmware_bin_path=None,
|
||||||
loaded_integrations=[],
|
loaded_integrations=[],
|
||||||
|
no_mdns=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -159,6 +175,7 @@ class StorageJSON:
|
|||||||
build_path = storage.get("build_path")
|
build_path = storage.get("build_path")
|
||||||
firmware_bin_path = storage.get("firmware_bin_path")
|
firmware_bin_path = storage.get("firmware_bin_path")
|
||||||
loaded_integrations = storage.get("loaded_integrations", [])
|
loaded_integrations = storage.get("loaded_integrations", [])
|
||||||
|
no_mdns = storage.get("no_mdns", False)
|
||||||
return StorageJSON(
|
return StorageJSON(
|
||||||
storage_version,
|
storage_version,
|
||||||
name,
|
name,
|
||||||
@ -172,6 +189,7 @@ class StorageJSON:
|
|||||||
build_path,
|
build_path,
|
||||||
firmware_bin_path,
|
firmware_bin_path,
|
||||||
loaded_integrations,
|
loaded_integrations,
|
||||||
|
no_mdns,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -299,6 +299,16 @@ def copy_src_tree():
|
|||||||
|
|
||||||
copy_files()
|
copy_files()
|
||||||
|
|
||||||
|
elif CORE.is_rp2040:
|
||||||
|
from esphome.components.rp2040 import copy_files
|
||||||
|
|
||||||
|
(pio) = copy_files()
|
||||||
|
if pio:
|
||||||
|
write_file_if_changed(
|
||||||
|
CORE.relative_src_path("esphome.h"),
|
||||||
|
ESPHOME_H_FORMAT.format(include_s + '\n#include "pio_includes.h"'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_defines_h():
|
def generate_defines_h():
|
||||||
define_content_l = [x.as_macro for x in CORE.defines]
|
define_content_l = [x.as_macro for x in CORE.defines]
|
||||||
|
@ -2,16 +2,16 @@ voluptuous==0.13.1
|
|||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
paho-mqtt==1.6.1
|
paho-mqtt==1.6.1
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
tornado==6.3.1
|
tornado==6.3.2
|
||||||
tzlocal==5.0.1 # from time
|
tzlocal==5.0.1 # from time
|
||||||
tzdata>=2021.1 # from time
|
tzdata>=2021.1 # from time
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
platformio==6.1.6 # When updating platformio, also update Dockerfile
|
platformio==6.1.7 # When updating platformio, also update Dockerfile
|
||||||
esptool==4.5.1
|
esptool==4.5.1
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
esphome-dashboard==20230516.0
|
esphome-dashboard==20230516.0
|
||||||
aioesphomeapi==13.7.5
|
aioesphomeapi==13.7.5
|
||||||
zeroconf==0.60.0
|
zeroconf==0.62.0
|
||||||
|
|
||||||
# esp-idf requires this, but doesn't bundle it by default
|
# esp-idf requires this, but doesn't bundle it by default
|
||||||
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
|
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
pylint==2.17.3
|
pylint==2.17.4
|
||||||
flake8==6.0.0 # also change in .pre-commit-config.yaml when updating
|
flake8==6.0.0 # also change in .pre-commit-config.yaml when updating
|
||||||
black==23.3.0 # also change in .pre-commit-config.yaml when updating
|
black==23.3.0 # also change in .pre-commit-config.yaml when updating
|
||||||
pyupgrade==3.3.2 # also change in .pre-commit-config.yaml when updating
|
pyupgrade==3.4.0 # also change in .pre-commit-config.yaml when updating
|
||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
|
@ -66,6 +66,7 @@ file_types = (
|
|||||||
".txt",
|
".txt",
|
||||||
".ico",
|
".ico",
|
||||||
".svg",
|
".svg",
|
||||||
|
".png",
|
||||||
".py",
|
".py",
|
||||||
".html",
|
".html",
|
||||||
".js",
|
".js",
|
||||||
@ -80,7 +81,7 @@ file_types = (
|
|||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
|
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
|
||||||
ignore_types = (".ico", ".woff", ".woff2", "")
|
ignore_types = (".ico", ".png", ".woff", ".woff2", "")
|
||||||
|
|
||||||
LINT_FILE_CHECKS = []
|
LINT_FILE_CHECKS = []
|
||||||
LINT_CONTENT_CHECKS = []
|
LINT_CONTENT_CHECKS = []
|
||||||
|
BIN
tests/pnglogo.png
Normal file
BIN
tests/pnglogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 685 B |
@ -3099,6 +3099,8 @@ text_sensor:
|
|||||||
name: BSSID
|
name: BSSID
|
||||||
mac_address:
|
mac_address:
|
||||||
name: Mac Address
|
name: Mac Address
|
||||||
|
dns_address:
|
||||||
|
name: DNS ADdress
|
||||||
- platform: version
|
- platform: version
|
||||||
name: ESPHome Version No Timestamp
|
name: ESPHome Version No Timestamp
|
||||||
hide_timestamp: true
|
hide_timestamp: true
|
||||||
|
@ -659,6 +659,27 @@ interval:
|
|||||||
|
|
||||||
display:
|
display:
|
||||||
|
|
||||||
|
image:
|
||||||
|
- id: binary_image
|
||||||
|
file: pnglogo.png
|
||||||
|
type: BINARY
|
||||||
|
dither: FloydSteinberg
|
||||||
|
- id: transparent_transparent_image
|
||||||
|
file: pnglogo.png
|
||||||
|
type: TRANSPARENT_BINARY
|
||||||
|
- id: rgba_image
|
||||||
|
file: pnglogo.png
|
||||||
|
type: RGBA
|
||||||
|
resize: 50x50
|
||||||
|
- id: rgb24_image
|
||||||
|
file: pnglogo.png
|
||||||
|
type: RGB24
|
||||||
|
use_transparency: yes
|
||||||
|
- id: rgb565_image
|
||||||
|
file: pnglogo.png
|
||||||
|
type: RGB565
|
||||||
|
use_transparency: no
|
||||||
|
|
||||||
cap1188:
|
cap1188:
|
||||||
id: cap1188_component
|
id: cap1188_component
|
||||||
address: 0x29
|
address: 0x29
|
||||||
|
@ -666,6 +666,7 @@ touchscreen:
|
|||||||
i2s_audio:
|
i2s_audio:
|
||||||
i2s_lrclk_pin: GPIO26
|
i2s_lrclk_pin: GPIO26
|
||||||
i2s_bclk_pin: GPIO27
|
i2s_bclk_pin: GPIO27
|
||||||
|
i2s_mclk_pin: GPIO25
|
||||||
|
|
||||||
media_player:
|
media_player:
|
||||||
- platform: i2s_audio
|
- platform: i2s_audio
|
||||||
|
@ -37,6 +37,26 @@ switch:
|
|||||||
output: pin_4
|
output: pin_4
|
||||||
id: pin_4_switch
|
id: pin_4_switch
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: rp2040_pio_led_strip
|
||||||
|
id: led_strip
|
||||||
|
pin: GPIO13
|
||||||
|
num_leds: 60
|
||||||
|
pio: 0
|
||||||
|
rgb_order: GRB
|
||||||
|
chipset: WS2812
|
||||||
|
- platform: rp2040_pio_led_strip
|
||||||
|
id: led_strip_custom_timings
|
||||||
|
pin: GPIO13
|
||||||
|
num_leds: 60
|
||||||
|
pio: 1
|
||||||
|
rgb_order: GRB
|
||||||
|
bit0_high: .1us
|
||||||
|
bit0_low: 1.2us
|
||||||
|
bit1_high: .69us
|
||||||
|
bit1_low: .4us
|
||||||
|
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
- platform: internal_temperature
|
- platform: internal_temperature
|
||||||
name: Internal Temperature
|
name: Internal Temperature
|
||||||
|
@ -4,7 +4,7 @@ wifi:
|
|||||||
ssid: "ssid"
|
ssid: "ssid"
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: esp32-c3-devkitm-1
|
board: esp32s3box
|
||||||
variant: ESP32S3
|
variant: ESP32S3
|
||||||
framework:
|
framework:
|
||||||
type: arduino
|
type: arduino
|
||||||
|
@ -6,7 +6,7 @@ from hypothesis.strategies import one_of, text, integers, builds
|
|||||||
|
|
||||||
from esphome import config_validation
|
from esphome import config_validation
|
||||||
from esphome.config_validation import Invalid
|
from esphome.config_validation import Invalid
|
||||||
from esphome.core import Lambda, HexInt
|
from esphome.core import CORE, Lambda, HexInt
|
||||||
|
|
||||||
|
|
||||||
def test_check_not_templatable__invalid():
|
def test_check_not_templatable__invalid():
|
||||||
@ -40,6 +40,47 @@ def test_valid_name__invalid(value):
|
|||||||
config_validation.valid_name(value)
|
config_validation.valid_name(value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ("${name}", "${NAME}", "$NAME", "${name}_name"))
|
||||||
|
def test_valid_name__substitution_valid(value):
|
||||||
|
CORE.vscode = True
|
||||||
|
actual = config_validation.valid_name(value)
|
||||||
|
assert actual == value
|
||||||
|
|
||||||
|
CORE.vscode = False
|
||||||
|
with pytest.raises(Invalid):
|
||||||
|
actual = config_validation.valid_name(value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ("{NAME}", "${A NAME}"))
|
||||||
|
def test_valid_name__substitution_like_invalid(value):
|
||||||
|
with pytest.raises(Invalid):
|
||||||
|
config_validation.valid_name(value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ("myid", "anID", "SOME_ID_test", "MYID_99"))
|
||||||
|
def test_validate_id_name__valid(value):
|
||||||
|
actual = config_validation.validate_id_name(value)
|
||||||
|
|
||||||
|
assert actual == value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ("id of mine", "id-4", "{name_id}", "id::name"))
|
||||||
|
def test_validate_id_name__invalid(value):
|
||||||
|
with pytest.raises(Invalid):
|
||||||
|
config_validation.validate_id_name(value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("value", ("${id}", "${ID}", "${ID}_test_1", "$MYID"))
|
||||||
|
def test_validate_id_name__substitution_valid(value):
|
||||||
|
CORE.vscode = True
|
||||||
|
actual = config_validation.validate_id_name(value)
|
||||||
|
assert actual == value
|
||||||
|
|
||||||
|
CORE.vscode = False
|
||||||
|
with pytest.raises(Invalid):
|
||||||
|
config_validation.validate_id_name(value)
|
||||||
|
|
||||||
|
|
||||||
@given(one_of(integers(), text()))
|
@given(one_of(integers(), text()))
|
||||||
def test_string__valid(value):
|
def test_string__valid(value):
|
||||||
actual = config_validation.string(value)
|
actual = config_validation.string(value)
|
||||||
|
Loading…
Reference in New Issue
Block a user