mirror of
https://github.com/esphome/esphome.git
synced 2025-11-01 15:41:52 +00:00
Compare commits
195 Commits
2023.6.0b1
...
2023.7.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b914d6e305 | ||
|
|
956e19be7d | ||
|
|
b3d5a4dfdb | ||
|
|
c63cdae84f | ||
|
|
dec044ad8b | ||
|
|
2a12ec09fb | ||
|
|
91e920c498 | ||
|
|
9b19c45735 | ||
|
|
3843d21dbf | ||
|
|
73db164fb1 | ||
|
|
ab32dd7420 | ||
|
|
2a7aa2fc0d | ||
|
|
f5e98eb86f | ||
|
|
362a19c2e1 | ||
|
|
f4a4956dd4 | ||
|
|
746488cabf | ||
|
|
4449248c6f | ||
|
|
036e14ab7f | ||
|
|
f840eee1b7 | ||
|
|
553132443f | ||
|
|
d20242f589 | ||
|
|
68affce274 | ||
|
|
c4b9065749 | ||
|
|
d57a5d1793 | ||
|
|
74e062fdb3 | ||
|
|
6bdc0c92fe | ||
|
|
d7945de001 | ||
|
|
3ba2a29e54 | ||
|
|
76b438f79c | ||
|
|
bc14f06a07 | ||
|
|
844cf316e2 | ||
|
|
9344d85414 | ||
|
|
a539197bc4 | ||
|
|
eb859e83f8 | ||
|
|
e4a640844c | ||
|
|
119bbba254 | ||
|
|
8c5978599a | ||
|
|
bbf3d382e8 | ||
|
|
c85f70a236 | ||
|
|
7e52d4f5d6 | ||
|
|
6d9dbf9e54 | ||
|
|
ec37dece12 | ||
|
|
e0fd8cd850 | ||
|
|
cf65bd8ad7 | ||
|
|
8a9352939a | ||
|
|
6ecc1c14d2 | ||
|
|
5f531ac9b0 | ||
|
|
7a551081ee | ||
|
|
74139985c9 | ||
|
|
f3cdcc008a | ||
|
|
a391815921 | ||
|
|
98fd092053 | ||
|
|
feee075122 | ||
|
|
ddde1ee31e | ||
|
|
c5aacdd682 | ||
|
|
a77cf1beec | ||
|
|
d7bfdd0efc | ||
|
|
62aee36f82 | ||
|
|
8ca9115dc8 | ||
|
|
8bf8892ab3 | ||
|
|
8739552c0b | ||
|
|
e6834f25ed | ||
|
|
f9fc438de8 | ||
|
|
677b2c6618 | ||
|
|
301a78f983 | ||
|
|
979f014799 | ||
|
|
a326dcaf0e | ||
|
|
5bf2fa5c56 | ||
|
|
fe0404a084 | ||
|
|
22a1134f0e | ||
|
|
fc3d558d47 | ||
|
|
45c72f1f22 | ||
|
|
fd9cca565b | ||
|
|
0709367587 | ||
|
|
98277f6ceb | ||
|
|
8dd509ba53 | ||
|
|
8df455f55b | ||
|
|
36782f13bf | ||
|
|
e823067a6b | ||
|
|
c3ef12d580 | ||
|
|
d64d1650e3 | ||
|
|
a74abb8ea8 | ||
|
|
e74ab00b3e | ||
|
|
2e2ac53071 | ||
|
|
87c0f48095 | ||
|
|
25b9bde0a5 | ||
|
|
63d3a0e8b3 | ||
|
|
4cc0f3fd53 | ||
|
|
5b2176562b | ||
|
|
099dc8d1d2 | ||
|
|
cf98c497d5 | ||
|
|
c5eb3941b9 | ||
|
|
0e93b8ee0d | ||
|
|
807621402d | ||
|
|
321155eb40 | ||
|
|
d34c074b92 | ||
|
|
abc8e903c1 | ||
|
|
832ba38f1b | ||
|
|
70de2f5278 | ||
|
|
604d4eec79 | ||
|
|
ac5246e21d | ||
|
|
951157dc26 | ||
|
|
68119ddcd4 | ||
|
|
c82be2cd60 | ||
|
|
9a149a7aba | ||
|
|
108fabe18f | ||
|
|
8ce98dd15a | ||
|
|
9d21cccac1 | ||
|
|
8f4abf6a63 | ||
|
|
bd9a4ff8de | ||
|
|
d9398a91d1 | ||
|
|
ef84937fd6 | ||
|
|
2a2d20a7fc | ||
|
|
8a1c49a4ae | ||
|
|
b806eb6a61 | ||
|
|
39948db59a | ||
|
|
fbfb4e2a73 | ||
|
|
595ac84779 | ||
|
|
eb145757e5 | ||
|
|
fc0e1a3cb9 | ||
|
|
746f72a279 | ||
|
|
ec3d5fc427 | ||
|
|
dec6f04499 | ||
|
|
a90d266017 | ||
|
|
df9fcf9850 | ||
|
|
85608a8ab7 | ||
|
|
5ef9cd5f86 | ||
|
|
52d7d2cae7 | ||
|
|
ceca91d1e7 | ||
|
|
bc7c11be96 | ||
|
|
a2734330e1 | ||
|
|
ef8180c8a8 | ||
|
|
cd773a1dec | ||
|
|
244a212592 | ||
|
|
f72b07eb0e | ||
|
|
314c1c8b5c | ||
|
|
98e3426769 | ||
|
|
7ef8d67831 | ||
|
|
c455a5dd6a | ||
|
|
74daca668e | ||
|
|
211453df43 | ||
|
|
1cc7428445 | ||
|
|
a40fa22055 | ||
|
|
2047bba4f7 | ||
|
|
a064ab5c2b | ||
|
|
e8ce7048d8 | ||
|
|
0c37d06936 | ||
|
|
d919373853 | ||
|
|
6c00be0a63 | ||
|
|
0671287b80 | ||
|
|
867b4719d1 | ||
|
|
de6c527ca4 | ||
|
|
9e7e3708e3 | ||
|
|
8bd9f50659 | ||
|
|
cb5a01da29 | ||
|
|
bfe85dd710 | ||
|
|
fc544dc389 | ||
|
|
24067312f6 | ||
|
|
501fe83710 | ||
|
|
5513b0e121 | ||
|
|
959e7745a6 | ||
|
|
76e947651d | ||
|
|
5ba04eb620 | ||
|
|
ee12c68b8f | ||
|
|
b2ccd32cd7 | ||
|
|
7ceb16cc5a | ||
|
|
5a8b7c17da | ||
|
|
41a618737b | ||
|
|
67771abc9d | ||
|
|
c151df32bc | ||
|
|
b346ad8080 | ||
|
|
cd57271386 | ||
|
|
b9f20b36cb | ||
|
|
62d2640c37 | ||
|
|
54eb52c19a | ||
|
|
77a7d3f24b | ||
|
|
8c9d63f48f | ||
|
|
5a8e93ed0a | ||
|
|
2d32e89b87 | ||
|
|
88c13768e3 | ||
|
|
29f4430658 | ||
|
|
b558a1c9dd | ||
|
|
a4ef26749b | ||
|
|
6aa3092be0 | ||
|
|
abca47f36f | ||
|
|
407b5e199e | ||
|
|
2ffd430b0b | ||
|
|
d4099d68a7 | ||
|
|
e1b0d86098 | ||
|
|
1a7f121ac6 | ||
|
|
ffa669899a | ||
|
|
17fed954bf | ||
|
|
467e42d8aa | ||
|
|
a023f24a08 | ||
|
|
27f69f5439 |
38
.github/actions/restore-python/action.yml
vendored
Normal file
38
.github/actions/restore-python/action.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Restore Python
|
||||
inputs:
|
||||
python-version:
|
||||
description: Python version to restore
|
||||
required: true
|
||||
type: string
|
||||
cache-key:
|
||||
description: Cache key to use
|
||||
required: true
|
||||
type: string
|
||||
outputs:
|
||||
python-version:
|
||||
description: Python version restored
|
||||
value: ${{ steps.python.outputs.python-version }}
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
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 .
|
||||
96
.github/workflows/ci.yml
vendored
96
.github/workflows/ci.yml
vendored
@@ -26,10 +26,16 @@ jobs:
|
||||
common:
|
||||
name: Create common environment
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -39,7 +45,7 @@ jobs:
|
||||
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') }}
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -66,12 +72,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run black
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -88,12 +93,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run flake8
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -110,12 +114,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run pylint
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -132,12 +135,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run pyupgrade
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -154,12 +156,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
|
||||
- name: Run script/ci-custom
|
||||
@@ -176,12 +177,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run pytest
|
||||
@@ -197,12 +197,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -237,18 +236,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Run esphome compile tests/test${{ matrix.file }}.yaml
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -300,13 +292,11 @@ jobs:
|
||||
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
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
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') }}
|
||||
# Use per check platformio cache because checks use different parts
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache@v3.3.1
|
||||
with:
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -49,9 +49,11 @@ jobs:
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Set up python environment
|
||||
env:
|
||||
ESPHOME_NO_VENV: 1
|
||||
run: |
|
||||
script/setup
|
||||
pip install setuptools wheel twine
|
||||
pip install twine
|
||||
- name: Build
|
||||
run: python setup.py sdist bdist_wheel
|
||||
- name: Upload
|
||||
|
||||
15
.github/workflows/sync-device-classes.yml
vendored
15
.github/workflows/sync-device-classes.yml
vendored
@@ -6,14 +6,12 @@ on:
|
||||
schedule:
|
||||
- cron: '45 6 * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync Device Classes
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'esphome/esphome'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -38,15 +36,6 @@ jobs:
|
||||
run: |
|
||||
python ./script/sync-device_class.py
|
||||
|
||||
- name: Get PR template
|
||||
id: pr-template-body
|
||||
run: |
|
||||
body=$(cat .github/PULL_REQUEST_TEMPLATE.md)
|
||||
delimiter="$(openssl rand -hex 8)"
|
||||
echo "body<<$delimiter" >> $GITHUB_OUTPUT
|
||||
echo "$body" >> $GITHUB_OUTPUT
|
||||
echo "$delimiter" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
@@ -56,5 +45,5 @@ jobs:
|
||||
branch: sync/device-classes
|
||||
delete-branch: true
|
||||
title: "Synchronise Device Classes from Home Assistant"
|
||||
body: ${{ steps.pr-template-body.outputs.body }}
|
||||
body-path: .github/PULL_REQUEST_TEMPLATE.md
|
||||
token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }}
|
||||
|
||||
@@ -27,7 +27,7 @@ repos:
|
||||
- --branch=release
|
||||
- --branch=beta
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.4.0
|
||||
rev: v3.7.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py39-plus]
|
||||
|
||||
12
CODEOWNERS
12
CODEOWNERS
@@ -17,9 +17,11 @@ esphome/components/adc/* @esphome/core
|
||||
esphome/components/adc128s102/* @DeerMaximum
|
||||
esphome/components/addressable_light/* @justfalter
|
||||
esphome/components/airthings_ble/* @jeromelaban
|
||||
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
|
||||
esphome/components/airthings_wave_mini/* @ncareau
|
||||
esphome/components/airthings_wave_plus/* @jeromelaban
|
||||
esphome/components/alarm_control_panel/* @grahambrown11
|
||||
esphome/components/alpha3/* @jan-hofmeier
|
||||
esphome/components/am43/* @buxtronix
|
||||
esphome/components/am43/cover/* @buxtronix
|
||||
esphome/components/am43/sensor/* @buxtronix
|
||||
@@ -30,6 +32,7 @@ esphome/components/api/* @OttoWinter
|
||||
esphome/components/as7341/* @mrgnr
|
||||
esphome/components/async_tcp/* @OttoWinter
|
||||
esphome/components/atc_mithermometer/* @ahpohl
|
||||
esphome/components/atm90e26/* @danieltwagner
|
||||
esphome/components/b_parasite/* @rbaron
|
||||
esphome/components/ballu/* @bazuchan
|
||||
esphome/components/bang_bang/* @OttoWinter
|
||||
@@ -75,6 +78,7 @@ esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/dsmr/* @glmnet @zuidwijk
|
||||
esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ee895/* @Stock-M
|
||||
esphome/components/ektf2232/* @jesserockz
|
||||
esphome/components/ens210/* @itn3rd77
|
||||
@@ -101,8 +105,9 @@ esphome/components/gp8403/* @jesserockz
|
||||
esphome/components/gpio/* @esphome/core
|
||||
esphome/components/gps/* @coogle
|
||||
esphome/components/graph/* @synco
|
||||
esphome/components/grove_tb6612fng/* @max246
|
||||
esphome/components/growatt_solar/* @leeuwte
|
||||
esphome/components/haier/* @Yarikx
|
||||
esphome/components/haier/* @paveldn
|
||||
esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
@@ -199,6 +204,7 @@ esphome/components/output/* @esphome/core
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9554/* @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
esphome/components/pid/* @OttoWinter
|
||||
esphome/components/pipsolar/* @andreashergert1984
|
||||
esphome/components/pm1006/* @habbie
|
||||
@@ -293,6 +299,7 @@ esphome/components/tof10120/* @wstrzalka
|
||||
esphome/components/toshiba/* @kbx81
|
||||
esphome/components/touchscreen/* @jesserockz
|
||||
esphome/components/tsl2591/* @wjcarpenter
|
||||
esphome/components/tt21100/* @kroimon
|
||||
esphome/components/tuya/binary_sensor/* @jesserockz
|
||||
esphome/components/tuya/climate/* @jesserockz
|
||||
esphome/components/tuya/number/* @frankiboy1
|
||||
@@ -309,6 +316,7 @@ esphome/components/version/* @esphome/core
|
||||
esphome/components/voice_assistant/* @jesserockz
|
||||
esphome/components/wake_on_lan/* @willwill2will54
|
||||
esphome/components/web_server_base/* @OttoWinter
|
||||
esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/whirlpool/* @glmnet
|
||||
esphome/components/whynter/* @aeonsablaze
|
||||
esphome/components/wiegand/* @ssieb
|
||||
@@ -318,4 +326,6 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
||||
esphome/components/xiaomi_mhoc303/* @drug123
|
||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
|
||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
||||
esphome/components/xl9535/* @mreditor97
|
||||
esphome/components/xpt2046/* @nielsnl68 @numo68
|
||||
esphome/components/zio_ultrasonic/* @kahrendt
|
||||
|
||||
@@ -32,7 +32,7 @@ from esphome.const import (
|
||||
SECRETS_FILES,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
from esphome.helpers import indent
|
||||
from esphome.helpers import indent, is_ip_address
|
||||
from esphome.util import (
|
||||
run_external_command,
|
||||
run_external_process,
|
||||
@@ -308,8 +308,10 @@ def upload_program(config, args, host):
|
||||
password = ota_conf.get(CONF_PASSWORD, "")
|
||||
|
||||
if (
|
||||
get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]
|
||||
) and CONF_MQTT in config:
|
||||
not is_ip_address(CORE.address)
|
||||
and (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(
|
||||
|
||||
@@ -24,6 +24,7 @@ ATTENUATION_MODES = {
|
||||
}
|
||||
|
||||
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
|
||||
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
|
||||
|
||||
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
|
||||
# pin to adc1 channel mapping
|
||||
@@ -78,6 +79,49 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
},
|
||||
}
|
||||
|
||||
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
# TODO: add other variants
|
||||
VARIANT_ESP32: {
|
||||
4: adc2_channel_t.ADC2_CHANNEL_0,
|
||||
0: adc2_channel_t.ADC2_CHANNEL_1,
|
||||
2: adc2_channel_t.ADC2_CHANNEL_2,
|
||||
15: adc2_channel_t.ADC2_CHANNEL_3,
|
||||
13: adc2_channel_t.ADC2_CHANNEL_4,
|
||||
12: adc2_channel_t.ADC2_CHANNEL_5,
|
||||
14: adc2_channel_t.ADC2_CHANNEL_6,
|
||||
27: adc2_channel_t.ADC2_CHANNEL_7,
|
||||
25: adc2_channel_t.ADC2_CHANNEL_8,
|
||||
26: adc2_channel_t.ADC2_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32S2: {
|
||||
11: adc2_channel_t.ADC2_CHANNEL_0,
|
||||
12: adc2_channel_t.ADC2_CHANNEL_1,
|
||||
13: adc2_channel_t.ADC2_CHANNEL_2,
|
||||
14: adc2_channel_t.ADC2_CHANNEL_3,
|
||||
15: adc2_channel_t.ADC2_CHANNEL_4,
|
||||
16: adc2_channel_t.ADC2_CHANNEL_5,
|
||||
17: adc2_channel_t.ADC2_CHANNEL_6,
|
||||
18: adc2_channel_t.ADC2_CHANNEL_7,
|
||||
19: adc2_channel_t.ADC2_CHANNEL_8,
|
||||
20: adc2_channel_t.ADC2_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32S3: {
|
||||
11: adc2_channel_t.ADC2_CHANNEL_0,
|
||||
12: adc2_channel_t.ADC2_CHANNEL_1,
|
||||
13: adc2_channel_t.ADC2_CHANNEL_2,
|
||||
14: adc2_channel_t.ADC2_CHANNEL_3,
|
||||
15: adc2_channel_t.ADC2_CHANNEL_4,
|
||||
16: adc2_channel_t.ADC2_CHANNEL_5,
|
||||
17: adc2_channel_t.ADC2_CHANNEL_6,
|
||||
18: adc2_channel_t.ADC2_CHANNEL_7,
|
||||
19: adc2_channel_t.ADC2_CHANNEL_8,
|
||||
20: adc2_channel_t.ADC2_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32C3: {
|
||||
5: adc2_channel_t.ADC2_CHANNEL_0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def validate_adc_pin(value):
|
||||
if str(value).upper() == "VCC":
|
||||
@@ -89,11 +133,18 @@ def validate_adc_pin(value):
|
||||
if CORE.is_esp32:
|
||||
value = pins.internal_gpio_input_pin_number(value)
|
||||
variant = get_esp32_variant()
|
||||
if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL:
|
||||
if (
|
||||
variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
|
||||
and variant not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
|
||||
):
|
||||
raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported")
|
||||
|
||||
if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]:
|
||||
if (
|
||||
value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
|
||||
and value not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
|
||||
):
|
||||
raise cv.Invalid(f"{variant} doesn't support ADC on this pin")
|
||||
|
||||
return pins.internal_gpio_input_pin_schema(value)
|
||||
|
||||
if CORE.is_esp8266:
|
||||
@@ -104,7 +155,7 @@ def validate_adc_pin(value):
|
||||
)
|
||||
|
||||
if value != 17: # A0
|
||||
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.")
|
||||
raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
|
||||
return pins.gpio_pin_schema(
|
||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||
)(value)
|
||||
@@ -112,7 +163,7 @@ def validate_adc_pin(value):
|
||||
if CORE.is_rp2040:
|
||||
value = pins.internal_gpio_input_pin_number(value)
|
||||
if value not in (26, 27, 28, 29):
|
||||
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.")
|
||||
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
|
||||
return pins.internal_gpio_input_pin_schema(value)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -20,15 +20,15 @@ namespace adc {
|
||||
|
||||
static const char *const TAG = "adc";
|
||||
|
||||
// 13bit for S2, and 12bit for all other esp32 variants
|
||||
// 13-bit for S2, 12-bit for all other ESP32 variants
|
||||
#ifdef USE_ESP32
|
||||
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
|
||||
|
||||
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
|
||||
#if USE_ESP32_VARIANT_ESP32S2
|
||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 13;
|
||||
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13;
|
||||
#else
|
||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 12;
|
||||
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -47,14 +47,21 @@ extern "C"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
|
||||
if (!autorange_) {
|
||||
adc1_config_channel_atten(channel_, attenuation_);
|
||||
if (channel1_ != ADC1_CHANNEL_MAX) {
|
||||
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
|
||||
if (!autorange_) {
|
||||
adc1_config_channel_atten(channel1_, attenuation_);
|
||||
}
|
||||
} else if (channel2_ != ADC2_CHANNEL_MAX) {
|
||||
if (!autorange_) {
|
||||
adc2_config_channel_atten(channel2_, attenuation_);
|
||||
}
|
||||
}
|
||||
|
||||
// load characteristics for each attenuation
|
||||
for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) {
|
||||
auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
|
||||
for (int32_t i = 0; i <= ADC_ATTEN_DB_11; i++) {
|
||||
auto adc_unit = channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
|
||||
auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
|
||||
1100, // default vref
|
||||
&cal_characteristics_[i]);
|
||||
switch (cal_value) {
|
||||
@@ -136,9 +143,9 @@ void ADCSensor::update() {
|
||||
#ifdef USE_ESP8266
|
||||
float ADCSensor::sample() {
|
||||
#ifdef USE_ADC_SENSOR_VCC
|
||||
int raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
|
||||
int32_t raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
|
||||
#else
|
||||
int raw = analogRead(this->pin_->get_pin()); // NOLINT
|
||||
int32_t raw = analogRead(this->pin_->get_pin()); // NOLINT
|
||||
#endif
|
||||
if (output_raw_) {
|
||||
return raw;
|
||||
@@ -150,29 +157,53 @@ float ADCSensor::sample() {
|
||||
#ifdef USE_ESP32
|
||||
float ADCSensor::sample() {
|
||||
if (!autorange_) {
|
||||
int raw = adc1_get_raw(channel_);
|
||||
int raw = -1;
|
||||
if (channel1_ != ADC1_CHANNEL_MAX) {
|
||||
raw = adc1_get_raw(channel1_);
|
||||
} else if (channel2_ != ADC2_CHANNEL_MAX) {
|
||||
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw);
|
||||
}
|
||||
|
||||
if (raw == -1) {
|
||||
return NAN;
|
||||
}
|
||||
if (output_raw_) {
|
||||
return raw;
|
||||
}
|
||||
uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]);
|
||||
uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int32_t) attenuation_]);
|
||||
return mv / 1000.0f;
|
||||
}
|
||||
|
||||
int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
|
||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11);
|
||||
raw11 = adc1_get_raw(channel_);
|
||||
if (raw11 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6);
|
||||
raw6 = adc1_get_raw(channel_);
|
||||
if (raw6 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5);
|
||||
raw2 = adc1_get_raw(channel_);
|
||||
if (raw2 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0);
|
||||
raw0 = adc1_get_raw(channel_);
|
||||
int raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
|
||||
|
||||
if (channel1_ != ADC1_CHANNEL_MAX) {
|
||||
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_11);
|
||||
raw11 = adc1_get_raw(channel1_);
|
||||
if (raw11 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_6);
|
||||
raw6 = adc1_get_raw(channel1_);
|
||||
if (raw6 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_2_5);
|
||||
raw2 = adc1_get_raw(channel1_);
|
||||
if (raw2 < ADC_MAX) {
|
||||
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_0);
|
||||
raw0 = adc1_get_raw(channel1_);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (channel2_ != ADC2_CHANNEL_MAX) {
|
||||
adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_11);
|
||||
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw11);
|
||||
if (raw11 < ADC_MAX) {
|
||||
adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_6);
|
||||
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6);
|
||||
if (raw6 < ADC_MAX) {
|
||||
adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_2_5);
|
||||
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2);
|
||||
if (raw2 < ADC_MAX) {
|
||||
adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_0);
|
||||
adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,10 +212,10 @@ float ADCSensor::sample() {
|
||||
return NAN;
|
||||
}
|
||||
|
||||
uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]);
|
||||
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int) ADC_ATTEN_DB_6]);
|
||||
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]);
|
||||
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]);
|
||||
uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_11]);
|
||||
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]);
|
||||
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
|
||||
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]);
|
||||
|
||||
// Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
|
||||
uint32_t c11 = std::min(raw11, ADC_HALF);
|
||||
@@ -212,7 +243,7 @@ float ADCSensor::sample() {
|
||||
adc_select_input(pin - 26);
|
||||
}
|
||||
|
||||
int raw = adc_read();
|
||||
int32_t raw = adc_read();
|
||||
if (this->is_temperature_) {
|
||||
adc_set_temp_sensor_enabled(false);
|
||||
}
|
||||
|
||||
@@ -19,16 +19,23 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
#ifdef USE_ESP32
|
||||
/// Set the attenuation for this pin. Only available on the ESP32.
|
||||
void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; }
|
||||
void set_channel(adc1_channel_t channel) { channel_ = channel; }
|
||||
void set_channel1(adc1_channel_t channel) {
|
||||
channel1_ = channel;
|
||||
channel2_ = ADC2_CHANNEL_MAX;
|
||||
}
|
||||
void set_channel2(adc2_channel_t channel) {
|
||||
channel2_ = channel;
|
||||
channel1_ = ADC1_CHANNEL_MAX;
|
||||
}
|
||||
void set_autorange(bool autorange) { autorange_ = autorange; }
|
||||
#endif
|
||||
|
||||
/// Update adc values.
|
||||
/// Update ADC values
|
||||
void update() override;
|
||||
/// Setup ADc
|
||||
/// Setup ADC
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
/// `HARDWARE_LATE` setup priority.
|
||||
/// `HARDWARE_LATE` setup priority
|
||||
float get_setup_priority() const override;
|
||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
||||
void set_output_raw(bool output_raw) { output_raw_ = output_raw; }
|
||||
@@ -52,9 +59,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
|
||||
#ifdef USE_ESP32
|
||||
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
|
||||
adc1_channel_t channel_{};
|
||||
adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
|
||||
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
|
||||
bool autorange_{false};
|
||||
esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {};
|
||||
esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {};
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
import esphome.final_validate as fv
|
||||
from esphome.core import CORE
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.const import (
|
||||
@@ -8,15 +10,15 @@ from esphome.const import (
|
||||
CONF_NUMBER,
|
||||
CONF_PIN,
|
||||
CONF_RAW,
|
||||
CONF_WIFI,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
from . import (
|
||||
ATTENUATION_MODES,
|
||||
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
|
||||
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
|
||||
validate_adc_pin,
|
||||
)
|
||||
|
||||
@@ -25,7 +27,23 @@ AUTO_LOAD = ["voltage_sampler"]
|
||||
|
||||
def validate_config(config):
|
||||
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
|
||||
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set.")
|
||||
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def final_validate_config(config):
|
||||
if CORE.is_esp32:
|
||||
variant = get_esp32_variant()
|
||||
if (
|
||||
CONF_WIFI in fv.full_config.get()
|
||||
and config[CONF_PIN][CONF_NUMBER]
|
||||
in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{variant} doesn't support ADC on this pin when Wi-Fi is configured"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -55,6 +73,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_config,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate_config
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -81,5 +101,15 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
variant = get_esp32_variant()
|
||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
|
||||
cg.add(var.set_channel(chan))
|
||||
if (
|
||||
variant in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
|
||||
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
|
||||
):
|
||||
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
|
||||
cg.add(var.set_channel1(chan))
|
||||
elif (
|
||||
variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
|
||||
and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
|
||||
):
|
||||
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
|
||||
cg.add(var.set_channel2(chan))
|
||||
|
||||
@@ -58,6 +58,6 @@ async def to_code(config):
|
||||
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(
|
||||
config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
|
||||
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
|
||||
103
esphome/components/airthings_wave_base/__init__.py
Normal file
103
esphome/components/airthings_wave_base/__init__.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, ble_client
|
||||
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TVOC,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"]
|
||||
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
|
||||
CONF_BATTERY_UPDATE_INTERVAL = "battery_update_interval"
|
||||
|
||||
airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base")
|
||||
AirthingsWaveBase = airthings_wave_base_ns.class_(
|
||||
"AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode
|
||||
)
|
||||
|
||||
|
||||
BASE_SCHEMA = (
|
||||
sensor.SENSOR_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TVOC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_BATTERY_UPDATE_INTERVAL,
|
||||
default="24h",
|
||||
): cv.update_interval,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("5min"))
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def wave_base_to_code(var, config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
if config_humidity := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(config_humidity)
|
||||
cg.add(var.set_humidity(sens))
|
||||
if config_temperature := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(config_temperature)
|
||||
cg.add(var.set_temperature(sens))
|
||||
if config_pressure := config.get(CONF_PRESSURE):
|
||||
sens = await sensor.new_sensor(config_pressure)
|
||||
cg.add(var.set_pressure(sens))
|
||||
if config_tvoc := config.get(CONF_TVOC):
|
||||
sens = await sensor.new_sensor(config_tvoc)
|
||||
cg.add(var.set_tvoc(sens))
|
||||
if config_battery_voltage := config.get(CONF_BATTERY_VOLTAGE):
|
||||
sens = await sensor.new_sensor(config_battery_voltage)
|
||||
cg.add(var.set_battery_voltage(sens))
|
||||
if config_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL):
|
||||
cg.add(var.set_battery_update_interval(config_battery_update_interval))
|
||||
211
esphome/components/airthings_wave_base/airthings_wave_base.cpp
Normal file
211
esphome/components/airthings_wave_base/airthings_wave_base.cpp
Normal file
@@ -0,0 +1,211 @@
|
||||
#include "airthings_wave_base.h"
|
||||
|
||||
// All information related to reading battery information came from the sensors.airthings_wave
|
||||
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace airthings_wave_base {
|
||||
|
||||
static const char *const TAG = "airthings_wave_base";
|
||||
|
||||
void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (param->open.status == ESP_GATT_OK) {
|
||||
ESP_LOGI(TAG, "Connected successfully!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
this->handle_ = 0;
|
||||
this->acp_handle_ = 0;
|
||||
this->cccd_handle_ = 0;
|
||||
ESP_LOGW(TAG, "Disconnected!");
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
if (this->request_read_values_()) {
|
||||
if (!this->read_battery_next_update_) {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
} else {
|
||||
// delay setting node_state to ESTABLISHED until confirmation of the notify registration
|
||||
this->request_battery_();
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that the client will be disconnected even if no responses arrive
|
||||
this->set_response_timeout_();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (param->read.conn_id != this->parent()->get_conn_id())
|
||||
break;
|
||||
if (param->read.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
|
||||
break;
|
||||
}
|
||||
if (param->read.handle == this->handle_) {
|
||||
this->read_sensors(param->read.value, param->read.value_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.conn_id != this->parent()->get_conn_id())
|
||||
break;
|
||||
if (param->notify.handle == this->acp_handle_) {
|
||||
this->read_battery_(param->notify.value, param->notify.value_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return voc <= 16383; }
|
||||
|
||||
void AirthingsWaveBase::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
if (!this->parent()->enabled) {
|
||||
ESP_LOGW(TAG, "Reconnecting to device");
|
||||
this->parent()->set_enabled(true);
|
||||
this->parent()->connect();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection in progress");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AirthingsWaveBase::request_read_values_() {
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
|
||||
this->sensors_data_characteristic_uuid_.to_string().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
this->handle_ = chr->handle;
|
||||
|
||||
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
|
||||
ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
this->response_pending_();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AirthingsWaveBase::request_battery_() {
|
||||
uint8_t battery_command = ACCESS_CONTROL_POINT_COMMAND;
|
||||
uint8_t cccd_value[2] = {1, 0};
|
||||
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s",
|
||||
this->service_uuid_.to_string().c_str(),
|
||||
this->access_control_point_characteristic_uuid_.to_string().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_,
|
||||
CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID);
|
||||
if (descr == nullptr) {
|
||||
ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(),
|
||||
this->access_control_point_characteristic_uuid_.to_string().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto reg_status =
|
||||
esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), chr->handle);
|
||||
if (reg_status) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", reg_status);
|
||||
return false;
|
||||
}
|
||||
|
||||
this->acp_handle_ = chr->handle;
|
||||
this->cccd_handle_ = descr->handle;
|
||||
|
||||
auto descr_status =
|
||||
esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->cccd_handle_,
|
||||
2, cccd_value, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (descr_status) {
|
||||
ESP_LOGW(TAG, "Error sending CCC descriptor write request, status=%d", descr_status);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto chr_status =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->acp_handle_, 1,
|
||||
&battery_command, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (chr_status) {
|
||||
ESP_LOGW(TAG, "Error sending read request for battery, status=%d", chr_status);
|
||||
return false;
|
||||
}
|
||||
|
||||
this->response_pending_();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) {
|
||||
auto *value = (AccessControlPointResponse *) (&raw_value[2]);
|
||||
|
||||
if ((value_len >= (sizeof(AccessControlPointResponse) + 2)) && (raw_value[0] == ACCESS_CONTROL_POINT_COMMAND)) {
|
||||
ESP_LOGD(TAG, "Battery received: %u mV", (unsigned int) value->battery);
|
||||
|
||||
if (this->battery_voltage_ != nullptr) {
|
||||
float voltage = value->battery / 1000.0f;
|
||||
|
||||
this->battery_voltage_->publish_state(voltage);
|
||||
}
|
||||
|
||||
// read the battery again at the configured update interval
|
||||
if (this->battery_update_interval_ != this->update_interval_) {
|
||||
this->read_battery_next_update_ = false;
|
||||
this->set_timeout("battery", this->battery_update_interval_,
|
||||
[this]() { this->read_battery_next_update_ = true; });
|
||||
}
|
||||
}
|
||||
|
||||
this->response_received_();
|
||||
}
|
||||
|
||||
void AirthingsWaveBase::response_pending_() {
|
||||
this->responses_pending_++;
|
||||
this->set_response_timeout_();
|
||||
}
|
||||
|
||||
void AirthingsWaveBase::response_received_() {
|
||||
if (--this->responses_pending_ == 0) {
|
||||
// This instance must not stay connected
|
||||
// so other clients can connect to it (e.g. the
|
||||
// mobile app).
|
||||
this->parent()->set_enabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void AirthingsWaveBase::set_response_timeout_() {
|
||||
this->set_timeout("response_timeout", 30 * 1000, [this]() {
|
||||
this->responses_pending_ = 1;
|
||||
this->response_received_();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace airthings_wave_base
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
90
esphome/components/airthings_wave_base/airthings_wave_base.h
Normal file
90
esphome/components/airthings_wave_base/airthings_wave_base.h
Normal file
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
// All information related to reading battery levels came from the sensors.airthings_wave
|
||||
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace airthings_wave_base {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
static const uint8_t ACCESS_CONTROL_POINT_COMMAND = 0x6d;
|
||||
static const auto CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID = espbt::ESPBTUUID::from_uint16(0x2902);
|
||||
|
||||
class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode {
|
||||
public:
|
||||
AirthingsWaveBase() = default;
|
||||
|
||||
void update() override;
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
|
||||
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
|
||||
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
||||
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
|
||||
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
|
||||
void set_battery_voltage(sensor::Sensor *voltage) {
|
||||
battery_voltage_ = voltage;
|
||||
this->read_battery_next_update_ = true;
|
||||
}
|
||||
void set_battery_update_interval(uint32_t interval) { battery_update_interval_ = interval; }
|
||||
|
||||
protected:
|
||||
bool is_valid_voc_value_(uint16_t voc);
|
||||
|
||||
bool request_read_values_();
|
||||
virtual void read_sensors(uint8_t *raw_value, uint16_t value_len) = 0;
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
sensor::Sensor *tvoc_sensor_{nullptr};
|
||||
sensor::Sensor *battery_voltage_{nullptr};
|
||||
|
||||
uint16_t handle_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID sensors_data_characteristic_uuid_;
|
||||
|
||||
uint16_t acp_handle_{0};
|
||||
uint16_t cccd_handle_{0};
|
||||
espbt::ESPBTUUID access_control_point_characteristic_uuid_;
|
||||
|
||||
uint8_t responses_pending_{0};
|
||||
void response_pending_();
|
||||
void response_received_();
|
||||
void set_response_timeout_();
|
||||
|
||||
// default to *not* reading battery voltage from the device; the
|
||||
// set_* function for the battery sensor will set this to 'true'
|
||||
bool read_battery_next_update_{false};
|
||||
bool request_battery_();
|
||||
void read_battery_(uint8_t *raw_value, uint16_t value_len);
|
||||
uint32_t battery_update_interval_{};
|
||||
|
||||
struct AccessControlPointResponse {
|
||||
uint32_t unused1;
|
||||
uint8_t unused2;
|
||||
uint8_t illuminance;
|
||||
uint8_t unused3[10];
|
||||
uint16_t unused4[4];
|
||||
uint16_t battery;
|
||||
uint16_t unused5;
|
||||
};
|
||||
};
|
||||
|
||||
} // namespace airthings_wave_base
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -7,105 +7,47 @@ namespace airthings_wave_mini {
|
||||
|
||||
static const char *const TAG = "airthings_wave_mini";
|
||||
|
||||
void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (param->open.status == ESP_GATT_OK) {
|
||||
ESP_LOGI(TAG, "Connected successfully!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
ESP_LOGW(TAG, "Disconnected!");
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
this->handle_ = 0;
|
||||
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(),
|
||||
sensors_data_characteristic_uuid_.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
this->handle_ = chr->handle;
|
||||
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
|
||||
|
||||
request_read_values_();
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (param->read.conn_id != this->parent()->get_conn_id())
|
||||
break;
|
||||
if (param->read.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
|
||||
break;
|
||||
}
|
||||
if (param->read.handle == this->handle_) {
|
||||
read_sensors_(param->read.value, param->read.value_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
|
||||
void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) {
|
||||
auto *value = (WaveMiniReadings *) raw_value;
|
||||
|
||||
if (sizeof(WaveMiniReadings) <= value_len) {
|
||||
this->humidity_sensor_->publish_state(value->humidity / 100.0f);
|
||||
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
|
||||
this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f);
|
||||
if (is_valid_voc_value_(value->voc)) {
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
this->humidity_sensor_->publish_state(value->humidity / 100.0f);
|
||||
}
|
||||
|
||||
if (this->pressure_sensor_ != nullptr) {
|
||||
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f);
|
||||
}
|
||||
|
||||
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
|
||||
this->tvoc_sensor_->publish_state(value->voc);
|
||||
}
|
||||
|
||||
// This instance must not stay connected
|
||||
// so other clients can connect to it (e.g. the
|
||||
// mobile app).
|
||||
parent()->set_enabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
|
||||
|
||||
void AirthingsWaveMini::update() {
|
||||
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
|
||||
if (!parent()->enabled) {
|
||||
ESP_LOGW(TAG, "Reconnecting to device");
|
||||
parent()->set_enabled(true);
|
||||
parent()->connect();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection in progress");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AirthingsWaveMini::request_read_values_() {
|
||||
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
|
||||
ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
|
||||
}
|
||||
this->response_received_();
|
||||
}
|
||||
|
||||
void AirthingsWaveMini::dump_config() {
|
||||
// these really don't belong here, but there doesn't seem to be a
|
||||
// practical way to have the base class use LOG_SENSOR and include
|
||||
// the TAG from this component
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
|
||||
}
|
||||
|
||||
AirthingsWaveMini::AirthingsWaveMini()
|
||||
: PollingComponent(10000),
|
||||
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
|
||||
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {}
|
||||
AirthingsWaveMini::AirthingsWaveMini() {
|
||||
this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
|
||||
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
|
||||
this->access_control_point_characteristic_uuid_ =
|
||||
espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID);
|
||||
}
|
||||
|
||||
} // namespace airthings_wave_mini
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,50 +2,25 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/airthings_wave_base/airthings_wave_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace airthings_wave_mini {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba";
|
||||
static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba";
|
||||
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e3ef4-ade7-11e4-89d3-123b93f75cba";
|
||||
|
||||
class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode {
|
||||
class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
|
||||
public:
|
||||
AirthingsWaveMini();
|
||||
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
|
||||
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
|
||||
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
||||
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
|
||||
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
|
||||
|
||||
protected:
|
||||
bool is_valid_voc_value_(uint16_t voc);
|
||||
|
||||
void read_sensors_(uint8_t *value, uint16_t value_len);
|
||||
void request_read_values_();
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
sensor::Sensor *tvoc_sensor_{nullptr};
|
||||
|
||||
uint16_t handle_;
|
||||
esp32_ble_tracker::ESPBTUUID service_uuid_;
|
||||
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
|
||||
void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
|
||||
|
||||
struct WaveMiniReadings {
|
||||
uint16_t unused01;
|
||||
|
||||
@@ -1,82 +1,28 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, ble_client
|
||||
from esphome.components import airthings_wave_base
|
||||
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PERCENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
CONF_ID,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TVOC,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
ICON_RADIATOR,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
|
||||
|
||||
AUTO_LOAD = ["airthings_wave_base"]
|
||||
|
||||
airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini")
|
||||
AirthingsWaveMini = airthings_wave_mini_ns.class_(
|
||||
"AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode
|
||||
"AirthingsWaveMini", airthings_wave_base.AirthingsWaveBase
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AirthingsWaveMini),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TVOC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("5min"))
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA),
|
||||
CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AirthingsWaveMini),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
if CONF_HUMIDITY in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
|
||||
cg.add(var.set_humidity(sens))
|
||||
if CONF_TEMPERATURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
|
||||
cg.add(var.set_temperature(sens))
|
||||
if CONF_PRESSURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_PRESSURE])
|
||||
cg.add(var.set_pressure(sens))
|
||||
if CONF_TVOC in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TVOC])
|
||||
cg.add(var.set_tvoc(sens))
|
||||
await airthings_wave_base.wave_base_to_code(var, config)
|
||||
|
||||
@@ -7,55 +7,7 @@ namespace airthings_wave_plus {
|
||||
|
||||
static const char *const TAG = "airthings_wave_plus";
|
||||
|
||||
void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (param->open.status == ESP_GATT_OK) {
|
||||
ESP_LOGI(TAG, "Connected successfully!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
ESP_LOGW(TAG, "Disconnected!");
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
this->handle_ = 0;
|
||||
auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(),
|
||||
sensors_data_characteristic_uuid_.to_string().c_str());
|
||||
break;
|
||||
}
|
||||
this->handle_ = chr->handle;
|
||||
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
|
||||
|
||||
request_read_values_();
|
||||
break;
|
||||
}
|
||||
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (param->read.conn_id != this->parent()->get_conn_id())
|
||||
break;
|
||||
if (param->read.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
|
||||
break;
|
||||
}
|
||||
if (param->read.handle == this->handle_) {
|
||||
read_sensors_(param->read.value, param->read.value_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
|
||||
void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
|
||||
auto *value = (WavePlusReadings *) raw_value;
|
||||
|
||||
if (sizeof(WavePlusReadings) <= value_len) {
|
||||
@@ -64,72 +16,66 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
|
||||
if (value->version == 1) {
|
||||
ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
|
||||
|
||||
this->humidity_sensor_->publish_state(value->humidity / 2.0f);
|
||||
if (is_valid_radon_value_(value->radon)) {
|
||||
this->radon_sensor_->publish_state(value->radon);
|
||||
}
|
||||
if (is_valid_radon_value_(value->radon_lt)) {
|
||||
this->radon_long_term_sensor_->publish_state(value->radon_lt);
|
||||
}
|
||||
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
|
||||
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
|
||||
if (is_valid_co2_value_(value->co2)) {
|
||||
this->co2_sensor_->publish_state(value->co2);
|
||||
}
|
||||
if (is_valid_voc_value_(value->voc)) {
|
||||
this->tvoc_sensor_->publish_state(value->voc);
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
this->humidity_sensor_->publish_state(value->humidity / 2.0f);
|
||||
}
|
||||
|
||||
// This instance must not stay connected
|
||||
// so other clients can connect to it (e.g. the
|
||||
// mobile app).
|
||||
parent()->set_enabled(false);
|
||||
if ((this->radon_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon)) {
|
||||
this->radon_sensor_->publish_state(value->radon);
|
||||
}
|
||||
|
||||
if ((this->radon_long_term_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon_lt)) {
|
||||
this->radon_long_term_sensor_->publish_state(value->radon_lt);
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
this->temperature_sensor_->publish_state(value->temperature / 100.0f);
|
||||
}
|
||||
|
||||
if (this->pressure_sensor_ != nullptr) {
|
||||
this->pressure_sensor_->publish_state(value->pressure / 50.0f);
|
||||
}
|
||||
|
||||
if ((this->co2_sensor_ != nullptr) && this->is_valid_co2_value_(value->co2)) {
|
||||
this->co2_sensor_->publish_state(value->co2);
|
||||
}
|
||||
|
||||
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
|
||||
this->tvoc_sensor_->publish_state(value->voc);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
|
||||
}
|
||||
}
|
||||
|
||||
this->response_received_();
|
||||
}
|
||||
|
||||
bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; }
|
||||
bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return radon <= 16383; }
|
||||
|
||||
bool AirthingsWavePlus::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
|
||||
|
||||
bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; }
|
||||
|
||||
void AirthingsWavePlus::update() {
|
||||
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
|
||||
if (!parent()->enabled) {
|
||||
ESP_LOGW(TAG, "Reconnecting to device");
|
||||
parent()->set_enabled(true);
|
||||
parent()->connect();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection in progress");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AirthingsWavePlus::request_read_values_() {
|
||||
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
|
||||
ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
|
||||
}
|
||||
}
|
||||
bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return co2 <= 16383; }
|
||||
|
||||
void AirthingsWavePlus::dump_config() {
|
||||
// these really don't belong here, but there doesn't seem to be a
|
||||
// practical way to have the base class use LOG_SENSOR and include
|
||||
// the TAG from this component
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
|
||||
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
|
||||
|
||||
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
|
||||
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
}
|
||||
|
||||
AirthingsWavePlus::AirthingsWavePlus()
|
||||
: PollingComponent(10000),
|
||||
service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)),
|
||||
sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {}
|
||||
AirthingsWavePlus::AirthingsWavePlus() {
|
||||
this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
|
||||
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
|
||||
this->access_control_point_characteristic_uuid_ =
|
||||
espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID);
|
||||
}
|
||||
|
||||
} // namespace airthings_wave_plus
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,58 +2,36 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/airthings_wave_base/airthings_wave_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace airthings_wave_plus {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
|
||||
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
|
||||
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba";
|
||||
|
||||
class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode {
|
||||
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
|
||||
public:
|
||||
AirthingsWavePlus();
|
||||
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
|
||||
void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
|
||||
void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
|
||||
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
|
||||
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
||||
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
|
||||
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
|
||||
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
|
||||
|
||||
protected:
|
||||
bool is_valid_radon_value_(uint16_t radon);
|
||||
bool is_valid_voc_value_(uint16_t voc);
|
||||
bool is_valid_co2_value_(uint16_t co2);
|
||||
|
||||
void read_sensors_(uint8_t *value, uint16_t value_len);
|
||||
void request_read_values_();
|
||||
void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *radon_sensor_{nullptr};
|
||||
sensor::Sensor *radon_long_term_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
sensor::Sensor *tvoc_sensor_{nullptr};
|
||||
|
||||
uint16_t handle_;
|
||||
esp32_ble_tracker::ESPBTUUID service_uuid_;
|
||||
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
|
||||
|
||||
struct WavePlusReadings {
|
||||
uint8_t version;
|
||||
|
||||
@@ -1,116 +1,64 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, ble_client
|
||||
from esphome.components import sensor, airthings_wave_base
|
||||
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PERCENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
ICON_RADIOACTIVE,
|
||||
CONF_ID,
|
||||
CONF_RADON,
|
||||
CONF_RADON_LONG_TERM,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TVOC,
|
||||
CONF_CO2,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
ICON_RADIATOR,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
|
||||
|
||||
AUTO_LOAD = ["airthings_wave_base"]
|
||||
|
||||
airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus")
|
||||
AirthingsWavePlus = airthings_wave_plus_ns.class_(
|
||||
"AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode
|
||||
"AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
accuracy_decimals=0,
|
||||
),
|
||||
cv.Optional(CONF_RADON): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TVOC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("5min"))
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA),
|
||||
CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
|
||||
cv.Optional(CONF_RADON): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
|
||||
icon=ICON_RADIOACTIVE,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await airthings_wave_base.wave_base_to_code(var, config)
|
||||
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
if CONF_HUMIDITY in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
|
||||
cg.add(var.set_humidity(sens))
|
||||
if CONF_RADON in config:
|
||||
sens = await sensor.new_sensor(config[CONF_RADON])
|
||||
if config_radon := config.get(CONF_RADON):
|
||||
sens = await sensor.new_sensor(config_radon)
|
||||
cg.add(var.set_radon(sens))
|
||||
if CONF_RADON_LONG_TERM in config:
|
||||
sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
|
||||
if config_radon_long_term := config.get(CONF_RADON_LONG_TERM):
|
||||
sens = await sensor.new_sensor(config_radon_long_term)
|
||||
cg.add(var.set_radon_long_term(sens))
|
||||
if CONF_TEMPERATURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
|
||||
cg.add(var.set_temperature(sens))
|
||||
if CONF_PRESSURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_PRESSURE])
|
||||
cg.add(var.set_pressure(sens))
|
||||
if CONF_CO2 in config:
|
||||
sens = await sensor.new_sensor(config[CONF_CO2])
|
||||
if config_co2 := config.get(CONF_CO2):
|
||||
sens = await sensor.new_sensor(config_co2)
|
||||
cg.add(var.set_co2(sens))
|
||||
if CONF_TVOC in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TVOC])
|
||||
cg.add(var.set_tvoc(sens))
|
||||
|
||||
1
esphome/components/alpha3/__init__.py
Normal file
1
esphome/components/alpha3/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@jan-hofmeier"]
|
||||
189
esphome/components/alpha3/alpha3.cpp
Normal file
189
esphome/components/alpha3/alpha3.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include "alpha3.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <lwip/sockets.h> //gives ntohl
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace alpha3 {
|
||||
|
||||
static const char *const TAG = "alpha3";
|
||||
|
||||
void Alpha3::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ALPHA3");
|
||||
LOG_SENSOR(" ", "Flow", this->flow_sensor_);
|
||||
LOG_SENSOR(" ", "Head", this->head_sensor_);
|
||||
LOG_SENSOR(" ", "Power", this->power_sensor_);
|
||||
LOG_SENSOR(" ", "Current", this->current_sensor_);
|
||||
LOG_SENSOR(" ", "Speed", this->speed_sensor_);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
}
|
||||
|
||||
void Alpha3::setup() {}
|
||||
|
||||
void Alpha3::extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset,
|
||||
int16_t value_offset, sensor::Sensor *sensor, float factor) {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
// we need to handle cases where a value is split over two packets
|
||||
const int16_t value_length = 4; // 32bit float
|
||||
// offset inside current response packet
|
||||
auto rel_offset = value_offset - response_offset;
|
||||
if (rel_offset <= -value_length)
|
||||
return; // aready passed the value completly
|
||||
if (rel_offset >= length)
|
||||
return; // value not in this packet
|
||||
|
||||
auto start_offset = std::max(0, rel_offset);
|
||||
auto end_offset = std::min((int16_t) (rel_offset + value_length), length);
|
||||
auto copy_length = end_offset - start_offset;
|
||||
auto buffer_offset = std::max(-rel_offset, 0);
|
||||
std::memcpy(this->buffer_ + buffer_offset, response + start_offset, copy_length);
|
||||
|
||||
if (rel_offset + value_length <= length) {
|
||||
// we have the whole value
|
||||
void *buffer = this->buffer_; // to prevent warnings when casting the pointer
|
||||
*((int32_t *) buffer) = ntohl(*((int32_t *) buffer)); // values are big endian
|
||||
float fvalue = *((float *) buffer);
|
||||
sensor->publish_state(fvalue * factor);
|
||||
}
|
||||
}
|
||||
|
||||
bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
|
||||
return !std::memcmp(this->response_type_, response_type, GENI_RESPONSE_TYPE_LENGTH);
|
||||
}
|
||||
|
||||
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
|
||||
if (this->response_offset_ >= this->response_length_) {
|
||||
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
|
||||
if (length < GENI_RESPONSE_HEADER_LENGTH) {
|
||||
ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
|
||||
return;
|
||||
}
|
||||
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
|
||||
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
|
||||
response[0], response[1], response[2], response[3], response[4]);
|
||||
return;
|
||||
}
|
||||
this->response_length_ = response[1] - GENI_RESPONSE_HEADER_LENGTH + 2; // maybe 2 byte checksum
|
||||
this->response_offset_ = -GENI_RESPONSE_HEADER_LENGTH;
|
||||
std::memcpy(this->response_type_, response + 5, GENI_RESPONSE_TYPE_LENGTH);
|
||||
}
|
||||
|
||||
auto extract_publish_sensor_value = [response, length, this](int16_t value_offset, sensor::Sensor *sensor,
|
||||
float factor) {
|
||||
this->extract_publish_sensor_value_(response, length, this->response_offset_, value_offset, sensor, factor);
|
||||
};
|
||||
|
||||
if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
|
||||
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
|
||||
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
|
||||
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
|
||||
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
|
||||
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_VOLTAGE_AC_OFFSET, this->voltage_sensor_, 1.0F);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "unkown GENI response Type %d %d %d %d %d %d %d %d", this->response_type_[0], this->response_type_[1],
|
||||
this->response_type_[2], this->response_type_[3], this->response_type_[4], this->response_type_[5],
|
||||
this->response_type_[6], this->response_type_[7]);
|
||||
}
|
||||
this->response_offset_ += length;
|
||||
}
|
||||
|
||||
void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
this->response_offset_ = 0;
|
||||
this->response_length_ = 0;
|
||||
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CONNECT_EVT: {
|
||||
if (std::memcmp(param->connect.remote_bda, this->parent_->get_remote_bda(), 6) != 0)
|
||||
return;
|
||||
auto ret = esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "esp_ble_set_encryption failed, status=%x", ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
this->node_state = espbt::ClientState::IDLE;
|
||||
if (this->flow_sensor_ != nullptr)
|
||||
this->flow_sensor_->publish_state(NAN);
|
||||
if (this->head_sensor_ != nullptr)
|
||||
this->head_sensor_->publish_state(NAN);
|
||||
if (this->power_sensor_ != nullptr)
|
||||
this->power_sensor_->publish_state(NAN);
|
||||
if (this->current_sensor_ != nullptr)
|
||||
this->current_sensor_->publish_state(NAN);
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
this->speed_sensor_->publish_state(NAN);
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
this->voltage_sensor_->publish_state(NAN);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
|
||||
break;
|
||||
}
|
||||
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
|
||||
chr->handle);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status);
|
||||
}
|
||||
this->geni_handle_ = chr->handle;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
this->update();
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle == this->geni_handle_) {
|
||||
this->handle_geni_response_(param->notify.value, param->notify.value_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Alpha3::send_request_(uint8_t *request, size_t len) {
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
|
||||
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status)
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
}
|
||||
|
||||
void Alpha3::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->flow_sensor_ != nullptr || this->head_sensor_ != nullptr) {
|
||||
uint8_t geni_request_flow_head[] = {39, 7, 231, 248, 10, 3, 93, 1, 33, 82, 31};
|
||||
this->send_request_(geni_request_flow_head, sizeof(geni_request_flow_head));
|
||||
delay(25); // need to wait between requests
|
||||
}
|
||||
if (this->power_sensor_ != nullptr || this->current_sensor_ != nullptr || this->speed_sensor_ != nullptr ||
|
||||
this->voltage_sensor_ != nullptr) {
|
||||
uint8_t geni_request_power[] = {39, 7, 231, 248, 10, 3, 87, 0, 69, 138, 205};
|
||||
this->send_request_(geni_request_power, sizeof(geni_request_power));
|
||||
delay(25); // need to wait between requests
|
||||
}
|
||||
}
|
||||
} // namespace alpha3
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
73
esphome/components/alpha3/alpha3.h
Normal file
73
esphome/components/alpha3/alpha3.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace alpha3 {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d);
|
||||
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID =
|
||||
espbt::ESPBTUUID::from_raw({static_cast<char>(0xa9), 0x7b, static_cast<char>(0xb8), static_cast<char>(0x85), 0x0,
|
||||
0x1a, 0x28, static_cast<char>(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast<char>(0xd1),
|
||||
static_cast<char>(0xff), static_cast<char>(0x9c), static_cast<char>(0x85)});
|
||||
static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13;
|
||||
static const size_t GENI_RESPONSE_TYPE_LENGTH = 8;
|
||||
|
||||
static const uint8_t GENI_RESPONSE_TYPE_FLOW_HEAD[GENI_RESPONSE_TYPE_LENGTH] = {31, 0, 1, 48, 1, 0, 0, 24};
|
||||
static const int16_t GENI_RESPONSE_FLOW_OFFSET = 0;
|
||||
static const int16_t GENI_RESPONSE_HEAD_OFFSET = 4;
|
||||
|
||||
static const uint8_t GENI_RESPONSE_TYPE_POWER[GENI_RESPONSE_TYPE_LENGTH] = {44, 0, 1, 0, 1, 0, 0, 37};
|
||||
static const int16_t GENI_RESPONSE_VOLTAGE_AC_OFFSET = 0;
|
||||
static const int16_t GENI_RESPONSE_VOLTAGE_DC_OFFSET = 4;
|
||||
static const int16_t GENI_RESPONSE_CURRENT_OFFSET = 8;
|
||||
static const int16_t GENI_RESPONSE_POWER_OFFSET = 12;
|
||||
static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure
|
||||
static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20;
|
||||
|
||||
class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
|
||||
void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; }
|
||||
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
|
||||
void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; }
|
||||
void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; }
|
||||
void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *flow_sensor_{nullptr};
|
||||
sensor::Sensor *head_sensor_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
sensor::Sensor *speed_sensor_{nullptr};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
uint16_t geni_handle_;
|
||||
int16_t response_length_;
|
||||
int16_t response_offset_;
|
||||
uint8_t response_type_[GENI_RESPONSE_TYPE_LENGTH];
|
||||
uint8_t buffer_[4];
|
||||
void extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset,
|
||||
int16_t value_offset, sensor::Sensor *sensor, float factor);
|
||||
void handle_geni_response_(const uint8_t *response, uint16_t length);
|
||||
void send_request_(uint8_t *request, size_t len);
|
||||
bool is_current_response_type_(const uint8_t *response_type);
|
||||
};
|
||||
} // namespace alpha3
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
85
esphome/components/alpha3/sensor.py
Normal file
85
esphome/components/alpha3/sensor.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, ble_client
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_CURRENT,
|
||||
CONF_FLOW,
|
||||
CONF_HEAD,
|
||||
CONF_POWER,
|
||||
CONF_SPEED,
|
||||
CONF_VOLTAGE,
|
||||
UNIT_AMPERE,
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
UNIT_METER,
|
||||
UNIT_CUBIC_METER_PER_HOUR,
|
||||
UNIT_REVOLUTIONS_PER_MINUTE,
|
||||
)
|
||||
|
||||
alpha3_ns = cg.esphome_ns.namespace("alpha3")
|
||||
Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Alpha3),
|
||||
cv.Optional(CONF_FLOW): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_HEAD): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_SPEED): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=2,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("15s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
if CONF_FLOW in config:
|
||||
sens = await sensor.new_sensor(config[CONF_FLOW])
|
||||
cg.add(var.set_flow_sensor(sens))
|
||||
|
||||
if CONF_HEAD in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HEAD])
|
||||
cg.add(var.set_head_sensor(sens))
|
||||
|
||||
if CONF_POWER in config:
|
||||
sens = await sensor.new_sensor(config[CONF_POWER])
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
|
||||
if CONF_CURRENT in config:
|
||||
sens = await sensor.new_sensor(config[CONF_CURRENT])
|
||||
cg.add(var.set_current_sensor(sens))
|
||||
|
||||
if CONF_SPEED in config:
|
||||
sens = await sensor.new_sensor(config[CONF_SPEED])
|
||||
cg.add(var.set_speed_sensor(sens))
|
||||
|
||||
if CONF_VOLTAGE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_VOLTAGE])
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
from esphome import core
|
||||
from esphome.components import display, font
|
||||
from esphome import automation, core
|
||||
from esphome.components import font
|
||||
import esphome.components.image as espImage
|
||||
from esphome.components.image import CONF_USE_TRANSPARENCY
|
||||
import esphome.config_validation as cv
|
||||
@@ -18,14 +18,30 @@ from esphome.core import CORE, HexInt
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_LOOP = "loop"
|
||||
CONF_START_FRAME = "start_frame"
|
||||
CONF_END_FRAME = "end_frame"
|
||||
CONF_FRAME = "frame"
|
||||
|
||||
Animation_ = display.display_ns.class_("Animation", espImage.Image_)
|
||||
animation_ns = cg.esphome_ns.namespace("animation")
|
||||
|
||||
Animation_ = animation_ns.class_("Animation", espImage.Image_)
|
||||
|
||||
# Actions
|
||||
NextFrameAction = animation_ns.class_(
|
||||
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
PrevFrameAction = animation_ns.class_(
|
||||
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
|
||||
def validate_cross_dependencies(config):
|
||||
@@ -74,7 +90,35 @@ ANIMATION_SCHEMA = cv.Schema(
|
||||
|
||||
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
|
||||
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
}
|
||||
)
|
||||
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
}
|
||||
)
|
||||
SET_FRAME_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
cv.Required(CONF_FRAME): cv.uint16_t,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
|
||||
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
|
||||
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
|
||||
async def animation_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
if CONF_FRAME in config:
|
||||
template_ = await cg.templatable(config[CONF_FRAME], args, cg.uint16)
|
||||
cg.add(var.set_frame(template_))
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
70
esphome/components/animation/animation.cpp
Normal file
70
esphome/components/animation/animation.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "animation.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace animation {
|
||||
|
||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||
image::ImageType type)
|
||||
: Image(data_start, width, height, type),
|
||||
animation_data_start_(data_start),
|
||||
current_frame_(0),
|
||||
animation_frame_count_(animation_frame_count),
|
||||
loop_start_frame_(0),
|
||||
loop_end_frame_(animation_frame_count_),
|
||||
loop_count_(0),
|
||||
loop_current_iteration_(1) {}
|
||||
void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) {
|
||||
loop_start_frame_ = std::min(start_frame, animation_frame_count_);
|
||||
loop_end_frame_ = std::min(end_frame, animation_frame_count_);
|
||||
loop_count_ = count;
|
||||
loop_current_iteration_ = 1;
|
||||
}
|
||||
|
||||
uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; }
|
||||
int Animation::get_current_frame() const { return this->current_frame_; }
|
||||
void Animation::next_frame() {
|
||||
this->current_frame_++;
|
||||
if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
|
||||
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
|
||||
this->current_frame_ = loop_start_frame_;
|
||||
this->loop_current_iteration_++;
|
||||
}
|
||||
if (this->current_frame_ >= animation_frame_count_) {
|
||||
this->loop_current_iteration_ = 1;
|
||||
this->current_frame_ = 0;
|
||||
}
|
||||
|
||||
this->update_data_start_();
|
||||
}
|
||||
void Animation::prev_frame() {
|
||||
this->current_frame_--;
|
||||
if (this->current_frame_ < 0) {
|
||||
this->current_frame_ = this->animation_frame_count_ - 1;
|
||||
}
|
||||
|
||||
this->update_data_start_();
|
||||
}
|
||||
|
||||
void Animation::set_frame(int frame) {
|
||||
unsigned abs_frame = abs(frame);
|
||||
|
||||
if (abs_frame < this->animation_frame_count_) {
|
||||
if (frame >= 0) {
|
||||
this->current_frame_ = frame;
|
||||
} else {
|
||||
this->current_frame_ = this->animation_frame_count_ - abs_frame;
|
||||
}
|
||||
}
|
||||
|
||||
this->update_data_start_();
|
||||
}
|
||||
|
||||
void Animation::update_data_start_() {
|
||||
const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_;
|
||||
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
|
||||
}
|
||||
|
||||
} // namespace animation
|
||||
} // namespace esphome
|
||||
67
esphome/components/animation/animation.h
Normal file
67
esphome/components/animation/animation.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
#include "esphome/components/image/image.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace animation {
|
||||
|
||||
class Animation : public image::Image {
|
||||
public:
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type);
|
||||
|
||||
uint32_t get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
void next_frame();
|
||||
void prev_frame();
|
||||
|
||||
/** Selects a specific frame within the animation.
|
||||
*
|
||||
* @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
|
||||
*/
|
||||
void set_frame(int frame);
|
||||
|
||||
void set_loop(uint32_t start_frame, uint32_t end_frame, int count);
|
||||
|
||||
protected:
|
||||
void update_data_start_();
|
||||
|
||||
const uint8_t *animation_data_start_;
|
||||
int current_frame_;
|
||||
uint32_t animation_frame_count_;
|
||||
uint32_t loop_start_frame_;
|
||||
uint32_t loop_end_frame_;
|
||||
int loop_count_;
|
||||
int loop_current_iteration_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
|
||||
public:
|
||||
AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
|
||||
void play(Ts... x) override { this->parent_->next_frame(); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
|
||||
public:
|
||||
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
|
||||
void play(Ts... x) override { this->parent_->prev_frame(); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
|
||||
public:
|
||||
AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(uint16_t, frame)
|
||||
void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
} // namespace animation
|
||||
} // namespace esphome
|
||||
@@ -1420,6 +1420,7 @@ message VoiceAssistantRequest {
|
||||
|
||||
bool start = 1;
|
||||
string conversation_id = 2;
|
||||
bool use_vad = 3;
|
||||
}
|
||||
|
||||
message VoiceAssistantResponse {
|
||||
|
||||
@@ -907,12 +907,13 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id) {
|
||||
bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id, bool use_vad) {
|
||||
if (!this->voice_assistant_subscription_)
|
||||
return false;
|
||||
VoiceAssistantRequest msg;
|
||||
msg.start = start;
|
||||
msg.conversation_id = conversation_id;
|
||||
msg.use_vad = use_vad;
|
||||
return this->send_voice_assistant_request(msg);
|
||||
}
|
||||
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
|
||||
|
||||
@@ -124,7 +124,7 @@ class APIConnection : public APIServerConnection {
|
||||
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override {
|
||||
this->voice_assistant_subscription_ = msg.subscribe;
|
||||
}
|
||||
bool request_voice_assistant(bool start, const std::string &conversation_id);
|
||||
bool request_voice_assistant(bool start, const std::string &conversation_id, bool use_vad);
|
||||
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
|
||||
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
|
||||
#endif
|
||||
|
||||
@@ -6348,6 +6348,10 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
|
||||
this->start = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
case 3: {
|
||||
this->use_vad = value.as_bool();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -6365,6 +6369,7 @@ bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimite
|
||||
void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_bool(1, this->start);
|
||||
buffer.encode_string(2, this->conversation_id);
|
||||
buffer.encode_bool(3, this->use_vad);
|
||||
}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void VoiceAssistantRequest::dump_to(std::string &out) const {
|
||||
@@ -6377,6 +6382,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const {
|
||||
out.append(" conversation_id: ");
|
||||
out.append("'").append(this->conversation_id).append("'");
|
||||
out.append("\n");
|
||||
|
||||
out.append(" use_vad: ");
|
||||
out.append(YESNO(this->use_vad));
|
||||
out.append("\n");
|
||||
out.append("}");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1655,6 +1655,7 @@ class VoiceAssistantRequest : public ProtoMessage {
|
||||
public:
|
||||
bool start{false};
|
||||
std::string conversation_id{};
|
||||
bool use_vad{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
|
||||
@@ -323,16 +323,16 @@ void APIServer::on_shutdown() {
|
||||
}
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
bool APIServer::start_voice_assistant(const std::string &conversation_id) {
|
||||
bool APIServer::start_voice_assistant(const std::string &conversation_id, bool use_vad) {
|
||||
for (auto &c : this->clients_) {
|
||||
if (c->request_voice_assistant(true, conversation_id))
|
||||
if (c->request_voice_assistant(true, conversation_id, use_vad))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void APIServer::stop_voice_assistant() {
|
||||
for (auto &c : this->clients_) {
|
||||
if (c->request_voice_assistant(false, ""))
|
||||
if (c->request_voice_assistant(false, "", false))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class APIServer : public Component, public Controller {
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
bool start_voice_assistant(const std::string &conversation_id);
|
||||
bool start_voice_assistant(const std::string &conversation_id, bool use_vad);
|
||||
void stop_voice_assistant();
|
||||
#endif
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ async def async_run_logs(config, address):
|
||||
except APIConnectionError:
|
||||
cli.disconnect()
|
||||
|
||||
async def on_disconnect():
|
||||
async def on_disconnect(expected_disconnect: bool) -> None:
|
||||
_LOGGER.warning("Disconnected from API")
|
||||
|
||||
zc = zeroconf.Zeroconf()
|
||||
|
||||
1
esphome/components/atm90e26/__init__.py
Normal file
1
esphome/components/atm90e26/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@danieltwagner"]
|
||||
235
esphome/components/atm90e26/atm90e26.cpp
Normal file
235
esphome/components/atm90e26/atm90e26.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
#include "atm90e26.h"
|
||||
#include "atm90e26_reg.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace atm90e26 {
|
||||
|
||||
static const char *const TAG = "atm90e26";
|
||||
|
||||
void ATM90E26Component::update() {
|
||||
if (this->read16_(ATM90E26_REGISTER_FUNCEN) != 0x0030) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
this->voltage_sensor_->publish_state(this->get_line_voltage_());
|
||||
}
|
||||
if (this->current_sensor_ != nullptr) {
|
||||
this->current_sensor_->publish_state(this->get_line_current_());
|
||||
}
|
||||
if (this->power_sensor_ != nullptr) {
|
||||
this->power_sensor_->publish_state(this->get_active_power_());
|
||||
}
|
||||
if (this->reactive_power_sensor_ != nullptr) {
|
||||
this->reactive_power_sensor_->publish_state(this->get_reactive_power_());
|
||||
}
|
||||
if (this->power_factor_sensor_ != nullptr) {
|
||||
this->power_factor_sensor_->publish_state(this->get_power_factor_());
|
||||
}
|
||||
if (this->forward_active_energy_sensor_ != nullptr) {
|
||||
this->forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_());
|
||||
}
|
||||
if (this->reverse_active_energy_sensor_ != nullptr) {
|
||||
this->reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_());
|
||||
}
|
||||
if (this->freq_sensor_ != nullptr) {
|
||||
this->freq_sensor_->publish_state(this->get_frequency_());
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void ATM90E26Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ATM90E26 Component...");
|
||||
this->spi_setup();
|
||||
|
||||
uint16_t mmode = 0x422; // default values for everything but L/N line current gains
|
||||
mmode |= (gain_pga_ & 0x7) << 13;
|
||||
mmode |= (n_line_gain_ & 0x3) << 11;
|
||||
|
||||
this->write16_(ATM90E26_REGISTER_SOFTRESET, 0x789A); // Perform soft reset
|
||||
this->write16_(ATM90E26_REGISTER_FUNCEN,
|
||||
0x0030); // Voltage sag irq=1, report on warnout pin=1, energy dir change irq=0
|
||||
uint16_t read = this->read16_(ATM90E26_REGISTER_LASTDATA);
|
||||
if (read != 0x0030) {
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC, check SPI settings: %d", read);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// TODO: 100 * <nominal voltage, e.g. 230> * sqrt(2) * <fraction of nominal, e.g. 0.9> / (4 * gain_voltage/32768)
|
||||
this->write16_(ATM90E26_REGISTER_SAGTH, 0x17DD); // Voltage sag threshhold 0x1F2F
|
||||
|
||||
// Set metering calibration values
|
||||
this->write16_(ATM90E26_REGISTER_CALSTART, 0x5678); // CAL Metering calibration startup command
|
||||
|
||||
// Configure
|
||||
this->write16_(ATM90E26_REGISTER_MMODE, mmode); // Metering Mode Configuration (see above)
|
||||
|
||||
this->write16_(ATM90E26_REGISTER_PLCONSTH, (pl_const_ >> 16)); // PL Constant MSB
|
||||
this->write16_(ATM90E26_REGISTER_PLCONSTL, pl_const_ & 0xFFFF); // PL Constant LSB
|
||||
|
||||
// Calibrate this to be 1 pulse per Wh
|
||||
this->write16_(ATM90E26_REGISTER_LGAIN, gain_metering_); // L Line Calibration Gain (active power metering)
|
||||
this->write16_(ATM90E26_REGISTER_LPHI, 0x0000); // L Line Calibration Angle
|
||||
this->write16_(ATM90E26_REGISTER_NGAIN, 0x0000); // N Line Calibration Gain
|
||||
this->write16_(ATM90E26_REGISTER_NPHI, 0x0000); // N Line Calibration Angle
|
||||
this->write16_(ATM90E26_REGISTER_PSTARTTH, 0x08BD); // Active Startup Power Threshold (default) = 2237
|
||||
this->write16_(ATM90E26_REGISTER_PNOLTH, 0x0000); // Active No-Load Power Threshold
|
||||
this->write16_(ATM90E26_REGISTER_QSTARTTH, 0x0AEC); // Reactive Startup Power Threshold (default) = 2796
|
||||
this->write16_(ATM90E26_REGISTER_QNOLTH, 0x0000); // Reactive No-Load Power Threshold
|
||||
|
||||
// Compute Checksum for the registers we set above
|
||||
// low byte = sum of all bytes
|
||||
uint16_t cs =
|
||||
((mmode >> 8) + (mmode & 0xFF) + (pl_const_ >> 24) + ((pl_const_ >> 16) & 0xFF) + ((pl_const_ >> 8) & 0xFF) +
|
||||
(pl_const_ & 0xFF) + (gain_metering_ >> 8) + (gain_metering_ & 0xFF) + 0x08 + 0xBD + 0x0A + 0xEC) &
|
||||
0xFF;
|
||||
// high byte = XOR of all bytes
|
||||
cs |= ((mmode >> 8) ^ (mmode & 0xFF) ^ (pl_const_ >> 24) ^ ((pl_const_ >> 16) & 0xFF) ^ ((pl_const_ >> 8) & 0xFF) ^
|
||||
(pl_const_ & 0xFF) ^ (gain_metering_ >> 8) ^ (gain_metering_ & 0xFF) ^ 0x08 ^ 0xBD ^ 0x0A ^ 0xEC)
|
||||
<< 8;
|
||||
|
||||
this->write16_(ATM90E26_REGISTER_CS1, cs);
|
||||
ESP_LOGVV(TAG, "Set CS1 to: 0x%04X", cs);
|
||||
|
||||
// Set measurement calibration values
|
||||
this->write16_(ATM90E26_REGISTER_ADJSTART, 0x5678); // Measurement calibration startup command, registers 31-3A
|
||||
this->write16_(ATM90E26_REGISTER_UGAIN, gain_voltage_); // Voltage RMS gain
|
||||
this->write16_(ATM90E26_REGISTER_IGAINL, gain_ct_); // L line current RMS gain
|
||||
this->write16_(ATM90E26_REGISTER_IGAINN, 0x7530); // N Line Current RMS Gain
|
||||
this->write16_(ATM90E26_REGISTER_UOFFSET, 0x0000); // Voltage Offset
|
||||
this->write16_(ATM90E26_REGISTER_IOFFSETL, 0x0000); // L Line Current Offset
|
||||
this->write16_(ATM90E26_REGISTER_IOFFSETN, 0x0000); // N Line Current Offse
|
||||
this->write16_(ATM90E26_REGISTER_POFFSETL, 0x0000); // L Line Active Power Offset
|
||||
this->write16_(ATM90E26_REGISTER_QOFFSETL, 0x0000); // L Line Reactive Power Offset
|
||||
this->write16_(ATM90E26_REGISTER_POFFSETN, 0x0000); // N Line Active Power Offset
|
||||
this->write16_(ATM90E26_REGISTER_QOFFSETN, 0x0000); // N Line Reactive Power Offset
|
||||
|
||||
// Compute Checksum for the registers we set above
|
||||
cs = ((gain_voltage_ >> 8) + (gain_voltage_ & 0xFF) + (gain_ct_ >> 8) + (gain_ct_ & 0xFF) + 0x75 + 0x30) & 0xFF;
|
||||
cs |= ((gain_voltage_ >> 8) ^ (gain_voltage_ & 0xFF) ^ (gain_ct_ >> 8) ^ (gain_ct_ & 0xFF) ^ 0x75 ^ 0x30) << 8;
|
||||
this->write16_(ATM90E26_REGISTER_CS2, cs);
|
||||
ESP_LOGVV(TAG, "Set CS2 to: 0x%04X", cs);
|
||||
|
||||
this->write16_(ATM90E26_REGISTER_CALSTART,
|
||||
0x8765); // Checks correctness of 21-2B registers and starts normal metering if ok
|
||||
this->write16_(ATM90E26_REGISTER_ADJSTART,
|
||||
0x8765); // Checks correctness of 31-3A registers and starts normal measurement if ok
|
||||
|
||||
uint16_t sys_status = this->read16_(ATM90E26_REGISTER_SYSSTATUS);
|
||||
if (sys_status & 0xC000) { // Checksum 1 Error
|
||||
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS1 was incorrect, expected: 0x%04X",
|
||||
this->read16_(ATM90E26_REGISTER_CS1));
|
||||
this->mark_failed();
|
||||
}
|
||||
if (sys_status & 0x3000) { // Checksum 2 Error
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS2 was incorrect, expected: 0x%04X",
|
||||
this->read16_(ATM90E26_REGISTER_CS2));
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E26Component::dump_config() {
|
||||
ESP_LOGCONFIG("", "ATM90E26:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication with ATM90E26 failed!");
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage A", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current A", this->current_sensor_);
|
||||
LOG_SENSOR(" ", "Power A", this->power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power A", this->reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "PF A", this->power_factor_sensor_);
|
||||
LOG_SENSOR(" ", "Active Forward Energy A", this->forward_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
|
||||
}
|
||||
float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
uint16_t ATM90E26Component::read16_(uint8_t a_register) {
|
||||
uint8_t data[2];
|
||||
uint16_t output;
|
||||
|
||||
this->enable();
|
||||
delayMicroseconds(4);
|
||||
this->write_byte(a_register | 0x80);
|
||||
delayMicroseconds(4);
|
||||
this->read_array(data, 2);
|
||||
this->disable();
|
||||
|
||||
output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
|
||||
ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
void ATM90E26Component::write16_(uint8_t a_register, uint16_t val) {
|
||||
ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val);
|
||||
this->enable();
|
||||
delayMicroseconds(4);
|
||||
this->write_byte(a_register & 0x7F);
|
||||
delayMicroseconds(4);
|
||||
this->write_byte((val >> 8) & 0xFF);
|
||||
this->write_byte(val & 0xFF);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
float ATM90E26Component::get_line_current_() {
|
||||
uint16_t current = this->read16_(ATM90E26_REGISTER_IRMS);
|
||||
return current / 1000.0f;
|
||||
}
|
||||
|
||||
float ATM90E26Component::get_line_voltage_() {
|
||||
uint16_t voltage = this->read16_(ATM90E26_REGISTER_URMS);
|
||||
return voltage / 100.0f;
|
||||
}
|
||||
|
||||
float ATM90E26Component::get_active_power_() {
|
||||
int16_t val = this->read16_(ATM90E26_REGISTER_PMEAN); // two's complement
|
||||
return (float) val;
|
||||
}
|
||||
|
||||
float ATM90E26Component::get_reactive_power_() {
|
||||
int16_t val = this->read16_(ATM90E26_REGISTER_QMEAN); // two's complement
|
||||
return (float) val;
|
||||
}
|
||||
|
||||
float ATM90E26Component::get_power_factor_() {
|
||||
uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
|
||||
if (val & 0x8000) {
|
||||
return -(val & 0x7FF) / 1000.0f;
|
||||
} else {
|
||||
return val / 1000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
float ATM90E26Component::get_forward_active_energy_() {
|
||||
uint16_t val = this->read16_(ATM90E26_REGISTER_APENERGY);
|
||||
if ((UINT32_MAX - this->cumulative_forward_active_energy_) > val) {
|
||||
this->cumulative_forward_active_energy_ += val;
|
||||
} else {
|
||||
this->cumulative_forward_active_energy_ = val;
|
||||
}
|
||||
// The register holds thenths of pulses, we want to output Wh
|
||||
return (this->cumulative_forward_active_energy_ * 100.0f / meter_constant_);
|
||||
}
|
||||
|
||||
float ATM90E26Component::get_reverse_active_energy_() {
|
||||
uint16_t val = this->read16_(ATM90E26_REGISTER_ANENERGY);
|
||||
if (UINT32_MAX - this->cumulative_reverse_active_energy_ > val) {
|
||||
this->cumulative_reverse_active_energy_ += val;
|
||||
} else {
|
||||
this->cumulative_reverse_active_energy_ = val;
|
||||
}
|
||||
return (this->cumulative_reverse_active_energy_ * 100.0f / meter_constant_);
|
||||
}
|
||||
|
||||
float ATM90E26Component::get_frequency_() {
|
||||
uint16_t freq = this->read16_(ATM90E26_REGISTER_FREQ);
|
||||
return freq / 100.0f;
|
||||
}
|
||||
|
||||
} // namespace atm90e26
|
||||
} // namespace esphome
|
||||
72
esphome/components/atm90e26/atm90e26.h
Normal file
72
esphome/components/atm90e26/atm90e26.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace atm90e26 {
|
||||
|
||||
class ATM90E26Component : public PollingComponent,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
|
||||
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; }
|
||||
void set_current_sensor(sensor::Sensor *obj) { this->current_sensor_ = obj; }
|
||||
void set_power_sensor(sensor::Sensor *obj) { this->power_sensor_ = obj; }
|
||||
void set_reactive_power_sensor(sensor::Sensor *obj) { this->reactive_power_sensor_ = obj; }
|
||||
void set_forward_active_energy_sensor(sensor::Sensor *obj) { this->forward_active_energy_sensor_ = obj; }
|
||||
void set_reverse_active_energy_sensor(sensor::Sensor *obj) { this->reverse_active_energy_sensor_ = obj; }
|
||||
void set_power_factor_sensor(sensor::Sensor *obj) { this->power_factor_sensor_ = obj; }
|
||||
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
|
||||
void set_line_freq(int freq) { line_freq_ = freq; }
|
||||
void set_meter_constant(float val) { meter_constant_ = val; }
|
||||
void set_pl_const(uint32_t pl_const) { pl_const_ = pl_const; }
|
||||
void set_gain_metering(uint16_t gain) { this->gain_metering_ = gain; }
|
||||
void set_gain_voltage(uint16_t gain) { this->gain_voltage_ = gain; }
|
||||
void set_gain_ct(uint16_t gain) { this->gain_ct_ = gain; }
|
||||
void set_gain_pga(uint16_t gain) { gain_pga_ = gain; }
|
||||
void set_n_line_gain(uint16_t gain) { n_line_gain_ = gain; }
|
||||
|
||||
protected:
|
||||
uint16_t read16_(uint8_t a_register);
|
||||
int read32_(uint8_t addr_h, uint8_t addr_l);
|
||||
void write16_(uint8_t a_register, uint16_t val);
|
||||
|
||||
float get_line_voltage_();
|
||||
float get_line_current_();
|
||||
float get_active_power_();
|
||||
float get_reactive_power_();
|
||||
float get_power_factor_();
|
||||
float get_forward_active_energy_();
|
||||
float get_reverse_active_energy_();
|
||||
float get_frequency_();
|
||||
float get_chip_temperature_();
|
||||
|
||||
sensor::Sensor *freq_sensor_{nullptr};
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
sensor::Sensor *reactive_power_sensor_{nullptr};
|
||||
sensor::Sensor *power_factor_sensor_{nullptr};
|
||||
sensor::Sensor *forward_active_energy_sensor_{nullptr};
|
||||
sensor::Sensor *reverse_active_energy_sensor_{nullptr};
|
||||
uint32_t cumulative_forward_active_energy_{0};
|
||||
uint32_t cumulative_reverse_active_energy_{0};
|
||||
uint16_t gain_metering_{7481};
|
||||
uint16_t gain_voltage_{26400};
|
||||
uint16_t gain_ct_{31251};
|
||||
uint16_t gain_pga_{0x4};
|
||||
uint16_t n_line_gain_{0x2};
|
||||
int line_freq_{60};
|
||||
float meter_constant_{3200.0f};
|
||||
uint32_t pl_const_{1429876};
|
||||
};
|
||||
|
||||
} // namespace atm90e26
|
||||
} // namespace esphome
|
||||
70
esphome/components/atm90e26/atm90e26_reg.h
Normal file
70
esphome/components/atm90e26/atm90e26_reg.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
namespace esphome {
|
||||
namespace atm90e26 {
|
||||
|
||||
/* Status and Special Register */
|
||||
static const uint8_t ATM90E26_REGISTER_SOFTRESET = 0x00; // Software Reset
|
||||
static const uint8_t ATM90E26_REGISTER_SYSSTATUS = 0x01; // System Status
|
||||
static const uint8_t ATM90E26_REGISTER_FUNCEN = 0x02; // Function Enable
|
||||
static const uint8_t ATM90E26_REGISTER_SAGTH = 0x03; // Voltage Sag Threshold
|
||||
static const uint8_t ATM90E26_REGISTER_SMALLPMOD = 0x04; // Small-Power Mode
|
||||
static const uint8_t ATM90E26_REGISTER_LASTDATA = 0x06; // Last Read/Write SPI/UART Value
|
||||
|
||||
/* Metering Calibration and Configuration Register */
|
||||
static const uint8_t ATM90E26_REGISTER_LSB = 0x08; // RMS/Power 16-bit LSB
|
||||
static const uint8_t ATM90E26_REGISTER_CALSTART = 0x20; // Calibration Start Command
|
||||
static const uint8_t ATM90E26_REGISTER_PLCONSTH = 0x21; // High Word of PL_Constant
|
||||
static const uint8_t ATM90E26_REGISTER_PLCONSTL = 0x22; // Low Word of PL_Constant
|
||||
static const uint8_t ATM90E26_REGISTER_LGAIN = 0x23; // L Line Calibration Gain
|
||||
static const uint8_t ATM90E26_REGISTER_LPHI = 0x24; // L Line Calibration Angle
|
||||
static const uint8_t ATM90E26_REGISTER_NGAIN = 0x25; // N Line Calibration Gain
|
||||
static const uint8_t ATM90E26_REGISTER_NPHI = 0x26; // N Line Calibration Angle
|
||||
static const uint8_t ATM90E26_REGISTER_PSTARTTH = 0x27; // Active Startup Power Threshold
|
||||
static const uint8_t ATM90E26_REGISTER_PNOLTH = 0x28; // Active No-Load Power Threshold
|
||||
static const uint8_t ATM90E26_REGISTER_QSTARTTH = 0x29; // Reactive Startup Power Threshold
|
||||
static const uint8_t ATM90E26_REGISTER_QNOLTH = 0x2A; // Reactive No-Load Power Threshold
|
||||
static const uint8_t ATM90E26_REGISTER_MMODE = 0x2B; // Metering Mode Configuration
|
||||
static const uint8_t ATM90E26_REGISTER_CS1 = 0x2C; // Checksum 1
|
||||
|
||||
/* Measurement Calibration Register */
|
||||
static const uint8_t ATM90E26_REGISTER_ADJSTART = 0x30; // Measurement Calibration Start Command
|
||||
static const uint8_t ATM90E26_REGISTER_UGAIN = 0x31; // Voltage RMS Gain
|
||||
static const uint8_t ATM90E26_REGISTER_IGAINL = 0x32; // L Line Current RMS Gain
|
||||
static const uint8_t ATM90E26_REGISTER_IGAINN = 0x33; // N Line Current RMS Gain
|
||||
static const uint8_t ATM90E26_REGISTER_UOFFSET = 0x34; // Voltage Offset
|
||||
static const uint8_t ATM90E26_REGISTER_IOFFSETL = 0x35; // L Line Current Offset
|
||||
static const uint8_t ATM90E26_REGISTER_IOFFSETN = 0x36; // N Line Current Offse
|
||||
static const uint8_t ATM90E26_REGISTER_POFFSETL = 0x37; // L Line Active Power Offset
|
||||
static const uint8_t ATM90E26_REGISTER_QOFFSETL = 0x38; // L Line Reactive Power Offset
|
||||
static const uint8_t ATM90E26_REGISTER_POFFSETN = 0x39; // N Line Active Power Offset
|
||||
static const uint8_t ATM90E26_REGISTER_QOFFSETN = 0x3A; // N Line Reactive Power Offset
|
||||
static const uint8_t ATM90E26_REGISTER_CS2 = 0x3B; // Checksum 2
|
||||
|
||||
/* Energy Register */
|
||||
static const uint8_t ATM90E26_REGISTER_APENERGY = 0x40; // Forward Active Energy
|
||||
static const uint8_t ATM90E26_REGISTER_ANENERGY = 0x41; // Reverse Active Energy
|
||||
static const uint8_t ATM90E26_REGISTER_ATENERGY = 0x42; // Absolute Active Energy
|
||||
static const uint8_t ATM90E26_REGISTER_RPENERGY = 0x43; // Forward (Inductive) Reactive Energy
|
||||
static const uint8_t ATM90E26_REGISTER_RNENERG = 0x44; // Reverse (Capacitive) Reactive Energy
|
||||
static const uint8_t ATM90E26_REGISTER_RTENERGY = 0x45; // Absolute Reactive Energy
|
||||
static const uint8_t ATM90E26_REGISTER_ENSTATUS = 0x46; // Metering Status
|
||||
|
||||
/* Measurement Register */
|
||||
static const uint8_t ATM90E26_REGISTER_IRMS = 0x48; // L Line Current RMS
|
||||
static const uint8_t ATM90E26_REGISTER_URMS = 0x49; // Voltage RMS
|
||||
static const uint8_t ATM90E26_REGISTER_PMEAN = 0x4A; // L Line Mean Active Power
|
||||
static const uint8_t ATM90E26_REGISTER_QMEAN = 0x4B; // L Line Mean Reactive Power
|
||||
static const uint8_t ATM90E26_REGISTER_FREQ = 0x4C; // Voltage Frequency
|
||||
static const uint8_t ATM90E26_REGISTER_POWERF = 0x4D; // L Line Power Factor
|
||||
static const uint8_t ATM90E26_REGISTER_PANGLE = 0x4E; // Phase Angle between Voltage and L Line Current
|
||||
static const uint8_t ATM90E26_REGISTER_SMEAN = 0x4F; // L Line Mean Apparent Power
|
||||
static const uint8_t ATM90E26_REGISTER_IRMS2 = 0x68; // N Line Current rms
|
||||
static const uint8_t ATM90E26_REGISTER_PMEAN2 = 0x6A; // N Line Mean Active Power
|
||||
static const uint8_t ATM90E26_REGISTER_QMEAN2 = 0x6B; // N Line Mean Reactive Power
|
||||
static const uint8_t ATM90E26_REGISTER_POWERF2 = 0x6D; // N Line Power Factor
|
||||
static const uint8_t ATM90E26_REGISTER_PANGLE2 = 0x6E; // Phase Angle between Voltage and N Line Current
|
||||
static const uint8_t ATM90E26_REGISTER_SMEAN2 = 0x6F; // N Line Mean Apparent Power
|
||||
|
||||
} // namespace atm90e26
|
||||
} // namespace esphome
|
||||
157
esphome/components/atm90e26/sensor.py
Normal file
157
esphome/components/atm90e26/sensor.py
Normal file
@@ -0,0 +1,157 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, spi
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_REACTIVE_POWER,
|
||||
CONF_VOLTAGE,
|
||||
CONF_CURRENT,
|
||||
CONF_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_FREQUENCY,
|
||||
CONF_FORWARD_ACTIVE_ENERGY,
|
||||
CONF_REVERSE_ACTIVE_ENERGY,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_LIGHTBULB,
|
||||
ICON_CURRENT_AC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_HERTZ,
|
||||
UNIT_VOLT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_WATT,
|
||||
UNIT_VOLT_AMPS_REACTIVE,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
|
||||
CONF_LINE_FREQUENCY = "line_frequency"
|
||||
CONF_METER_CONSTANT = "meter_constant"
|
||||
CONF_PL_CONST = "pl_const"
|
||||
CONF_GAIN_PGA = "gain_pga"
|
||||
CONF_GAIN_METERING = "gain_metering"
|
||||
CONF_GAIN_VOLTAGE = "gain_voltage"
|
||||
CONF_GAIN_CT = "gain_ct"
|
||||
LINE_FREQS = {
|
||||
"50HZ": 50,
|
||||
"60HZ": 60,
|
||||
}
|
||||
PGA_GAINS = {
|
||||
"1X": 0x4,
|
||||
"4X": 0x0,
|
||||
"8X": 0x1,
|
||||
"16X": 0x2,
|
||||
"24X": 0x3,
|
||||
}
|
||||
|
||||
atm90e26_ns = cg.esphome_ns.namespace("atm90e26")
|
||||
ATM90E26Component = atm90e26_ns.class_(
|
||||
"ATM90E26Component", cg.PollingComponent, spi.SPIDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ATM90E26Component),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
|
||||
icon=ICON_LIGHTBULB,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER_FACTOR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HERTZ,
|
||||
icon=ICON_CURRENT_AC,
|
||||
accuracy_decimals=1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True),
|
||||
cv.Required(CONF_METER_CONSTANT): cv.positive_float,
|
||||
cv.Optional(CONF_PL_CONST, default=1429876): cv.uint32_t,
|
||||
cv.Optional(CONF_GAIN_METERING, default=7481): cv.uint16_t,
|
||||
cv.Optional(CONF_GAIN_VOLTAGE, default=26400): cv.int_range(
|
||||
min=0, max=32767
|
||||
),
|
||||
cv.Optional(CONF_GAIN_CT, default=31251): cv.uint16_t,
|
||||
cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(spi.spi_device_schema())
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
if CONF_VOLTAGE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_VOLTAGE])
|
||||
cg.add(var.set_voltage_sensor(sens))
|
||||
if CONF_CURRENT in config:
|
||||
sens = await sensor.new_sensor(config[CONF_CURRENT])
|
||||
cg.add(var.set_current_sensor(sens))
|
||||
if CONF_POWER in config:
|
||||
sens = await sensor.new_sensor(config[CONF_POWER])
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
if CONF_REACTIVE_POWER in config:
|
||||
sens = await sensor.new_sensor(config[CONF_REACTIVE_POWER])
|
||||
cg.add(var.set_reactive_power_sensor(sens))
|
||||
if CONF_POWER_FACTOR in config:
|
||||
sens = await sensor.new_sensor(config[CONF_POWER_FACTOR])
|
||||
cg.add(var.set_power_factor_sensor(sens))
|
||||
if CONF_FORWARD_ACTIVE_ENERGY in config:
|
||||
sens = await sensor.new_sensor(config[CONF_FORWARD_ACTIVE_ENERGY])
|
||||
cg.add(var.set_forward_active_energy_sensor(sens))
|
||||
if CONF_REVERSE_ACTIVE_ENERGY in config:
|
||||
sens = await sensor.new_sensor(config[CONF_REVERSE_ACTIVE_ENERGY])
|
||||
cg.add(var.set_reverse_active_energy_sensor(sens))
|
||||
if CONF_FREQUENCY in config:
|
||||
sens = await sensor.new_sensor(config[CONF_FREQUENCY])
|
||||
cg.add(var.set_freq_sensor(sens))
|
||||
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
|
||||
cg.add(var.set_meter_constant(config[CONF_METER_CONSTANT]))
|
||||
cg.add(var.set_pl_const(config[CONF_PL_CONST]))
|
||||
cg.add(var.set_gain_metering(config[CONF_GAIN_METERING]))
|
||||
cg.add(var.set_gain_voltage(config[CONF_GAIN_VOLTAGE]))
|
||||
cg.add(var.set_gain_ct(config[CONF_GAIN_CT]))
|
||||
cg.add(var.set_gain_pga(config[CONF_GAIN_PGA]))
|
||||
@@ -95,6 +95,14 @@ DEVICE_CLASSES = [
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
CONF_TIME_OFF = "time_off"
|
||||
CONF_TIME_ON = "time_on"
|
||||
|
||||
DEFAULT_DELAY = "1s"
|
||||
DEFAULT_TIME_OFF = "100ms"
|
||||
DEFAULT_TIME_ON = "900ms"
|
||||
|
||||
|
||||
binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor")
|
||||
BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase)
|
||||
BinarySensorInitiallyOff = binary_sensor_ns.class_(
|
||||
@@ -138,47 +146,75 @@ FILTER_REGISTRY = Registry()
|
||||
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("invert", InvertFilter, {})
|
||||
def register_filter(name, filter_type, schema):
|
||||
return FILTER_REGISTRY.register(name, filter_type, schema)
|
||||
|
||||
|
||||
@register_filter("invert", InvertFilter, {})
|
||||
async def invert_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds
|
||||
@register_filter(
|
||||
"delayed_on_off",
|
||||
DelayedOnOffFilter,
|
||||
cv.Any(
|
||||
cv.templatable(cv.positive_time_period_milliseconds),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TIME_ON): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
cv.Required(CONF_TIME_OFF): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
}
|
||||
),
|
||||
msg="'delayed_on_off' filter requires either a delay time to be used for both "
|
||||
"turn-on and turn-off delays, or two parameters 'time_on' and 'time_off' if "
|
||||
"different delay times are required.",
|
||||
),
|
||||
)
|
||||
async def delayed_on_off_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id, config)
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
if isinstance(config, dict):
|
||||
template_ = await cg.templatable(config[CONF_TIME_ON], [], cg.uint32)
|
||||
cg.add(var.set_on_delay(template_))
|
||||
template_ = await cg.templatable(config[CONF_TIME_OFF], [], cg.uint32)
|
||||
cg.add(var.set_off_delay(template_))
|
||||
else:
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_on_delay(template_))
|
||||
cg.add(var.set_off_delay(template_))
|
||||
return var
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds
|
||||
@register_filter(
|
||||
"delayed_on", DelayedOnFilter, cv.templatable(cv.positive_time_period_milliseconds)
|
||||
)
|
||||
async def delayed_on_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id, config)
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_delay(template_))
|
||||
return var
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds
|
||||
@register_filter(
|
||||
"delayed_off",
|
||||
DelayedOffFilter,
|
||||
cv.templatable(cv.positive_time_period_milliseconds),
|
||||
)
|
||||
async def delayed_off_filter_to_code(config, filter_id):
|
||||
var = cg.new_Pvariable(filter_id, config)
|
||||
var = cg.new_Pvariable(filter_id)
|
||||
await cg.register_component(var, {})
|
||||
template_ = await cg.templatable(config, [], cg.uint32)
|
||||
cg.add(var.set_delay(template_))
|
||||
return var
|
||||
|
||||
|
||||
CONF_TIME_OFF = "time_off"
|
||||
CONF_TIME_ON = "time_on"
|
||||
|
||||
DEFAULT_DELAY = "1s"
|
||||
DEFAULT_TIME_OFF = "100ms"
|
||||
DEFAULT_TIME_ON = "900ms"
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
@register_filter(
|
||||
"autorepeat",
|
||||
AutorepeatFilter,
|
||||
cv.All(
|
||||
@@ -215,7 +251,7 @@ async def autorepeat_filter_to_code(config, filter_id):
|
||||
return var
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda)
|
||||
@register_filter("lambda", LambdaFilter, cv.returning_lambda)
|
||||
async def lambda_filter_to_code(config, filter_id):
|
||||
lambda_ = await cg.process_lambda(
|
||||
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
||||
@@ -323,6 +359,18 @@ def validate_multi_click_timing(value):
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
|
||||
|
||||
def validate_click_timing(value):
|
||||
for v in value:
|
||||
min_length = v.get(CONF_MIN_LENGTH)
|
||||
max_length = v.get(CONF_MAX_LENGTH)
|
||||
if max_length < min_length:
|
||||
raise cv.Invalid(
|
||||
f"Max length ({max_length}) must be larger than min length ({min_length})."
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BinarySensor),
|
||||
@@ -342,27 +390,33 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLICK): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
cv.Optional(CONF_ON_CLICK): cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_click_timing,
|
||||
),
|
||||
cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger),
|
||||
cv.Optional(
|
||||
CONF_MIN_LENGTH, default="50ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_LENGTH, default="350ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
validate_click_timing,
|
||||
),
|
||||
cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation(
|
||||
{
|
||||
|
||||
@@ -26,22 +26,20 @@ void Filter::input(bool value, bool is_initial) {
|
||||
}
|
||||
}
|
||||
|
||||
DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {}
|
||||
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
|
||||
if (value) {
|
||||
this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
|
||||
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
|
||||
} else {
|
||||
this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
|
||||
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {}
|
||||
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
|
||||
if (value) {
|
||||
this->set_timeout("ON", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
|
||||
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
|
||||
return {};
|
||||
} else {
|
||||
this->cancel_timeout("ON");
|
||||
@@ -51,10 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
|
||||
|
||||
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
DelayedOffFilter::DelayedOffFilter(uint32_t delay) : delay_(delay) {}
|
||||
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
|
||||
if (!value) {
|
||||
this->set_timeout("OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
|
||||
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
|
||||
return {};
|
||||
} else {
|
||||
this->cancel_timeout("OFF");
|
||||
@@ -114,15 +111,6 @@ LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move
|
||||
|
||||
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
|
||||
|
||||
optional<bool> UniqueFilter::new_value(bool value, bool is_initial) {
|
||||
if (this->last_value_.has_value() && *this->last_value_ == value) {
|
||||
return {};
|
||||
} else {
|
||||
this->last_value_ = value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
@@ -29,38 +30,40 @@ class Filter {
|
||||
|
||||
class DelayedOnOffFilter : public Filter, public Component {
|
||||
public:
|
||||
explicit DelayedOnOffFilter(uint32_t delay);
|
||||
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
template<typename T> void set_on_delay(T delay) { this->on_delay_ = delay; }
|
||||
template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
|
||||
|
||||
protected:
|
||||
uint32_t delay_;
|
||||
TemplatableValue<uint32_t> on_delay_{};
|
||||
TemplatableValue<uint32_t> off_delay_{};
|
||||
};
|
||||
|
||||
class DelayedOnFilter : public Filter, public Component {
|
||||
public:
|
||||
explicit DelayedOnFilter(uint32_t delay);
|
||||
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
uint32_t delay_;
|
||||
TemplatableValue<uint32_t> delay_{};
|
||||
};
|
||||
|
||||
class DelayedOffFilter : public Filter, public Component {
|
||||
public:
|
||||
explicit DelayedOffFilter(uint32_t delay);
|
||||
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
uint32_t delay_;
|
||||
TemplatableValue<uint32_t> delay_{};
|
||||
};
|
||||
|
||||
class InvertFilter : public Filter {
|
||||
@@ -105,14 +108,6 @@ class LambdaFilter : public Filter {
|
||||
std::function<optional<bool>(bool)> f_;
|
||||
};
|
||||
|
||||
class UniqueFilter : public Filter {
|
||||
public:
|
||||
optional<bool> new_value(bool value, bool is_initial) override;
|
||||
|
||||
protected:
|
||||
optional<bool> last_value_{};
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -39,7 +39,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
|
||||
cv.Optional(CONF_IBEACON_UUID): cv.uuid,
|
||||
cv.Optional(CONF_MIN_RSSI): cv.All(
|
||||
cv.decibel, cv.int_range(min=-90, max=-30)
|
||||
cv.decibel, cv.int_range(min=-100, max=-30)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
|
||||
this->found_ = false;
|
||||
}
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
|
||||
if (this->check_minimum_rssi_ && this->minimum_rssi_ <= device.get_rssi()) {
|
||||
if (this->check_minimum_rssi_ && this->minimum_rssi_ > device.get_rssi()) {
|
||||
return false;
|
||||
}
|
||||
switch (this->match_by_) {
|
||||
|
||||
@@ -275,6 +275,10 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() {
|
||||
return this->proxy_->get_advertisement_parser_type();
|
||||
}
|
||||
|
||||
} // namespace bluetooth_proxy
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
esp_err_t read_characteristic(uint16_t handle);
|
||||
esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response);
|
||||
|
||||
@@ -198,6 +198,12 @@ void BluetoothProxy::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
|
||||
if (this->raw_advertisements_)
|
||||
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
|
||||
return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS;
|
||||
}
|
||||
|
||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
||||
for (auto *connection : this->connections_) {
|
||||
if (connection->get_address() == address)
|
||||
@@ -435,6 +441,7 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
|
||||
}
|
||||
this->api_connection_ = api_connection;
|
||||
this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS;
|
||||
this->parent_->recalculate_advertisement_parser_types();
|
||||
}
|
||||
|
||||
void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) {
|
||||
@@ -444,6 +451,7 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
this->raw_advertisements_ = false;
|
||||
this->parent_->recalculate_advertisement_parser_types();
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) {
|
||||
|
||||
@@ -51,6 +51,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
void register_connection(BluetoothConnection *connection) {
|
||||
this->connections_.push_back(connection);
|
||||
|
||||
@@ -6,8 +6,10 @@ from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
@@ -17,8 +19,6 @@ from esphome.const import (
|
||||
UNIT_PERCENT,
|
||||
ICON_GAS_CYLINDER,
|
||||
ICON_GAUGE,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
)
|
||||
from . import (
|
||||
BME680BSECComponent,
|
||||
@@ -35,7 +35,6 @@ CONF_CO2_EQUIVALENT = "co2_equivalent"
|
||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||
UNIT_IAQ = "IAQ"
|
||||
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
|
||||
ICON_TEST_TUBE = "mdi:test-tube"
|
||||
|
||||
TYPES = [
|
||||
CONF_TEMPERATURE,
|
||||
@@ -53,7 +52,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
@@ -62,16 +60,14 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
icon=ICON_GAUGE,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_WATER_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
@@ -97,14 +93,14 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
),
|
||||
cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
icon=ICON_TEST_TUBE,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
icon=ICON_TEST_TUBE,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -94,3 +94,5 @@ async def to_code(config):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING]))
|
||||
|
||||
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_MQTT_ID,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_IDENTIFY,
|
||||
DEVICE_CLASS_RESTART,
|
||||
DEVICE_CLASS_UPDATE,
|
||||
)
|
||||
@@ -24,6 +25,7 @@ IS_PLATFORM_COMPONENT = True
|
||||
|
||||
DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_IDENTIFY,
|
||||
DEVICE_CLASS_RESTART,
|
||||
DEVICE_CLASS_UPDATE,
|
||||
]
|
||||
|
||||
@@ -30,7 +30,7 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm
|
||||
if (use_extended_id) {
|
||||
ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
|
||||
ESP_LOGD(TAG, "send standard id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
|
||||
}
|
||||
if (size > CAN_MAX_DATA_LENGTH)
|
||||
size = CAN_MAX_DATA_LENGTH;
|
||||
|
||||
@@ -21,7 +21,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_with_arduino,
|
||||
cv.only_on(["esp32", "esp8266"]),
|
||||
)
|
||||
|
||||
@@ -34,8 +33,9 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
cg.add_define("USE_CAPTIVE_PORTAL")
|
||||
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("DNSServer", None)
|
||||
cg.add_library("WiFi", None)
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("DNSServer", None)
|
||||
if CORE.using_arduino:
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("DNSServer", None)
|
||||
cg.add_library("WiFi", None)
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("DNSServer", None)
|
||||
|
||||
@@ -6,102 +6,100 @@ namespace esphome {
|
||||
namespace captive_portal {
|
||||
|
||||
const uint8_t INDEX_GZ[] PROGMEM = {
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xdd, 0x58, 0x09, 0x6f, 0xdc, 0x36, 0x16, 0xfe, 0x2b,
|
||||
0xac, 0x92, 0x74, 0x34, 0x8d, 0xc5, 0xd1, 0x31, 0x97, 0x35, 0xd2, 0x14, 0x89, 0x37, 0x45, 0x0b, 0x24, 0x69, 0x00,
|
||||
0xbb, 0x5d, 0x14, 0x69, 0x00, 0x73, 0x24, 0x6a, 0xc4, 0x58, 0xa2, 0x54, 0x91, 0x9a, 0x23, 0x83, 0xd9, 0xdf, 0xde,
|
||||
0x47, 0x52, 0x73, 0x38, 0x6b, 0x2f, 0x90, 0x62, 0x8b, 0xa2, 0x4d, 0x6c, 0x9a, 0xc7, 0x3b, 0x3f, 0xf2, 0xf1, 0x3d,
|
||||
0x2a, 0xfa, 0x2a, 0xad, 0x12, 0xb9, 0xad, 0x29, 0xca, 0x65, 0x59, 0xcc, 0x23, 0xd5, 0xa2, 0x82, 0xf0, 0x65, 0x4c,
|
||||
0x39, 0x8c, 0x28, 0x49, 0xe7, 0x51, 0x49, 0x25, 0x41, 0x49, 0x4e, 0x1a, 0x41, 0x65, 0xfc, 0xd3, 0xcd, 0x77, 0xce,
|
||||
0x14, 0x0d, 0xe6, 0x51, 0xc1, 0xf8, 0x1d, 0x6a, 0x68, 0x11, 0xb3, 0xa4, 0xe2, 0x28, 0x6f, 0x68, 0x16, 0xa7, 0x44,
|
||||
0x92, 0x90, 0x95, 0x64, 0x49, 0x15, 0x81, 0x66, 0xe3, 0xa4, 0xa4, 0xf1, 0x8a, 0xd1, 0x75, 0x5d, 0x35, 0x12, 0x01,
|
||||
0xa5, 0xa4, 0x5c, 0xc6, 0xd6, 0x9a, 0xa5, 0x32, 0x8f, 0x53, 0xba, 0x62, 0x09, 0x75, 0xf4, 0xe0, 0x82, 0x71, 0x26,
|
||||
0x19, 0x29, 0x1c, 0x91, 0x90, 0x82, 0xc6, 0xde, 0x45, 0x2b, 0x68, 0xa3, 0x07, 0x64, 0x01, 0x63, 0x5e, 0x59, 0x20,
|
||||
0x52, 0x24, 0x0d, 0xab, 0x25, 0x52, 0xf6, 0xc6, 0x65, 0x95, 0xb6, 0x05, 0x9d, 0x67, 0x2d, 0x4f, 0x24, 0x03, 0x0b,
|
||||
0x84, 0xcd, 0xfb, 0xbb, 0x82, 0x4a, 0x44, 0xe3, 0x37, 0x44, 0xe6, 0xb8, 0x24, 0x1b, 0xdb, 0x74, 0x18, 0xb7, 0xfd,
|
||||
0x6f, 0x6c, 0xfe, 0xdc, 0x73, 0xdd, 0xfe, 0x85, 0x6e, 0xdc, 0xfe, 0x00, 0xfe, 0xce, 0x1a, 0x2a, 0xdb, 0x86, 0x23,
|
||||
0x62, 0xdf, 0x46, 0x35, 0x50, 0xa2, 0x34, 0xb6, 0x4a, 0xcf, 0xc7, 0xae, 0x3b, 0x45, 0xde, 0x25, 0xf6, 0x47, 0x8e,
|
||||
0xe7, 0xe1, 0xc0, 0xf1, 0x46, 0xc9, 0xc4, 0x19, 0x21, 0x6f, 0x08, 0x8d, 0xef, 0xe3, 0x11, 0x72, 0x3f, 0x59, 0x28,
|
||||
0x63, 0x45, 0x11, 0x5b, 0xbc, 0xe2, 0xd4, 0x42, 0x42, 0x36, 0xd5, 0x1d, 0x8d, 0xad, 0xa4, 0x6d, 0x1a, 0xf0, 0xee,
|
||||
0xaa, 0x2a, 0xaa, 0x06, 0xac, 0xfd, 0x95, 0xa3, 0x7b, 0xff, 0xbe, 0x58, 0x87, 0x6c, 0x08, 0x17, 0x59, 0xd5, 0x94,
|
||||
0xb1, 0xa5, 0x41, 0xb1, 0x9f, 0xee, 0xe8, 0x1e, 0xa9, 0xa6, 0x7f, 0xb6, 0xe8, 0x54, 0x0d, 0x5b, 0x32, 0x1e, 0x5b,
|
||||
0x9e, 0x8f, 0xbc, 0x29, 0xe8, 0xbd, 0xed, 0xef, 0x8f, 0xa0, 0x10, 0x05, 0x4a, 0xe7, 0x66, 0x65, 0xbf, 0xbf, 0x8d,
|
||||
0xc4, 0x6a, 0x89, 0x36, 0x65, 0xc1, 0x45, 0x6c, 0xe5, 0x52, 0xd6, 0xe1, 0x60, 0xb0, 0x5e, 0xaf, 0xf1, 0x3a, 0xc0,
|
||||
0x55, 0xb3, 0x1c, 0xf8, 0xae, 0xeb, 0x0e, 0x80, 0xc2, 0x42, 0x66, 0x7f, 0x2c, 0x7f, 0x68, 0xa1, 0x9c, 0xb2, 0x65,
|
||||
0x2e, 0x75, 0x7f, 0xfe, 0x74, 0xc7, 0xf7, 0x91, 0xa2, 0x98, 0xdf, 0x7e, 0x38, 0xd3, 0xd2, 0x9c, 0x69, 0xe1, 0xdf,
|
||||
0x12, 0xdb, 0x3a, 0xb8, 0xda, 0x7b, 0xa3, 0x8c, 0x9a, 0x10, 0x1f, 0xf9, 0xc8, 0xd5, 0xff, 0x7d, 0x47, 0xf5, 0xbb,
|
||||
0x91, 0xf3, 0xd9, 0x08, 0x9d, 0x8d, 0xe0, 0xaf, 0x02, 0xd0, 0x2f, 0xc7, 0xce, 0xe5, 0x91, 0xdf, 0x53, 0xeb, 0x2b,
|
||||
0xcf, 0x3d, 0x4d, 0x28, 0xa6, 0xef, 0xc7, 0xe7, 0x63, 0xc7, 0xff, 0x59, 0x11, 0x68, 0xf4, 0x8f, 0x5c, 0x8e, 0x9f,
|
||||
0x7b, 0x3f, 0x8f, 0xc9, 0x08, 0x8d, 0xba, 0x99, 0x91, 0xa3, 0xfa, 0xc7, 0x91, 0xd6, 0x85, 0x46, 0x2b, 0x20, 0x2b,
|
||||
0x9d, 0xb1, 0x33, 0x22, 0x01, 0x0a, 0x3a, 0xab, 0xa0, 0x07, 0xd3, 0x63, 0xe0, 0x3e, 0x9b, 0x73, 0x82, 0x4f, 0xbd,
|
||||
0xc1, 0xdc, 0xea, 0x87, 0x96, 0x75, 0x82, 0xa1, 0x3a, 0x87, 0x01, 0x7f, 0xac, 0xe0, 0xdc, 0x59, 0x56, 0x7f, 0x6f,
|
||||
0x7d, 0x2b, 0xc8, 0x8a, 0x5a, 0x71, 0x1c, 0x43, 0xa8, 0xb5, 0x25, 0x9c, 0x10, 0x5c, 0x54, 0x09, 0x51, 0x2c, 0x58,
|
||||
0x50, 0xd2, 0x24, 0xf9, 0xd7, 0x5f, 0xdb, 0xc7, 0xa5, 0x25, 0x95, 0xaf, 0x0a, 0xaa, 0xba, 0xe2, 0xe5, 0xf6, 0x86,
|
||||
0x2c, 0xdf, 0x42, 0x00, 0xd9, 0x16, 0x11, 0x2c, 0xa5, 0x56, 0xff, 0xbd, 0xfb, 0x01, 0x0b, 0xb9, 0x2d, 0x28, 0x4e,
|
||||
0x99, 0xa8, 0x0b, 0xb2, 0x8d, 0xad, 0x05, 0xc8, 0xba, 0xb3, 0xfa, 0x17, 0x19, 0x95, 0x49, 0x6e, 0x5b, 0x03, 0x08,
|
||||
0xb1, 0x8c, 0x2d, 0xf1, 0x47, 0x51, 0x71, 0xab, 0x8f, 0x65, 0x4e, 0xb9, 0x6d, 0x1f, 0x2c, 0x54, 0xf6, 0x71, 0xbd,
|
||||
0x64, 0x3f, 0xb4, 0x74, 0xb4, 0x41, 0x32, 0xa9, 0x42, 0x0e, 0xab, 0xe0, 0xbd, 0x38, 0xce, 0x2e, 0xaa, 0x74, 0xfb,
|
||||
0x88, 0x79, 0xb9, 0x67, 0x6c, 0x63, 0x9c, 0xd3, 0xe6, 0x86, 0x6e, 0xe0, 0xb8, 0xfc, 0x9b, 0x7d, 0xc7, 0xd0, 0x5b,
|
||||
0x2a, 0xd7, 0x55, 0x73, 0x27, 0x42, 0x64, 0x3d, 0x37, 0xe2, 0x66, 0x26, 0x42, 0x39, 0x26, 0xb5, 0xc0, 0xa2, 0x80,
|
||||
0xf0, 0xb7, 0xbd, 0x3e, 0xc4, 0x6a, 0x7d, 0xdf, 0x14, 0x83, 0xe2, 0x6d, 0x94, 0xb2, 0x15, 0x4a, 0x0a, 0x22, 0xe0,
|
||||
0xb8, 0x72, 0x23, 0xcb, 0x42, 0x87, 0xb8, 0xaa, 0x78, 0x02, 0xfc, 0x77, 0xb1, 0xf5, 0x00, 0x76, 0x2f, 0xb7, 0x3f,
|
||||
0xa4, 0x76, 0x4f, 0x00, 0x6a, 0xbd, 0x3e, 0x5e, 0x91, 0xa2, 0xa5, 0x28, 0x46, 0x32, 0x67, 0xe2, 0x64, 0xe2, 0xec,
|
||||
0x51, 0xb6, 0x5a, 0xdc, 0x01, 0x57, 0x06, 0xcb, 0xc2, 0xee, 0x5b, 0xc7, 0x38, 0x8e, 0x88, 0xb9, 0xe5, 0xac, 0x27,
|
||||
0xd6, 0x67, 0x36, 0x39, 0x05, 0xcd, 0xa4, 0x75, 0x16, 0xf0, 0x4f, 0x77, 0x70, 0x1b, 0xe1, 0x06, 0xf4, 0xf7, 0xf7,
|
||||
0xa7, 0xd9, 0x48, 0xd4, 0x84, 0x7f, 0xce, 0xaa, 0x6c, 0xd4, 0x81, 0x85, 0x55, 0x4f, 0x45, 0x17, 0x10, 0x9d, 0x74,
|
||||
0x0e, 0xc8, 0xb1, 0xff, 0x74, 0x07, 0x71, 0xa6, 0x8e, 0xce, 0xdd, 0x49, 0x68, 0x34, 0x00, 0x84, 0xe6, 0xb7, 0xfb,
|
||||
0x7e, 0xff, 0xe4, 0xce, 0x6f, 0x2d, 0x6d, 0xb6, 0xd7, 0xb4, 0xa0, 0x89, 0xac, 0x1a, 0xdb, 0x7a, 0x02, 0x9a, 0xe0,
|
||||
0x24, 0x68, 0xbf, 0xbf, 0xbf, 0x79, 0xf3, 0x3a, 0xae, 0x6c, 0xda, 0xbf, 0x78, 0x8c, 0x5a, 0xdd, 0xea, 0xef, 0xe1,
|
||||
0x56, 0xff, 0x4f, 0xdc, 0x53, 0xf7, 0x7a, 0xef, 0x03, 0xb0, 0x1a, 0xaf, 0x4f, 0x97, 0xbb, 0xba, 0x00, 0x9e, 0xc3,
|
||||
0x25, 0x72, 0x61, 0x3d, 0x17, 0xb6, 0x33, 0x1e, 0xf5, 0x41, 0x3d, 0xfc, 0x80, 0xe9, 0xfa, 0x7a, 0x86, 0x6b, 0x5a,
|
||||
0x1d, 0xd1, 0xf9, 0x37, 0xbb, 0x45, 0xb5, 0x71, 0x04, 0xfb, 0xc4, 0xf8, 0x32, 0x64, 0x3c, 0xa7, 0x0d, 0x93, 0x7b,
|
||||
0x30, 0x17, 0x6e, 0xfa, 0xba, 0x95, 0xbb, 0x9a, 0xa4, 0xa9, 0x5a, 0x19, 0xd5, 0x9b, 0x59, 0x06, 0x79, 0x41, 0x51,
|
||||
0xd2, 0xd0, 0xa3, 0xe5, 0xde, 0xac, 0xeb, 0x2b, 0x28, 0xbc, 0x1c, 0x3d, 0xdb, 0xab, 0x83, 0xb7, 0x93, 0xb0, 0x65,
|
||||
0x0e, 0x29, 0xd8, 0x92, 0x87, 0x09, 0xd8, 0x4d, 0x1b, 0xc3, 0x94, 0x91, 0x92, 0x15, 0xdb, 0x50, 0xc0, 0x65, 0xe8,
|
||||
0x40, 0xc2, 0x60, 0xd9, 0x7e, 0xd1, 0x4a, 0x59, 0x71, 0xd0, 0xdd, 0xa4, 0xb4, 0x09, 0xdd, 0x99, 0xe9, 0x38, 0x0d,
|
||||
0x49, 0x59, 0x2b, 0x42, 0x1c, 0x34, 0xb4, 0x9c, 0x2d, 0x48, 0x72, 0xb7, 0x6c, 0xaa, 0x96, 0xa7, 0x4e, 0xa2, 0x6e,
|
||||
0xeb, 0xf0, 0x89, 0x97, 0x91, 0x80, 0x26, 0xb3, 0x6e, 0x94, 0x65, 0xd9, 0x0c, 0x90, 0xa0, 0x8e, 0xb9, 0xfc, 0x42,
|
||||
0x1f, 0x0f, 0x15, 0xdb, 0x99, 0x99, 0xd8, 0x57, 0x13, 0xc6, 0x46, 0x48, 0x25, 0xcf, 0x66, 0x07, 0x77, 0xdc, 0x19,
|
||||
0xa4, 0x01, 0x01, 0x42, 0x6a, 0x88, 0x7f, 0x30, 0x73, 0x5f, 0x12, 0xc6, 0xcf, 0xad, 0x57, 0x67, 0x65, 0xd6, 0x85,
|
||||
0x2f, 0xc0, 0xa2, 0xd5, 0xe8, 0x20, 0x9e, 0x41, 0xa2, 0x32, 0xb9, 0x30, 0xf4, 0xc7, 0x6e, 0xbd, 0xd9, 0xe3, 0xee,
|
||||
0x8c, 0xec, 0x0e, 0xd4, 0x59, 0x41, 0x37, 0xb3, 0x8f, 0xad, 0x90, 0x2c, 0xdb, 0x3a, 0x5d, 0x2e, 0x0d, 0xe1, 0xbc,
|
||||
0x40, 0x0e, 0x5d, 0x00, 0x29, 0xa5, 0x7c, 0xa6, 0x75, 0x38, 0x4c, 0xd2, 0x52, 0x74, 0x38, 0x1d, 0xc5, 0xe8, 0x53,
|
||||
0x7a, 0x5f, 0xd6, 0xff, 0xa2, 0x56, 0xc7, 0x71, 0x57, 0x92, 0x06, 0x72, 0x8b, 0xb3, 0xa8, 0x00, 0xd3, 0x32, 0x74,
|
||||
0x26, 0xb0, 0x57, 0xdd, 0x94, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xfa, 0x6e, 0x3a, 0xe0, 0xed, 0xd5, 0x1b, 0x24, 0xaa,
|
||||
0x82, 0xa5, 0x1d, 0x9d, 0x26, 0x41, 0xee, 0x11, 0x1e, 0x0f, 0xb6, 0x1b, 0xa9, 0xb9, 0x03, 0xd4, 0xc3, 0x6c, 0x4a,
|
||||
0x3c, 0xf7, 0x81, 0x1d, 0x49, 0xb3, 0xcc, 0x5f, 0x64, 0x47, 0xa4, 0x54, 0xaa, 0xdd, 0xb3, 0xee, 0x54, 0xf8, 0x43,
|
||||
0x10, 0x70, 0xd8, 0x1b, 0xe8, 0xef, 0x99, 0x8e, 0x8b, 0xdd, 0x99, 0x14, 0x7d, 0x52, 0xc3, 0xb6, 0x29, 0xec, 0x87,
|
||||
0x4e, 0xee, 0xb3, 0xe0, 0xea, 0x94, 0x09, 0x7b, 0x8f, 0x67, 0xc2, 0x1e, 0x52, 0xb5, 0xcb, 0xcb, 0x6a, 0x13, 0xf7,
|
||||
0x74, 0x4e, 0x1a, 0xc2, 0x4f, 0xef, 0x59, 0xf0, 0x0a, 0xf8, 0xff, 0x2f, 0x29, 0xee, 0x0f, 0xa7, 0xb7, 0x2f, 0x48,
|
||||
0x6d, 0x5f, 0x98, 0xd5, 0x8c, 0x77, 0xca, 0x79, 0xe8, 0x41, 0xfa, 0x62, 0x58, 0xb0, 0xa5, 0xf7, 0x67, 0x40, 0xfb,
|
||||
0xdf, 0x38, 0x06, 0x2f, 0xbc, 0x29, 0xbe, 0x44, 0xba, 0x31, 0x10, 0xe1, 0x60, 0x8a, 0x26, 0x57, 0x43, 0x3c, 0xf4,
|
||||
0x90, 0xaa, 0x9a, 0xc6, 0x68, 0x82, 0xa7, 0x40, 0x30, 0xc6, 0xc1, 0x04, 0x26, 0x90, 0xef, 0xe1, 0xd1, 0x6b, 0x3f,
|
||||
0xc0, 0xe3, 0x11, 0x50, 0xf9, 0x2e, 0x0e, 0x7c, 0x64, 0x68, 0xc7, 0xd8, 0x07, 0x71, 0x8a, 0x24, 0x28, 0x01, 0xe8,
|
||||
0x24, 0xc0, 0xee, 0x04, 0xc4, 0x8d, 0xb1, 0x7b, 0x89, 0xa7, 0x63, 0x34, 0xc5, 0x13, 0x80, 0x0e, 0x0f, 0x47, 0x85,
|
||||
0x33, 0xc2, 0x1e, 0x4c, 0x07, 0x63, 0x32, 0xc5, 0xc3, 0x00, 0xe9, 0xc6, 0xc0, 0x31, 0x01, 0x11, 0x0e, 0x76, 0xbd,
|
||||
0xd7, 0x01, 0xf6, 0x27, 0xa0, 0x77, 0x38, 0x7c, 0x01, 0x62, 0x2f, 0x87, 0xc8, 0xb4, 0x06, 0x5e, 0x50, 0x30, 0x7a,
|
||||
0x0c, 0x34, 0xff, 0x9f, 0x0b, 0x1a, 0x40, 0xe2, 0xa1, 0x00, 0x5f, 0x42, 0xec, 0x7a, 0x8a, 0xdf, 0xb4, 0x06, 0x37,
|
||||
0xcf, 0x43, 0xee, 0x1f, 0xc6, 0x2c, 0xf8, 0xe7, 0x62, 0xe6, 0x29, 0x04, 0xa0, 0x0b, 0xba, 0x41, 0x0e, 0xd2, 0x8d,
|
||||
0xd1, 0x0d, 0xcc, 0xd3, 0xab, 0x4b, 0x34, 0x05, 0xae, 0xf1, 0x14, 0x5d, 0xa2, 0x91, 0x42, 0x17, 0xd8, 0x87, 0x86,
|
||||
0xc9, 0x01, 0xa6, 0x2f, 0x84, 0x71, 0xf8, 0x37, 0x86, 0xf1, 0x31, 0x9f, 0xfe, 0xc6, 0x2e, 0xfd, 0x15, 0x57, 0x10,
|
||||
0x94, 0x63, 0xba, 0x0c, 0x8b, 0x06, 0xe6, 0x15, 0xaf, 0xaa, 0x28, 0x78, 0x94, 0x43, 0x35, 0x02, 0xef, 0x7a, 0x0f,
|
||||
0xb1, 0x34, 0xce, 0xbd, 0xf9, 0xbd, 0x2a, 0x1d, 0x28, 0xbd, 0x79, 0xa4, 0xd3, 0xf9, 0xfc, 0x26, 0xa7, 0xe8, 0xd5,
|
||||
0xf5, 0x3b, 0x78, 0x08, 0x16, 0x05, 0xe2, 0xd5, 0x1a, 0xde, 0x9b, 0x5b, 0x24, 0x2b, 0xf5, 0x82, 0xe7, 0x50, 0x2a,
|
||||
0xaa, 0x2e, 0x3c, 0x20, 0x50, 0x57, 0x2c, 0x60, 0x8c, 0xa3, 0x45, 0x33, 0x7f, 0x57, 0x50, 0x22, 0x28, 0x5a, 0xb2,
|
||||
0x15, 0x45, 0x4c, 0x42, 0x1d, 0x50, 0x52, 0x24, 0x99, 0x6a, 0x8e, 0x8c, 0x9a, 0xee, 0x6d, 0x25, 0x69, 0x88, 0xae,
|
||||
0xaa, 0x7a, 0xab, 0x85, 0x24, 0x39, 0xe1, 0x4b, 0x9a, 0x1e, 0x84, 0x29, 0xea, 0x6d, 0xd5, 0x36, 0xe8, 0x97, 0x17,
|
||||
0x6f, 0x5e, 0xab, 0x87, 0x36, 0x45, 0x4e, 0xa7, 0x6c, 0x23, 0xd1, 0x8f, 0x37, 0x2f, 0x50, 0x5b, 0xc3, 0xa6, 0x53,
|
||||
0x63, 0x5b, 0xb5, 0xa2, 0xcd, 0x1a, 0x2a, 0x4b, 0xaa, 0x48, 0x40, 0xb9, 0xa0, 0x52, 0x42, 0xa1, 0x21, 0x30, 0x94,
|
||||
0xce, 0xda, 0x13, 0x53, 0x75, 0x83, 0xbb, 0x20, 0x7e, 0xde, 0x95, 0xd7, 0x51, 0x1e, 0x18, 0xd7, 0xaf, 0x3b, 0x6a,
|
||||
0x70, 0x3d, 0x98, 0x47, 0xea, 0x39, 0x8d, 0x88, 0x7e, 0x84, 0xc4, 0x83, 0x35, 0xcb, 0x98, 0x7a, 0xb8, 0xcd, 0x23,
|
||||
0x5d, 0x8f, 0x2a, 0x09, 0xaa, 0x24, 0x32, 0x5f, 0x34, 0x74, 0xaf, 0xa0, 0x7c, 0x09, 0xaf, 0x64, 0xd8, 0x70, 0xa8,
|
||||
0x50, 0x12, 0x9a, 0x57, 0x05, 0x54, 0x40, 0xf1, 0xf5, 0xf5, 0x0f, 0xff, 0x52, 0x9f, 0x3f, 0xc0, 0xcf, 0x13, 0x27,
|
||||
0x3c, 0x29, 0x0c, 0xa3, 0xea, 0x74, 0x7c, 0xe3, 0xa1, 0xf9, 0x90, 0x51, 0xc3, 0x7b, 0x00, 0xfc, 0x4e, 0xef, 0x49,
|
||||
0x79, 0x77, 0x98, 0xec, 0x24, 0xe9, 0x5f, 0x5d, 0xd9, 0x1a, 0x26, 0xd1, 0x2e, 0x4a, 0x26, 0xe7, 0xd7, 0x60, 0x60,
|
||||
0x34, 0x30, 0x0b, 0xe0, 0x9c, 0x72, 0xc0, 0xd0, 0xe6, 0x1d, 0x0f, 0xec, 0xa8, 0x42, 0xec, 0x27, 0x8d, 0x98, 0xd9,
|
||||
0x60, 0xed, 0x65, 0x49, 0x65, 0x5e, 0xa5, 0xf1, 0xbb, 0x1f, 0xaf, 0x6f, 0x8e, 0x1e, 0x77, 0xb0, 0x52, 0x9e, 0x98,
|
||||
0x0f, 0x2c, 0x6d, 0x21, 0x59, 0x4d, 0x1a, 0xa9, 0xc5, 0x3a, 0x2a, 0xce, 0x0e, 0x1e, 0xe9, 0x75, 0xbd, 0x33, 0xda,
|
||||
0xa9, 0x8e, 0x71, 0x30, 0x47, 0x0f, 0xd9, 0x78, 0xd0, 0xfd, 0x99, 0x95, 0x03, 0x73, 0x14, 0x07, 0xe6, 0x5c, 0x0e,
|
||||
0xf4, 0xe7, 0xa7, 0xdf, 0x01, 0xf1, 0x69, 0xfc, 0xac, 0x8e, 0x12, 0x00, 0x00};
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x5b, 0x8f, 0xdb, 0x36, 0x16, 0x7e, 0xef,
|
||||
0xaf, 0xe0, 0x2a, 0x49, 0x2d, 0x37, 0x23, 0xea, 0x66, 0xf9, 0x2a, 0xa9, 0x48, 0xb2, 0x29, 0x5a, 0x20, 0x69, 0x03,
|
||||
0xcc, 0xb4, 0xfb, 0x10, 0x04, 0x18, 0x5a, 0xa2, 0x2c, 0x66, 0x24, 0x4a, 0x15, 0xe9, 0x5b, 0x0c, 0xef, 0x6f, 0xdf,
|
||||
0x43, 0x52, 0xf6, 0x38, 0xb3, 0x99, 0x05, 0x52, 0xec, 0x62, 0xd1, 0x4e, 0x26, 0x1c, 0x92, 0x3a, 0xd7, 0x4f, 0x3c,
|
||||
0x17, 0x2a, 0xfe, 0x5b, 0xde, 0x64, 0x72, 0xdf, 0x52, 0x54, 0xca, 0xba, 0x4a, 0x63, 0x35, 0xa2, 0x8a, 0xf0, 0x55,
|
||||
0x42, 0x39, 0xac, 0x28, 0xc9, 0xd3, 0xb8, 0xa6, 0x92, 0xa0, 0xac, 0x24, 0x9d, 0xa0, 0x32, 0xf9, 0xf5, 0xe6, 0x07,
|
||||
0x67, 0x8a, 0xdc, 0x34, 0xae, 0x18, 0xbf, 0x43, 0x1d, 0xad, 0x12, 0x96, 0x35, 0x1c, 0x95, 0x1d, 0x2d, 0x92, 0x9c,
|
||||
0x48, 0x32, 0x67, 0x35, 0x59, 0x51, 0x45, 0xa0, 0xd9, 0x38, 0xa9, 0x69, 0xb2, 0x61, 0x74, 0xdb, 0x36, 0x9d, 0x44,
|
||||
0x40, 0x29, 0x29, 0x97, 0x89, 0xb5, 0x65, 0xb9, 0x2c, 0x93, 0x9c, 0x6e, 0x58, 0x46, 0x1d, 0xbd, 0xb8, 0x62, 0x9c,
|
||||
0x49, 0x46, 0x2a, 0x47, 0x64, 0xa4, 0xa2, 0x89, 0x7f, 0xb5, 0x16, 0xb4, 0xd3, 0x0b, 0xb2, 0x84, 0x35, 0x6f, 0x2c,
|
||||
0x10, 0x29, 0xb2, 0x8e, 0xb5, 0x12, 0x29, 0x7b, 0x93, 0xba, 0xc9, 0xd7, 0x15, 0x4d, 0x5d, 0x97, 0x08, 0xb0, 0x4b,
|
||||
0xb8, 0x8c, 0xe7, 0x74, 0x87, 0xa7, 0xb3, 0x68, 0x32, 0x9e, 0xe6, 0x13, 0xfc, 0x51, 0x7c, 0x03, 0x9e, 0xad, 0x6b,
|
||||
0x50, 0x87, 0xab, 0x26, 0x23, 0x92, 0x35, 0x1c, 0x0b, 0x4a, 0xba, 0xac, 0x4c, 0x92, 0xc4, 0xfa, 0x5e, 0x90, 0x0d,
|
||||
0xb5, 0xbe, 0xfd, 0xd6, 0x3e, 0x13, 0xad, 0xa8, 0x7c, 0x5d, 0x51, 0x35, 0x15, 0x2f, 0xf7, 0x37, 0x64, 0xf5, 0x33,
|
||||
0x58, 0x6e, 0x5b, 0x44, 0xb0, 0x9c, 0x5a, 0xc3, 0xf7, 0xde, 0x07, 0x2c, 0xe4, 0xbe, 0xa2, 0x38, 0x67, 0xa2, 0xad,
|
||||
0xc8, 0x3e, 0xb1, 0x96, 0x20, 0xf5, 0xce, 0x1a, 0x2e, 0x8a, 0x35, 0xcf, 0x94, 0x70, 0x24, 0x6c, 0x3a, 0x3c, 0x54,
|
||||
0x14, 0xcc, 0x4b, 0xde, 0x12, 0x59, 0xe2, 0x9a, 0xec, 0x6c, 0x33, 0x61, 0xdc, 0x0e, 0xbe, 0xb3, 0xe9, 0x73, 0xdf,
|
||||
0xf3, 0x86, 0x57, 0x7a, 0xf0, 0x86, 0x2e, 0xfc, 0x5d, 0x74, 0x54, 0xae, 0x3b, 0x8e, 0x88, 0x7d, 0x1b, 0xb7, 0x40,
|
||||
0x89, 0xf2, 0xc4, 0xaa, 0xfd, 0x00, 0x7b, 0xde, 0x14, 0xf9, 0x33, 0x1c, 0x44, 0x8e, 0xef, 0xe3, 0xd0, 0xf1, 0xa3,
|
||||
0x6c, 0xe2, 0x44, 0xc8, 0x1f, 0xc1, 0x10, 0x04, 0x38, 0x42, 0xde, 0x27, 0x0b, 0x15, 0xac, 0xaa, 0x12, 0x8b, 0x37,
|
||||
0x9c, 0x5a, 0x48, 0xc8, 0xae, 0xb9, 0xa3, 0x89, 0x95, 0xad, 0xbb, 0x0e, 0xec, 0x7f, 0xd5, 0x54, 0x4d, 0x07, 0x70,
|
||||
0x7d, 0x83, 0x3e, 0xfb, 0xf9, 0x6a, 0x15, 0xb2, 0x23, 0x5c, 0x14, 0x4d, 0x57, 0x27, 0x96, 0x7e, 0x29, 0xf6, 0xd3,
|
||||
0x83, 0x3c, 0x22, 0x35, 0x0c, 0x2f, 0x1e, 0x3a, 0x4d, 0xc7, 0x56, 0x8c, 0x27, 0x96, 0x1f, 0x20, 0x7f, 0x0a, 0x6a,
|
||||
0x6f, 0x87, 0xc7, 0x33, 0x26, 0x44, 0x61, 0xd2, 0x7b, 0xd9, 0xd8, 0xef, 0x6f, 0x63, 0xb1, 0x59, 0xa1, 0x5d, 0x5d,
|
||||
0x71, 0x91, 0x58, 0xa5, 0x94, 0xed, 0xdc, 0x75, 0xb7, 0xdb, 0x2d, 0xde, 0x86, 0xb8, 0xe9, 0x56, 0x6e, 0xe0, 0x79,
|
||||
0x9e, 0x0b, 0x14, 0x16, 0x32, 0xe7, 0xc3, 0x0a, 0x46, 0x16, 0x2a, 0x29, 0x5b, 0x95, 0x52, 0xcf, 0xd3, 0xa7, 0x07,
|
||||
0x7a, 0x8c, 0x15, 0x45, 0x7a, 0xfb, 0xe1, 0x42, 0x4b, 0x77, 0xa1, 0x85, 0x7e, 0x7f, 0x81, 0xe6, 0xe0, 0xad, 0x32,
|
||||
0x6a, 0x42, 0x02, 0x14, 0x20, 0x4f, 0xff, 0x0b, 0x1c, 0x35, 0xef, 0x57, 0xce, 0x83, 0x15, 0xba, 0x58, 0xc1, 0x5f,
|
||||
0xc0, 0x2f, 0xa8, 0xc7, 0xce, 0xec, 0xcc, 0xee, 0xab, 0xc7, 0x1b, 0xdf, 0xbb, 0xdf, 0x50, 0x3c, 0x3f, 0x8e, 0x2f,
|
||||
0xd7, 0x4e, 0xf0, 0x9b, 0x22, 0x50, 0xd8, 0x9f, 0x99, 0x9c, 0xa0, 0xf4, 0x7f, 0x1b, 0x93, 0x08, 0x45, 0xfd, 0x4e,
|
||||
0xe4, 0xa8, 0xf9, 0x79, 0xa5, 0x34, 0xa1, 0x68, 0x03, 0x54, 0xb5, 0x33, 0x76, 0x22, 0x12, 0xa2, 0xb0, 0x37, 0x09,
|
||||
0x66, 0xb0, 0x3d, 0x06, 0xe6, 0x8b, 0x3d, 0x27, 0xfc, 0x34, 0x50, 0x30, 0xcf, 0x2d, 0xeb, 0x1e, 0x83, 0xe6, 0x12,
|
||||
0x03, 0xfc, 0xb1, 0x81, 0x33, 0x67, 0x59, 0x80, 0x11, 0x95, 0x59, 0x69, 0x5b, 0x2e, 0x44, 0x5e, 0xc1, 0x56, 0x10,
|
||||
0x15, 0x0d, 0xb7, 0x86, 0x58, 0x96, 0x94, 0xdb, 0x27, 0x56, 0xc5, 0x48, 0xf5, 0x13, 0xfb, 0xe1, 0x13, 0x39, 0x3c,
|
||||
0x9c, 0xe3, 0x43, 0x32, 0x09, 0x71, 0x28, 0xb1, 0x8a, 0xe8, 0xab, 0xf3, 0xee, 0xb2, 0xc9, 0xf7, 0x8f, 0x84, 0x4e,
|
||||
0xe9, 0x9b, 0xb8, 0x61, 0x9c, 0xd3, 0xee, 0x86, 0xee, 0xe0, 0x1d, 0xfe, 0x83, 0xfd, 0xc0, 0xd0, 0xcf, 0x54, 0x6e,
|
||||
0x9b, 0xee, 0x4e, 0xcc, 0x91, 0xf5, 0xdc, 0x88, 0x5b, 0xa8, 0xa8, 0x61, 0x20, 0x9b, 0xb4, 0x02, 0x8b, 0x0a, 0x72,
|
||||
0x82, 0xed, 0x0f, 0x21, 0x7e, 0xda, 0x7b, 0x4b, 0xf8, 0xc9, 0xb9, 0xdb, 0x38, 0x67, 0x1b, 0x94, 0x55, 0x10, 0xf5,
|
||||
0x70, 0xfc, 0x8d, 0x28, 0x0b, 0xf5, 0x47, 0xbd, 0xe1, 0x19, 0x70, 0xdf, 0x25, 0xd6, 0x17, 0xa2, 0xfa, 0xe5, 0xfe,
|
||||
0xa7, 0xdc, 0x1e, 0x08, 0x88, 0xe7, 0xc1, 0x10, 0x6f, 0x48, 0xb5, 0xa6, 0x28, 0x41, 0xb2, 0x64, 0xe2, 0xde, 0xc0,
|
||||
0xc5, 0xa3, 0x6c, 0xad, 0xb8, 0x03, 0xae, 0x02, 0x1e, 0x0b, 0x7b, 0x68, 0x9d, 0x22, 0x2b, 0x26, 0x26, 0xef, 0x59,
|
||||
0x4f, 0xac, 0x07, 0x16, 0x39, 0x15, 0x2d, 0xa4, 0x75, 0x1f, 0x81, 0x4f, 0x0f, 0xc2, 0xe6, 0xb8, 0x03, 0xed, 0xc3,
|
||||
0xe3, 0x79, 0x33, 0x16, 0x2d, 0xe1, 0x0f, 0x19, 0x95, 0x81, 0xea, 0xa0, 0x43, 0xb2, 0x82, 0x99, 0x3a, 0xed, 0x40,
|
||||
0x74, 0x56, 0xe8, 0x92, 0xd3, 0xf4, 0xe9, 0xa1, 0x03, 0x89, 0x2a, 0x07, 0x9d, 0x25, 0xc6, 0x2e, 0x40, 0x93, 0xde,
|
||||
0x1e, 0x87, 0xf7, 0x7e, 0xfc, 0xbe, 0xa6, 0xdd, 0xfe, 0x9a, 0x56, 0x34, 0x93, 0x4d, 0x67, 0x5b, 0x4f, 0x40, 0x0b,
|
||||
0xbc, 0x7e, 0xed, 0xf0, 0x8f, 0x37, 0x6f, 0xdf, 0x24, 0x8d, 0xcd, 0x86, 0x57, 0x8f, 0x51, 0xab, 0x0c, 0xff, 0x1e,
|
||||
0x32, 0xfc, 0x3f, 0x93, 0x81, 0xca, 0xf1, 0x83, 0x0f, 0xc0, 0xaa, 0xfd, 0xbd, 0xbd, 0x4f, 0xf4, 0x2a, 0x18, 0x9f,
|
||||
0x43, 0x40, 0x5f, 0x29, 0x0f, 0x9d, 0x71, 0x34, 0x3c, 0x82, 0x7e, 0xb0, 0x00, 0xec, 0xd6, 0xb9, 0x1a, 0x72, 0xb6,
|
||||
0x4a, 0x9b, 0xe9, 0x77, 0x87, 0x65, 0xb3, 0x73, 0x04, 0xfb, 0xc4, 0xf8, 0x6a, 0xce, 0x78, 0x49, 0x3b, 0x26, 0x8f,
|
||||
0x60, 0x2e, 0xa4, 0xfd, 0x76, 0x2d, 0x0f, 0x2d, 0xc9, 0x73, 0xf5, 0x24, 0x6a, 0x77, 0x8b, 0x02, 0x8a, 0x84, 0xa2,
|
||||
0xa4, 0x73, 0x9f, 0xd6, 0x47, 0xf3, 0x5c, 0xe7, 0x83, 0xf9, 0x2c, 0x7a, 0x76, 0x54, 0x07, 0xee, 0x20, 0xe1, 0x65,
|
||||
0x39, 0xa4, 0x62, 0x2b, 0x3e, 0xcf, 0xc0, 0x70, 0xda, 0x19, 0xa6, 0x82, 0xd4, 0xac, 0xda, 0xcf, 0x05, 0x64, 0x26,
|
||||
0x07, 0xaa, 0x07, 0x2b, 0x8e, 0xcb, 0xb5, 0x94, 0x0d, 0x07, 0xdd, 0x5d, 0x4e, 0xbb, 0xb9, 0xb7, 0x30, 0x13, 0xa7,
|
||||
0x23, 0x39, 0x5b, 0x8b, 0x39, 0x0e, 0x3b, 0x5a, 0x2f, 0x96, 0x24, 0xbb, 0x5b, 0x75, 0xcd, 0x9a, 0xe7, 0x4e, 0xa6,
|
||||
0x32, 0xe7, 0xfc, 0x89, 0x5f, 0x90, 0x90, 0x66, 0x8b, 0x7e, 0x55, 0x14, 0xc5, 0x02, 0xa0, 0xa0, 0x8e, 0xc9, 0x44,
|
||||
0xf3, 0x00, 0x8f, 0x14, 0xdb, 0x85, 0x99, 0x38, 0x50, 0x1b, 0xc6, 0x46, 0x48, 0xeb, 0xcf, 0x16, 0x27, 0x77, 0xbc,
|
||||
0x05, 0xa4, 0x64, 0x01, 0x42, 0x5a, 0x88, 0x47, 0x30, 0xf3, 0x58, 0x13, 0xc6, 0x2f, 0xad, 0x57, 0xc7, 0x64, 0xd1,
|
||||
0x97, 0x14, 0x80, 0x45, 0xab, 0xd1, 0x85, 0x65, 0x01, 0x45, 0xc3, 0x14, 0xc6, 0x79, 0x30, 0xf6, 0xda, 0xdd, 0x11,
|
||||
0xf7, 0x07, 0xe4, 0x70, 0xa2, 0x2e, 0x2a, 0xba, 0x5b, 0x7c, 0x5c, 0x0b, 0xc9, 0x8a, 0xbd, 0xd3, 0x17, 0xd6, 0x39,
|
||||
0x1c, 0x16, 0x28, 0xa8, 0x4b, 0x20, 0xa5, 0x94, 0x2f, 0xb4, 0x0e, 0x87, 0x49, 0x5a, 0x8b, 0x1e, 0xa7, 0xb3, 0x18,
|
||||
0x7d, 0x40, 0x3f, 0x97, 0xf5, 0x9f, 0xa8, 0xd5, 0x59, 0x3c, 0xd4, 0xa4, 0x83, 0x44, 0xef, 0x2c, 0x1b, 0xc0, 0xb4,
|
||||
0x9e, 0x3b, 0x13, 0x78, 0x57, 0xfd, 0x96, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xba, 0x5e, 0x9e, 0xf0, 0xf6, 0xdb, 0x1d,
|
||||
0x12, 0x4d, 0xc5, 0xf2, 0x9e, 0x4e, 0x93, 0x20, 0xef, 0x0c, 0x8f, 0x0f, 0xaf, 0x1b, 0xa9, 0xbd, 0x13, 0xd4, 0xa3,
|
||||
0x62, 0x4a, 0x7c, 0xef, 0x0b, 0x6f, 0x24, 0x2f, 0x8a, 0x60, 0x59, 0x9c, 0x91, 0x52, 0x65, 0xef, 0xc8, 0xfa, 0x53,
|
||||
0x11, 0x8c, 0x40, 0xc0, 0xe9, 0xdd, 0xc0, 0xfc, 0xc8, 0x74, 0x58, 0x1c, 0x2e, 0xa4, 0xe8, 0xa3, 0x3a, 0x5f, 0x77,
|
||||
0x95, 0x6d, 0x7d, 0xe1, 0xe8, 0x3e, 0x0b, 0x5f, 0xdd, 0x97, 0xa5, 0xc1, 0xe3, 0x65, 0x69, 0x80, 0x54, 0x23, 0xf3,
|
||||
0xb2, 0xd9, 0x25, 0x03, 0x5d, 0x20, 0x46, 0xf0, 0x3b, 0x78, 0x16, 0xbe, 0x06, 0xfe, 0xff, 0x4a, 0xbd, 0xf9, 0xc3,
|
||||
0xc5, 0xe6, 0x2b, 0x2a, 0xcd, 0x57, 0x56, 0x19, 0xe3, 0x9d, 0x72, 0x1e, 0x66, 0x50, 0x4e, 0x18, 0x16, 0x6c, 0xe5,
|
||||
0xff, 0x2f, 0xa0, 0xfd, 0x77, 0x1c, 0xc3, 0x17, 0xfe, 0x14, 0xcf, 0x90, 0x1e, 0x0c, 0x44, 0x38, 0x9c, 0xa2, 0xc9,
|
||||
0xab, 0x11, 0x1e, 0xf9, 0x48, 0xb5, 0x30, 0x63, 0x34, 0x81, 0x7e, 0x0f, 0xf9, 0x63, 0x1c, 0x4e, 0x60, 0x03, 0x05,
|
||||
0x3e, 0x8e, 0xde, 0x04, 0x21, 0x1e, 0x47, 0x40, 0x15, 0x78, 0x38, 0x0c, 0x90, 0xa1, 0x1d, 0xe3, 0x00, 0xc4, 0x29,
|
||||
0x92, 0xb0, 0x06, 0xa0, 0xb3, 0x10, 0x7b, 0x13, 0x10, 0x37, 0xc6, 0xde, 0x0c, 0x4f, 0xc7, 0x68, 0x8a, 0x27, 0x00,
|
||||
0x1d, 0x1e, 0x45, 0x95, 0x13, 0x61, 0x1f, 0xb6, 0xc3, 0x31, 0x99, 0xe2, 0x51, 0x88, 0xf4, 0x60, 0xe0, 0x98, 0x80,
|
||||
0x08, 0x07, 0x7b, 0xfe, 0x9b, 0x10, 0x07, 0x13, 0xd0, 0x3b, 0x1a, 0xbd, 0x00, 0xb1, 0xb3, 0x11, 0x32, 0xa3, 0x81,
|
||||
0x17, 0x14, 0x44, 0x8f, 0x81, 0x16, 0xfc, 0x75, 0x41, 0x03, 0x48, 0x7c, 0x14, 0xe2, 0x19, 0xc4, 0xae, 0xaf, 0xf8,
|
||||
0xcd, 0x68, 0x70, 0xf3, 0x7d, 0xe4, 0xfd, 0x61, 0xcc, 0xc2, 0xbf, 0x2e, 0x66, 0xbe, 0x42, 0x00, 0xa6, 0xa0, 0x1b,
|
||||
0xe4, 0x20, 0x3d, 0x18, 0xdd, 0xc0, 0x3c, 0x7d, 0x35, 0x43, 0x53, 0xe0, 0x1a, 0x4f, 0xd1, 0x0c, 0x45, 0x0a, 0x5d,
|
||||
0x60, 0x1f, 0x19, 0x26, 0x07, 0x98, 0xbe, 0x12, 0xc6, 0xd1, 0x9f, 0x18, 0xc6, 0xc7, 0x7c, 0xfa, 0x13, 0xbb, 0xf4,
|
||||
0xff, 0x48, 0x41, 0xd0, 0x8e, 0xe9, 0x36, 0x2c, 0x76, 0xcd, 0x95, 0x5e, 0x75, 0x51, 0x70, 0x43, 0x87, 0x6e, 0x04,
|
||||
0x2e, 0xf9, 0x3e, 0x62, 0x79, 0x52, 0xfa, 0xe9, 0x67, 0xdd, 0x39, 0x50, 0xfa, 0x69, 0xac, 0xcb, 0x79, 0x7a, 0x53,
|
||||
0x52, 0xf4, 0xfa, 0xfa, 0x1d, 0xdc, 0xca, 0xaa, 0x0a, 0xf1, 0x66, 0x0b, 0x97, 0xbf, 0x3d, 0x92, 0x8d, 0xba, 0xce,
|
||||
0x73, 0xe8, 0x15, 0xd5, 0x14, 0xee, 0x0d, 0xa8, 0x6f, 0x16, 0x30, 0xc6, 0xf1, 0xb2, 0x4b, 0xdf, 0x55, 0x94, 0x08,
|
||||
0x8a, 0x56, 0x6c, 0x43, 0x11, 0x93, 0xd0, 0x07, 0xd4, 0x14, 0x49, 0xa6, 0x86, 0x33, 0xa3, 0xa6, 0x83, 0x9e, 0x56,
|
||||
0x2b, 0x31, 0xdd, 0x30, 0x58, 0x02, 0x62, 0xd2, 0xbe, 0xed, 0x8d, 0xcb, 0xd0, 0x58, 0x75, 0x4d, 0xa5, 0x84, 0x8e,
|
||||
0x41, 0x59, 0x15, 0xa6, 0xb1, 0xba, 0x76, 0x22, 0xa2, 0x2f, 0x06, 0x89, 0xbb, 0x65, 0x05, 0x53, 0x97, 0xf9, 0x34,
|
||||
0xd6, 0xad, 0xa2, 0x92, 0xa0, 0xba, 0x15, 0xf3, 0xe5, 0x41, 0xcf, 0x2a, 0xca, 0x57, 0x70, 0x9b, 0x84, 0x77, 0x01,
|
||||
0xcd, 0x43, 0x46, 0xcb, 0xa6, 0x82, 0xe6, 0x24, 0xb9, 0xbe, 0xfe, 0xe9, 0xef, 0xea, 0x33, 0x85, 0x32, 0xe1, 0xcc,
|
||||
0x09, 0x7d, 0xbe, 0x61, 0x54, 0x93, 0x9e, 0x6f, 0x3c, 0x32, 0x1f, 0x1c, 0x5a, 0xe8, 0xd3, 0xc1, 0xbf, 0xfc, 0x33,
|
||||
0x29, 0xef, 0x4e, 0x9b, 0xbd, 0x24, 0xfd, 0x5f, 0x37, 0x9d, 0x86, 0x49, 0xac, 0x97, 0x35, 0x93, 0xe9, 0x35, 0x18,
|
||||
0x18, 0xbb, 0xe6, 0x01, 0x38, 0xa7, 0x1c, 0x30, 0xb4, 0x65, 0xcf, 0x03, 0x60, 0xff, 0x72, 0xf3, 0x02, 0xfd, 0xda,
|
||||
0xc2, 0x09, 0xa6, 0x06, 0x7b, 0xed, 0x65, 0x4d, 0x65, 0xd9, 0xe4, 0xc9, 0xbb, 0x5f, 0xae, 0x6f, 0xce, 0x1e, 0xaf,
|
||||
0x35, 0x11, 0xa2, 0x3c, 0x33, 0x1f, 0x42, 0xd6, 0x95, 0x64, 0x2d, 0xe9, 0xa4, 0x16, 0xeb, 0xa8, 0x10, 0x38, 0x79,
|
||||
0xa4, 0x9f, 0x17, 0xac, 0xa2, 0xc6, 0xa9, 0x9e, 0xd1, 0x4d, 0xd1, 0x97, 0x6c, 0x3c, 0xe9, 0x7e, 0x60, 0xa5, 0x6b,
|
||||
0x4e, 0x89, 0x6b, 0x8e, 0x8c, 0xab, 0x3f, 0x13, 0xfd, 0x0b, 0x65, 0x37, 0xa3, 0x8e, 0x36, 0x12, 0x00, 0x00};
|
||||
|
||||
} // namespace captive_portal
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "captive_portal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
@@ -46,10 +44,12 @@ void CaptivePortal::start() {
|
||||
this->base_->add_ota_handler();
|
||||
}
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
this->dns_server_ = make_unique<DNSServer>();
|
||||
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
this->dns_server_->start(53, "*", (uint32_t) ip);
|
||||
#endif
|
||||
|
||||
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
|
||||
if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) {
|
||||
@@ -67,7 +67,7 @@ void CaptivePortal::start() {
|
||||
|
||||
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
||||
if (req->url() == "/") {
|
||||
AsyncWebServerResponse *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
|
||||
auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
|
||||
response->addHeader("Content-Encoding", "gzip");
|
||||
req->send(response);
|
||||
return;
|
||||
@@ -91,5 +91,3 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo
|
||||
|
||||
} // namespace captive_portal
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include <memory>
|
||||
#ifdef USE_ARDUINO
|
||||
#include <DNSServer.h>
|
||||
#endif
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
@@ -18,18 +18,22 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||
CaptivePortal(web_server_base::WebServerBase *base);
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
#ifdef USE_ARDUINO
|
||||
void loop() override {
|
||||
if (this->dns_server_ != nullptr)
|
||||
this->dns_server_->processNextRequest();
|
||||
}
|
||||
#endif
|
||||
float get_setup_priority() const override;
|
||||
void start();
|
||||
bool is_active() const { return this->active_; }
|
||||
void end() {
|
||||
this->active_ = false;
|
||||
this->base_->deinit();
|
||||
#ifdef USE_ARDUINO
|
||||
this->dns_server_->stop();
|
||||
this->dns_server_ = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool canHandle(AsyncWebServerRequest *request) override {
|
||||
@@ -58,12 +62,12 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||
web_server_base::WebServerBase *base_;
|
||||
bool initialized_{false};
|
||||
bool active_{false};
|
||||
#ifdef USE_ARDUINO
|
||||
std::unique_ptr<DNSServer> dns_server_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace captive_portal
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
|
||||
@@ -6,7 +6,7 @@ from esphome.const import (
|
||||
ICON_RADIATOR,
|
||||
ICON_RESTART,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
@@ -43,7 +43,7 @@ CONFIG_SCHEMA = (
|
||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
|
||||
@@ -38,7 +38,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
).extend(cv.polling_component_schema("60s")),
|
||||
cv.only_on(["esp32", "esp8266"]),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/version.h"
|
||||
#include <cinttypes>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
@@ -13,6 +14,7 @@
|
||||
|
||||
#if ESP_IDF_VERSION_MAJOR >= 4
|
||||
#include <esp32/rom/rtc.h>
|
||||
#include <esp_chip_info.h>
|
||||
#else
|
||||
#include <rom/rtc.h>
|
||||
#endif
|
||||
@@ -20,8 +22,12 @@
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#ifdef USE_RP2040
|
||||
#include <Arduino.h>
|
||||
#else
|
||||
#include <Esp.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace debug {
|
||||
@@ -33,6 +39,8 @@ static uint32_t get_free_heap() {
|
||||
return ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance)
|
||||
#elif defined(USE_ESP32)
|
||||
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||
#elif defined(USE_RP2040)
|
||||
return rp2040.getFreeHeap();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -61,9 +69,9 @@ void DebugComponent::dump_config() {
|
||||
device_info += ESPHOME_VERSION;
|
||||
|
||||
this->free_heap_ = get_free_heap();
|
||||
ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_);
|
||||
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) && !defined(USE_RP2040)
|
||||
const char *flash_mode;
|
||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
||||
case FM_QIO:
|
||||
@@ -272,6 +280,11 @@ void DebugComponent::dump_config() {
|
||||
reset_reason = ESP.getResetReason().c_str();
|
||||
#endif
|
||||
|
||||
#ifdef USE_RP2040
|
||||
ESP_LOGD(TAG, "CPU Frequency: %u", rp2040.f_cpu());
|
||||
device_info += "CPU Frequency: " + to_string(rp2040.f_cpu());
|
||||
#endif // USE_RP2040
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->device_info_ != nullptr) {
|
||||
if (device_info.length() > 255)
|
||||
@@ -289,7 +302,7 @@ void DebugComponent::loop() {
|
||||
uint32_t new_free_heap = get_free_heap();
|
||||
if (new_free_heap < this->free_heap_ / 2) {
|
||||
this->free_heap_ = new_free_heap;
|
||||
ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_);
|
||||
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
|
||||
this->status_momentary_warning("heap", 1000);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,11 @@ from esphome.core import coroutine_with_priority
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
display_ns = cg.esphome_ns.namespace("display")
|
||||
Display = display_ns.class_("Display")
|
||||
DisplayBuffer = display_ns.class_("DisplayBuffer")
|
||||
DisplayPage = display_ns.class_("DisplayPage")
|
||||
DisplayPagePtr = DisplayPage.operator("ptr")
|
||||
DisplayBufferRef = DisplayBuffer.operator("ref")
|
||||
DisplayRef = Display.operator("ref")
|
||||
DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action)
|
||||
DisplayPageShowNextAction = display_ns.class_(
|
||||
"DisplayPageShowNextAction", automation.Action
|
||||
@@ -96,7 +97,7 @@ async def setup_display_core_(var, config):
|
||||
pages = []
|
||||
for conf in config[CONF_PAGES]:
|
||||
lambda_ = await cg.process_lambda(
|
||||
conf[CONF_LAMBDA], [(DisplayBufferRef, "it")], return_type=cg.void
|
||||
conf[CONF_LAMBDA], [(DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
page = cg.new_Pvariable(conf[CONF_ID], lambda_)
|
||||
pages.append(page)
|
||||
|
||||
343
esphome/components/display/display.cpp
Normal file
343
esphome/components/display/display.cpp
Normal file
@@ -0,0 +1,343 @@
|
||||
#include "display.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace display {
|
||||
|
||||
static const char *const TAG = "display";
|
||||
|
||||
const Color COLOR_OFF(0, 0, 0, 0);
|
||||
const Color COLOR_ON(255, 255, 255, 255);
|
||||
|
||||
void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); }
|
||||
void Display::clear() { this->fill(COLOR_OFF); }
|
||||
void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; }
|
||||
void HOT Display::line(int x1, int y1, int x2, int y2, Color color) {
|
||||
const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
|
||||
const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
|
||||
int32_t err = dx + dy;
|
||||
|
||||
while (true) {
|
||||
this->draw_pixel_at(x1, y1, color);
|
||||
if (x1 == x2 && y1 == y2)
|
||||
break;
|
||||
int32_t e2 = 2 * err;
|
||||
if (e2 >= dy) {
|
||||
err += dy;
|
||||
x1 += sx;
|
||||
}
|
||||
if (e2 <= dx) {
|
||||
err += dx;
|
||||
y1 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
this->draw_pixel_at(i, y, color);
|
||||
}
|
||||
void HOT Display::vertical_line(int x, int y, int height, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = y; i < y + height; i++)
|
||||
this->draw_pixel_at(x, i, color);
|
||||
}
|
||||
void Display::rectangle(int x1, int y1, int width, int height, Color color) {
|
||||
this->horizontal_line(x1, y1, width, color);
|
||||
this->horizontal_line(x1, y1 + height - 1, width, color);
|
||||
this->vertical_line(x1, y1, height, color);
|
||||
this->vertical_line(x1 + width - 1, y1, height, color);
|
||||
}
|
||||
void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) {
|
||||
// Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses.
|
||||
for (int i = y1; i < y1 + height; i++) {
|
||||
this->horizontal_line(x1, i, width, color);
|
||||
}
|
||||
}
|
||||
void HOT Display::circle(int center_x, int center_xy, int radius, Color color) {
|
||||
int dx = -radius;
|
||||
int dy = 0;
|
||||
int err = 2 - 2 * radius;
|
||||
int e2;
|
||||
|
||||
do {
|
||||
this->draw_pixel_at(center_x - dx, center_xy + dy, color);
|
||||
this->draw_pixel_at(center_x + dx, center_xy + dy, color);
|
||||
this->draw_pixel_at(center_x + dx, center_xy - dy, color);
|
||||
this->draw_pixel_at(center_x - dx, center_xy - dy, color);
|
||||
e2 = err;
|
||||
if (e2 < dy) {
|
||||
err += ++dy * 2 + 1;
|
||||
if (-dx == dy && e2 <= dx) {
|
||||
e2 = 0;
|
||||
}
|
||||
}
|
||||
if (e2 > dx) {
|
||||
err += ++dx * 2 + 1;
|
||||
}
|
||||
} while (dx <= 0);
|
||||
}
|
||||
void Display::filled_circle(int center_x, int center_y, int radius, Color color) {
|
||||
int dx = -int32_t(radius);
|
||||
int dy = 0;
|
||||
int err = 2 - 2 * radius;
|
||||
int e2;
|
||||
|
||||
do {
|
||||
this->draw_pixel_at(center_x - dx, center_y + dy, color);
|
||||
this->draw_pixel_at(center_x + dx, center_y + dy, color);
|
||||
this->draw_pixel_at(center_x + dx, center_y - dy, color);
|
||||
this->draw_pixel_at(center_x - dx, center_y - dy, color);
|
||||
int hline_width = 2 * (-dx) + 1;
|
||||
this->horizontal_line(center_x + dx, center_y + dy, hline_width, color);
|
||||
this->horizontal_line(center_x + dx, center_y - dy, hline_width, color);
|
||||
e2 = err;
|
||||
if (e2 < dy) {
|
||||
err += ++dy * 2 + 1;
|
||||
if (-dx == dy && e2 <= dx) {
|
||||
e2 = 0;
|
||||
}
|
||||
}
|
||||
if (e2 > dx) {
|
||||
err += ++dx * 2 + 1;
|
||||
}
|
||||
} while (dx <= 0);
|
||||
}
|
||||
|
||||
void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
|
||||
int x_start, y_start;
|
||||
int width, height;
|
||||
this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
|
||||
font->print(x_start, y_start, this, color, text);
|
||||
}
|
||||
void Display::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg) {
|
||||
char buffer[256];
|
||||
int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
|
||||
if (ret > 0)
|
||||
this->print(x, y, font, color, align, buffer);
|
||||
}
|
||||
|
||||
void Display::image(int x, int y, BaseImage *image, Color color_on, Color color_off) {
|
||||
this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off);
|
||||
}
|
||||
|
||||
void Display::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) {
|
||||
auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT)));
|
||||
auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT)));
|
||||
|
||||
switch (x_align) {
|
||||
case ImageAlign::RIGHT:
|
||||
x -= image->get_width();
|
||||
break;
|
||||
case ImageAlign::CENTER_HORIZONTAL:
|
||||
x -= image->get_width() / 2;
|
||||
break;
|
||||
case ImageAlign::LEFT:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (y_align) {
|
||||
case ImageAlign::BOTTOM:
|
||||
y -= image->get_height();
|
||||
break;
|
||||
case ImageAlign::CENTER_VERTICAL:
|
||||
y -= image->get_height() / 2;
|
||||
break;
|
||||
case ImageAlign::TOP:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
image->draw(x, y, this, color_on, color_off);
|
||||
}
|
||||
|
||||
#ifdef USE_GRAPH
|
||||
void Display::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); }
|
||||
void Display::legend(int x, int y, graph::Graph *graph, Color color_on) { graph->draw_legend(this, x, y, color_on); }
|
||||
#endif // USE_GRAPH
|
||||
|
||||
#ifdef USE_QR_CODE
|
||||
void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) {
|
||||
qr_code->draw(this, x, y, color_on, scale);
|
||||
}
|
||||
#endif // USE_QR_CODE
|
||||
|
||||
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
|
||||
int *width, int *height) {
|
||||
int x_offset, baseline;
|
||||
font->measure(text, width, &x_offset, &baseline, height);
|
||||
|
||||
auto x_align = TextAlign(int(align) & 0x18);
|
||||
auto y_align = TextAlign(int(align) & 0x07);
|
||||
|
||||
switch (x_align) {
|
||||
case TextAlign::RIGHT:
|
||||
*x1 = x - *width;
|
||||
break;
|
||||
case TextAlign::CENTER_HORIZONTAL:
|
||||
*x1 = x - (*width) / 2;
|
||||
break;
|
||||
case TextAlign::LEFT:
|
||||
default:
|
||||
// LEFT
|
||||
*x1 = x;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (y_align) {
|
||||
case TextAlign::BOTTOM:
|
||||
*y1 = y - *height;
|
||||
break;
|
||||
case TextAlign::BASELINE:
|
||||
*y1 = y - baseline;
|
||||
break;
|
||||
case TextAlign::CENTER_VERTICAL:
|
||||
*y1 = y - (*height) / 2;
|
||||
break;
|
||||
case TextAlign::TOP:
|
||||
default:
|
||||
*y1 = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
void Display::print(int x, int y, BaseFont *font, Color color, const char *text) {
|
||||
this->print(x, y, font, color, TextAlign::TOP_LEFT, text);
|
||||
}
|
||||
void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) {
|
||||
this->print(x, y, font, COLOR_ON, align, text);
|
||||
}
|
||||
void Display::print(int x, int y, BaseFont *font, const char *text) {
|
||||
this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
|
||||
}
|
||||
void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_(x, y, font, color, align, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_(x, y, font, COLOR_ON, align, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
void Display::printf(int x, int y, BaseFont *font, const char *format, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; }
|
||||
void Display::set_pages(std::vector<DisplayPage *> pages) {
|
||||
for (auto *page : pages)
|
||||
page->set_parent(this);
|
||||
|
||||
for (uint32_t i = 0; i < pages.size() - 1; i++) {
|
||||
pages[i]->set_next(pages[i + 1]);
|
||||
pages[i + 1]->set_prev(pages[i]);
|
||||
}
|
||||
pages[0]->set_prev(pages[pages.size() - 1]);
|
||||
pages[pages.size() - 1]->set_next(pages[0]);
|
||||
this->show_page(pages[0]);
|
||||
}
|
||||
void Display::show_page(DisplayPage *page) {
|
||||
this->previous_page_ = this->page_;
|
||||
this->page_ = page;
|
||||
if (this->previous_page_ != this->page_) {
|
||||
for (auto *t : on_page_change_triggers_)
|
||||
t->process(this->previous_page_, this->page_);
|
||||
}
|
||||
}
|
||||
void Display::show_next_page() { this->page_->show_next(); }
|
||||
void Display::show_prev_page() { this->page_->show_prev(); }
|
||||
void Display::do_update_() {
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
}
|
||||
if (this->page_ != nullptr) {
|
||||
this->page_->get_writer()(*this);
|
||||
} else if (this->writer_.has_value()) {
|
||||
(*this->writer_)(*this);
|
||||
}
|
||||
// remove all not ended clipping regions
|
||||
while (is_clipping()) {
|
||||
end_clipping();
|
||||
}
|
||||
}
|
||||
void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
|
||||
if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
|
||||
this->trigger(from, to);
|
||||
}
|
||||
void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
|
||||
char buffer[64];
|
||||
size_t ret = time.strftime(buffer, sizeof(buffer), format);
|
||||
if (ret > 0)
|
||||
this->print(x, y, font, color, align, buffer);
|
||||
}
|
||||
void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
|
||||
this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time);
|
||||
}
|
||||
void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) {
|
||||
this->strftime(x, y, font, COLOR_ON, align, format, time);
|
||||
}
|
||||
void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) {
|
||||
this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time);
|
||||
}
|
||||
|
||||
void Display::start_clipping(Rect rect) {
|
||||
if (!this->clipping_rectangle_.empty()) {
|
||||
Rect r = this->clipping_rectangle_.back();
|
||||
rect.shrink(r);
|
||||
}
|
||||
this->clipping_rectangle_.push_back(rect);
|
||||
}
|
||||
void Display::end_clipping() {
|
||||
if (this->clipping_rectangle_.empty()) {
|
||||
ESP_LOGE(TAG, "clear: Clipping is not set.");
|
||||
} else {
|
||||
this->clipping_rectangle_.pop_back();
|
||||
}
|
||||
}
|
||||
void Display::extend_clipping(Rect add_rect) {
|
||||
if (this->clipping_rectangle_.empty()) {
|
||||
ESP_LOGE(TAG, "add: Clipping is not set.");
|
||||
} else {
|
||||
this->clipping_rectangle_.back().extend(add_rect);
|
||||
}
|
||||
}
|
||||
void Display::shrink_clipping(Rect add_rect) {
|
||||
if (this->clipping_rectangle_.empty()) {
|
||||
ESP_LOGE(TAG, "add: Clipping is not set.");
|
||||
} else {
|
||||
this->clipping_rectangle_.back().shrink(add_rect);
|
||||
}
|
||||
}
|
||||
Rect Display::get_clipping() {
|
||||
if (this->clipping_rectangle_.empty()) {
|
||||
return Rect();
|
||||
} else {
|
||||
return this->clipping_rectangle_.back();
|
||||
}
|
||||
}
|
||||
|
||||
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
|
||||
void DisplayPage::show() { this->parent_->show_page(this); }
|
||||
void DisplayPage::show_next() { this->next_->show(); }
|
||||
void DisplayPage::show_prev() { this->prev_->show(); }
|
||||
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
|
||||
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
|
||||
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
|
||||
const display_writer_t &DisplayPage::get_writer() const { return this->writer_; }
|
||||
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
566
esphome/components/display/display.h
Normal file
566
esphome/components/display/display.h
Normal file
@@ -0,0 +1,566 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdarg>
|
||||
#include <vector>
|
||||
|
||||
#include "rect.h"
|
||||
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/time.h"
|
||||
|
||||
#ifdef USE_GRAPH
|
||||
#include "esphome/components/graph/graph.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_QR_CODE
|
||||
#include "esphome/components/qr_code/qr_code.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace display {
|
||||
|
||||
/** TextAlign is used to tell the display class how to position a piece of text. By default
|
||||
* the coordinates you enter for the print*() functions take the upper left corner of the text
|
||||
* as the "anchor" point. You can customize this behavior to, for example, make the coordinates
|
||||
* refer to the *center* of the text.
|
||||
*
|
||||
* All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
|
||||
* these options are allowed:
|
||||
*
|
||||
* - LEFT (x-coordinate of anchor point is on left)
|
||||
* - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text)
|
||||
* - RIGHT (x-coordinate of anchor point is on right)
|
||||
*
|
||||
* For the Y-Axis alignment these options are allowed:
|
||||
*
|
||||
* - TOP (y-coordinate of anchor is on the top of the text)
|
||||
* - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text)
|
||||
* - BASELINE (y-coordinate of anchor is on the baseline of the text)
|
||||
* - BOTTOM (y-coordinate of anchor is on the bottom of the text)
|
||||
*
|
||||
* These options are then combined to create combined TextAlignment options like:
|
||||
* - TOP_LEFT (default)
|
||||
* - CENTER (anchor point is in the middle of the text bounds)
|
||||
* - ...
|
||||
*/
|
||||
enum class TextAlign {
|
||||
TOP = 0x00,
|
||||
CENTER_VERTICAL = 0x01,
|
||||
BASELINE = 0x02,
|
||||
BOTTOM = 0x04,
|
||||
|
||||
LEFT = 0x00,
|
||||
CENTER_HORIZONTAL = 0x08,
|
||||
RIGHT = 0x10,
|
||||
|
||||
TOP_LEFT = TOP | LEFT,
|
||||
TOP_CENTER = TOP | CENTER_HORIZONTAL,
|
||||
TOP_RIGHT = TOP | RIGHT,
|
||||
|
||||
CENTER_LEFT = CENTER_VERTICAL | LEFT,
|
||||
CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
|
||||
CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
|
||||
|
||||
BASELINE_LEFT = BASELINE | LEFT,
|
||||
BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL,
|
||||
BASELINE_RIGHT = BASELINE | RIGHT,
|
||||
|
||||
BOTTOM_LEFT = BOTTOM | LEFT,
|
||||
BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
|
||||
BOTTOM_RIGHT = BOTTOM | RIGHT,
|
||||
};
|
||||
|
||||
/** ImageAlign is used to tell the display class how to position a image. By default
|
||||
* the coordinates you enter for the image() functions take the upper left corner of the image
|
||||
* as the "anchor" point. You can customize this behavior to, for example, make the coordinates
|
||||
* refer to the *center* of the image.
|
||||
*
|
||||
* All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
|
||||
* these options are allowed:
|
||||
*
|
||||
* - LEFT (x-coordinate of anchor point is on left)
|
||||
* - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image)
|
||||
* - RIGHT (x-coordinate of anchor point is on right)
|
||||
*
|
||||
* For the Y-Axis alignment these options are allowed:
|
||||
*
|
||||
* - TOP (y-coordinate of anchor is on the top of the image)
|
||||
* - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image)
|
||||
* - BOTTOM (y-coordinate of anchor is on the bottom of the image)
|
||||
*
|
||||
* These options are then combined to create combined TextAlignment options like:
|
||||
* - TOP_LEFT (default)
|
||||
* - CENTER (anchor point is in the middle of the image bounds)
|
||||
* - ...
|
||||
*/
|
||||
enum class ImageAlign {
|
||||
TOP = 0x00,
|
||||
CENTER_VERTICAL = 0x01,
|
||||
BOTTOM = 0x02,
|
||||
|
||||
LEFT = 0x00,
|
||||
CENTER_HORIZONTAL = 0x04,
|
||||
RIGHT = 0x08,
|
||||
|
||||
TOP_LEFT = TOP | LEFT,
|
||||
TOP_CENTER = TOP | CENTER_HORIZONTAL,
|
||||
TOP_RIGHT = TOP | RIGHT,
|
||||
|
||||
CENTER_LEFT = CENTER_VERTICAL | LEFT,
|
||||
CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
|
||||
CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
|
||||
|
||||
BOTTOM_LEFT = BOTTOM | LEFT,
|
||||
BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
|
||||
BOTTOM_RIGHT = BOTTOM | RIGHT,
|
||||
|
||||
HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT,
|
||||
VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM
|
||||
};
|
||||
|
||||
enum DisplayType {
|
||||
DISPLAY_TYPE_BINARY = 1,
|
||||
DISPLAY_TYPE_GRAYSCALE = 2,
|
||||
DISPLAY_TYPE_COLOR = 3,
|
||||
};
|
||||
|
||||
enum DisplayRotation {
|
||||
DISPLAY_ROTATION_0_DEGREES = 0,
|
||||
DISPLAY_ROTATION_90_DEGREES = 90,
|
||||
DISPLAY_ROTATION_180_DEGREES = 180,
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
class Display;
|
||||
class DisplayPage;
|
||||
class DisplayOnPageChangeTrigger;
|
||||
|
||||
using display_writer_t = std::function<void(Display &)>;
|
||||
|
||||
#define LOG_DISPLAY(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, prefix type); \
|
||||
ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, (obj)->rotation_); \
|
||||
ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \
|
||||
}
|
||||
|
||||
/// Turn the pixel OFF.
|
||||
extern const Color COLOR_OFF;
|
||||
/// Turn the pixel ON.
|
||||
extern const Color COLOR_ON;
|
||||
|
||||
class BaseImage {
|
||||
public:
|
||||
virtual void draw(int x, int y, Display *display, Color color_on, Color color_off) = 0;
|
||||
virtual int get_width() const = 0;
|
||||
virtual int get_height() const = 0;
|
||||
};
|
||||
|
||||
class BaseFont {
|
||||
public:
|
||||
virtual void print(int x, int y, Display *display, Color color, const char *text) = 0;
|
||||
virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
|
||||
};
|
||||
|
||||
class Display {
|
||||
public:
|
||||
/// Fill the entire screen with the given color.
|
||||
virtual void fill(Color color);
|
||||
/// Clear the entire screen by filling it with OFF pixels.
|
||||
void clear();
|
||||
|
||||
/// Get the width of the image in pixels with rotation applied.
|
||||
virtual int get_width() = 0;
|
||||
/// Get the height of the image in pixels with rotation applied.
|
||||
virtual int get_height() = 0;
|
||||
|
||||
/// Set a single pixel at the specified coordinates to default color.
|
||||
inline void draw_pixel_at(int x, int y) { this->draw_pixel_at(x, y, COLOR_ON); }
|
||||
|
||||
/// Set a single pixel at the specified coordinates to the given color.
|
||||
virtual void draw_pixel_at(int x, int y, Color color) = 0;
|
||||
|
||||
/// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color.
|
||||
void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
|
||||
|
||||
/// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color.
|
||||
void horizontal_line(int x, int y, int width, Color color = COLOR_ON);
|
||||
|
||||
/// Draw a vertical line from the point [x,y] to [x,y+width] with the given color.
|
||||
void vertical_line(int x, int y, int height, Color color = COLOR_ON);
|
||||
|
||||
/// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at
|
||||
/// [x1+width,y1+height].
|
||||
void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
|
||||
|
||||
/// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height].
|
||||
void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
|
||||
|
||||
/// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color.
|
||||
void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON);
|
||||
|
||||
/// Fill a circle centered around [center_x,center_y] with the radius radius with the given color.
|
||||
void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON);
|
||||
|
||||
/** Print `text` with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param text The text to draw.
|
||||
*/
|
||||
void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text);
|
||||
|
||||
/** Print `text` with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param text The text to draw.
|
||||
*/
|
||||
void print(int x, int y, BaseFont *font, Color color, const char *text);
|
||||
|
||||
/** Print `text` with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param text The text to draw.
|
||||
*/
|
||||
void print(int x, int y, BaseFont *font, TextAlign align, const char *text);
|
||||
|
||||
/** Print `text` with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param text The text to draw.
|
||||
*/
|
||||
void print(int x, int y, BaseFont *font, const char *text);
|
||||
|
||||
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param format The format to use.
|
||||
* @param ... The arguments to use for the text formatting.
|
||||
*/
|
||||
void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...)
|
||||
__attribute__((format(printf, 7, 8)));
|
||||
|
||||
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param format The format to use.
|
||||
* @param ... The arguments to use for the text formatting.
|
||||
*/
|
||||
void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
|
||||
|
||||
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param format The format to use.
|
||||
* @param ... The arguments to use for the text formatting.
|
||||
*/
|
||||
void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...)
|
||||
__attribute__((format(printf, 6, 7)));
|
||||
|
||||
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param format The format to use.
|
||||
* @param ... The arguments to use for the text formatting.
|
||||
*/
|
||||
void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
|
||||
|
||||
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param format The strftime format to use.
|
||||
* @param time The time to format.
|
||||
*/
|
||||
void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time)
|
||||
__attribute__((format(strftime, 7, 0)));
|
||||
|
||||
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param format The strftime format to use.
|
||||
* @param time The time to format.
|
||||
*/
|
||||
void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time)
|
||||
__attribute__((format(strftime, 6, 0)));
|
||||
|
||||
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param format The strftime format to use.
|
||||
* @param time The time to format.
|
||||
*/
|
||||
void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time)
|
||||
__attribute__((format(strftime, 6, 0)));
|
||||
|
||||
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param format The strftime format to use.
|
||||
* @param time The time to format.
|
||||
*/
|
||||
void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0)));
|
||||
|
||||
/** Draw the `image` with the top-left corner at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param image The image to draw.
|
||||
* @param color_on The color to replace in binary images for the on bits.
|
||||
* @param color_off The color to replace in binary images for the off bits.
|
||||
*/
|
||||
void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
|
||||
|
||||
/** Draw the `image` at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param image The image to draw.
|
||||
* @param align The alignment of the image.
|
||||
* @param color_on The color to replace in binary images for the on bits.
|
||||
* @param color_off The color to replace in binary images for the off bits.
|
||||
*/
|
||||
void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
|
||||
|
||||
#ifdef USE_GRAPH
|
||||
/** Draw the `graph` with the top-left corner at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param graph The graph id to draw
|
||||
* @param color_on The color to replace in binary images for the on bits.
|
||||
*/
|
||||
void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
|
||||
|
||||
/** Draw the `legend` for graph with the top-left corner at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param graph The graph id for which the legend applies to
|
||||
* @param graph The graph id for which the legend applies to
|
||||
* @param graph The graph id for which the legend applies to
|
||||
* @param name_font The font used for the trace name
|
||||
* @param value_font The font used for the trace value and units
|
||||
* @param color_on The color of the border
|
||||
*/
|
||||
void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
|
||||
#endif // USE_GRAPH
|
||||
|
||||
#ifdef USE_QR_CODE
|
||||
/** Draw the `qr_code` with the top-left corner at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param qr_code The qr_code to draw
|
||||
* @param color_on The color to replace in binary images for the on bits.
|
||||
*/
|
||||
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
|
||||
#endif
|
||||
|
||||
/** Get the text bounds of the given string.
|
||||
*
|
||||
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
|
||||
* @param y The y coordinate to place the string at, can be 0 if only interested in dimensions.
|
||||
* @param text The text to measure.
|
||||
* @param font The font to measure the text bounds with.
|
||||
* @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions.
|
||||
* @param x1 A pointer to store the returned x coordinate of the upper left corner in.
|
||||
* @param y1 A pointer to store the returned y coordinate of the upper left corner in.
|
||||
* @param width A pointer to store the returned text width in.
|
||||
* @param height A pointer to store the returned text height in.
|
||||
*/
|
||||
void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width,
|
||||
int *height);
|
||||
|
||||
/// Internal method to set the display writer lambda.
|
||||
void set_writer(display_writer_t &&writer);
|
||||
|
||||
void show_page(DisplayPage *page);
|
||||
void show_next_page();
|
||||
void show_prev_page();
|
||||
|
||||
void set_pages(std::vector<DisplayPage *> pages);
|
||||
|
||||
const DisplayPage *get_active_page() const { return this->page_; }
|
||||
|
||||
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
|
||||
|
||||
/// Internal method to set the display rotation with.
|
||||
void set_rotation(DisplayRotation rotation);
|
||||
|
||||
// Internal method to set display auto clearing.
|
||||
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
|
||||
|
||||
DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
virtual DisplayType get_display_type() = 0;
|
||||
|
||||
/** Set the clipping rectangle for further drawing
|
||||
*
|
||||
* @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen)
|
||||
*
|
||||
* return true if success, false if error
|
||||
*/
|
||||
void start_clipping(Rect rect);
|
||||
void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
|
||||
start_clipping(Rect(left, top, right - left, bottom - top));
|
||||
};
|
||||
|
||||
/** Add a rectangular region to the invalidation region
|
||||
* - This is usually called when an element has been modified
|
||||
*
|
||||
* @param[in] rect: Rectangle to add to the invalidation region
|
||||
*/
|
||||
void extend_clipping(Rect rect);
|
||||
void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
|
||||
this->extend_clipping(Rect(left, top, right - left, bottom - top));
|
||||
};
|
||||
|
||||
/** substract a rectangular region to the invalidation region
|
||||
* - This is usually called when an element has been modified
|
||||
*
|
||||
* @param[in] rect: Rectangle to add to the invalidation region
|
||||
*/
|
||||
void shrink_clipping(Rect rect);
|
||||
void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
|
||||
this->shrink_clipping(Rect(left, top, right - left, bottom - top));
|
||||
};
|
||||
|
||||
/** Reset the invalidation region
|
||||
*/
|
||||
void end_clipping();
|
||||
|
||||
/** Get the current the clipping rectangle
|
||||
*
|
||||
* return rect for active clipping region
|
||||
*/
|
||||
Rect get_clipping();
|
||||
|
||||
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
|
||||
|
||||
protected:
|
||||
void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg);
|
||||
|
||||
void do_update_();
|
||||
|
||||
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
|
||||
optional<display_writer_t> writer_{};
|
||||
DisplayPage *page_{nullptr};
|
||||
DisplayPage *previous_page_{nullptr};
|
||||
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
|
||||
bool auto_clear_enabled_{true};
|
||||
std::vector<Rect> clipping_rectangle_;
|
||||
};
|
||||
|
||||
class DisplayPage {
|
||||
public:
|
||||
DisplayPage(display_writer_t writer);
|
||||
void show();
|
||||
void show_next();
|
||||
void show_prev();
|
||||
void set_parent(Display *parent);
|
||||
void set_prev(DisplayPage *prev);
|
||||
void set_next(DisplayPage *next);
|
||||
const display_writer_t &get_writer() const;
|
||||
|
||||
protected:
|
||||
Display *parent_;
|
||||
display_writer_t writer_;
|
||||
DisplayPage *prev_{nullptr};
|
||||
DisplayPage *next_{nullptr};
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(DisplayPage *, page)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto *page = this->page_.value(x...);
|
||||
if (page != nullptr) {
|
||||
page->show();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayPageShowNextAction : public Action<Ts...> {
|
||||
public:
|
||||
DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {}
|
||||
|
||||
void play(Ts... x) override { this->buffer_->show_next_page(); }
|
||||
|
||||
Display *buffer_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...> {
|
||||
public:
|
||||
DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {}
|
||||
|
||||
void play(Ts... x) override { this->buffer_->show_prev_page(); }
|
||||
|
||||
Display *buffer_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayIsDisplayingPageCondition : public Condition<Ts...> {
|
||||
public:
|
||||
DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {}
|
||||
|
||||
void set_page(DisplayPage *page) { this->page_ = page; }
|
||||
bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; }
|
||||
|
||||
protected:
|
||||
Display *parent_;
|
||||
DisplayPage *page_;
|
||||
};
|
||||
|
||||
class DisplayOnPageChangeTrigger : public Trigger<DisplayPage *, DisplayPage *> {
|
||||
public:
|
||||
explicit DisplayOnPageChangeTrigger(Display *parent) { parent->add_on_page_change_trigger(this); }
|
||||
void process(DisplayPage *from, DisplayPage *to);
|
||||
void set_from(DisplayPage *p) { this->from_ = p; }
|
||||
void set_to(DisplayPage *p) { this->to_ = p; }
|
||||
|
||||
protected:
|
||||
DisplayPage *from_{nullptr};
|
||||
DisplayPage *to_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
@@ -1,10 +1,8 @@
|
||||
#include "display_buffer.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -12,115 +10,6 @@ namespace display {
|
||||
|
||||
static const char *const TAG = "display";
|
||||
|
||||
const Color COLOR_OFF(0, 0, 0, 255);
|
||||
const Color COLOR_ON(255, 255, 255, 255);
|
||||
|
||||
static int image_type_to_bpp(ImageType type) {
|
||||
switch (type) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
return 1;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
return 8;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
return 16;
|
||||
case IMAGE_TYPE_RGB24:
|
||||
return 24;
|
||||
case IMAGE_TYPE_RGBA:
|
||||
return 32;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; }
|
||||
|
||||
void Rect::expand(int16_t horizontal, int16_t vertical) {
|
||||
if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
|
||||
this->x = this->x - horizontal;
|
||||
this->y = this->y - vertical;
|
||||
this->w = this->w + (2 * horizontal);
|
||||
this->h = this->h + (2 * vertical);
|
||||
}
|
||||
}
|
||||
|
||||
void Rect::extend(Rect rect) {
|
||||
if (!this->is_set()) {
|
||||
this->x = rect.x;
|
||||
this->y = rect.y;
|
||||
this->w = rect.w;
|
||||
this->h = rect.h;
|
||||
} else {
|
||||
if (this->x > rect.x) {
|
||||
this->w = this->w + (this->x - rect.x);
|
||||
this->x = rect.x;
|
||||
}
|
||||
if (this->y > rect.y) {
|
||||
this->h = this->h + (this->y - rect.y);
|
||||
this->y = rect.y;
|
||||
}
|
||||
if (this->x2() < rect.x2()) {
|
||||
this->w = rect.x2() - this->x;
|
||||
}
|
||||
if (this->y2() < rect.y2()) {
|
||||
this->h = rect.y2() - this->y;
|
||||
}
|
||||
}
|
||||
}
|
||||
void Rect::shrink(Rect rect) {
|
||||
if (!this->inside(rect)) {
|
||||
(*this) = Rect();
|
||||
} else {
|
||||
if (this->x2() > rect.x2()) {
|
||||
this->w = rect.x2() - this->x;
|
||||
}
|
||||
if (this->x < rect.x) {
|
||||
this->w = this->w + (this->x - rect.x);
|
||||
this->x = rect.x;
|
||||
}
|
||||
if (this->y2() > rect.y2()) {
|
||||
this->h = rect.y2() - this->y;
|
||||
}
|
||||
if (this->y < rect.y) {
|
||||
this->h = this->h + (this->y - rect.y);
|
||||
this->y = rect.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Rect::equal(Rect rect) {
|
||||
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
|
||||
}
|
||||
|
||||
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT
|
||||
if (!this->is_set()) {
|
||||
return true;
|
||||
}
|
||||
if (absolute) {
|
||||
return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
|
||||
} else {
|
||||
return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
|
||||
}
|
||||
}
|
||||
|
||||
bool Rect::inside(Rect rect, bool absolute) {
|
||||
if (!this->is_set() || !rect.is_set()) {
|
||||
return true;
|
||||
}
|
||||
if (absolute) {
|
||||
return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
|
||||
} else {
|
||||
return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
|
||||
}
|
||||
}
|
||||
|
||||
void Rect::info(const std::string &prefix) {
|
||||
if (this->is_set()) {
|
||||
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
|
||||
this->y2());
|
||||
} else
|
||||
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
|
||||
}
|
||||
|
||||
void DisplayBuffer::init_internal_(uint32_t buffer_length) {
|
||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->buffer_ = allocator.allocate(buffer_length);
|
||||
@@ -131,8 +20,6 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); }
|
||||
void DisplayBuffer::clear() { this->fill(COLOR_OFF); }
|
||||
int DisplayBuffer::get_width() {
|
||||
switch (this->rotation_) {
|
||||
case DISPLAY_ROTATION_90_DEGREES:
|
||||
@@ -144,6 +31,7 @@ int DisplayBuffer::get_width() {
|
||||
return this->get_width_internal();
|
||||
}
|
||||
}
|
||||
|
||||
int DisplayBuffer::get_height() {
|
||||
switch (this->rotation_) {
|
||||
case DISPLAY_ROTATION_0_DEGREES:
|
||||
@@ -155,7 +43,7 @@ int DisplayBuffer::get_height() {
|
||||
return this->get_width_internal();
|
||||
}
|
||||
}
|
||||
void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; }
|
||||
|
||||
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return; // NOLINT
|
||||
@@ -179,621 +67,6 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||
this->draw_absolute_pixel_internal(x, y, color);
|
||||
App.feed_wdt();
|
||||
}
|
||||
void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, Color color) {
|
||||
const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
|
||||
const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
|
||||
int32_t err = dx + dy;
|
||||
|
||||
while (true) {
|
||||
this->draw_pixel_at(x1, y1, color);
|
||||
if (x1 == x2 && y1 == y2)
|
||||
break;
|
||||
int32_t e2 = 2 * err;
|
||||
if (e2 >= dy) {
|
||||
err += dy;
|
||||
x1 += sx;
|
||||
}
|
||||
if (e2 <= dx) {
|
||||
err += dx;
|
||||
y1 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
void HOT DisplayBuffer::horizontal_line(int x, int y, int width, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = x; i < x + width; i++)
|
||||
this->draw_pixel_at(i, y, color);
|
||||
}
|
||||
void HOT DisplayBuffer::vertical_line(int x, int y, int height, Color color) {
|
||||
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
|
||||
for (int i = y; i < y + height; i++)
|
||||
this->draw_pixel_at(x, i, color);
|
||||
}
|
||||
void DisplayBuffer::rectangle(int x1, int y1, int width, int height, Color color) {
|
||||
this->horizontal_line(x1, y1, width, color);
|
||||
this->horizontal_line(x1, y1 + height - 1, width, color);
|
||||
this->vertical_line(x1, y1, height, color);
|
||||
this->vertical_line(x1 + width - 1, y1, height, color);
|
||||
}
|
||||
void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, Color color) {
|
||||
// Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses.
|
||||
for (int i = y1; i < y1 + height; i++) {
|
||||
this->horizontal_line(x1, i, width, color);
|
||||
}
|
||||
}
|
||||
void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, Color color) {
|
||||
int dx = -radius;
|
||||
int dy = 0;
|
||||
int err = 2 - 2 * radius;
|
||||
int e2;
|
||||
|
||||
do {
|
||||
this->draw_pixel_at(center_x - dx, center_xy + dy, color);
|
||||
this->draw_pixel_at(center_x + dx, center_xy + dy, color);
|
||||
this->draw_pixel_at(center_x + dx, center_xy - dy, color);
|
||||
this->draw_pixel_at(center_x - dx, center_xy - dy, color);
|
||||
e2 = err;
|
||||
if (e2 < dy) {
|
||||
err += ++dy * 2 + 1;
|
||||
if (-dx == dy && e2 <= dx) {
|
||||
e2 = 0;
|
||||
}
|
||||
}
|
||||
if (e2 > dx) {
|
||||
err += ++dx * 2 + 1;
|
||||
}
|
||||
} while (dx <= 0);
|
||||
}
|
||||
void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color color) {
|
||||
int dx = -int32_t(radius);
|
||||
int dy = 0;
|
||||
int err = 2 - 2 * radius;
|
||||
int e2;
|
||||
|
||||
do {
|
||||
this->draw_pixel_at(center_x - dx, center_y + dy, color);
|
||||
this->draw_pixel_at(center_x + dx, center_y + dy, color);
|
||||
this->draw_pixel_at(center_x + dx, center_y - dy, color);
|
||||
this->draw_pixel_at(center_x - dx, center_y - dy, color);
|
||||
int hline_width = 2 * (-dx) + 1;
|
||||
this->horizontal_line(center_x + dx, center_y + dy, hline_width, color);
|
||||
this->horizontal_line(center_x + dx, center_y - dy, hline_width, color);
|
||||
e2 = err;
|
||||
if (e2 < dy) {
|
||||
err += ++dy * 2 + 1;
|
||||
if (-dx == dy && e2 <= dx) {
|
||||
e2 = 0;
|
||||
}
|
||||
}
|
||||
if (e2 > dx) {
|
||||
err += ++dx * 2 + 1;
|
||||
}
|
||||
} while (dx <= 0);
|
||||
}
|
||||
|
||||
void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) {
|
||||
int x_start, y_start;
|
||||
int width, height;
|
||||
this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
|
||||
|
||||
int i = 0;
|
||||
int x_at = x_start;
|
||||
while (text[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = font->match_next_glyph(text + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
|
||||
if (!font->get_glyphs().empty()) {
|
||||
uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width;
|
||||
for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) {
|
||||
for (int glyph_y = 0; glyph_y < height; glyph_y++)
|
||||
this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
|
||||
}
|
||||
x_at += glyph_width;
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = font->get_glyphs()[glyph_n];
|
||||
int scan_x1, scan_y1, scan_width, scan_height;
|
||||
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
|
||||
|
||||
{
|
||||
const int glyph_x_max = scan_x1 + scan_width;
|
||||
const int glyph_y_max = scan_y1 + scan_height;
|
||||
for (int glyph_x = scan_x1; glyph_x < glyph_x_max; glyph_x++) {
|
||||
for (int glyph_y = scan_y1; glyph_y < glyph_y_max; glyph_y++) {
|
||||
if (glyph.get_pixel(glyph_x, glyph_y)) {
|
||||
this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
|
||||
|
||||
i += match_length;
|
||||
}
|
||||
}
|
||||
void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) {
|
||||
char buffer[256];
|
||||
int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
|
||||
if (ret > 0)
|
||||
this->print(x, y, font, color, align, buffer);
|
||||
}
|
||||
|
||||
void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) {
|
||||
image->draw(x, y, this, color_on, color_off);
|
||||
}
|
||||
|
||||
#ifdef USE_GRAPH
|
||||
void DisplayBuffer::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); }
|
||||
void DisplayBuffer::legend(int x, int y, graph::Graph *graph, Color color_on) {
|
||||
graph->draw_legend(this, x, y, color_on);
|
||||
}
|
||||
#endif // USE_GRAPH
|
||||
|
||||
#ifdef USE_QR_CODE
|
||||
void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) {
|
||||
qr_code->draw(this, x, y, color_on, scale);
|
||||
}
|
||||
#endif // USE_QR_CODE
|
||||
|
||||
void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1,
|
||||
int *width, int *height) {
|
||||
int x_offset, baseline;
|
||||
font->measure(text, width, &x_offset, &baseline, height);
|
||||
|
||||
auto x_align = TextAlign(int(align) & 0x18);
|
||||
auto y_align = TextAlign(int(align) & 0x07);
|
||||
|
||||
switch (x_align) {
|
||||
case TextAlign::RIGHT:
|
||||
*x1 = x - *width;
|
||||
break;
|
||||
case TextAlign::CENTER_HORIZONTAL:
|
||||
*x1 = x - (*width) / 2;
|
||||
break;
|
||||
case TextAlign::LEFT:
|
||||
default:
|
||||
// LEFT
|
||||
*x1 = x;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (y_align) {
|
||||
case TextAlign::BOTTOM:
|
||||
*y1 = y - *height;
|
||||
break;
|
||||
case TextAlign::BASELINE:
|
||||
*y1 = y - baseline;
|
||||
break;
|
||||
case TextAlign::CENTER_VERTICAL:
|
||||
*y1 = y - (*height) / 2;
|
||||
break;
|
||||
case TextAlign::TOP:
|
||||
default:
|
||||
*y1 = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) {
|
||||
this->print(x, y, font, color, TextAlign::TOP_LEFT, text);
|
||||
}
|
||||
void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) {
|
||||
this->print(x, y, font, COLOR_ON, align, text);
|
||||
}
|
||||
void DisplayBuffer::print(int x, int y, Font *font, const char *text) {
|
||||
this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
|
||||
}
|
||||
void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_(x, y, font, color, align, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char *format, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_(x, y, font, COLOR_ON, align, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) {
|
||||
va_list arg;
|
||||
va_start(arg, format);
|
||||
this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg);
|
||||
va_end(arg);
|
||||
}
|
||||
void DisplayBuffer::set_writer(display_writer_t &&writer) { this->writer_ = writer; }
|
||||
void DisplayBuffer::set_pages(std::vector<DisplayPage *> pages) {
|
||||
for (auto *page : pages)
|
||||
page->set_parent(this);
|
||||
|
||||
for (uint32_t i = 0; i < pages.size() - 1; i++) {
|
||||
pages[i]->set_next(pages[i + 1]);
|
||||
pages[i + 1]->set_prev(pages[i]);
|
||||
}
|
||||
pages[0]->set_prev(pages[pages.size() - 1]);
|
||||
pages[pages.size() - 1]->set_next(pages[0]);
|
||||
this->show_page(pages[0]);
|
||||
}
|
||||
void DisplayBuffer::show_page(DisplayPage *page) {
|
||||
this->previous_page_ = this->page_;
|
||||
this->page_ = page;
|
||||
if (this->previous_page_ != this->page_) {
|
||||
for (auto *t : on_page_change_triggers_)
|
||||
t->process(this->previous_page_, this->page_);
|
||||
}
|
||||
}
|
||||
void DisplayBuffer::show_next_page() { this->page_->show_next(); }
|
||||
void DisplayBuffer::show_prev_page() { this->page_->show_prev(); }
|
||||
void DisplayBuffer::do_update_() {
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
}
|
||||
if (this->page_ != nullptr) {
|
||||
this->page_->get_writer()(*this);
|
||||
} else if (this->writer_.has_value()) {
|
||||
(*this->writer_)(*this);
|
||||
}
|
||||
// remove all not ended clipping regions
|
||||
while (is_clipping()) {
|
||||
end_clipping();
|
||||
}
|
||||
}
|
||||
void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
|
||||
if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
|
||||
this->trigger(from, to);
|
||||
}
|
||||
void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) {
|
||||
char buffer[64];
|
||||
size_t ret = time.strftime(buffer, sizeof(buffer), format);
|
||||
if (ret > 0)
|
||||
this->print(x, y, font, color, align, buffer);
|
||||
}
|
||||
void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) {
|
||||
this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time);
|
||||
}
|
||||
void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) {
|
||||
this->strftime(x, y, font, COLOR_ON, align, format, time);
|
||||
}
|
||||
void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, ESPTime time) {
|
||||
this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time);
|
||||
}
|
||||
|
||||
void DisplayBuffer::start_clipping(Rect rect) {
|
||||
if (!this->clipping_rectangle_.empty()) {
|
||||
Rect r = this->clipping_rectangle_.back();
|
||||
rect.shrink(r);
|
||||
}
|
||||
this->clipping_rectangle_.push_back(rect);
|
||||
}
|
||||
void DisplayBuffer::end_clipping() {
|
||||
if (this->clipping_rectangle_.empty()) {
|
||||
ESP_LOGE(TAG, "clear: Clipping is not set.");
|
||||
} else {
|
||||
this->clipping_rectangle_.pop_back();
|
||||
}
|
||||
}
|
||||
void DisplayBuffer::extend_clipping(Rect add_rect) {
|
||||
if (this->clipping_rectangle_.empty()) {
|
||||
ESP_LOGE(TAG, "add: Clipping is not set.");
|
||||
} else {
|
||||
this->clipping_rectangle_.back().extend(add_rect);
|
||||
}
|
||||
}
|
||||
void DisplayBuffer::shrink_clipping(Rect add_rect) {
|
||||
if (this->clipping_rectangle_.empty()) {
|
||||
ESP_LOGE(TAG, "add: Clipping is not set.");
|
||||
} else {
|
||||
this->clipping_rectangle_.back().shrink(add_rect);
|
||||
}
|
||||
}
|
||||
Rect DisplayBuffer::get_clipping() {
|
||||
if (this->clipping_rectangle_.empty()) {
|
||||
return Rect();
|
||||
} else {
|
||||
return this->clipping_rectangle_.back();
|
||||
}
|
||||
}
|
||||
bool Glyph::get_pixel(int x, int y) const {
|
||||
const int x_data = x - this->glyph_data_->offset_x;
|
||||
const int y_data = y - this->glyph_data_->offset_y;
|
||||
if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height)
|
||||
return false;
|
||||
const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u;
|
||||
const uint32_t pos = x_data + y_data * width_8;
|
||||
return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||
}
|
||||
const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
|
||||
bool Glyph::compare_to(const char *str) const {
|
||||
// 1 -> this->char_
|
||||
// 2 -> str
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return true;
|
||||
if (str[i] == '\0')
|
||||
return false;
|
||||
if (this->glyph_data_->a_char[i] > str[i])
|
||||
return false;
|
||||
if (this->glyph_data_->a_char[i] < str[i])
|
||||
return true;
|
||||
}
|
||||
// this should not happen
|
||||
return false;
|
||||
}
|
||||
int Glyph::match_length(const char *str) const {
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return i;
|
||||
if (str[i] != this->glyph_data_->a_char[i])
|
||||
return 0;
|
||||
}
|
||||
// this should not happen
|
||||
return 0;
|
||||
}
|
||||
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
|
||||
*x1 = this->glyph_data_->offset_x;
|
||||
*y1 = this->glyph_data_->offset_y;
|
||||
*width = this->glyph_data_->width;
|
||||
*height = this->glyph_data_->height;
|
||||
}
|
||||
int Font::match_next_glyph(const char *str, int *match_length) {
|
||||
int lo = 0;
|
||||
int hi = this->glyphs_.size() - 1;
|
||||
while (lo != hi) {
|
||||
int mid = (lo + hi + 1) / 2;
|
||||
if (this->glyphs_[mid].compare_to(str)) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
*match_length = this->glyphs_[lo].match_length(str);
|
||||
if (*match_length <= 0)
|
||||
return -1;
|
||||
return lo;
|
||||
}
|
||||
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
|
||||
*baseline = this->baseline_;
|
||||
*height = this->height_;
|
||||
int i = 0;
|
||||
int min_x = 0;
|
||||
bool has_char = false;
|
||||
int x = 0;
|
||||
while (str[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph(str + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
if (!this->get_glyphs().empty())
|
||||
x += this->get_glyphs()[0].glyph_data_->width;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = this->glyphs_[glyph_n];
|
||||
if (!has_char) {
|
||||
min_x = glyph.glyph_data_->offset_x;
|
||||
} else {
|
||||
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
|
||||
}
|
||||
x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
|
||||
|
||||
i += match_length;
|
||||
has_char = true;
|
||||
}
|
||||
*x_offset = min_x;
|
||||
*width = x - min_x;
|
||||
}
|
||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
|
||||
glyphs_.reserve(data_nr);
|
||||
for (int i = 0; i < data_nr; ++i)
|
||||
glyphs_.emplace_back(&data[i]);
|
||||
}
|
||||
|
||||
void Image::draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) {
|
||||
switch (type_) {
|
||||
case IMAGE_TYPE_BINARY: {
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (!this->transparent_) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_grayscale_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgb565_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB24:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgb24_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGBA:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgba_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return color_off;
|
||||
switch (this->type_) {
|
||||
case IMAGE_TYPE_BINARY:
|
||||
return this->get_binary_pixel_(x, y) ? color_on : color_off;
|
||||
case IMAGE_TYPE_GRAYSCALE:
|
||||
return this->get_grayscale_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB565:
|
||||
return this->get_rgb565_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGB24:
|
||||
return this->get_rgb24_pixel_(x, y);
|
||||
case IMAGE_TYPE_RGBA:
|
||||
return this->get_rgba_pixel_(x, y);
|
||||
default:
|
||||
return color_off;
|
||||
}
|
||||
}
|
||||
bool Image::get_binary_pixel_(int x, int y) const {
|
||||
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
|
||||
const uint32_t pos = x + y * width_8;
|
||||
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
|
||||
}
|
||||
Color Image::get_rgba_pixel_(int x, int y) const {
|
||||
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_rgb24_pixel_(int x, int y) const {
|
||||
const uint32_t pos = (x + y * this->width_) * 3;
|
||||
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 + 2));
|
||||
if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
|
||||
// (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 {
|
||||
const uint32_t pos = (x + y * this->width_) * 2;
|
||||
uint16_t rgb565 =
|
||||
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
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 {
|
||||
const uint32_t pos = (x + y * this->width_);
|
||||
const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
|
||||
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_height() const { return this->height_; }
|
||||
ImageType Image::get_type() const { return this->type_; }
|
||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
|
||||
: width_(width), height_(height), type_(type), data_start_(data_start) {}
|
||||
|
||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
|
||||
: Image(data_start, width, height, type),
|
||||
animation_data_start_(data_start),
|
||||
current_frame_(0),
|
||||
animation_frame_count_(animation_frame_count),
|
||||
loop_start_frame_(0),
|
||||
loop_end_frame_(animation_frame_count_),
|
||||
loop_count_(0),
|
||||
loop_current_iteration_(1) {}
|
||||
void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) {
|
||||
loop_start_frame_ = std::min(start_frame, animation_frame_count_);
|
||||
loop_end_frame_ = std::min(end_frame, animation_frame_count_);
|
||||
loop_count_ = count;
|
||||
loop_current_iteration_ = 1;
|
||||
}
|
||||
|
||||
uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; }
|
||||
int Animation::get_current_frame() const { return this->current_frame_; }
|
||||
void Animation::next_frame() {
|
||||
this->current_frame_++;
|
||||
if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
|
||||
(this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
|
||||
this->current_frame_ = loop_start_frame_;
|
||||
this->loop_current_iteration_++;
|
||||
}
|
||||
if (this->current_frame_ >= animation_frame_count_) {
|
||||
this->loop_current_iteration_ = 1;
|
||||
this->current_frame_ = 0;
|
||||
}
|
||||
|
||||
this->update_data_start_();
|
||||
}
|
||||
void Animation::prev_frame() {
|
||||
this->current_frame_--;
|
||||
if (this->current_frame_ < 0) {
|
||||
this->current_frame_ = this->animation_frame_count_ - 1;
|
||||
}
|
||||
|
||||
this->update_data_start_();
|
||||
}
|
||||
|
||||
void Animation::set_frame(int frame) {
|
||||
unsigned abs_frame = abs(frame);
|
||||
|
||||
if (abs_frame < this->animation_frame_count_) {
|
||||
if (frame >= 0) {
|
||||
this->current_frame_ = frame;
|
||||
} else {
|
||||
this->current_frame_ = this->animation_frame_count_ - abs_frame;
|
||||
}
|
||||
}
|
||||
|
||||
this->update_data_start_();
|
||||
}
|
||||
|
||||
void Animation::update_data_start_() {
|
||||
const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_;
|
||||
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
|
||||
}
|
||||
|
||||
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
|
||||
void DisplayPage::show() { this->parent_->show_page(this); }
|
||||
void DisplayPage::show_next() { this->next_->show(); }
|
||||
void DisplayPage::show_prev() { this->prev_->show(); }
|
||||
void DisplayPage::set_parent(DisplayBuffer *parent) { this->parent_ = parent; }
|
||||
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
|
||||
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
|
||||
const display_writer_t &DisplayPage::get_writer() const { return this->writer_; }
|
||||
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
|
||||
@@ -2,648 +2,35 @@
|
||||
|
||||
#include <cstdarg>
|
||||
#include <vector>
|
||||
|
||||
#include "display.h"
|
||||
#include "display_color_utils.h"
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/time.h"
|
||||
|
||||
#ifdef USE_GRAPH
|
||||
#include "esphome/components/graph/graph.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_QR_CODE
|
||||
#include "esphome/components/qr_code/qr_code.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace display {
|
||||
|
||||
/** TextAlign is used to tell the display class how to position a piece of text. By default
|
||||
* the coordinates you enter for the print*() functions take the upper left corner of the text
|
||||
* as the "anchor" point. You can customize this behavior to, for example, make the coordinates
|
||||
* refer to the *center* of the text.
|
||||
*
|
||||
* All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
|
||||
* these options are allowed:
|
||||
*
|
||||
* - LEFT (x-coordinate of anchor point is on left)
|
||||
* - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text)
|
||||
* - RIGHT (x-coordinate of anchor point is on right)
|
||||
*
|
||||
* For the Y-Axis alignment these options are allowed:
|
||||
*
|
||||
* - TOP (y-coordinate of anchor is on the top of the text)
|
||||
* - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text)
|
||||
* - BASELINE (y-coordinate of anchor is on the baseline of the text)
|
||||
* - BOTTOM (y-coordinate of anchor is on the bottom of the text)
|
||||
*
|
||||
* These options are then combined to create combined TextAlignment options like:
|
||||
* - TOP_LEFT (default)
|
||||
* - CENTER (anchor point is in the middle of the text bounds)
|
||||
* - ...
|
||||
*/
|
||||
enum class TextAlign {
|
||||
TOP = 0x00,
|
||||
CENTER_VERTICAL = 0x01,
|
||||
BASELINE = 0x02,
|
||||
BOTTOM = 0x04,
|
||||
|
||||
LEFT = 0x00,
|
||||
CENTER_HORIZONTAL = 0x08,
|
||||
RIGHT = 0x10,
|
||||
|
||||
TOP_LEFT = TOP | LEFT,
|
||||
TOP_CENTER = TOP | CENTER_HORIZONTAL,
|
||||
TOP_RIGHT = TOP | RIGHT,
|
||||
|
||||
CENTER_LEFT = CENTER_VERTICAL | LEFT,
|
||||
CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
|
||||
CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
|
||||
|
||||
BASELINE_LEFT = BASELINE | LEFT,
|
||||
BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL,
|
||||
BASELINE_RIGHT = BASELINE | RIGHT,
|
||||
|
||||
BOTTOM_LEFT = BOTTOM | LEFT,
|
||||
BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
|
||||
BOTTOM_RIGHT = BOTTOM | RIGHT,
|
||||
};
|
||||
|
||||
/// Turn the pixel OFF.
|
||||
extern const Color COLOR_OFF;
|
||||
/// Turn the pixel ON.
|
||||
extern const Color COLOR_ON;
|
||||
|
||||
enum ImageType {
|
||||
IMAGE_TYPE_BINARY = 0,
|
||||
IMAGE_TYPE_GRAYSCALE = 1,
|
||||
IMAGE_TYPE_RGB24 = 2,
|
||||
IMAGE_TYPE_RGB565 = 3,
|
||||
IMAGE_TYPE_RGBA = 4,
|
||||
};
|
||||
|
||||
enum DisplayType {
|
||||
DISPLAY_TYPE_BINARY = 1,
|
||||
DISPLAY_TYPE_GRAYSCALE = 2,
|
||||
DISPLAY_TYPE_COLOR = 3,
|
||||
};
|
||||
|
||||
enum DisplayRotation {
|
||||
DISPLAY_ROTATION_0_DEGREES = 0,
|
||||
DISPLAY_ROTATION_90_DEGREES = 90,
|
||||
DISPLAY_ROTATION_180_DEGREES = 180,
|
||||
DISPLAY_ROTATION_270_DEGREES = 270,
|
||||
};
|
||||
|
||||
static const int16_t VALUE_NO_SET = 32766;
|
||||
|
||||
class Rect {
|
||||
class DisplayBuffer : public Display {
|
||||
public:
|
||||
int16_t x; ///< X coordinate of corner
|
||||
int16_t y; ///< Y coordinate of corner
|
||||
int16_t w; ///< Width of region
|
||||
int16_t h; ///< Height of region
|
||||
|
||||
Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT
|
||||
inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
|
||||
inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner
|
||||
inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner
|
||||
|
||||
inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
|
||||
|
||||
void expand(int16_t horizontal, int16_t vertical);
|
||||
|
||||
void extend(Rect rect);
|
||||
void shrink(Rect rect);
|
||||
|
||||
bool inside(Rect rect, bool absolute = true);
|
||||
bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
|
||||
bool equal(Rect rect);
|
||||
void info(const std::string &prefix = "rect info:");
|
||||
};
|
||||
|
||||
class BaseImage;
|
||||
class Font;
|
||||
class DisplayBuffer;
|
||||
class DisplayPage;
|
||||
class DisplayOnPageChangeTrigger;
|
||||
|
||||
using display_writer_t = std::function<void(DisplayBuffer &)>;
|
||||
|
||||
#define LOG_DISPLAY(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, prefix type); \
|
||||
ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, (obj)->rotation_); \
|
||||
ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \
|
||||
}
|
||||
|
||||
class DisplayBuffer {
|
||||
public:
|
||||
/// Fill the entire screen with the given color.
|
||||
virtual void fill(Color color);
|
||||
/// Clear the entire screen by filling it with OFF pixels.
|
||||
void clear();
|
||||
|
||||
/// Get the width of the image in pixels with rotation applied.
|
||||
int get_width();
|
||||
int get_width() override;
|
||||
/// Get the height of the image in pixels with rotation applied.
|
||||
int get_height();
|
||||
int get_height() override;
|
||||
|
||||
/// Set a single pixel at the specified coordinates to the given color.
|
||||
void draw_pixel_at(int x, int y, Color color = COLOR_ON);
|
||||
|
||||
/// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color.
|
||||
void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
|
||||
|
||||
/// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color.
|
||||
void horizontal_line(int x, int y, int width, Color color = COLOR_ON);
|
||||
|
||||
/// Draw a vertical line from the point [x,y] to [x,y+width] with the given color.
|
||||
void vertical_line(int x, int y, int height, Color color = COLOR_ON);
|
||||
|
||||
/// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at
|
||||
/// [x1+width,y1+height].
|
||||
void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
|
||||
|
||||
/// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height].
|
||||
void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
|
||||
|
||||
/// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color.
|
||||
void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON);
|
||||
|
||||
/// Fill a circle centered around [center_x,center_y] with the radius radius with the given color.
|
||||
void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON);
|
||||
|
||||
/** Print `text` with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param text The text to draw.
|
||||
*/
|
||||
void print(int x, int y, Font *font, Color color, TextAlign align, const char *text);
|
||||
|
||||
/** Print `text` with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param text The text to draw.
|
||||
*/
|
||||
void print(int x, int y, Font *font, Color color, const char *text);
|
||||
|
||||
/** Print `text` with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param text The text to draw.
|
||||
*/
|
||||
void print(int x, int y, Font *font, TextAlign align, const char *text);
|
||||
|
||||
/** Print `text` with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param text The text to draw.
|
||||
*/
|
||||
void print(int x, int y, Font *font, const char *text);
|
||||
|
||||
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param format The format to use.
|
||||
* @param ... The arguments to use for the text formatting.
|
||||
*/
|
||||
void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...)
|
||||
__attribute__((format(printf, 7, 8)));
|
||||
|
||||
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param format The format to use.
|
||||
* @param ... The arguments to use for the text formatting.
|
||||
*/
|
||||
void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
|
||||
|
||||
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param format The format to use.
|
||||
* @param ... The arguments to use for the text formatting.
|
||||
*/
|
||||
void printf(int x, int y, Font *font, TextAlign align, const char *format, ...) __attribute__((format(printf, 6, 7)));
|
||||
|
||||
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param format The format to use.
|
||||
* @param ... The arguments to use for the text formatting.
|
||||
*/
|
||||
void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
|
||||
|
||||
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param format The strftime format to use.
|
||||
* @param time The time to format.
|
||||
*/
|
||||
void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time)
|
||||
__attribute__((format(strftime, 7, 0)));
|
||||
|
||||
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param color The color to draw the text with.
|
||||
* @param format The strftime format to use.
|
||||
* @param time The time to format.
|
||||
*/
|
||||
void strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time)
|
||||
__attribute__((format(strftime, 6, 0)));
|
||||
|
||||
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the text alignment anchor point.
|
||||
* @param y The y coordinate of the text alignment anchor point.
|
||||
* @param font The font to draw the text with.
|
||||
* @param align The alignment of the text.
|
||||
* @param format The strftime format to use.
|
||||
* @param time The time to format.
|
||||
*/
|
||||
void strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time)
|
||||
__attribute__((format(strftime, 6, 0)));
|
||||
|
||||
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param font The font to draw the text with.
|
||||
* @param format The strftime format to use.
|
||||
* @param time The time to format.
|
||||
*/
|
||||
void strftime(int x, int y, Font *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0)));
|
||||
|
||||
/** Draw the `image` with the top-left corner at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param image The image to draw
|
||||
* @param color_on The color to replace in binary images for the on bits.
|
||||
* @param color_off The color to replace in binary images for the off bits.
|
||||
*/
|
||||
void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
|
||||
|
||||
#ifdef USE_GRAPH
|
||||
/** Draw the `graph` with the top-left corner at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param graph The graph id to draw
|
||||
* @param color_on The color to replace in binary images for the on bits.
|
||||
*/
|
||||
void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
|
||||
|
||||
/** Draw the `legend` for graph with the top-left corner at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param graph The graph id for which the legend applies to
|
||||
* @param graph The graph id for which the legend applies to
|
||||
* @param graph The graph id for which the legend applies to
|
||||
* @param name_font The font used for the trace name
|
||||
* @param value_font The font used for the trace value and units
|
||||
* @param color_on The color of the border
|
||||
*/
|
||||
void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
|
||||
#endif // USE_GRAPH
|
||||
|
||||
#ifdef USE_QR_CODE
|
||||
/** Draw the `qr_code` with the top-left corner at [x,y] to the screen.
|
||||
*
|
||||
* @param x The x coordinate of the upper left corner.
|
||||
* @param y The y coordinate of the upper left corner.
|
||||
* @param qr_code The qr_code to draw
|
||||
* @param color_on The color to replace in binary images for the on bits.
|
||||
*/
|
||||
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
|
||||
#endif
|
||||
|
||||
/** Get the text bounds of the given string.
|
||||
*
|
||||
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
|
||||
* @param y The y coordinate to place the string at, can be 0 if only interested in dimensions.
|
||||
* @param text The text to measure.
|
||||
* @param font The font to measure the text bounds with.
|
||||
* @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions.
|
||||
* @param x1 A pointer to store the returned x coordinate of the upper left corner in.
|
||||
* @param y1 A pointer to store the returned y coordinate of the upper left corner in.
|
||||
* @param width A pointer to store the returned text width in.
|
||||
* @param height A pointer to store the returned text height in.
|
||||
*/
|
||||
void get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width,
|
||||
int *height);
|
||||
|
||||
/// Internal method to set the display writer lambda.
|
||||
void set_writer(display_writer_t &&writer);
|
||||
|
||||
void show_page(DisplayPage *page);
|
||||
void show_next_page();
|
||||
void show_prev_page();
|
||||
|
||||
void set_pages(std::vector<DisplayPage *> pages);
|
||||
|
||||
const DisplayPage *get_active_page() const { return this->page_; }
|
||||
|
||||
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
|
||||
|
||||
/// Internal method to set the display rotation with.
|
||||
void set_rotation(DisplayRotation rotation);
|
||||
|
||||
// Internal method to set display auto clearing.
|
||||
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
|
||||
virtual int get_height_internal() = 0;
|
||||
virtual int get_width_internal() = 0;
|
||||
DisplayRotation get_rotation() const { return this->rotation_; }
|
||||
|
||||
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
|
||||
* returns the type the display is currently configured to.
|
||||
*/
|
||||
virtual DisplayType get_display_type() = 0;
|
||||
|
||||
/** Set the clipping rectangle for further drawing
|
||||
*
|
||||
* @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen)
|
||||
*
|
||||
* return true if success, false if error
|
||||
*/
|
||||
void start_clipping(Rect rect);
|
||||
void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
|
||||
start_clipping(Rect(left, top, right - left, bottom - top));
|
||||
};
|
||||
|
||||
/** Add a rectangular region to the invalidation region
|
||||
* - This is usually called when an element has been modified
|
||||
*
|
||||
* @param[in] rect: Rectangle to add to the invalidation region
|
||||
*/
|
||||
void extend_clipping(Rect rect);
|
||||
void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
|
||||
this->extend_clipping(Rect(left, top, right - left, bottom - top));
|
||||
};
|
||||
|
||||
/** substract a rectangular region to the invalidation region
|
||||
* - This is usually called when an element has been modified
|
||||
*
|
||||
* @param[in] rect: Rectangle to add to the invalidation region
|
||||
*/
|
||||
void shrink_clipping(Rect rect);
|
||||
void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
|
||||
this->shrink_clipping(Rect(left, top, right - left, bottom - top));
|
||||
};
|
||||
|
||||
/** Reset the invalidation region
|
||||
*/
|
||||
void end_clipping();
|
||||
|
||||
/** Get the current the clipping rectangle
|
||||
*
|
||||
* return rect for active clipping region
|
||||
*/
|
||||
Rect get_clipping();
|
||||
|
||||
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
|
||||
|
||||
protected:
|
||||
void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
|
||||
|
||||
virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0;
|
||||
|
||||
void init_internal_(uint32_t buffer_length);
|
||||
|
||||
void do_update_();
|
||||
|
||||
uint8_t *buffer_{nullptr};
|
||||
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
|
||||
optional<display_writer_t> writer_{};
|
||||
DisplayPage *page_{nullptr};
|
||||
DisplayPage *previous_page_{nullptr};
|
||||
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
|
||||
bool auto_clear_enabled_{true};
|
||||
std::vector<Rect> clipping_rectangle_;
|
||||
};
|
||||
|
||||
class DisplayPage {
|
||||
public:
|
||||
DisplayPage(display_writer_t writer);
|
||||
void show();
|
||||
void show_next();
|
||||
void show_prev();
|
||||
void set_parent(DisplayBuffer *parent);
|
||||
void set_prev(DisplayPage *prev);
|
||||
void set_next(DisplayPage *next);
|
||||
const display_writer_t &get_writer() const;
|
||||
|
||||
protected:
|
||||
DisplayBuffer *parent_;
|
||||
display_writer_t writer_;
|
||||
DisplayPage *prev_{nullptr};
|
||||
DisplayPage *next_{nullptr};
|
||||
};
|
||||
|
||||
struct GlyphData {
|
||||
const char *a_char;
|
||||
const uint8_t *data;
|
||||
int offset_x;
|
||||
int offset_y;
|
||||
int width;
|
||||
int height;
|
||||
};
|
||||
|
||||
class Glyph {
|
||||
public:
|
||||
Glyph(const GlyphData *data) : glyph_data_(data) {}
|
||||
|
||||
bool get_pixel(int x, int y) const;
|
||||
|
||||
const char *get_char() const;
|
||||
|
||||
bool compare_to(const char *str) const;
|
||||
|
||||
int match_length(const char *str) const;
|
||||
|
||||
void scan_area(int *x1, int *y1, int *width, int *height) const;
|
||||
|
||||
protected:
|
||||
friend Font;
|
||||
friend DisplayBuffer;
|
||||
|
||||
const GlyphData *glyph_data_;
|
||||
};
|
||||
|
||||
class Font {
|
||||
public:
|
||||
/** Construct the font with the given glyphs.
|
||||
*
|
||||
* @param glyphs A vector of glyphs, must be sorted lexicographically.
|
||||
* @param baseline The y-offset from the top of the text to the baseline.
|
||||
* @param bottom The y-offset from the top of the text to the bottom (i.e. height).
|
||||
*/
|
||||
Font(const GlyphData *data, int data_nr, int baseline, int height);
|
||||
|
||||
int match_next_glyph(const char *str, int *match_length);
|
||||
|
||||
void measure(const char *str, int *width, int *x_offset, int *baseline, int *height);
|
||||
inline int get_baseline() { return this->baseline_; }
|
||||
inline int get_height() { return this->height_; }
|
||||
|
||||
const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
|
||||
|
||||
protected:
|
||||
std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_;
|
||||
int baseline_;
|
||||
int height_;
|
||||
};
|
||||
|
||||
class BaseImage {
|
||||
public:
|
||||
virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0;
|
||||
virtual int get_width() const = 0;
|
||||
virtual int get_height() const = 0;
|
||||
};
|
||||
|
||||
class Image : public BaseImage {
|
||||
public:
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type);
|
||||
Color get_pixel(int x, int y, Color color_on = COLOR_ON, Color color_off = COLOR_OFF) const;
|
||||
int get_width() const override;
|
||||
int get_height() const override;
|
||||
ImageType get_type() const;
|
||||
|
||||
void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) override;
|
||||
|
||||
void set_transparency(bool transparent) { transparent_ = transparent; }
|
||||
bool has_transparency() const { return transparent_; }
|
||||
|
||||
protected:
|
||||
bool get_binary_pixel_(int x, int y) const;
|
||||
Color get_rgb24_pixel_(int x, int y) const;
|
||||
Color get_rgba_pixel_(int x, int y) const;
|
||||
Color get_rgb565_pixel_(int x, int y) const;
|
||||
Color get_grayscale_pixel_(int x, int y) const;
|
||||
|
||||
int width_;
|
||||
int height_;
|
||||
ImageType type_;
|
||||
const uint8_t *data_start_;
|
||||
bool transparent_;
|
||||
};
|
||||
|
||||
class Animation : public Image {
|
||||
public:
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
|
||||
|
||||
uint32_t get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
void next_frame();
|
||||
void prev_frame();
|
||||
|
||||
/** Selects a specific frame within the animation.
|
||||
*
|
||||
* @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
|
||||
*/
|
||||
void set_frame(int frame);
|
||||
|
||||
void set_loop(uint32_t start_frame, uint32_t end_frame, int count);
|
||||
|
||||
protected:
|
||||
void update_data_start_();
|
||||
|
||||
const uint8_t *animation_data_start_;
|
||||
int current_frame_;
|
||||
uint32_t animation_frame_count_;
|
||||
uint32_t loop_start_frame_;
|
||||
uint32_t loop_end_frame_;
|
||||
int loop_count_;
|
||||
int loop_current_iteration_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(DisplayPage *, page)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto *page = this->page_.value(x...);
|
||||
if (page != nullptr) {
|
||||
page->show();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayPageShowNextAction : public Action<Ts...> {
|
||||
public:
|
||||
DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {}
|
||||
|
||||
void play(Ts... x) override { this->buffer_->show_next_page(); }
|
||||
|
||||
DisplayBuffer *buffer_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...> {
|
||||
public:
|
||||
DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {}
|
||||
|
||||
void play(Ts... x) override { this->buffer_->show_prev_page(); }
|
||||
|
||||
DisplayBuffer *buffer_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayIsDisplayingPageCondition : public Condition<Ts...> {
|
||||
public:
|
||||
DisplayIsDisplayingPageCondition(DisplayBuffer *parent) : parent_(parent) {}
|
||||
|
||||
void set_page(DisplayPage *page) { this->page_ = page; }
|
||||
bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; }
|
||||
|
||||
protected:
|
||||
DisplayBuffer *parent_;
|
||||
DisplayPage *page_;
|
||||
};
|
||||
|
||||
class DisplayOnPageChangeTrigger : public Trigger<DisplayPage *, DisplayPage *> {
|
||||
public:
|
||||
explicit DisplayOnPageChangeTrigger(DisplayBuffer *parent) { parent->add_on_page_change_trigger(this); }
|
||||
void process(DisplayPage *from, DisplayPage *to);
|
||||
void set_from(DisplayPage *p) { this->from_ = p; }
|
||||
void set_to(DisplayPage *p) { this->to_ = p; }
|
||||
|
||||
protected:
|
||||
DisplayPage *from_{nullptr};
|
||||
DisplayPage *to_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace display
|
||||
|
||||
98
esphome/components/display/rect.cpp
Normal file
98
esphome/components/display/rect.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
#include "rect.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace display {
|
||||
|
||||
static const char *const TAG = "display";
|
||||
|
||||
void Rect::expand(int16_t horizontal, int16_t vertical) {
|
||||
if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
|
||||
this->x = this->x - horizontal;
|
||||
this->y = this->y - vertical;
|
||||
this->w = this->w + (2 * horizontal);
|
||||
this->h = this->h + (2 * vertical);
|
||||
}
|
||||
}
|
||||
|
||||
void Rect::extend(Rect rect) {
|
||||
if (!this->is_set()) {
|
||||
this->x = rect.x;
|
||||
this->y = rect.y;
|
||||
this->w = rect.w;
|
||||
this->h = rect.h;
|
||||
} else {
|
||||
if (this->x > rect.x) {
|
||||
this->w = this->w + (this->x - rect.x);
|
||||
this->x = rect.x;
|
||||
}
|
||||
if (this->y > rect.y) {
|
||||
this->h = this->h + (this->y - rect.y);
|
||||
this->y = rect.y;
|
||||
}
|
||||
if (this->x2() < rect.x2()) {
|
||||
this->w = rect.x2() - this->x;
|
||||
}
|
||||
if (this->y2() < rect.y2()) {
|
||||
this->h = rect.y2() - this->y;
|
||||
}
|
||||
}
|
||||
}
|
||||
void Rect::shrink(Rect rect) {
|
||||
if (!this->inside(rect)) {
|
||||
(*this) = Rect();
|
||||
} else {
|
||||
if (this->x2() > rect.x2()) {
|
||||
this->w = rect.x2() - this->x;
|
||||
}
|
||||
if (this->x < rect.x) {
|
||||
this->w = this->w + (this->x - rect.x);
|
||||
this->x = rect.x;
|
||||
}
|
||||
if (this->y2() > rect.y2()) {
|
||||
this->h = rect.y2() - this->y;
|
||||
}
|
||||
if (this->y < rect.y) {
|
||||
this->h = this->h + (this->y - rect.y);
|
||||
this->y = rect.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Rect::equal(Rect rect) {
|
||||
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
|
||||
}
|
||||
|
||||
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT
|
||||
if (!this->is_set()) {
|
||||
return true;
|
||||
}
|
||||
if (absolute) {
|
||||
return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
|
||||
} else {
|
||||
return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
|
||||
}
|
||||
}
|
||||
|
||||
bool Rect::inside(Rect rect, bool absolute) {
|
||||
if (!this->is_set() || !rect.is_set()) {
|
||||
return true;
|
||||
}
|
||||
if (absolute) {
|
||||
return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
|
||||
} else {
|
||||
return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
|
||||
}
|
||||
}
|
||||
|
||||
void Rect::info(const std::string &prefix) {
|
||||
if (this->is_set()) {
|
||||
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
|
||||
this->y2());
|
||||
} else
|
||||
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
|
||||
}
|
||||
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
36
esphome/components/display/rect.h
Normal file
36
esphome/components/display/rect.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace display {
|
||||
|
||||
static const int16_t VALUE_NO_SET = 32766;
|
||||
|
||||
class Rect {
|
||||
public:
|
||||
int16_t x; ///< X coordinate of corner
|
||||
int16_t y; ///< Y coordinate of corner
|
||||
int16_t w; ///< Width of region
|
||||
int16_t h; ///< Height of region
|
||||
|
||||
Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT
|
||||
inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
|
||||
inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner
|
||||
inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner
|
||||
|
||||
inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
|
||||
|
||||
void expand(int16_t horizontal, int16_t vertical);
|
||||
|
||||
void extend(Rect rect);
|
||||
void shrink(Rect rect);
|
||||
|
||||
bool inside(Rect rect, bool absolute = true);
|
||||
bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
|
||||
bool equal(Rect rect);
|
||||
void info(const std::string &prefix = "rect info:");
|
||||
};
|
||||
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
@@ -19,6 +19,7 @@ CONF_CRC_CHECK = "crc_check"
|
||||
CONF_DECRYPTION_KEY = "decryption_key"
|
||||
CONF_DSMR_ID = "dsmr_id"
|
||||
CONF_GAS_MBUS_ID = "gas_mbus_id"
|
||||
CONF_WATER_MBUS_ID = "water_mbus_id"
|
||||
CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
|
||||
CONF_REQUEST_INTERVAL = "request_interval"
|
||||
CONF_REQUEST_PIN = "request_pin"
|
||||
@@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
|
||||
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
|
||||
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
|
||||
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
|
||||
cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_,
|
||||
cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(
|
||||
@@ -82,9 +84,10 @@ async def to_code(config):
|
||||
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds))
|
||||
|
||||
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
|
||||
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
|
||||
|
||||
# DSMR Parser
|
||||
cg.add_library("glmnet/Dsmr", "0.5")
|
||||
cg.add_library("glmnet/Dsmr", "0.8")
|
||||
|
||||
# Crypto
|
||||
cg.add_library("rweather/Crypto", "0.4.0")
|
||||
|
||||
@@ -8,6 +8,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
DEVICE_CLASS_WATER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
@@ -236,6 +237,36 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
device_class=DEVICE_CLASS_GAS,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("water_delivered"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CUBIC_METER,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_WATER,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional(
|
||||
"active_energy_import_current_average_demand"
|
||||
): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(
|
||||
"active_energy_import_maximum_demand_running_month"
|
||||
): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(
|
||||
"active_energy_import_maximum_demand_last_13_months"
|
||||
): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
1
esphome/components/duty_time/__init__.py
Normal file
1
esphome/components/duty_time/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@dudanov"]
|
||||
103
esphome/components/duty_time/duty_time_sensor.cpp
Normal file
103
esphome/components/duty_time/duty_time_sensor.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "duty_time_sensor.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace duty_time_sensor {
|
||||
|
||||
static const char *const TAG = "duty_time_sensor";
|
||||
|
||||
void DutyTimeSensor::set_sensor(binary_sensor::BinarySensor *const sensor) {
|
||||
sensor->add_on_state_callback([this](bool state) { this->process_state_(state); });
|
||||
}
|
||||
|
||||
void DutyTimeSensor::start() {
|
||||
if (!this->last_state_)
|
||||
this->process_state_(true);
|
||||
}
|
||||
|
||||
void DutyTimeSensor::stop() {
|
||||
if (this->last_state_)
|
||||
this->process_state_(false);
|
||||
}
|
||||
|
||||
void DutyTimeSensor::update() {
|
||||
if (this->last_state_)
|
||||
this->process_state_(true);
|
||||
}
|
||||
|
||||
void DutyTimeSensor::loop() {
|
||||
if (this->func_ == nullptr)
|
||||
return;
|
||||
|
||||
const bool state = this->func_();
|
||||
|
||||
if (state != this->last_state_)
|
||||
this->process_state_(state);
|
||||
}
|
||||
|
||||
void DutyTimeSensor::setup() {
|
||||
uint32_t seconds = 0;
|
||||
|
||||
if (this->restore_) {
|
||||
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_object_id_hash());
|
||||
this->pref_.load(&seconds);
|
||||
}
|
||||
|
||||
this->set_value_(seconds);
|
||||
}
|
||||
|
||||
void DutyTimeSensor::set_value_(const uint32_t sec) {
|
||||
this->last_time_ = 0;
|
||||
if (this->last_state_)
|
||||
this->last_time_ = millis(); // last time with 0 ms correction
|
||||
this->publish_and_save_(sec, 0);
|
||||
}
|
||||
|
||||
void DutyTimeSensor::process_state_(const bool state) {
|
||||
const uint32_t now = millis();
|
||||
|
||||
if (this->last_state_) {
|
||||
// update or falling edge
|
||||
const uint32_t tm = now - this->last_time_;
|
||||
const uint32_t ms = tm % 1000;
|
||||
|
||||
this->publish_and_save_(this->total_sec_ + tm / 1000, ms);
|
||||
this->last_time_ = now - ms; // store time with ms correction
|
||||
|
||||
if (!state) {
|
||||
// falling edge
|
||||
this->last_time_ = ms; // temporary store ms correction only
|
||||
this->last_state_ = false;
|
||||
|
||||
if (this->last_duty_time_sensor_ != nullptr) {
|
||||
const uint32_t turn_on_ms = now - this->edge_time_;
|
||||
this->last_duty_time_sensor_->publish_state(turn_on_ms * 1e-3f);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (state) {
|
||||
// rising edge
|
||||
this->last_time_ = now - this->last_time_; // store time with ms correction
|
||||
this->edge_time_ = now; // store turn-on start time
|
||||
this->last_state_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) {
|
||||
this->total_sec_ = sec;
|
||||
this->publish_state(sec + ms * 1e-3f);
|
||||
|
||||
if (this->restore_)
|
||||
this->pref_.save(&sec);
|
||||
}
|
||||
|
||||
void DutyTimeSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Duty Time:");
|
||||
ESP_LOGCONFIG(TAG, " Update Interval: %dms", this->get_update_interval());
|
||||
ESP_LOGCONFIG(TAG, " Restore: %s", ONOFF(this->restore_));
|
||||
LOG_SENSOR(" ", "Duty Time Sensor:", this);
|
||||
LOG_SENSOR(" ", "Last Duty Time Sensor:", this->last_duty_time_sensor_);
|
||||
}
|
||||
|
||||
} // namespace duty_time_sensor
|
||||
} // namespace esphome
|
||||
88
esphome/components/duty_time/duty_time_sensor.h
Normal file
88
esphome/components/duty_time/duty_time_sensor.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace duty_time_sensor {
|
||||
|
||||
class DutyTimeSensor : public sensor::Sensor, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bool is_running() const { return this->last_state_; }
|
||||
void reset() { this->set_value_(0); }
|
||||
|
||||
void set_lambda(std::function<bool()> &&func) { this->func_ = func; }
|
||||
void set_sensor(binary_sensor::BinarySensor *sensor);
|
||||
void set_last_duty_time_sensor(sensor::Sensor *sensor) { this->last_duty_time_sensor_ = sensor; }
|
||||
void set_restore(bool restore) { this->restore_ = restore; }
|
||||
|
||||
protected:
|
||||
void set_value_(uint32_t sec);
|
||||
void process_state_(bool state);
|
||||
void publish_and_save_(uint32_t sec, uint32_t ms);
|
||||
|
||||
std::function<bool()> func_{nullptr};
|
||||
sensor::Sensor *last_duty_time_sensor_{nullptr};
|
||||
ESPPreferenceObject pref_;
|
||||
|
||||
uint32_t total_sec_;
|
||||
uint32_t last_time_;
|
||||
uint32_t edge_time_;
|
||||
bool last_state_{false};
|
||||
bool restore_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class StartAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit StartAction(DutyTimeSensor *parent) : parent_(parent) {}
|
||||
|
||||
void play(Ts... x) override { this->parent_->start(); }
|
||||
|
||||
protected:
|
||||
DutyTimeSensor *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class StopAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit StopAction(DutyTimeSensor *parent) : parent_(parent) {}
|
||||
|
||||
void play(Ts... x) override { this->parent_->stop(); }
|
||||
|
||||
protected:
|
||||
DutyTimeSensor *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResetAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit ResetAction(DutyTimeSensor *parent) : parent_(parent) {}
|
||||
|
||||
void play(Ts... x) override { this->parent_->reset(); }
|
||||
|
||||
protected:
|
||||
DutyTimeSensor *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class RunningCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit RunningCondition(DutyTimeSensor *parent, bool state) : parent_(parent), state_(state) {}
|
||||
|
||||
bool check(Ts... x) override { return this->parent_->is_running() == this->state_; }
|
||||
|
||||
protected:
|
||||
DutyTimeSensor *parent_;
|
||||
bool state_;
|
||||
};
|
||||
|
||||
} // namespace duty_time_sensor
|
||||
} // namespace esphome
|
||||
121
esphome/components/duty_time/sensor.py
Normal file
121
esphome/components/duty_time/sensor.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.automation import (
|
||||
Action,
|
||||
Condition,
|
||||
maybe_simple_id,
|
||||
register_action,
|
||||
register_condition,
|
||||
)
|
||||
from esphome.components import binary_sensor, sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_SENSOR,
|
||||
CONF_RESTORE,
|
||||
CONF_LAMBDA,
|
||||
UNIT_SECOND,
|
||||
STATE_CLASS_TOTAL,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
DEVICE_CLASS_DURATION,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
|
||||
CONF_LAST_TIME = "last_time"
|
||||
|
||||
duty_time_sensor_ns = cg.esphome_ns.namespace("duty_time_sensor")
|
||||
DutyTimeSensor = duty_time_sensor_ns.class_(
|
||||
"DutyTimeSensor", sensor.Sensor, cg.PollingComponent
|
||||
)
|
||||
StartAction = duty_time_sensor_ns.class_("StartAction", Action)
|
||||
StopAction = duty_time_sensor_ns.class_("StopAction", Action)
|
||||
ResetAction = duty_time_sensor_ns.class_("ResetAction", Action)
|
||||
SetAction = duty_time_sensor_ns.class_("SetAction", Action)
|
||||
RunningCondition = duty_time_sensor_ns.class_("RunningCondition", Condition)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
DutyTimeSensor,
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
icon="mdi:timer-play-outline",
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_SENSOR): cv.use_id(binary_sensor.BinarySensor),
|
||||
cv.Optional(CONF_LAMBDA): cv.lambda_,
|
||||
cv.Optional(CONF_RESTORE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_LAST_TIME): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
icon="mdi:timer-marker-outline",
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_TOTAL,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
cv.has_at_most_one_key(CONF_SENSOR, CONF_LAMBDA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_restore(config[CONF_RESTORE]))
|
||||
if CONF_SENSOR in config:
|
||||
sens = await cg.get_variable(config[CONF_SENSOR])
|
||||
cg.add(var.set_sensor(sens))
|
||||
if CONF_LAMBDA in config:
|
||||
lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.bool_)
|
||||
cg.add(var.set_lambda(lambda_))
|
||||
if CONF_LAST_TIME in config:
|
||||
sens = await sensor.new_sensor(config[CONF_LAST_TIME])
|
||||
cg.add(var.set_last_duty_time_sensor(sens))
|
||||
|
||||
|
||||
# AUTOMATIONS
|
||||
|
||||
DUTY_TIME_ID_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(DutyTimeSensor),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA)
|
||||
async def sensor_runtime_start_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA)
|
||||
async def sensor_runtime_stop_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA)
|
||||
async def sensor_runtime_reset_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@register_condition(
|
||||
"sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA
|
||||
)
|
||||
async def duty_time_is_running_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, True)
|
||||
|
||||
|
||||
@register_condition(
|
||||
"sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA
|
||||
)
|
||||
async def duty_time_is_not_running_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, False)
|
||||
@@ -547,6 +547,8 @@ def copy_files():
|
||||
CORE.relative_build_path(f"components/{name}"),
|
||||
dirs_exist_ok=True,
|
||||
ignore=shutil.ignore_patterns(".git", ".github"),
|
||||
symlinks=True,
|
||||
ignore_dangling_symlinks=True,
|
||||
)
|
||||
|
||||
dir = os.path.dirname(__file__)
|
||||
|
||||
@@ -55,3 +55,4 @@ async def to_code(config):
|
||||
|
||||
if CORE.using_esp_idf:
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
|
||||
|
||||
@@ -263,6 +263,7 @@ async def to_code(config):
|
||||
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
|
||||
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
|
||||
add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
|
||||
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
||||
cg.add_define("USE_ESP32_BLE_CLIENT")
|
||||
|
||||
@@ -107,16 +107,16 @@ void ESP32BLETracker::loop() {
|
||||
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
|
||||
}
|
||||
|
||||
bool bulk_parsed = false;
|
||||
|
||||
for (auto *listener : this->listeners_) {
|
||||
bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
|
||||
}
|
||||
for (auto *client : this->clients_) {
|
||||
bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
|
||||
if (this->raw_advertisements_) {
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
|
||||
}
|
||||
for (auto *client : this->clients_) {
|
||||
client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bulk_parsed) {
|
||||
if (this->parse_advertisements_) {
|
||||
for (size_t i = 0; i < index; i++) {
|
||||
ESPBTDevice device;
|
||||
device.parse_scan_rst(this->scan_result_buffer_[i]);
|
||||
@@ -284,6 +284,32 @@ void ESP32BLETracker::end_of_scan_() {
|
||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
||||
client->app_id = ++this->app_id_;
|
||||
this->clients_.push_back(client);
|
||||
this->recalculate_advertisement_parser_types();
|
||||
}
|
||||
|
||||
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
|
||||
listener->set_parent(this);
|
||||
this->listeners_.push_back(listener);
|
||||
this->recalculate_advertisement_parser_types();
|
||||
}
|
||||
|
||||
void ESP32BLETracker::recalculate_advertisement_parser_types() {
|
||||
this->raw_advertisements_ = false;
|
||||
this->parse_advertisements_ = false;
|
||||
for (auto *listener : this->listeners_) {
|
||||
if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
|
||||
this->parse_advertisements_ = true;
|
||||
} else {
|
||||
this->raw_advertisements_ = true;
|
||||
}
|
||||
}
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
|
||||
this->parse_advertisements_ = true;
|
||||
} else {
|
||||
this->raw_advertisements_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
|
||||
@@ -27,6 +27,11 @@ using namespace esp32_ble;
|
||||
|
||||
using adv_data_t = std::vector<uint8_t>;
|
||||
|
||||
enum AdvertisementParserType {
|
||||
PARSED_ADVERTISEMENTS,
|
||||
RAW_ADVERTISEMENTS,
|
||||
};
|
||||
|
||||
struct ServiceData {
|
||||
ESPBTUUID uuid;
|
||||
adv_data_t data;
|
||||
@@ -116,6 +121,9 @@ class ESPBTDeviceListener {
|
||||
virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
|
||||
return false;
|
||||
};
|
||||
virtual AdvertisementParserType get_advertisement_parser_type() {
|
||||
return AdvertisementParserType::PARSED_ADVERTISEMENTS;
|
||||
};
|
||||
void set_parent(ESP32BLETracker *parent) { parent_ = parent; }
|
||||
|
||||
protected:
|
||||
@@ -184,12 +192,9 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv
|
||||
|
||||
void loop() override;
|
||||
|
||||
void register_listener(ESPBTDeviceListener *listener) {
|
||||
listener->set_parent(this);
|
||||
this->listeners_.push_back(listener);
|
||||
}
|
||||
|
||||
void register_listener(ESPBTDeviceListener *listener);
|
||||
void register_client(ESPBTClient *client);
|
||||
void recalculate_advertisement_parser_types();
|
||||
|
||||
void print_bt_device_info(const ESPBTDevice &device);
|
||||
|
||||
@@ -231,6 +236,8 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
bool scanner_idle_;
|
||||
bool raw_advertisements_{false};
|
||||
bool parse_advertisements_{false};
|
||||
SemaphoreHandle_t scan_result_lock_;
|
||||
SemaphoreHandle_t scan_end_lock_;
|
||||
size_t scan_result_index_{0};
|
||||
|
||||
@@ -35,6 +35,7 @@ ETHERNET_TYPES = {
|
||||
"IP101": EthernetType.ETHERNET_TYPE_IP101,
|
||||
"JL1101": EthernetType.ETHERNET_TYPE_JL1101,
|
||||
"KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
|
||||
"KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA,
|
||||
}
|
||||
|
||||
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
|
||||
|
||||
@@ -19,7 +19,11 @@
|
||||
#include <sys/cdefs.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_eth.h"
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
#include "esp_eth_phy_802_3.h"
|
||||
#else
|
||||
#include "eth_phy_regs_struct.h"
|
||||
#endif
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "driver/gpio.h"
|
||||
@@ -170,7 +174,11 @@ static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy, eth_phy_autoneg_cmd_t cmd, bool *nego_state) {
|
||||
#else
|
||||
static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) {
|
||||
#endif
|
||||
phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
|
||||
esp_eth_mediator_t *eth = jl1101->eth;
|
||||
/* in case any link status has changed, let's assume we're in link down status */
|
||||
@@ -285,7 +293,11 @@ static esp_err_t jl1101_init(esp_eth_phy_t *phy) {
|
||||
esp_eth_mediator_t *eth = jl1101->eth;
|
||||
// Detect PHY address
|
||||
if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) {
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
PHY_CHECK(esp_eth_phy_802_3_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err);
|
||||
#else
|
||||
PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err);
|
||||
#endif
|
||||
}
|
||||
/* Power on Ethernet PHY */
|
||||
PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err);
|
||||
@@ -324,7 +336,11 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) {
|
||||
jl1101->parent.init = jl1101_init;
|
||||
jl1101->parent.deinit = jl1101_deinit;
|
||||
jl1101->parent.set_mediator = jl1101_set_mediator;
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
jl1101->parent.autonego_ctrl = jl1101_negotiate;
|
||||
#else
|
||||
jl1101->parent.negotiate = jl1101_negotiate;
|
||||
#endif
|
||||
jl1101->parent.get_link = jl1101_get_link;
|
||||
jl1101->parent.pwrctl = jl1101_pwrctl;
|
||||
jl1101->parent.get_addr = jl1101_get_addr;
|
||||
|
||||
@@ -41,18 +41,27 @@ void EthernetComponent::setup() {
|
||||
this->eth_netif_ = esp_netif_new(&cfg);
|
||||
|
||||
// Init MAC and PHY configs to default
|
||||
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
|
||||
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
|
||||
|
||||
phy_config.phy_addr = this->phy_addr_;
|
||||
phy_config.reset_gpio_num = this->power_pin_;
|
||||
|
||||
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG();
|
||||
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
|
||||
esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
|
||||
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
|
||||
esp32_emac_config.clock_config.rmii.clock_gpio = this->clk_gpio_;
|
||||
|
||||
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
|
||||
#else
|
||||
mac_config.smi_mdc_gpio_num = this->mdc_pin_;
|
||||
mac_config.smi_mdio_gpio_num = this->mdio_pin_;
|
||||
mac_config.clock_config.rmii.clock_mode = this->clk_mode_;
|
||||
mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_;
|
||||
|
||||
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config);
|
||||
#endif
|
||||
|
||||
switch (this->type_) {
|
||||
case ETHERNET_TYPE_LAN8720: {
|
||||
@@ -75,8 +84,13 @@ void EthernetComponent::setup() {
|
||||
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
|
||||
break;
|
||||
}
|
||||
case ETHERNET_TYPE_KSZ8081: {
|
||||
case ETHERNET_TYPE_KSZ8081:
|
||||
case ETHERNET_TYPE_KSZ8081RNA: {
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
|
||||
#else
|
||||
this->phy_ = esp_eth_phy_new_ksz8081(&phy_config);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -89,6 +103,12 @@ void EthernetComponent::setup() {
|
||||
this->eth_handle_ = nullptr;
|
||||
err = esp_eth_driver_install(ð_config, &this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH driver install error");
|
||||
|
||||
if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
|
||||
// KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
|
||||
this->ksz8081_set_clock_reference_(mac);
|
||||
}
|
||||
|
||||
/* attach Ethernet driver to TCP/IP stack */
|
||||
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
|
||||
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
|
||||
@@ -171,6 +191,10 @@ void EthernetComponent::dump_config() {
|
||||
eth_type = "KSZ8081";
|
||||
break;
|
||||
|
||||
case ETHERNET_TYPE_KSZ8081RNA:
|
||||
eth_type = "KSZ8081RNA";
|
||||
break;
|
||||
|
||||
default:
|
||||
eth_type = "Unknown";
|
||||
break;
|
||||
@@ -221,13 +245,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event);
|
||||
ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event);
|
||||
}
|
||||
|
||||
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
|
||||
void *event_data) {
|
||||
global_eth_component->connected_ = true;
|
||||
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%d)", event_id);
|
||||
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%" PRId32 ")", event_id);
|
||||
}
|
||||
|
||||
void EthernetComponent::start_connect_() {
|
||||
@@ -372,6 +396,37 @@ bool EthernetComponent::powerdown() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
#define KSZ80XX_PC2R_REG_ADDR (0x1F)
|
||||
|
||||
esp_err_t err;
|
||||
|
||||
uint32_t phy_control_2;
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str());
|
||||
|
||||
/*
|
||||
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
|
||||
* KSZ8081RNA:
|
||||
* 0 - clock input to XI (Pin 8) is 25 MHz for RMII – 25 MHz clock mode.
|
||||
* 1 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode.
|
||||
* KSZ8081RND:
|
||||
* 0 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode.
|
||||
* 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII – 25 MHz clock mode.
|
||||
*/
|
||||
if ((phy_control_2 & (1 << 7)) != (1 << 7)) {
|
||||
phy_control_2 |= 1 << 7;
|
||||
err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2);
|
||||
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str());
|
||||
}
|
||||
|
||||
#undef KSZ80XX_PC2R_REG_ADDR
|
||||
}
|
||||
|
||||
} // namespace ethernet
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ enum EthernetType {
|
||||
ETHERNET_TYPE_IP101,
|
||||
ETHERNET_TYPE_JL1101,
|
||||
ETHERNET_TYPE_KSZ8081,
|
||||
ETHERNET_TYPE_KSZ8081RNA,
|
||||
};
|
||||
|
||||
struct ManualIP {
|
||||
@@ -67,6 +68,8 @@ class EthernetComponent : public Component {
|
||||
|
||||
void start_connect_();
|
||||
void dump_connect_params_();
|
||||
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
|
||||
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
|
||||
|
||||
std::string use_address_;
|
||||
uint8_t phy_addr_{0};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "ethernet_info_text_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ethernet_info {
|
||||
@@ -13,4 +13,4 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP
|
||||
} // namespace ethernet_info
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/components/ethernet/ethernet_component.h"
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ethernet_info {
|
||||
@@ -30,4 +30,4 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS
|
||||
} // namespace ethernet_info
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -3,11 +3,11 @@ from pathlib import Path
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from packaging import version
|
||||
|
||||
import requests
|
||||
|
||||
from esphome import core
|
||||
from esphome.components import display
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
@@ -29,9 +29,11 @@ DOMAIN = "font"
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
|
||||
Font = display.display_ns.class_("Font")
|
||||
Glyph = display.display_ns.class_("Glyph")
|
||||
GlyphData = display.display_ns.struct("GlyphData")
|
||||
font_ns = cg.esphome_ns.namespace("font")
|
||||
|
||||
Font = font_ns.class_("Font")
|
||||
Glyph = font_ns.class_("Glyph")
|
||||
GlyphData = font_ns.struct("GlyphData")
|
||||
|
||||
|
||||
def validate_glyphs(value):
|
||||
@@ -65,13 +67,18 @@ def validate_pillow_installed(value):
|
||||
except ImportError as err:
|
||||
raise cv.Invalid(
|
||||
"Please install the pillow python package to use this feature. "
|
||||
"(pip install pillow)"
|
||||
'(pip install pillow">4.0.0,<10.0.0")'
|
||||
) from err
|
||||
|
||||
if PIL.__version__[0] < "4":
|
||||
if version.parse(PIL.__version__) < version.parse("4.0.0"):
|
||||
raise cv.Invalid(
|
||||
"Please update your pillow installation to at least 4.0.x. "
|
||||
"(pip install -U pillow)"
|
||||
'(pip install pillow">4.0.0,<10.0.0")'
|
||||
)
|
||||
if version.parse(PIL.__version__) >= version.parse("10.0.0"):
|
||||
raise cv.Invalid(
|
||||
"Please downgrade your pillow installation to below 10.0.0. "
|
||||
'(pip install pillow">4.0.0,<10.0.0")'
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
149
esphome/components/font/font.cpp
Normal file
149
esphome/components/font/font.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
#include "font.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace font {
|
||||
|
||||
static const char *const TAG = "font";
|
||||
|
||||
void Glyph::draw(int x_at, int y_start, display::Display *display, Color color) const {
|
||||
int scan_x1, scan_y1, scan_width, scan_height;
|
||||
this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
|
||||
|
||||
const unsigned char *data = this->glyph_data_->data;
|
||||
const int max_x = x_at + scan_x1 + scan_width;
|
||||
const int max_y = y_start + scan_y1 + scan_height;
|
||||
|
||||
for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) {
|
||||
for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) {
|
||||
uint8_t pixel_data = progmem_read_byte(data);
|
||||
const int pixel_max_x = std::min(max_x, glyph_x + 8);
|
||||
|
||||
for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) {
|
||||
if (pixel_data & 0x80) {
|
||||
display->draw_pixel_at(pixel_x, glyph_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
|
||||
bool Glyph::compare_to(const char *str) const {
|
||||
// 1 -> this->char_
|
||||
// 2 -> str
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return true;
|
||||
if (str[i] == '\0')
|
||||
return false;
|
||||
if (this->glyph_data_->a_char[i] > str[i])
|
||||
return false;
|
||||
if (this->glyph_data_->a_char[i] < str[i])
|
||||
return true;
|
||||
}
|
||||
// this should not happen
|
||||
return false;
|
||||
}
|
||||
int Glyph::match_length(const char *str) const {
|
||||
for (uint32_t i = 0;; i++) {
|
||||
if (this->glyph_data_->a_char[i] == '\0')
|
||||
return i;
|
||||
if (str[i] != this->glyph_data_->a_char[i])
|
||||
return 0;
|
||||
}
|
||||
// this should not happen
|
||||
return 0;
|
||||
}
|
||||
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
|
||||
*x1 = this->glyph_data_->offset_x;
|
||||
*y1 = this->glyph_data_->offset_y;
|
||||
*width = this->glyph_data_->width;
|
||||
*height = this->glyph_data_->height;
|
||||
}
|
||||
|
||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
|
||||
glyphs_.reserve(data_nr);
|
||||
for (int i = 0; i < data_nr; ++i)
|
||||
glyphs_.emplace_back(&data[i]);
|
||||
}
|
||||
int Font::match_next_glyph(const char *str, int *match_length) {
|
||||
int lo = 0;
|
||||
int hi = this->glyphs_.size() - 1;
|
||||
while (lo != hi) {
|
||||
int mid = (lo + hi + 1) / 2;
|
||||
if (this->glyphs_[mid].compare_to(str)) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
*match_length = this->glyphs_[lo].match_length(str);
|
||||
if (*match_length <= 0)
|
||||
return -1;
|
||||
return lo;
|
||||
}
|
||||
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
|
||||
*baseline = this->baseline_;
|
||||
*height = this->height_;
|
||||
int i = 0;
|
||||
int min_x = 0;
|
||||
bool has_char = false;
|
||||
int x = 0;
|
||||
while (str[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph(str + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
if (!this->get_glyphs().empty())
|
||||
x += this->get_glyphs()[0].glyph_data_->width;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = this->glyphs_[glyph_n];
|
||||
if (!has_char) {
|
||||
min_x = glyph.glyph_data_->offset_x;
|
||||
} else {
|
||||
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
|
||||
}
|
||||
x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
|
||||
|
||||
i += match_length;
|
||||
has_char = true;
|
||||
}
|
||||
*x_offset = min_x;
|
||||
*width = x - min_x;
|
||||
}
|
||||
void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text) {
|
||||
int i = 0;
|
||||
int x_at = x_start;
|
||||
while (text[i] != '\0') {
|
||||
int match_length;
|
||||
int glyph_n = this->match_next_glyph(text + i, &match_length);
|
||||
if (glyph_n < 0) {
|
||||
// Unknown char, skip
|
||||
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
|
||||
if (!this->get_glyphs().empty()) {
|
||||
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width;
|
||||
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
|
||||
x_at += glyph_width;
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Glyph &glyph = this->get_glyphs()[glyph_n];
|
||||
glyph.draw(x_at, y_start, display, color);
|
||||
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
|
||||
|
||||
i += match_length;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace font
|
||||
} // namespace esphome
|
||||
67
esphome/components/font/font.h
Normal file
67
esphome/components/font/font.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/datatypes.h"
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace font {
|
||||
|
||||
class Font;
|
||||
|
||||
struct GlyphData {
|
||||
const char *a_char;
|
||||
const uint8_t *data;
|
||||
int offset_x;
|
||||
int offset_y;
|
||||
int width;
|
||||
int height;
|
||||
};
|
||||
|
||||
class Glyph {
|
||||
public:
|
||||
Glyph(const GlyphData *data) : glyph_data_(data) {}
|
||||
|
||||
void draw(int x, int y, display::Display *display, Color color) const;
|
||||
|
||||
const char *get_char() const;
|
||||
|
||||
bool compare_to(const char *str) const;
|
||||
|
||||
int match_length(const char *str) const;
|
||||
|
||||
void scan_area(int *x1, int *y1, int *width, int *height) const;
|
||||
|
||||
protected:
|
||||
friend Font;
|
||||
|
||||
const GlyphData *glyph_data_;
|
||||
};
|
||||
|
||||
class Font : public display::BaseFont {
|
||||
public:
|
||||
/** Construct the font with the given glyphs.
|
||||
*
|
||||
* @param glyphs A vector of glyphs, must be sorted lexicographically.
|
||||
* @param baseline The y-offset from the top of the text to the baseline.
|
||||
* @param bottom The y-offset from the top of the text to the bottom (i.e. height).
|
||||
*/
|
||||
Font(const GlyphData *data, int data_nr, int baseline, int height);
|
||||
|
||||
int match_next_glyph(const char *str, int *match_length);
|
||||
|
||||
void print(int x_start, int y_start, display::Display *display, Color color, const char *text) override;
|
||||
void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override;
|
||||
inline int get_baseline() { return this->baseline_; }
|
||||
inline int get_height() { return this->height_; }
|
||||
|
||||
const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
|
||||
|
||||
protected:
|
||||
std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_;
|
||||
int baseline_;
|
||||
int height_;
|
||||
};
|
||||
|
||||
} // namespace font
|
||||
} // namespace esphome
|
||||
@@ -151,11 +151,13 @@ void FujitsuGeneralClimate::transmit_state() {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_QUIET:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO);
|
||||
break;
|
||||
// TODO Quiet / Silent
|
||||
}
|
||||
|
||||
// Set swing
|
||||
@@ -345,8 +347,9 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE);
|
||||
ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode);
|
||||
switch (recv_fan_mode) {
|
||||
// TODO No Quiet / Silent in ESPH
|
||||
case FUJITSU_GENERAL_FAN_SILENT:
|
||||
this->fan_mode = climate::CLIMATE_FAN_QUIET;
|
||||
break;
|
||||
case FUJITSU_GENERAL_FAN_LOW:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
|
||||
@@ -52,7 +52,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR {
|
||||
FujitsuGeneralClimate()
|
||||
: ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_BOTH}) {}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "graph.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -56,7 +56,7 @@ void GraphTrace::init(Graph *g) {
|
||||
this->data_.set_update_time_ms(g->get_duration() * 1000 / g->get_width());
|
||||
}
|
||||
|
||||
void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
|
||||
void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
|
||||
/// Plot border
|
||||
if (this->border_) {
|
||||
buff->horizontal_line(x_offset, y_offset, this->width_, color);
|
||||
@@ -303,7 +303,7 @@ void GraphLegend::init(Graph *g) {
|
||||
}
|
||||
}
|
||||
|
||||
void Graph::draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
|
||||
void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
|
||||
if (!legend_)
|
||||
return;
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// forward declare DisplayBuffer
|
||||
// forward declare Display
|
||||
namespace display {
|
||||
class DisplayBuffer;
|
||||
class Font;
|
||||
class Display;
|
||||
class BaseFont;
|
||||
} // namespace display
|
||||
|
||||
namespace graph {
|
||||
@@ -45,8 +45,8 @@ enum ValuePositionType {
|
||||
class GraphLegend {
|
||||
public:
|
||||
void init(Graph *g);
|
||||
void set_name_font(display::Font *font) { this->font_label_ = font; }
|
||||
void set_value_font(display::Font *font) { this->font_value_ = font; }
|
||||
void set_name_font(display::BaseFont *font) { this->font_label_ = font; }
|
||||
void set_value_font(display::BaseFont *font) { this->font_value_ = font; }
|
||||
void set_width(uint32_t width) { this->width_ = width; }
|
||||
void set_height(uint32_t height) { this->height_ = height; }
|
||||
void set_border(bool val) { this->border_ = val; }
|
||||
@@ -63,8 +63,8 @@ class GraphLegend {
|
||||
ValuePositionType values_{VALUE_POSITION_TYPE_AUTO};
|
||||
bool units_{true};
|
||||
DirectionType direction_{DIRECTION_TYPE_AUTO};
|
||||
display::Font *font_label_{nullptr};
|
||||
display::Font *font_value_{nullptr};
|
||||
display::BaseFont *font_label_{nullptr};
|
||||
display::BaseFont *font_value_{nullptr};
|
||||
// Calculated values
|
||||
Graph *parent_{nullptr};
|
||||
// (x0) (xs,ys) (xs,ys)
|
||||
@@ -133,8 +133,8 @@ class GraphTrace {
|
||||
|
||||
class Graph : public Component {
|
||||
public:
|
||||
void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color);
|
||||
void draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color);
|
||||
void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color);
|
||||
void draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color);
|
||||
|
||||
void setup() override;
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
152
esphome/components/grove_tb6612fng/__init__.py
Normal file
152
esphome/components/grove_tb6612fng/__init__.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.components import i2c
|
||||
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_CHANNEL,
|
||||
CONF_SPEED,
|
||||
CONF_DIRECTION,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CODEOWNERS = ["@max246"]
|
||||
|
||||
grove_tb6612fng_ns = cg.esphome_ns.namespace("grove_tb6612fng")
|
||||
GROVE_TB6612FNG = grove_tb6612fng_ns.class_(
|
||||
"GroveMotorDriveTB6612FNG", cg.Component, i2c.I2CDevice
|
||||
)
|
||||
GROVETB6612FNGMotorRunAction = grove_tb6612fng_ns.class_(
|
||||
"GROVETB6612FNGMotorRunAction", automation.Action
|
||||
)
|
||||
GROVETB6612FNGMotorBrakeAction = grove_tb6612fng_ns.class_(
|
||||
"GROVETB6612FNGMotorBrakeAction", automation.Action
|
||||
)
|
||||
GROVETB6612FNGMotorStopAction = grove_tb6612fng_ns.class_(
|
||||
"GROVETB6612FNGMotorStopAction", automation.Action
|
||||
)
|
||||
GROVETB6612FNGMotorStandbyAction = grove_tb6612fng_ns.class_(
|
||||
"GROVETB6612FNGMotorStandbyAction", automation.Action
|
||||
)
|
||||
GROVETB6612FNGMotorNoStandbyAction = grove_tb6612fng_ns.class_(
|
||||
"GROVETB6612FNGMotorNoStandbyAction", automation.Action
|
||||
)
|
||||
|
||||
DIRECTION_TYPE = {
|
||||
"FORWARD": 1,
|
||||
"BACKWARD": 2,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(GROVE_TB6612FNG),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x14))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"grove_tb6612fng.run",
|
||||
GROVETB6612FNGMotorRunAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
|
||||
cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)),
|
||||
cv.Required(CONF_SPEED): cv.templatable(cv.int_range(min=0, max=255)),
|
||||
cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
|
||||
template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16)
|
||||
template_speed = (
|
||||
template_speed if config[CONF_DIRECTION] == "FORWARD" else -template_speed
|
||||
)
|
||||
cg.add(var.set_channel(template_channel))
|
||||
cg.add(var.set_speed(template_speed))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"grove_tb6612fng.break",
|
||||
GROVETB6612FNGMotorBrakeAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
|
||||
cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
|
||||
cg.add(var.set_channel(template_channel))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"grove_tb6612fng.stop",
|
||||
GROVETB6612FNGMotorStopAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
|
||||
cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
|
||||
cg.add(var.set_channel(template_channel))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"grove_tb6612fng.standby",
|
||||
GROVETB6612FNGMotorStandbyAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"grove_tb6612fng.no_standby",
|
||||
GROVETB6612FNGMotorNoStandbyAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
return var
|
||||
171
esphome/components/grove_tb6612fng/grove_tb6612fng.cpp
Normal file
171
esphome/components/grove_tb6612fng/grove_tb6612fng.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
#include "grove_tb6612fng.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace grove_tb6612fng {
|
||||
|
||||
static const char *const TAG = "GroveMotorDriveTB6612FNG";
|
||||
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE = 0x00;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STOP = 0x01;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CW = 0x02;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CCW = 0x03;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY = 0x04;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY = 0x05;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN = 0x06;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP = 0x07;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN = 0x08;
|
||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR = 0x11;
|
||||
|
||||
void GroveMotorDriveTB6612FNG::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "GroveMotorDriveTB6612FNG:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up Grove Motor Drive TB6612FNG ...");
|
||||
if (!this->standby()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool GroveMotorDriveTB6612FNG::standby() {
|
||||
uint8_t status = 0;
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY, &status, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Set standby failed!");
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GroveMotorDriveTB6612FNG::not_standby() {
|
||||
uint8_t status = 0;
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY, &status, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Set not standby failed!");
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::set_i2c_addr(uint8_t addr) {
|
||||
if (addr == 0x00 || addr >= 0x80) {
|
||||
return;
|
||||
}
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR, &addr, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Set new i2c address failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
this->set_i2c_address(addr);
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::dc_motor_run(uint8_t channel, int16_t speed) {
|
||||
speed = clamp<int16_t>(speed, -255, 255);
|
||||
|
||||
buffer_[0] = channel;
|
||||
if (speed >= 0) {
|
||||
buffer_[1] = speed;
|
||||
} else {
|
||||
buffer_[1] = (uint8_t) (-speed);
|
||||
}
|
||||
|
||||
if (speed >= 0) {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Run motor failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, buffer_, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Run motor failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::dc_motor_brake(uint8_t channel) {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE, &channel, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Break motor failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::dc_motor_stop(uint8_t channel) {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STOP, &channel, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Stop dc motor failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm) {
|
||||
uint8_t cw = 0;
|
||||
// 0.1ms_per_step
|
||||
uint16_t ms_per_step = 0;
|
||||
|
||||
if (steps > 0) {
|
||||
cw = 1;
|
||||
}
|
||||
// stop
|
||||
else if (steps == 0) {
|
||||
this->stepper_stop();
|
||||
return;
|
||||
} else if (steps == INT16_MIN) {
|
||||
steps = INT16_MAX;
|
||||
} else {
|
||||
steps = -steps;
|
||||
}
|
||||
|
||||
rpm = clamp<uint16_t>(rpm, 1, 300);
|
||||
|
||||
ms_per_step = (uint16_t) (3000.0 / (float) rpm);
|
||||
buffer_[0] = mode;
|
||||
buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw
|
||||
buffer_[2] = steps;
|
||||
buffer_[3] = (steps >> 8);
|
||||
buffer_[4] = ms_per_step;
|
||||
buffer_[5] = (ms_per_step >> 8);
|
||||
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Run stepper failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::stepper_stop() {
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Send stop stepper failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw) {
|
||||
// 4=>infinite ccw 5=>infinite cw
|
||||
uint8_t cw = (is_cw) ? 5 : 4;
|
||||
// 0.1ms_per_step
|
||||
uint16_t ms_per_step = 0;
|
||||
|
||||
rpm = clamp<uint16_t>(rpm, 1, 300);
|
||||
ms_per_step = (uint16_t) (3000.0 / (float) rpm);
|
||||
|
||||
buffer_[0] = mode;
|
||||
buffer_[1] = cw; //(cw=1) => cw; (cw=0) => ccw
|
||||
buffer_[2] = ms_per_step;
|
||||
buffer_[3] = (ms_per_step >> 8);
|
||||
|
||||
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN, buffer_, 4) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Write stepper keep run failed");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} // namespace grove_tb6612fng
|
||||
} // namespace esphome
|
||||
208
esphome/components/grove_tb6612fng/grove_tb6612fng.h
Normal file
208
esphome/components/grove_tb6612fng/grove_tb6612fng.h
Normal file
@@ -0,0 +1,208 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/automation.h"
|
||||
//#include "esphome/core/helpers.h"
|
||||
|
||||
/*
|
||||
Grove_Motor_Driver_TB6612FNG.h
|
||||
A library for the Grove - Motor Driver(TB6612FNG)
|
||||
Copyright (c) 2018 seeed technology co., ltd.
|
||||
Website : www.seeed.cc
|
||||
Author : Jerry Yip
|
||||
Create Time: 2018-06
|
||||
Version : 0.1
|
||||
Change Log :
|
||||
The MIT License (MIT)
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace esphome {
|
||||
namespace grove_tb6612fng {
|
||||
|
||||
enum MotorChannelTypeT {
|
||||
MOTOR_CHA = 0,
|
||||
MOTOR_CHB = 1,
|
||||
};
|
||||
|
||||
enum StepperModeTypeT {
|
||||
FULL_STEP = 0,
|
||||
WAVE_DRIVE = 1,
|
||||
HALF_STEP = 2,
|
||||
MICRO_STEPPING = 3,
|
||||
};
|
||||
|
||||
class GroveMotorDriveTB6612FNG : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
/*************************************************************
|
||||
Description
|
||||
Enter standby mode. Normally you don't need to call this, except that
|
||||
you have called notStandby() before.
|
||||
Parameter
|
||||
Null.
|
||||
Return
|
||||
True/False.
|
||||
*************************************************************/
|
||||
bool standby();
|
||||
|
||||
/*************************************************************
|
||||
Description
|
||||
Exit standby mode. Motor driver does't do any action at this mode.
|
||||
Parameter
|
||||
Null.
|
||||
Return
|
||||
True/False.
|
||||
*************************************************************/
|
||||
bool not_standby();
|
||||
|
||||
/*************************************************************
|
||||
Description
|
||||
Set an new I2C address.
|
||||
Parameter
|
||||
addr: 0x01~0x7f
|
||||
Return
|
||||
Null.
|
||||
*************************************************************/
|
||||
void set_i2c_addr(uint8_t addr);
|
||||
|
||||
/*************************************************************
|
||||
Description
|
||||
Drive a motor.
|
||||
Parameter
|
||||
chl: MOTOR_CHA or MOTOR_CHB
|
||||
speed: -255~255, if speed > 0, motor moves clockwise.
|
||||
Note that there is always a starting speed(a starting voltage) for motor.
|
||||
If the input voltage is 5V, the starting speed should larger than 100 or
|
||||
smaller than -100.
|
||||
Return
|
||||
Null.
|
||||
*************************************************************/
|
||||
void dc_motor_run(uint8_t channel, int16_t speed);
|
||||
|
||||
/*************************************************************
|
||||
Description
|
||||
Brake, stop the motor immediately
|
||||
Parameter
|
||||
chl: MOTOR_CHA or MOTOR_CHB
|
||||
Return
|
||||
Null.
|
||||
*************************************************************/
|
||||
void dc_motor_brake(uint8_t channel);
|
||||
|
||||
/*************************************************************
|
||||
Description
|
||||
Stop the motor slowly.
|
||||
Parameter
|
||||
chl: MOTOR_CHA or MOTOR_CHB
|
||||
Return
|
||||
Null.
|
||||
*************************************************************/
|
||||
void dc_motor_stop(uint8_t channel);
|
||||
|
||||
/*************************************************************
|
||||
Description
|
||||
Drive a stepper.
|
||||
Parameter
|
||||
mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING,
|
||||
for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png
|
||||
steps: The number of steps to run, range from -32768 to 32767.
|
||||
When steps = 0, the stepper stops.
|
||||
When steps > 0, the stepper runs clockwise. When steps < 0, the stepper runs anticlockwise.
|
||||
rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300.
|
||||
Note that high rpm will lead to step lose, so rpm should not be larger than 150.
|
||||
Return
|
||||
Null.
|
||||
*************************************************************/
|
||||
void stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm);
|
||||
|
||||
/*************************************************************
|
||||
Description
|
||||
Stop a stepper.
|
||||
Parameter
|
||||
Null.
|
||||
Return
|
||||
Null.
|
||||
*************************************************************/
|
||||
void stepper_stop();
|
||||
|
||||
// keeps moving(direction same as the last move, default to clockwise)
|
||||
/*************************************************************
|
||||
Description
|
||||
Keep a stepper running.
|
||||
Parameter
|
||||
mode: 4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING,
|
||||
for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png
|
||||
rpm: Revolutions per minute, the speed of a stepper, range from 1 to 300.
|
||||
Note that high rpm will lead to step lose, so rpm should not be larger than 150.
|
||||
is_cw: Set the running direction, true for clockwise and false for anti-clockwise.
|
||||
Return
|
||||
Null.
|
||||
*************************************************************/
|
||||
void stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw);
|
||||
|
||||
private:
|
||||
uint8_t buffer_[16];
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class GROVETB6612FNGMotorRunAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, channel)
|
||||
TEMPLATABLE_VALUE(uint16_t, speed)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto channel = this->channel_.value(x...);
|
||||
auto speed = this->speed_.value(x...);
|
||||
this->parent_->dc_motor_run(channel, speed);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class GROVETB6612FNGMotorBrakeAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, channel)
|
||||
|
||||
void play(Ts... x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); }
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class GROVETB6612FNGMotorStopAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, channel)
|
||||
|
||||
void play(Ts... x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); }
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class GROVETB6612FNGMotorStandbyAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->standby(); }
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class GROVETB6612FNGMotorNoStandbyAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->not_standby(); }
|
||||
};
|
||||
|
||||
} // namespace grove_tb6612fng
|
||||
} // namespace esphome
|
||||
@@ -9,11 +9,42 @@ static const char *const TAG = "growatt_solar";
|
||||
static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04;
|
||||
static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
|
||||
|
||||
void GrowattSolar::loop() {
|
||||
// If update() was unable to send we retry until we can send.
|
||||
if (!this->waiting_to_update_)
|
||||
return;
|
||||
update();
|
||||
}
|
||||
|
||||
void GrowattSolar::update() {
|
||||
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_send_ < this->get_update_interval() / 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
|
||||
if (this->waiting_for_response()) {
|
||||
this->waiting_to_update_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this->waiting_to_update_ = false;
|
||||
this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
|
||||
this->last_send_ = millis();
|
||||
}
|
||||
|
||||
void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
// Other components might be sending commands to our device. But we don't get called with enough
|
||||
// context to know what is what. So if we didn't do a send, we ignore the data.
|
||||
if (!this->last_send_)
|
||||
return;
|
||||
this->last_send_ = 0;
|
||||
|
||||
// Also ignore the data if the message is too short. Otherwise we will publish invalid values.
|
||||
if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2)
|
||||
return;
|
||||
|
||||
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
|
||||
@@ -19,6 +19,7 @@ enum GrowattProtocolVersion {
|
||||
|
||||
class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
|
||||
public:
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void dump_config() override;
|
||||
@@ -55,6 +56,9 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool waiting_to_update_;
|
||||
uint32_t last_send_;
|
||||
|
||||
struct GrowattPhase {
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
CODEOWNERS = ["@Yarikx"]
|
||||
|
||||
130
esphome/components/haier/automation.h
Normal file
130
esphome/components/haier/automation.h
Normal file
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "haier_base.h"
|
||||
#include "hon_climate.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace haier {
|
||||
|
||||
template<typename... Ts> class DisplayOnAction : public Action<Ts...> {
|
||||
public:
|
||||
DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->set_display_state(true); }
|
||||
|
||||
protected:
|
||||
HaierClimateBase *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayOffAction : public Action<Ts...> {
|
||||
public:
|
||||
DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->set_display_state(false); }
|
||||
|
||||
protected:
|
||||
HaierClimateBase *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BeeperOnAction : public Action<Ts...> {
|
||||
public:
|
||||
BeeperOnAction(HonClimate *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->set_beeper_state(true); }
|
||||
|
||||
protected:
|
||||
HonClimate *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BeeperOffAction : public Action<Ts...> {
|
||||
public:
|
||||
BeeperOffAction(HonClimate *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->set_beeper_state(false); }
|
||||
|
||||
protected:
|
||||
HonClimate *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class VerticalAirflowAction : public Action<Ts...> {
|
||||
public:
|
||||
VerticalAirflowAction(HonClimate *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(AirflowVerticalDirection, direction)
|
||||
void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); }
|
||||
|
||||
protected:
|
||||
HonClimate *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class HorizontalAirflowAction : public Action<Ts...> {
|
||||
public:
|
||||
HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction)
|
||||
void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); }
|
||||
|
||||
protected:
|
||||
HonClimate *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class HealthOnAction : public Action<Ts...> {
|
||||
public:
|
||||
HealthOnAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->set_health_mode(true); }
|
||||
|
||||
protected:
|
||||
HaierClimateBase *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class HealthOffAction : public Action<Ts...> {
|
||||
public:
|
||||
HealthOffAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->set_health_mode(false); }
|
||||
|
||||
protected:
|
||||
HaierClimateBase *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class StartSelfCleaningAction : public Action<Ts...> {
|
||||
public:
|
||||
StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->start_self_cleaning(); }
|
||||
|
||||
protected:
|
||||
HonClimate *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class StartSteriCleaningAction : public Action<Ts...> {
|
||||
public:
|
||||
StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->start_steri_cleaning(); }
|
||||
|
||||
protected:
|
||||
HonClimate *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class PowerOnAction : public Action<Ts...> {
|
||||
public:
|
||||
PowerOnAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->send_power_on_command(); }
|
||||
|
||||
protected:
|
||||
HaierClimateBase *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class PowerOffAction : public Action<Ts...> {
|
||||
public:
|
||||
PowerOffAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->send_power_off_command(); }
|
||||
|
||||
protected:
|
||||
HaierClimateBase *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class PowerToggleAction : public Action<Ts...> {
|
||||
public:
|
||||
PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||
void play(Ts... x) { this->parent_->toggle_power(); }
|
||||
|
||||
protected:
|
||||
HaierClimateBase *parent_;
|
||||
};
|
||||
|
||||
} // namespace haier
|
||||
} // namespace esphome
|
||||
@@ -1,43 +1,364 @@
|
||||
from esphome.components import climate
|
||||
import logging
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import uart
|
||||
from esphome.components.climate import ClimateSwingMode
|
||||
from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES
|
||||
import esphome.final_validate as fv
|
||||
from esphome.components import uart, sensor, climate, logger
|
||||
from esphome import automation
|
||||
from esphome.const import (
|
||||
CONF_BEEPER,
|
||||
CONF_ID,
|
||||
CONF_LEVEL,
|
||||
CONF_LOGGER,
|
||||
CONF_LOGS,
|
||||
CONF_MAX_TEMPERATURE,
|
||||
CONF_MIN_TEMPERATURE,
|
||||
CONF_PROTOCOL,
|
||||
CONF_SUPPORTED_MODES,
|
||||
CONF_SUPPORTED_SWING_MODES,
|
||||
CONF_VISUAL,
|
||||
CONF_WIFI,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_THERMOMETER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
)
|
||||
from esphome.components.climate import (
|
||||
ClimateSwingMode,
|
||||
ClimateMode,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PROTOCOL_MIN_TEMPERATURE = 16.0
|
||||
PROTOCOL_MAX_TEMPERATURE = 30.0
|
||||
PROTOCOL_TEMPERATURE_STEP = 1.0
|
||||
|
||||
CODEOWNERS = ["@paveldn"]
|
||||
AUTO_LOAD = ["sensor"]
|
||||
DEPENDENCIES = ["climate", "uart"]
|
||||
CONF_WIFI_SIGNAL = "wifi_signal"
|
||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
|
||||
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
|
||||
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
|
||||
|
||||
|
||||
PROTOCOL_HON = "HON"
|
||||
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
|
||||
PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2]
|
||||
|
||||
haier_ns = cg.esphome_ns.namespace("haier")
|
||||
HaierClimate = haier_ns.class_(
|
||||
"HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice
|
||||
HaierClimateBase = haier_ns.class_(
|
||||
"HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component
|
||||
)
|
||||
HonClimate = haier_ns.class_("HonClimate", HaierClimateBase)
|
||||
Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase)
|
||||
|
||||
ALLOWED_CLIMATE_SWING_MODES = {
|
||||
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
|
||||
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
|
||||
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
|
||||
|
||||
AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection")
|
||||
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
|
||||
"UP": AirflowVerticalDirection.UP,
|
||||
"CENTER": AirflowVerticalDirection.CENTER,
|
||||
"DOWN": AirflowVerticalDirection.DOWN,
|
||||
}
|
||||
|
||||
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
|
||||
AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection")
|
||||
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
|
||||
"LEFT": AirflowHorizontalDirection.LEFT,
|
||||
"CENTER": AirflowHorizontalDirection.CENTER,
|
||||
"RIGHT": AirflowHorizontalDirection.RIGHT,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
SUPPORTED_SWING_MODES_OPTIONS = {
|
||||
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available
|
||||
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available
|
||||
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
|
||||
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
|
||||
}
|
||||
|
||||
SUPPORTED_CLIMATE_MODES_OPTIONS = {
|
||||
"OFF": ClimateMode.CLIMATE_MODE_OFF, # always available
|
||||
"AUTO": ClimateMode.CLIMATE_MODE_AUTO, # always available
|
||||
"COOL": ClimateMode.CLIMATE_MODE_COOL,
|
||||
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
|
||||
"DRY": ClimateMode.CLIMATE_MODE_DRY,
|
||||
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
|
||||
}
|
||||
|
||||
|
||||
def validate_visual(config):
|
||||
if CONF_VISUAL in config:
|
||||
visual_config = config[CONF_VISUAL]
|
||||
if CONF_MIN_TEMPERATURE in visual_config:
|
||||
min_temp = visual_config[CONF_MIN_TEMPERATURE]
|
||||
if min_temp < PROTOCOL_MIN_TEMPERATURE:
|
||||
raise cv.Invalid(
|
||||
f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}"
|
||||
)
|
||||
else:
|
||||
config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE
|
||||
if CONF_MAX_TEMPERATURE in visual_config:
|
||||
max_temp = visual_config[CONF_MAX_TEMPERATURE]
|
||||
if max_temp > PROTOCOL_MAX_TEMPERATURE:
|
||||
raise cv.Invalid(
|
||||
f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}"
|
||||
)
|
||||
else:
|
||||
config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE
|
||||
else:
|
||||
config[CONF_VISUAL] = {
|
||||
CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE,
|
||||
CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE,
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
BASE_CONFIG_SCHEMA = (
|
||||
climate.CLIMATE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HaierClimate),
|
||||
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
|
||||
validate_swing_modes
|
||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||
cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_SUPPORTED_SWING_MODES,
|
||||
default=[
|
||||
"OFF",
|
||||
"VERTICAL",
|
||||
"HORIZONTAL",
|
||||
"BOTH",
|
||||
],
|
||||
): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("5s"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.typed_schema(
|
||||
{
|
||||
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Smartair2Climate),
|
||||
}
|
||||
),
|
||||
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HonClimate),
|
||||
cv.Optional(CONF_WIFI_SIGNAL, default=True): cv.boolean,
|
||||
cv.Optional(CONF_BEEPER, default=True): cv.boolean,
|
||||
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
),
|
||||
},
|
||||
key=CONF_PROTOCOL,
|
||||
default_type=PROTOCOL_SMARTAIR2,
|
||||
upper=True,
|
||||
),
|
||||
validate_visual,
|
||||
)
|
||||
|
||||
|
||||
# Actions
|
||||
DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action)
|
||||
DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action)
|
||||
BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action)
|
||||
BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action)
|
||||
StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action)
|
||||
StartSteriCleaningAction = haier_ns.class_(
|
||||
"StartSteriCleaningAction", automation.Action
|
||||
)
|
||||
VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action)
|
||||
HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action)
|
||||
HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action)
|
||||
HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action)
|
||||
PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action)
|
||||
PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action)
|
||||
PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action)
|
||||
|
||||
HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HaierClimateBase),
|
||||
}
|
||||
)
|
||||
|
||||
HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HonClimate),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA
|
||||
)
|
||||
async def display_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA
|
||||
)
|
||||
async def beeper_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
|
||||
|
||||
# Start self cleaning or steri-cleaning action action
|
||||
@automation.register_action(
|
||||
"climate.haier.start_self_cleaning",
|
||||
StartSelfCleaningAction,
|
||||
HAIER_HON_BASE_ACTION_SCHEMA,
|
||||
)
|
||||
@automation.register_action(
|
||||
"climate.haier.start_steri_cleaning",
|
||||
StartSteriCleaningAction,
|
||||
HAIER_HON_BASE_ACTION_SCHEMA,
|
||||
)
|
||||
async def start_cleaning_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
|
||||
|
||||
# Set vertical airflow direction action
|
||||
@automation.register_action(
|
||||
"climate.haier.set_vertical_airflow",
|
||||
VerticalAirflowAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HonClimate),
|
||||
cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable(
|
||||
cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection
|
||||
)
|
||||
cg.add(var.set_direction(template_))
|
||||
return var
|
||||
|
||||
|
||||
# Set horizontal airflow direction action
|
||||
@automation.register_action(
|
||||
"climate.haier.set_horizontal_airflow",
|
||||
HorizontalAirflowAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HonClimate),
|
||||
cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable(
|
||||
cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection
|
||||
)
|
||||
cg.add(var.set_direction(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA
|
||||
)
|
||||
async def health_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA
|
||||
)
|
||||
@automation.register_action(
|
||||
"climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA
|
||||
)
|
||||
async def power_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
full_config = fv.full_config.get()
|
||||
if CONF_LOGGER in full_config:
|
||||
_level = "NONE"
|
||||
logger_config = full_config[CONF_LOGGER]
|
||||
if CONF_LOGS in logger_config:
|
||||
if "haier.protocol" in logger_config[CONF_LOGS]:
|
||||
_level = logger_config[CONF_LOGS]["haier.protocol"]
|
||||
else:
|
||||
_level = logger_config[CONF_LEVEL]
|
||||
_LOGGER.info("Detected log level for Haier protocol: %s", _level)
|
||||
if _level not in logger.LOG_LEVEL_SEVERITY:
|
||||
raise cv.Invalid("Unknown log level for Haier protocol")
|
||||
_severity = logger.LOG_LEVEL_SEVERITY.index(_level)
|
||||
cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}")
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"No logger component found, logging for Haier protocol is disabled"
|
||||
)
|
||||
cg.add_build_flag("-DHAIER_LOG_LEVEL=0")
|
||||
if (
|
||||
(CONF_WIFI_SIGNAL in config)
|
||||
and (config[CONF_WIFI_SIGNAL])
|
||||
and CONF_WIFI not in full_config
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add(haier_ns.init_haier_protocol_logging())
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
|
||||
if (CONF_WIFI_SIGNAL in config) and (config[CONF_WIFI_SIGNAL]):
|
||||
cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
|
||||
if CONF_BEEPER in config:
|
||||
cg.add(var.set_beeper_state(config[CONF_BEEPER]))
|
||||
if CONF_OUTDOOR_TEMPERATURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
|
||||
cg.add(var.set_outdoor_temperature_sensor(sens))
|
||||
if CONF_SUPPORTED_MODES in config:
|
||||
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
|
||||
if CONF_SUPPORTED_SWING_MODES in config:
|
||||
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
|
||||
# https://github.com/paveldn/HaierProtocol
|
||||
cg.add_library("pavlodn/HaierProtocol", "0.9.18")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user