mirror of
https://github.com/esphome/esphome.git
synced 2024-12-23 22:14:54 +01:00
commit
ab32dd7420
199 changed files with 9243 additions and 2024 deletions
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:
|
common:
|
||||||
name: Create common environment
|
name: Create common environment
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
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 }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
id: python
|
||||||
uses: actions/setup-python@v4.6.0
|
uses: actions/setup-python@v4.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
@ -39,7 +45,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
# yamllint disable-line rule:line-length
|
# 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
|
- name: Create Python virtual environment
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
|
@ -66,12 +72,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
|
||||||
- name: Run black
|
- name: Run black
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
@ -88,12 +93,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
|
||||||
- name: Run flake8
|
- name: Run flake8
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
@ -110,12 +114,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
|
||||||
- name: Run pylint
|
- name: Run pylint
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
@ -132,12 +135,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
|
||||||
- name: Run pyupgrade
|
- name: Run pyupgrade
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
@ -154,12 +156,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
|
||||||
- name: Register matcher
|
- name: Register matcher
|
||||||
run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
|
run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
|
||||||
- name: Run script/ci-custom
|
- name: Run script/ci-custom
|
||||||
|
@ -176,12 +177,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
|
||||||
- name: Register matcher
|
- name: Register matcher
|
||||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||||
- name: Run pytest
|
- name: Run pytest
|
||||||
|
@ -197,12 +197,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
|
||||||
- name: Install clang-format
|
- name: Install clang-format
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
@ -237,18 +236,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
|
|
||||||
- name: Cache platformio
|
|
||||||
uses: actions/cache@v3.3.1
|
|
||||||
with:
|
|
||||||
path: ~/.platformio
|
|
||||||
# yamllint disable-line rule:line-length
|
|
||||||
key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }}
|
|
||||||
- name: Run esphome compile tests/test${{ matrix.file }}.yaml
|
- name: Run esphome compile tests/test${{ matrix.file }}.yaml
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
|
@ -300,13 +292,11 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.5.2
|
uses: actions/checkout@v3.5.2
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python
|
||||||
uses: actions/cache/restore@v3.3.1
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
path: venv
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
# yamllint disable-line rule:line-length
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
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
|
|
||||||
- name: Cache platformio
|
- name: Cache platformio
|
||||||
uses: actions/cache@v3.3.1
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
|
|
15
.github/workflows/sync-device-classes.yml
vendored
15
.github/workflows/sync-device-classes.yml
vendored
|
@ -6,14 +6,12 @@ on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '45 6 * * *'
|
- cron: '45 6 * * *'
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
name: Sync Device Classes
|
name: Sync Device Classes
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'esphome/esphome'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -38,15 +36,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python ./script/sync-device_class.py
|
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
|
- name: Commit changes
|
||||||
uses: peter-evans/create-pull-request@v5
|
uses: peter-evans/create-pull-request@v5
|
||||||
with:
|
with:
|
||||||
|
@ -56,5 +45,5 @@ jobs:
|
||||||
branch: sync/device-classes
|
branch: sync/device-classes
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
title: "Synchronise Device Classes from Home Assistant"
|
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 }}
|
token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }}
|
||||||
|
|
|
@ -27,7 +27,7 @@ repos:
|
||||||
- --branch=release
|
- --branch=release
|
||||||
- --branch=beta
|
- --branch=beta
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.4.0
|
rev: v3.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py39-plus]
|
args: [--py39-plus]
|
||||||
|
|
13
CODEOWNERS
13
CODEOWNERS
|
@ -17,10 +17,11 @@ esphome/components/adc/* @esphome/core
|
||||||
esphome/components/adc128s102/* @DeerMaximum
|
esphome/components/adc128s102/* @DeerMaximum
|
||||||
esphome/components/addressable_light/* @justfalter
|
esphome/components/addressable_light/* @justfalter
|
||||||
esphome/components/airthings_ble/* @jeromelaban
|
esphome/components/airthings_ble/* @jeromelaban
|
||||||
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
|
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
|
||||||
esphome/components/airthings_wave_mini/* @ncareau
|
esphome/components/airthings_wave_mini/* @ncareau
|
||||||
esphome/components/airthings_wave_plus/* @jeromelaban
|
esphome/components/airthings_wave_plus/* @jeromelaban
|
||||||
esphome/components/alarm_control_panel/* @grahambrown11
|
esphome/components/alarm_control_panel/* @grahambrown11
|
||||||
|
esphome/components/alpha3/* @jan-hofmeier
|
||||||
esphome/components/am43/* @buxtronix
|
esphome/components/am43/* @buxtronix
|
||||||
esphome/components/am43/cover/* @buxtronix
|
esphome/components/am43/cover/* @buxtronix
|
||||||
esphome/components/am43/sensor/* @buxtronix
|
esphome/components/am43/sensor/* @buxtronix
|
||||||
|
@ -31,6 +32,7 @@ esphome/components/api/* @OttoWinter
|
||||||
esphome/components/as7341/* @mrgnr
|
esphome/components/as7341/* @mrgnr
|
||||||
esphome/components/async_tcp/* @OttoWinter
|
esphome/components/async_tcp/* @OttoWinter
|
||||||
esphome/components/atc_mithermometer/* @ahpohl
|
esphome/components/atc_mithermometer/* @ahpohl
|
||||||
|
esphome/components/atm90e26/* @danieltwagner
|
||||||
esphome/components/b_parasite/* @rbaron
|
esphome/components/b_parasite/* @rbaron
|
||||||
esphome/components/ballu/* @bazuchan
|
esphome/components/ballu/* @bazuchan
|
||||||
esphome/components/bang_bang/* @OttoWinter
|
esphome/components/bang_bang/* @OttoWinter
|
||||||
|
@ -76,6 +78,7 @@ esphome/components/display_menu_base/* @numo68
|
||||||
esphome/components/dps310/* @kbx81
|
esphome/components/dps310/* @kbx81
|
||||||
esphome/components/ds1307/* @badbadc0ffee
|
esphome/components/ds1307/* @badbadc0ffee
|
||||||
esphome/components/dsmr/* @glmnet @zuidwijk
|
esphome/components/dsmr/* @glmnet @zuidwijk
|
||||||
|
esphome/components/duty_time/* @dudanov
|
||||||
esphome/components/ee895/* @Stock-M
|
esphome/components/ee895/* @Stock-M
|
||||||
esphome/components/ektf2232/* @jesserockz
|
esphome/components/ektf2232/* @jesserockz
|
||||||
esphome/components/ens210/* @itn3rd77
|
esphome/components/ens210/* @itn3rd77
|
||||||
|
@ -102,8 +105,9 @@ esphome/components/gp8403/* @jesserockz
|
||||||
esphome/components/gpio/* @esphome/core
|
esphome/components/gpio/* @esphome/core
|
||||||
esphome/components/gps/* @coogle
|
esphome/components/gps/* @coogle
|
||||||
esphome/components/graph/* @synco
|
esphome/components/graph/* @synco
|
||||||
|
esphome/components/grove_tb6612fng/* @max246
|
||||||
esphome/components/growatt_solar/* @leeuwte
|
esphome/components/growatt_solar/* @leeuwte
|
||||||
esphome/components/haier/* @Yarikx
|
esphome/components/haier/* @paveldn
|
||||||
esphome/components/havells_solar/* @sourabhjaiswal
|
esphome/components/havells_solar/* @sourabhjaiswal
|
||||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||||
esphome/components/hbridge/light/* @DotNetDann
|
esphome/components/hbridge/light/* @DotNetDann
|
||||||
|
@ -200,6 +204,7 @@ esphome/components/output/* @esphome/core
|
||||||
esphome/components/pca6416a/* @Mat931
|
esphome/components/pca6416a/* @Mat931
|
||||||
esphome/components/pca9554/* @hwstar
|
esphome/components/pca9554/* @hwstar
|
||||||
esphome/components/pcf85063/* @brogon
|
esphome/components/pcf85063/* @brogon
|
||||||
|
esphome/components/pcf8563/* @KoenBreeman
|
||||||
esphome/components/pid/* @OttoWinter
|
esphome/components/pid/* @OttoWinter
|
||||||
esphome/components/pipsolar/* @andreashergert1984
|
esphome/components/pipsolar/* @andreashergert1984
|
||||||
esphome/components/pm1006/* @habbie
|
esphome/components/pm1006/* @habbie
|
||||||
|
@ -294,6 +299,7 @@ esphome/components/tof10120/* @wstrzalka
|
||||||
esphome/components/toshiba/* @kbx81
|
esphome/components/toshiba/* @kbx81
|
||||||
esphome/components/touchscreen/* @jesserockz
|
esphome/components/touchscreen/* @jesserockz
|
||||||
esphome/components/tsl2591/* @wjcarpenter
|
esphome/components/tsl2591/* @wjcarpenter
|
||||||
|
esphome/components/tt21100/* @kroimon
|
||||||
esphome/components/tuya/binary_sensor/* @jesserockz
|
esphome/components/tuya/binary_sensor/* @jesserockz
|
||||||
esphome/components/tuya/climate/* @jesserockz
|
esphome/components/tuya/climate/* @jesserockz
|
||||||
esphome/components/tuya/number/* @frankiboy1
|
esphome/components/tuya/number/* @frankiboy1
|
||||||
|
@ -310,6 +316,7 @@ esphome/components/version/* @esphome/core
|
||||||
esphome/components/voice_assistant/* @jesserockz
|
esphome/components/voice_assistant/* @jesserockz
|
||||||
esphome/components/wake_on_lan/* @willwill2will54
|
esphome/components/wake_on_lan/* @willwill2will54
|
||||||
esphome/components/web_server_base/* @OttoWinter
|
esphome/components/web_server_base/* @OttoWinter
|
||||||
|
esphome/components/web_server_idf/* @dentra
|
||||||
esphome/components/whirlpool/* @glmnet
|
esphome/components/whirlpool/* @glmnet
|
||||||
esphome/components/whynter/* @aeonsablaze
|
esphome/components/whynter/* @aeonsablaze
|
||||||
esphome/components/wiegand/* @ssieb
|
esphome/components/wiegand/* @ssieb
|
||||||
|
@ -319,4 +326,6 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
||||||
esphome/components/xiaomi_mhoc303/* @drug123
|
esphome/components/xiaomi_mhoc303/* @drug123
|
||||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
|
esphome/components/xiaomi_mhoc401/* @vevsvevs
|
||||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
||||||
|
esphome/components/xl9535/* @mreditor97
|
||||||
esphome/components/xpt2046/* @nielsnl68 @numo68
|
esphome/components/xpt2046/* @nielsnl68 @numo68
|
||||||
|
esphome/components/zio_ultrasonic/* @kahrendt
|
||||||
|
|
|
@ -32,7 +32,7 @@ from esphome.const import (
|
||||||
SECRETS_FILES,
|
SECRETS_FILES,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, EsphomeError, coroutine
|
from esphome.core import CORE, EsphomeError, coroutine
|
||||||
from esphome.helpers import indent
|
from esphome.helpers import indent, is_ip_address
|
||||||
from esphome.util import (
|
from esphome.util import (
|
||||||
run_external_command,
|
run_external_command,
|
||||||
run_external_process,
|
run_external_process,
|
||||||
|
@ -308,8 +308,10 @@ def upload_program(config, args, host):
|
||||||
password = ota_conf.get(CONF_PASSWORD, "")
|
password = ota_conf.get(CONF_PASSWORD, "")
|
||||||
|
|
||||||
if (
|
if (
|
||||||
get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]
|
not is_ip_address(CORE.address)
|
||||||
) and CONF_MQTT in config:
|
and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED])
|
||||||
|
and CONF_MQTT in config
|
||||||
|
):
|
||||||
from esphome import mqtt
|
from esphome import mqtt
|
||||||
|
|
||||||
host = mqtt.get_esphome_device_ip(
|
host = mqtt.get_esphome_device_ip(
|
||||||
|
|
|
@ -24,6 +24,7 @@ ATTENUATION_MODES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
|
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
|
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
|
||||||
# pin to adc1 channel mapping
|
# 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):
|
def validate_adc_pin(value):
|
||||||
if str(value).upper() == "VCC":
|
if str(value).upper() == "VCC":
|
||||||
|
@ -89,11 +133,18 @@ def validate_adc_pin(value):
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
value = pins.internal_gpio_input_pin_number(value)
|
value = pins.internal_gpio_input_pin_number(value)
|
||||||
variant = get_esp32_variant()
|
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")
|
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")
|
raise cv.Invalid(f"{variant} doesn't support ADC on this pin")
|
||||||
|
|
||||||
return pins.internal_gpio_input_pin_schema(value)
|
return pins.internal_gpio_input_pin_schema(value)
|
||||||
|
|
||||||
if CORE.is_esp8266:
|
if CORE.is_esp8266:
|
||||||
|
@ -104,7 +155,7 @@ def validate_adc_pin(value):
|
||||||
)
|
)
|
||||||
|
|
||||||
if value != 17: # A0
|
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(
|
return pins.gpio_pin_schema(
|
||||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||||
)(value)
|
)(value)
|
||||||
|
@ -112,7 +163,7 @@ def validate_adc_pin(value):
|
||||||
if CORE.is_rp2040:
|
if CORE.is_rp2040:
|
||||||
value = pins.internal_gpio_input_pin_number(value)
|
value = pins.internal_gpio_input_pin_number(value)
|
||||||
if value not in (26, 27, 28, 29):
|
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)
|
return pins.internal_gpio_input_pin_schema(value)
|
||||||
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
|
@ -20,20 +20,20 @@ namespace adc {
|
||||||
|
|
||||||
static const char *const TAG = "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
|
#ifdef USE_ESP32
|
||||||
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
|
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
|
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
|
||||||
#if USE_ESP32_VARIANT_ESP32S2
|
#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
|
#else
|
||||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 12;
|
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit)
|
static const int32_t ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; // 4095 (12 bit) or 8191 (13 bit)
|
||||||
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit)
|
static const int32_t ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; // 2048 (12 bit) or 4096 (13 bit)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_RP2040
|
#ifdef USE_RP2040
|
||||||
|
@ -47,14 +47,21 @@ extern "C"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
if (channel1_ != ADC1_CHANNEL_MAX) {
|
||||||
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
|
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
|
||||||
if (!autorange_) {
|
if (!autorange_) {
|
||||||
adc1_config_channel_atten(channel_, attenuation_);
|
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
|
// load characteristics for each attenuation
|
||||||
for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) {
|
for (int32_t i = 0; i < (int32_t) ADC_ATTEN_MAX; i++) {
|
||||||
auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
|
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
|
1100, // default vref
|
||||||
&cal_characteristics_[i]);
|
&cal_characteristics_[i]);
|
||||||
switch (cal_value) {
|
switch (cal_value) {
|
||||||
|
@ -136,9 +143,9 @@ void ADCSensor::update() {
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
float ADCSensor::sample() {
|
float ADCSensor::sample() {
|
||||||
#ifdef USE_ADC_SENSOR_VCC
|
#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
|
#else
|
||||||
int raw = analogRead(this->pin_->get_pin()); // NOLINT
|
int32_t raw = analogRead(this->pin_->get_pin()); // NOLINT
|
||||||
#endif
|
#endif
|
||||||
if (output_raw_) {
|
if (output_raw_) {
|
||||||
return raw;
|
return raw;
|
||||||
|
@ -150,29 +157,53 @@ float ADCSensor::sample() {
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
float ADCSensor::sample() {
|
float ADCSensor::sample() {
|
||||||
if (!autorange_) {
|
if (!autorange_) {
|
||||||
int raw = adc1_get_raw(channel_);
|
int32_t 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) {
|
if (raw == -1) {
|
||||||
return NAN;
|
return NAN;
|
||||||
}
|
}
|
||||||
if (output_raw_) {
|
if (output_raw_) {
|
||||||
return 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;
|
return mv / 1000.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
|
int32_t raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
|
||||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11);
|
|
||||||
raw11 = adc1_get_raw(channel_);
|
if (channel1_ != ADC1_CHANNEL_MAX) {
|
||||||
|
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_11);
|
||||||
|
raw11 = adc1_get_raw(channel1_);
|
||||||
if (raw11 < ADC_MAX) {
|
if (raw11 < ADC_MAX) {
|
||||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6);
|
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_6);
|
||||||
raw6 = adc1_get_raw(channel_);
|
raw6 = adc1_get_raw(channel1_);
|
||||||
if (raw6 < ADC_MAX) {
|
if (raw6 < ADC_MAX) {
|
||||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5);
|
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_2_5);
|
||||||
raw2 = adc1_get_raw(channel_);
|
raw2 = adc1_get_raw(channel1_);
|
||||||
if (raw2 < ADC_MAX) {
|
if (raw2 < ADC_MAX) {
|
||||||
adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0);
|
adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_0);
|
||||||
raw0 = adc1_get_raw(channel_);
|
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;
|
return NAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]);
|
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_[(int) ADC_ATTEN_DB_6]);
|
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_[(int) ADC_ATTEN_DB_2_5]);
|
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_[(int) ADC_ATTEN_DB_0]);
|
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)
|
// 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);
|
uint32_t c11 = std::min(raw11, ADC_HALF);
|
||||||
|
@ -212,7 +243,7 @@ float ADCSensor::sample() {
|
||||||
adc_select_input(pin - 26);
|
adc_select_input(pin - 26);
|
||||||
}
|
}
|
||||||
|
|
||||||
int raw = adc_read();
|
int32_t raw = adc_read();
|
||||||
if (this->is_temperature_) {
|
if (this->is_temperature_) {
|
||||||
adc_set_temp_sensor_enabled(false);
|
adc_set_temp_sensor_enabled(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,16 +19,23 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
/// Set the attenuation for this pin. Only available on the ESP32.
|
/// Set the attenuation for this pin. Only available on the ESP32.
|
||||||
void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; }
|
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; }
|
void set_autorange(bool autorange) { autorange_ = autorange; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// Update adc values.
|
/// Update ADC values
|
||||||
void update() override;
|
void update() override;
|
||||||
/// Setup ADc
|
/// Setup ADC
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
/// `HARDWARE_LATE` setup priority.
|
/// `HARDWARE_LATE` setup priority
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
||||||
void set_output_raw(bool output_raw) { output_raw_ = output_raw; }
|
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
|
#ifdef USE_ESP32
|
||||||
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
|
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};
|
bool autorange_{false};
|
||||||
esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {};
|
esp_adc_cal_characteristics_t cal_characteristics_[(int32_t) ADC_ATTEN_MAX] = {};
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
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 import sensor, voltage_sampler
|
||||||
from esphome.components.esp32 import get_esp32_variant
|
from esphome.components.esp32 import get_esp32_variant
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
@ -8,15 +10,15 @@ from esphome.const import (
|
||||||
CONF_NUMBER,
|
CONF_NUMBER,
|
||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
CONF_RAW,
|
CONF_RAW,
|
||||||
|
CONF_WIFI,
|
||||||
DEVICE_CLASS_VOLTAGE,
|
DEVICE_CLASS_VOLTAGE,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
UNIT_VOLT,
|
UNIT_VOLT,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
ATTENUATION_MODES,
|
ATTENUATION_MODES,
|
||||||
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
|
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
|
||||||
|
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
|
||||||
validate_adc_pin,
|
validate_adc_pin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,7 +27,23 @@ AUTO_LOAD = ["voltage_sampler"]
|
||||||
|
|
||||||
def validate_config(config):
|
def validate_config(config):
|
||||||
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
|
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
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,6 +73,8 @@ CONFIG_SCHEMA = cv.All(
|
||||||
validate_config,
|
validate_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = final_validate_config
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
@ -81,5 +101,15 @@ async def to_code(config):
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
variant = get_esp32_variant()
|
variant = get_esp32_variant()
|
||||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||||
|
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]
|
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
|
||||||
cg.add(var.set_channel(chan))
|
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:
|
if CONF_LAMBDA in config:
|
||||||
lambda_ = await cg.process_lambda(
|
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_))
|
cg.add(var.set_writer(lambda_))
|
||||||
|
|
|
@ -3,26 +3,31 @@ import esphome.config_validation as cv
|
||||||
from esphome.components import sensor, ble_client
|
from esphome.components import sensor, ble_client
|
||||||
|
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
DEVICE_CLASS_HUMIDITY,
|
CONF_BATTERY_VOLTAGE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
DEVICE_CLASS_PRESSURE,
|
|
||||||
STATE_CLASS_MEASUREMENT,
|
|
||||||
UNIT_PERCENT,
|
|
||||||
UNIT_CELSIUS,
|
|
||||||
UNIT_HECTOPASCAL,
|
|
||||||
CONF_HUMIDITY,
|
CONF_HUMIDITY,
|
||||||
CONF_TVOC,
|
|
||||||
CONF_PRESSURE,
|
CONF_PRESSURE,
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
|
CONF_TVOC,
|
||||||
|
DEVICE_CLASS_VOLTAGE,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
|
DEVICE_CLASS_PRESSURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
|
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
UNIT_HECTOPASCAL,
|
||||||
UNIT_PARTS_PER_BILLION,
|
UNIT_PARTS_PER_BILLION,
|
||||||
ICON_RADIATOR,
|
UNIT_PERCENT,
|
||||||
|
UNIT_VOLT,
|
||||||
)
|
)
|
||||||
|
|
||||||
CODEOWNERS = ["@ncareau", "@jeromelaban"]
|
CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"]
|
||||||
|
|
||||||
DEPENDENCIES = ["ble_client"]
|
DEPENDENCIES = ["ble_client"]
|
||||||
|
|
||||||
|
CONF_BATTERY_UPDATE_INTERVAL = "battery_update_interval"
|
||||||
|
|
||||||
airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base")
|
airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base")
|
||||||
AirthingsWaveBase = airthings_wave_base_ns.class_(
|
AirthingsWaveBase = airthings_wave_base_ns.class_(
|
||||||
"AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode
|
"AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode
|
||||||
|
@ -34,9 +39,9 @@ BASE_SCHEMA = (
|
||||||
{
|
{
|
||||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_PERCENT,
|
unit_of_measurement=UNIT_PERCENT,
|
||||||
|
accuracy_decimals=0,
|
||||||
device_class=DEVICE_CLASS_HUMIDITY,
|
device_class=DEVICE_CLASS_HUMIDITY,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
accuracy_decimals=0,
|
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_CELSIUS,
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
@ -52,11 +57,21 @@ BASE_SCHEMA = (
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_TVOC): sensor.sensor_schema(
|
cv.Optional(CONF_TVOC): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||||
icon=ICON_RADIATOR,
|
|
||||||
accuracy_decimals=0,
|
accuracy_decimals=0,
|
||||||
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
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(cv.polling_component_schema("5min"))
|
||||||
|
@ -69,15 +84,20 @@ async def wave_base_to_code(var, config):
|
||||||
|
|
||||||
await ble_client.register_ble_node(var, config)
|
await ble_client.register_ble_node(var, config)
|
||||||
|
|
||||||
if CONF_HUMIDITY in config:
|
if config_humidity := config.get(CONF_HUMIDITY):
|
||||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
|
sens = await sensor.new_sensor(config_humidity)
|
||||||
cg.add(var.set_humidity(sens))
|
cg.add(var.set_humidity(sens))
|
||||||
if CONF_TEMPERATURE in config:
|
if config_temperature := config.get(CONF_TEMPERATURE):
|
||||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
|
sens = await sensor.new_sensor(config_temperature)
|
||||||
cg.add(var.set_temperature(sens))
|
cg.add(var.set_temperature(sens))
|
||||||
if CONF_PRESSURE in config:
|
if config_pressure := config.get(CONF_PRESSURE):
|
||||||
sens = await sensor.new_sensor(config[CONF_PRESSURE])
|
sens = await sensor.new_sensor(config_pressure)
|
||||||
cg.add(var.set_pressure(sens))
|
cg.add(var.set_pressure(sens))
|
||||||
if CONF_TVOC in config:
|
if config_tvoc := config.get(CONF_TVOC):
|
||||||
sens = await sensor.new_sensor(config[CONF_TVOC])
|
sens = await sensor.new_sensor(config_tvoc)
|
||||||
cg.add(var.set_tvoc(sens))
|
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))
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
#include "airthings_wave_base.h"
|
#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
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
@ -18,22 +21,26 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt
|
||||||
}
|
}
|
||||||
|
|
||||||
case ESP_GATTC_DISCONNECT_EVT: {
|
case ESP_GATTC_DISCONNECT_EVT: {
|
||||||
|
this->handle_ = 0;
|
||||||
|
this->acp_handle_ = 0;
|
||||||
|
this->cccd_handle_ = 0;
|
||||||
ESP_LOGW(TAG, "Disconnected!");
|
ESP_LOGW(TAG, "Disconnected!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||||
this->handle_ = 0;
|
if (this->request_read_values_()) {
|
||||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
|
if (!this->read_battery_next_update_) {
|
||||||
if (chr == nullptr) {
|
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
|
} else {
|
||||||
this->sensors_data_characteristic_uuid_.to_string().c_str());
|
// delay setting node_state to ESTABLISHED until confirmation of the notify registration
|
||||||
break;
|
this->request_battery_();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this->handle_ = chr->handle;
|
|
||||||
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
|
|
||||||
|
|
||||||
this->request_read_values_();
|
// ensure that the client will be disconnected even if no responses arrive
|
||||||
|
this->set_response_timeout_();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,15 +57,29 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
|
bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return voc <= 16383; }
|
||||||
|
|
||||||
void AirthingsWaveBase::update() {
|
void AirthingsWaveBase::update() {
|
||||||
if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
|
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||||
if (!this->parent()->enabled) {
|
if (!this->parent()->enabled) {
|
||||||
ESP_LOGW(TAG, "Reconnecting to device");
|
ESP_LOGW(TAG, "Reconnecting to device");
|
||||||
this->parent()->set_enabled(true);
|
this->parent()->set_enabled(true);
|
||||||
|
@ -69,12 +90,119 @@ void AirthingsWaveBase::update() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AirthingsWaveBase::request_read_values_() {
|
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_,
|
auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
|
||||||
ESP_GATT_AUTH_REQ_NONE);
|
ESP_GATT_AUTH_REQ_NONE);
|
||||||
if (status) {
|
if (status) {
|
||||||
ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", 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 airthings_wave_base
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
#pragma once
|
#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
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
#include <esp_gattc_api.h>
|
#include <esp_gattc_api.h>
|
||||||
|
@ -14,6 +17,11 @@
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace airthings_wave_base {
|
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 {
|
class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode {
|
||||||
public:
|
public:
|
||||||
AirthingsWaveBase() = default;
|
AirthingsWaveBase() = default;
|
||||||
|
@ -27,21 +35,53 @@ class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientN
|
||||||
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
|
||||||
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
|
void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
|
||||||
void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
|
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:
|
protected:
|
||||||
bool is_valid_voc_value_(uint16_t voc);
|
bool is_valid_voc_value_(uint16_t voc);
|
||||||
|
|
||||||
virtual void read_sensors(uint8_t *value, uint16_t value_len) = 0;
|
bool request_read_values_();
|
||||||
void request_read_values_();
|
virtual void read_sensors(uint8_t *raw_value, uint16_t value_len) = 0;
|
||||||
|
|
||||||
sensor::Sensor *temperature_sensor_{nullptr};
|
sensor::Sensor *temperature_sensor_{nullptr};
|
||||||
sensor::Sensor *humidity_sensor_{nullptr};
|
sensor::Sensor *humidity_sensor_{nullptr};
|
||||||
sensor::Sensor *pressure_sensor_{nullptr};
|
sensor::Sensor *pressure_sensor_{nullptr};
|
||||||
sensor::Sensor *tvoc_sensor_{nullptr};
|
sensor::Sensor *tvoc_sensor_{nullptr};
|
||||||
|
sensor::Sensor *battery_voltage_{nullptr};
|
||||||
|
|
||||||
uint16_t handle_;
|
uint16_t handle_;
|
||||||
esp32_ble_tracker::ESPBTUUID service_uuid_;
|
espbt::ESPBTUUID service_uuid_;
|
||||||
esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_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 airthings_wave_base
|
||||||
|
|
|
@ -26,12 +26,9 @@ void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) {
|
||||||
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
|
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
|
||||||
this->tvoc_sensor_->publish_state(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).
|
|
||||||
this->parent()->set_enabled(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->response_received_();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AirthingsWaveMini::dump_config() {
|
void AirthingsWaveMini::dump_config() {
|
||||||
|
@ -42,11 +39,14 @@ void AirthingsWaveMini::dump_config() {
|
||||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||||
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
|
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
|
||||||
|
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
|
||||||
}
|
}
|
||||||
|
|
||||||
AirthingsWaveMini::AirthingsWaveMini() {
|
AirthingsWaveMini::AirthingsWaveMini() {
|
||||||
this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
|
this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
|
||||||
this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_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 airthings_wave_mini
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace airthings_wave_mini {
|
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 SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba";
|
||||||
static const char *const CHARACTERISTIC_UUID = "b42e3b98-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 airthings_wave_base::AirthingsWaveBase {
|
class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
|
||||||
public:
|
public:
|
||||||
|
@ -17,7 +20,7 @@ class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void read_sensors(uint8_t *value, uint16_t value_len) override;
|
void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
|
||||||
|
|
||||||
struct WaveMiniReadings {
|
struct WaveMiniReadings {
|
||||||
uint16_t unused01;
|
uint16_t unused01;
|
||||||
|
|
|
@ -43,20 +43,17 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
|
||||||
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
|
if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
|
||||||
this->tvoc_sensor_->publish_state(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).
|
|
||||||
this->parent()->set_enabled(false);
|
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
|
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_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; }
|
bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return co2 <= 16383; }
|
||||||
|
|
||||||
void AirthingsWavePlus::dump_config() {
|
void AirthingsWavePlus::dump_config() {
|
||||||
// these really don't belong here, but there doesn't seem to be a
|
// these really don't belong here, but there doesn't seem to be a
|
||||||
|
@ -66,6 +63,7 @@ void AirthingsWavePlus::dump_config() {
|
||||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||||
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
|
LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_);
|
||||||
|
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
|
||||||
|
|
||||||
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
|
LOG_SENSOR(" ", "Radon", this->radon_sensor_);
|
||||||
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
|
LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_);
|
||||||
|
@ -73,8 +71,10 @@ void AirthingsWavePlus::dump_config() {
|
||||||
}
|
}
|
||||||
|
|
||||||
AirthingsWavePlus::AirthingsWavePlus() {
|
AirthingsWavePlus::AirthingsWavePlus() {
|
||||||
this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
|
this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
|
||||||
this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_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 airthings_wave_plus
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace airthings_wave_plus {
|
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 SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
|
||||||
static const char *const CHARACTERISTIC_UUID = "b42e2a68-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 airthings_wave_base::AirthingsWaveBase {
|
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
|
||||||
public:
|
public:
|
||||||
|
@ -24,7 +27,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
|
||||||
bool is_valid_radon_value_(uint16_t radon);
|
bool is_valid_radon_value_(uint16_t radon);
|
||||||
bool is_valid_co2_value_(uint16_t co2);
|
bool is_valid_co2_value_(uint16_t co2);
|
||||||
|
|
||||||
void read_sensors(uint8_t *value, uint16_t value_len) override;
|
void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
|
||||||
|
|
||||||
sensor::Sensor *radon_sensor_{nullptr};
|
sensor::Sensor *radon_sensor_{nullptr};
|
||||||
sensor::Sensor *radon_long_term_sensor_{nullptr};
|
sensor::Sensor *radon_long_term_sensor_{nullptr};
|
||||||
|
|
|
@ -53,12 +53,12 @@ async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await airthings_wave_base.wave_base_to_code(var, config)
|
await airthings_wave_base.wave_base_to_code(var, config)
|
||||||
|
|
||||||
if CONF_RADON in config:
|
if config_radon := config.get(CONF_RADON):
|
||||||
sens = await sensor.new_sensor(config[CONF_RADON])
|
sens = await sensor.new_sensor(config_radon)
|
||||||
cg.add(var.set_radon(sens))
|
cg.add(var.set_radon(sens))
|
||||||
if CONF_RADON_LONG_TERM in config:
|
if config_radon_long_term := config.get(CONF_RADON_LONG_TERM):
|
||||||
sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
|
sens = await sensor.new_sensor(config_radon_long_term)
|
||||||
cg.add(var.set_radon_long_term(sens))
|
cg.add(var.set_radon_long_term(sens))
|
||||||
if CONF_CO2 in config:
|
if config_co2 := config.get(CONF_CO2):
|
||||||
sens = await sensor.new_sensor(config[CONF_CO2])
|
sens = await sensor.new_sensor(config_co2)
|
||||||
cg.add(var.set_co2(sens))
|
cg.add(var.set_co2(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
|
import logging
|
||||||
|
|
||||||
from esphome import core
|
from esphome import automation, core
|
||||||
from esphome.components import display, font
|
from esphome.components import font
|
||||||
import esphome.components.image as espImage
|
import esphome.components.image as espImage
|
||||||
from esphome.components.image import CONF_USE_TRANSPARENCY
|
from esphome.components.image import CONF_USE_TRANSPARENCY
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
@ -18,14 +18,30 @@ from esphome.core import CORE, HexInt
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
AUTO_LOAD = ["image"]
|
||||||
|
CODEOWNERS = ["@syndlex"]
|
||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
CONF_LOOP = "loop"
|
CONF_LOOP = "loop"
|
||||||
CONF_START_FRAME = "start_frame"
|
CONF_START_FRAME = "start_frame"
|
||||||
CONF_END_FRAME = "end_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):
|
def validate_cross_dependencies(config):
|
||||||
|
@ -74,7 +90,35 @@ ANIMATION_SCHEMA = cv.Schema(
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_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):
|
async def to_code(config):
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display {
|
namespace animation {
|
||||||
|
|
||||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
|
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
|
||||||
|
image::ImageType type)
|
||||||
: Image(data_start, width, height, type),
|
: Image(data_start, width, height, type),
|
||||||
animation_data_start_(data_start),
|
animation_data_start_(data_start),
|
||||||
current_frame_(0),
|
current_frame_(0),
|
||||||
|
@ -65,5 +66,5 @@ void Animation::update_data_start_() {
|
||||||
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
|
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace display
|
} // namespace animation
|
||||||
} // namespace esphome
|
} // 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;
|
bool start = 1;
|
||||||
string conversation_id = 2;
|
string conversation_id = 2;
|
||||||
|
bool use_vad = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message VoiceAssistantResponse {
|
message VoiceAssistantResponse {
|
||||||
|
|
|
@ -907,12 +907,13 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#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_)
|
if (!this->voice_assistant_subscription_)
|
||||||
return false;
|
return false;
|
||||||
VoiceAssistantRequest msg;
|
VoiceAssistantRequest msg;
|
||||||
msg.start = start;
|
msg.start = start;
|
||||||
msg.conversation_id = conversation_id;
|
msg.conversation_id = conversation_id;
|
||||||
|
msg.use_vad = use_vad;
|
||||||
return this->send_voice_assistant_request(msg);
|
return this->send_voice_assistant_request(msg);
|
||||||
}
|
}
|
||||||
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &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 {
|
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override {
|
||||||
this->voice_assistant_subscription_ = msg.subscribe;
|
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_response(const VoiceAssistantResponse &msg) override;
|
||||||
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
|
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -6348,6 +6348,10 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
|
||||||
this->start = value.as_bool();
|
this->start = value.as_bool();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
case 3: {
|
||||||
|
this->use_vad = value.as_bool();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -6365,6 +6369,7 @@ bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimite
|
||||||
void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const {
|
void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_bool(1, this->start);
|
buffer.encode_bool(1, this->start);
|
||||||
buffer.encode_string(2, this->conversation_id);
|
buffer.encode_string(2, this->conversation_id);
|
||||||
|
buffer.encode_bool(3, this->use_vad);
|
||||||
}
|
}
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void VoiceAssistantRequest::dump_to(std::string &out) const {
|
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(" conversation_id: ");
|
||||||
out.append("'").append(this->conversation_id).append("'");
|
out.append("'").append(this->conversation_id).append("'");
|
||||||
out.append("\n");
|
out.append("\n");
|
||||||
|
|
||||||
|
out.append(" use_vad: ");
|
||||||
|
out.append(YESNO(this->use_vad));
|
||||||
|
out.append("\n");
|
||||||
out.append("}");
|
out.append("}");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1655,6 +1655,7 @@ class VoiceAssistantRequest : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
bool start{false};
|
bool start{false};
|
||||||
std::string conversation_id{};
|
std::string conversation_id{};
|
||||||
|
bool use_vad{false};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
|
|
|
@ -323,16 +323,16 @@ void APIServer::on_shutdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#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_) {
|
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 true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
void APIServer::stop_voice_assistant() {
|
void APIServer::stop_voice_assistant() {
|
||||||
for (auto &c : this->clients_) {
|
for (auto &c : this->clients_) {
|
||||||
if (c->request_voice_assistant(false, ""))
|
if (c->request_voice_assistant(false, "", false))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ class APIServer : public Component, public Controller {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#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();
|
void stop_voice_assistant();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ async def async_run_logs(config, address):
|
||||||
except APIConnectionError:
|
except APIConnectionError:
|
||||||
cli.disconnect()
|
cli.disconnect()
|
||||||
|
|
||||||
async def on_disconnect():
|
async def on_disconnect(expected_disconnect: bool) -> None:
|
||||||
_LOGGER.warning("Disconnected from API")
|
_LOGGER.warning("Disconnected from API")
|
||||||
|
|
||||||
zc = zeroconf.Zeroconf()
|
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
|
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")
|
binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor")
|
||||||
BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase)
|
BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase)
|
||||||
BinarySensorInitiallyOff = binary_sensor_ns.class_(
|
BinarySensorInitiallyOff = binary_sensor_ns.class_(
|
||||||
|
@ -138,47 +146,75 @@ FILTER_REGISTRY = Registry()
|
||||||
validate_filters = cv.validate_registry("filter", FILTER_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):
|
async def invert_filter_to_code(config, filter_id):
|
||||||
return cg.new_Pvariable(filter_id)
|
return cg.new_Pvariable(filter_id)
|
||||||
|
|
||||||
|
|
||||||
@FILTER_REGISTRY.register(
|
@register_filter(
|
||||||
"delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds
|
"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):
|
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, {})
|
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
|
return var
|
||||||
|
|
||||||
|
|
||||||
@FILTER_REGISTRY.register(
|
@register_filter(
|
||||||
"delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds
|
"delayed_on", DelayedOnFilter, cv.templatable(cv.positive_time_period_milliseconds)
|
||||||
)
|
)
|
||||||
async def delayed_on_filter_to_code(config, filter_id):
|
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, {})
|
await cg.register_component(var, {})
|
||||||
|
template_ = await cg.templatable(config, [], cg.uint32)
|
||||||
|
cg.add(var.set_delay(template_))
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
@FILTER_REGISTRY.register(
|
@register_filter(
|
||||||
"delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds
|
"delayed_off",
|
||||||
|
DelayedOffFilter,
|
||||||
|
cv.templatable(cv.positive_time_period_milliseconds),
|
||||||
)
|
)
|
||||||
async def delayed_off_filter_to_code(config, filter_id):
|
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, {})
|
await cg.register_component(var, {})
|
||||||
|
template_ = await cg.templatable(config, [], cg.uint32)
|
||||||
|
cg.add(var.set_delay(template_))
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
CONF_TIME_OFF = "time_off"
|
@register_filter(
|
||||||
CONF_TIME_ON = "time_on"
|
|
||||||
|
|
||||||
DEFAULT_DELAY = "1s"
|
|
||||||
DEFAULT_TIME_OFF = "100ms"
|
|
||||||
DEFAULT_TIME_ON = "900ms"
|
|
||||||
|
|
||||||
|
|
||||||
@FILTER_REGISTRY.register(
|
|
||||||
"autorepeat",
|
"autorepeat",
|
||||||
AutorepeatFilter,
|
AutorepeatFilter,
|
||||||
cv.All(
|
cv.All(
|
||||||
|
@ -215,7 +251,7 @@ async def autorepeat_filter_to_code(config, filter_id):
|
||||||
return var
|
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):
|
async def lambda_filter_to_code(config, filter_id):
|
||||||
lambda_ = await cg.process_lambda(
|
lambda_ = await cg.process_lambda(
|
||||||
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
||||||
|
|
|
@ -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) {
|
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
|
||||||
if (value) {
|
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 {
|
} 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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
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) {
|
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
|
||||||
if (value) {
|
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 {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
this->cancel_timeout("ON");
|
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; }
|
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) {
|
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
|
||||||
if (!value) {
|
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 {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
this->cancel_timeout("OFF");
|
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> 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 binary_sensor
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
@ -29,38 +30,40 @@ class Filter {
|
||||||
|
|
||||||
class DelayedOnOffFilter : public Filter, public Component {
|
class DelayedOnOffFilter : public Filter, public Component {
|
||||||
public:
|
public:
|
||||||
explicit DelayedOnOffFilter(uint32_t delay);
|
|
||||||
|
|
||||||
optional<bool> new_value(bool value, bool is_initial) override;
|
optional<bool> new_value(bool value, bool is_initial) override;
|
||||||
|
|
||||||
float get_setup_priority() const 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:
|
protected:
|
||||||
uint32_t delay_;
|
TemplatableValue<uint32_t> on_delay_{};
|
||||||
|
TemplatableValue<uint32_t> off_delay_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
class DelayedOnFilter : public Filter, public Component {
|
class DelayedOnFilter : public Filter, public Component {
|
||||||
public:
|
public:
|
||||||
explicit DelayedOnFilter(uint32_t delay);
|
|
||||||
|
|
||||||
optional<bool> new_value(bool value, bool is_initial) override;
|
optional<bool> new_value(bool value, bool is_initial) override;
|
||||||
|
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
|
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint32_t delay_;
|
TemplatableValue<uint32_t> delay_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
class DelayedOffFilter : public Filter, public Component {
|
class DelayedOffFilter : public Filter, public Component {
|
||||||
public:
|
public:
|
||||||
explicit DelayedOffFilter(uint32_t delay);
|
|
||||||
|
|
||||||
optional<bool> new_value(bool value, bool is_initial) override;
|
optional<bool> new_value(bool value, bool is_initial) override;
|
||||||
|
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
|
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint32_t delay_;
|
TemplatableValue<uint32_t> delay_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
class InvertFilter : public Filter {
|
class InvertFilter : public Filter {
|
||||||
|
@ -105,14 +108,6 @@ class LambdaFilter : public Filter {
|
||||||
std::function<optional<bool>(bool)> f_;
|
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 binary_sensor
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -6,8 +6,10 @@ from esphome.const import (
|
||||||
CONF_HUMIDITY,
|
CONF_HUMIDITY,
|
||||||
CONF_PRESSURE,
|
CONF_PRESSURE,
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||||
DEVICE_CLASS_HUMIDITY,
|
DEVICE_CLASS_HUMIDITY,
|
||||||
DEVICE_CLASS_PRESSURE,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
|
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
UNIT_CELSIUS,
|
UNIT_CELSIUS,
|
||||||
|
@ -17,8 +19,6 @@ from esphome.const import (
|
||||||
UNIT_PERCENT,
|
UNIT_PERCENT,
|
||||||
ICON_GAS_CYLINDER,
|
ICON_GAS_CYLINDER,
|
||||||
ICON_GAUGE,
|
ICON_GAUGE,
|
||||||
ICON_THERMOMETER,
|
|
||||||
ICON_WATER_PERCENT,
|
|
||||||
)
|
)
|
||||||
from . import (
|
from . import (
|
||||||
BME680BSECComponent,
|
BME680BSECComponent,
|
||||||
|
@ -35,7 +35,6 @@ CONF_CO2_EQUIVALENT = "co2_equivalent"
|
||||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||||
UNIT_IAQ = "IAQ"
|
UNIT_IAQ = "IAQ"
|
||||||
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
|
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
|
||||||
ICON_TEST_TUBE = "mdi:test-tube"
|
|
||||||
|
|
||||||
TYPES = [
|
TYPES = [
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
|
@ -53,7 +52,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||||
cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent),
|
cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent),
|
||||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_CELSIUS,
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
icon=ICON_THERMOMETER,
|
|
||||||
accuracy_decimals=1,
|
accuracy_decimals=1,
|
||||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
@ -62,16 +60,14 @@ CONFIG_SCHEMA = cv.Schema(
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||||
icon=ICON_GAUGE,
|
|
||||||
accuracy_decimals=1,
|
accuracy_decimals=1,
|
||||||
device_class=DEVICE_CLASS_PRESSURE,
|
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
).extend(
|
).extend(
|
||||||
{cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
|
{cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_PERCENT,
|
unit_of_measurement=UNIT_PERCENT,
|
||||||
icon=ICON_WATER_PERCENT,
|
|
||||||
accuracy_decimals=1,
|
accuracy_decimals=1,
|
||||||
device_class=DEVICE_CLASS_HUMIDITY,
|
device_class=DEVICE_CLASS_HUMIDITY,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
@ -97,14 +93,14 @@ CONFIG_SCHEMA = cv.Schema(
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema(
|
cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||||
icon=ICON_TEST_TUBE,
|
|
||||||
accuracy_decimals=1,
|
accuracy_decimals=1,
|
||||||
|
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema(
|
cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||||
icon=ICON_TEST_TUBE,
|
|
||||||
accuracy_decimals=1,
|
accuracy_decimals=1,
|
||||||
|
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
state_class=STATE_CLASS_MEASUREMENT,
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm
|
||||||
if (use_extended_id) {
|
if (use_extended_id) {
|
||||||
ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
|
ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
|
||||||
} else {
|
} 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)
|
if (size > CAN_MAX_DATA_LENGTH)
|
||||||
size = CAN_MAX_DATA_LENGTH;
|
size = CAN_MAX_DATA_LENGTH;
|
||||||
|
|
|
@ -21,7 +21,6 @@ CONFIG_SCHEMA = cv.All(
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.only_with_arduino,
|
|
||||||
cv.only_on(["esp32", "esp8266"]),
|
cv.only_on(["esp32", "esp8266"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,6 +33,7 @@ async def to_code(config):
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
cg.add_define("USE_CAPTIVE_PORTAL")
|
cg.add_define("USE_CAPTIVE_PORTAL")
|
||||||
|
|
||||||
|
if CORE.using_arduino:
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
cg.add_library("DNSServer", None)
|
cg.add_library("DNSServer", None)
|
||||||
cg.add_library("WiFi", None)
|
cg.add_library("WiFi", None)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#ifdef USE_ARDUINO
|
|
||||||
|
|
||||||
#include "captive_portal.h"
|
#include "captive_portal.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
@ -46,10 +44,12 @@ void CaptivePortal::start() {
|
||||||
this->base_->add_ota_handler();
|
this->base_->add_ota_handler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_ARDUINO
|
||||||
this->dns_server_ = make_unique<DNSServer>();
|
this->dns_server_ = make_unique<DNSServer>();
|
||||||
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
|
||||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||||
this->dns_server_->start(53, "*", (uint32_t) ip);
|
this->dns_server_->start(53, "*", (uint32_t) ip);
|
||||||
|
#endif
|
||||||
|
|
||||||
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
|
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
|
||||||
if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) {
|
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) {
|
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
||||||
if (req->url() == "/") {
|
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");
|
response->addHeader("Content-Encoding", "gzip");
|
||||||
req->send(response);
|
req->send(response);
|
||||||
return;
|
return;
|
||||||
|
@ -91,5 +91,3 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo
|
||||||
|
|
||||||
} // namespace captive_portal
|
} // namespace captive_portal
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ARDUINO
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#ifdef USE_ARDUINO
|
||||||
#include <DNSServer.h>
|
#include <DNSServer.h>
|
||||||
|
#endif
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
|
@ -18,18 +18,22 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||||
CaptivePortal(web_server_base::WebServerBase *base);
|
CaptivePortal(web_server_base::WebServerBase *base);
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
#ifdef USE_ARDUINO
|
||||||
void loop() override {
|
void loop() override {
|
||||||
if (this->dns_server_ != nullptr)
|
if (this->dns_server_ != nullptr)
|
||||||
this->dns_server_->processNextRequest();
|
this->dns_server_->processNextRequest();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
void start();
|
void start();
|
||||||
bool is_active() const { return this->active_; }
|
bool is_active() const { return this->active_; }
|
||||||
void end() {
|
void end() {
|
||||||
this->active_ = false;
|
this->active_ = false;
|
||||||
this->base_->deinit();
|
this->base_->deinit();
|
||||||
|
#ifdef USE_ARDUINO
|
||||||
this->dns_server_->stop();
|
this->dns_server_->stop();
|
||||||
this->dns_server_ = nullptr;
|
this->dns_server_ = nullptr;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canHandle(AsyncWebServerRequest *request) override {
|
bool canHandle(AsyncWebServerRequest *request) override {
|
||||||
|
@ -58,12 +62,12 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||||
web_server_base::WebServerBase *base_;
|
web_server_base::WebServerBase *base_;
|
||||||
bool initialized_{false};
|
bool initialized_{false};
|
||||||
bool active_{false};
|
bool active_{false};
|
||||||
|
#ifdef USE_ARDUINO
|
||||||
std::unique_ptr<DNSServer> dns_server_{nullptr};
|
std::unique_ptr<DNSServer> dns_server_{nullptr};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
} // namespace captive_portal
|
} // namespace captive_portal
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif // USE_ARDUINO
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ CONFIG_SCHEMA = cv.All(
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.polling_component_schema("60s")),
|
).extend(cv.polling_component_schema("60s")),
|
||||||
cv.only_on(["esp32", "esp8266"]),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/version.h"
|
#include "esphome/core/version.h"
|
||||||
|
#include <cinttypes>
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@
|
||||||
|
|
||||||
#if ESP_IDF_VERSION_MAJOR >= 4
|
#if ESP_IDF_VERSION_MAJOR >= 4
|
||||||
#include <esp32/rom/rtc.h>
|
#include <esp32/rom/rtc.h>
|
||||||
|
#include <esp_chip_info.h>
|
||||||
#else
|
#else
|
||||||
#include <rom/rtc.h>
|
#include <rom/rtc.h>
|
||||||
#endif
|
#endif
|
||||||
|
@ -20,8 +22,12 @@
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
|
#ifdef USE_RP2040
|
||||||
|
#include <Arduino.h>
|
||||||
|
#else
|
||||||
#include <Esp.h>
|
#include <Esp.h>
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace debug {
|
namespace debug {
|
||||||
|
@ -33,6 +39,8 @@ static uint32_t get_free_heap() {
|
||||||
return ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance)
|
return ESP.getFreeHeap(); // NOLINT(readability-static-accessed-through-instance)
|
||||||
#elif defined(USE_ESP32)
|
#elif defined(USE_ESP32)
|
||||||
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
|
||||||
|
#elif defined(USE_RP2040)
|
||||||
|
return rp2040.getFreeHeap();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,9 +69,9 @@ void DebugComponent::dump_config() {
|
||||||
device_info += ESPHOME_VERSION;
|
device_info += ESPHOME_VERSION;
|
||||||
|
|
||||||
this->free_heap_ = get_free_heap();
|
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;
|
const char *flash_mode;
|
||||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
||||||
case FM_QIO:
|
case FM_QIO:
|
||||||
|
@ -272,6 +280,11 @@ void DebugComponent::dump_config() {
|
||||||
reset_reason = ESP.getResetReason().c_str();
|
reset_reason = ESP.getResetReason().c_str();
|
||||||
#endif
|
#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
|
#ifdef USE_TEXT_SENSOR
|
||||||
if (this->device_info_ != nullptr) {
|
if (this->device_info_ != nullptr) {
|
||||||
if (device_info.length() > 255)
|
if (device_info.length() > 255)
|
||||||
|
@ -289,7 +302,7 @@ void DebugComponent::loop() {
|
||||||
uint32_t new_free_heap = get_free_heap();
|
uint32_t new_free_heap = get_free_heap();
|
||||||
if (new_free_heap < this->free_heap_ / 2) {
|
if (new_free_heap < this->free_heap_ / 2) {
|
||||||
this->free_heap_ = new_free_heap;
|
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);
|
this->status_momentary_warning("heap", 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,11 @@ from esphome.core import coroutine_with_priority
|
||||||
IS_PLATFORM_COMPONENT = True
|
IS_PLATFORM_COMPONENT = True
|
||||||
|
|
||||||
display_ns = cg.esphome_ns.namespace("display")
|
display_ns = cg.esphome_ns.namespace("display")
|
||||||
|
Display = display_ns.class_("Display")
|
||||||
DisplayBuffer = display_ns.class_("DisplayBuffer")
|
DisplayBuffer = display_ns.class_("DisplayBuffer")
|
||||||
DisplayPage = display_ns.class_("DisplayPage")
|
DisplayPage = display_ns.class_("DisplayPage")
|
||||||
DisplayPagePtr = DisplayPage.operator("ptr")
|
DisplayPagePtr = DisplayPage.operator("ptr")
|
||||||
DisplayBufferRef = DisplayBuffer.operator("ref")
|
DisplayRef = Display.operator("ref")
|
||||||
DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action)
|
DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action)
|
||||||
DisplayPageShowNextAction = display_ns.class_(
|
DisplayPageShowNextAction = display_ns.class_(
|
||||||
"DisplayPageShowNextAction", automation.Action
|
"DisplayPageShowNextAction", automation.Action
|
||||||
|
@ -96,7 +97,7 @@ async def setup_display_core_(var, config):
|
||||||
pages = []
|
pages = []
|
||||||
for conf in config[CONF_PAGES]:
|
for conf in config[CONF_PAGES]:
|
||||||
lambda_ = await cg.process_lambda(
|
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_)
|
page = cg.new_Pvariable(conf[CONF_ID], lambda_)
|
||||||
pages.append(page)
|
pages.append(page)
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include "image.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace display {
|
|
||||||
|
|
||||||
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_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace display
|
|
||||||
} // namespace esphome
|
|
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,111 +1,15 @@
|
||||||
#include "display_buffer.h"
|
#include "display_buffer.h"
|
||||||
|
|
||||||
#include <utility>
|
#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"
|
|
||||||
|
|
||||||
#include "animation.h"
|
#include "esphome/core/application.h"
|
||||||
#include "image.h"
|
#include "esphome/core/log.h"
|
||||||
#include "font.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display {
|
namespace display {
|
||||||
|
|
||||||
static const char *const TAG = "display";
|
static const char *const TAG = "display";
|
||||||
|
|
||||||
const Color COLOR_OFF(0, 0, 0, 0);
|
|
||||||
const Color COLOR_ON(255, 255, 255, 255);
|
|
||||||
|
|
||||||
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) {
|
void DisplayBuffer::init_internal_(uint32_t buffer_length) {
|
||||||
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||||
this->buffer_ = allocator.allocate(buffer_length);
|
this->buffer_ = allocator.allocate(buffer_length);
|
||||||
|
@ -116,8 +20,6 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) {
|
||||||
this->clear();
|
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() {
|
int DisplayBuffer::get_width() {
|
||||||
switch (this->rotation_) {
|
switch (this->rotation_) {
|
||||||
case DISPLAY_ROTATION_90_DEGREES:
|
case DISPLAY_ROTATION_90_DEGREES:
|
||||||
|
@ -129,6 +31,7 @@ int DisplayBuffer::get_width() {
|
||||||
return this->get_width_internal();
|
return this->get_width_internal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int DisplayBuffer::get_height() {
|
int DisplayBuffer::get_height() {
|
||||||
switch (this->rotation_) {
|
switch (this->rotation_) {
|
||||||
case DISPLAY_ROTATION_0_DEGREES:
|
case DISPLAY_ROTATION_0_DEGREES:
|
||||||
|
@ -140,7 +43,7 @@ int DisplayBuffer::get_height() {
|
||||||
return this->get_width_internal();
|
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) {
|
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||||
if (!this->get_clipping().inside(x, y))
|
if (!this->get_clipping().inside(x, y))
|
||||||
return; // NOLINT
|
return; // NOLINT
|
||||||
|
@ -164,372 +67,6 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
|
||||||
this->draw_absolute_pixel_internal(x, y, color);
|
this->draw_absolute_pixel_internal(x, y, color);
|
||||||
App.feed_wdt();
|
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) {
|
|
||||||
this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off);
|
|
||||||
}
|
|
||||||
|
|
||||||
void DisplayBuffer::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 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 display
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -2,579 +2,35 @@
|
||||||
|
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "display.h"
|
||||||
#include "display_color_utils.h"
|
#include "display_color_utils.h"
|
||||||
#include "esphome/core/automation.h"
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/defines.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
|
|
||||||
|
|
||||||
#include "animation.h"
|
|
||||||
#include "font.h"
|
|
||||||
#include "image.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display {
|
namespace display {
|
||||||
|
|
||||||
/** TextAlign is used to tell the display class how to position a piece of text. By default
|
class DisplayBuffer : public Display {
|
||||||
* 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
static const int16_t VALUE_NO_SET = 32766;
|
|
||||||
|
|
||||||
class Rect {
|
|
||||||
public:
|
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 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.
|
/// 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.
|
/// 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.
|
/// Set a single pixel at the specified coordinates to the given color.
|
||||||
void draw_pixel_at(int x, int y, Color color = COLOR_ON);
|
void draw_pixel_at(int x, int y, Color color) override;
|
||||||
|
|
||||||
/// 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);
|
|
||||||
|
|
||||||
/** 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, 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; }
|
|
||||||
|
|
||||||
virtual int get_height_internal() = 0;
|
virtual int get_height_internal() = 0;
|
||||||
virtual int get_width_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:
|
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;
|
virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0;
|
||||||
|
|
||||||
void init_internal_(uint32_t buffer_length);
|
void init_internal_(uint32_t buffer_length);
|
||||||
|
|
||||||
void do_update_();
|
|
||||||
|
|
||||||
uint8_t *buffer_{nullptr};
|
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};
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
} // 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_DECRYPTION_KEY = "decryption_key"
|
||||||
CONF_DSMR_ID = "dsmr_id"
|
CONF_DSMR_ID = "dsmr_id"
|
||||||
CONF_GAS_MBUS_ID = "gas_mbus_id"
|
CONF_GAS_MBUS_ID = "gas_mbus_id"
|
||||||
|
CONF_WATER_MBUS_ID = "water_mbus_id"
|
||||||
CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
|
CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
|
||||||
CONF_REQUEST_INTERVAL = "request_interval"
|
CONF_REQUEST_INTERVAL = "request_interval"
|
||||||
CONF_REQUEST_PIN = "request_pin"
|
CONF_REQUEST_PIN = "request_pin"
|
||||||
|
@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All(
|
||||||
cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
|
cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
|
||||||
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
|
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
|
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_MAX_TELEGRAM_LENGTH, default=1500): cv.int_,
|
||||||
cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
|
cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
|
||||||
cv.Optional(
|
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(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_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
|
# DSMR Parser
|
||||||
cg.add_library("glmnet/Dsmr", "0.5")
|
cg.add_library("glmnet/Dsmr", "0.8")
|
||||||
|
|
||||||
# Crypto
|
# Crypto
|
||||||
cg.add_library("rweather/Crypto", "0.4.0")
|
cg.add_library("rweather/Crypto", "0.4.0")
|
||||||
|
|
|
@ -8,6 +8,7 @@ from esphome.const import (
|
||||||
DEVICE_CLASS_GAS,
|
DEVICE_CLASS_GAS,
|
||||||
DEVICE_CLASS_POWER,
|
DEVICE_CLASS_POWER,
|
||||||
DEVICE_CLASS_VOLTAGE,
|
DEVICE_CLASS_VOLTAGE,
|
||||||
|
DEVICE_CLASS_WATER,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
STATE_CLASS_TOTAL_INCREASING,
|
STATE_CLASS_TOTAL_INCREASING,
|
||||||
UNIT_AMPERE,
|
UNIT_AMPERE,
|
||||||
|
@ -236,6 +237,36 @@ CONFIG_SCHEMA = cv.Schema(
|
||||||
device_class=DEVICE_CLASS_GAS,
|
device_class=DEVICE_CLASS_GAS,
|
||||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
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)
|
).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}"),
|
CORE.relative_build_path(f"components/{name}"),
|
||||||
dirs_exist_ok=True,
|
dirs_exist_ok=True,
|
||||||
ignore=shutil.ignore_patterns(".git", ".github"),
|
ignore=shutil.ignore_patterns(".git", ".github"),
|
||||||
|
symlinks=True,
|
||||||
|
ignore_dangling_symlinks=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
dir = os.path.dirname(__file__)
|
dir = os.path.dirname(__file__)
|
||||||
|
|
|
@ -35,6 +35,7 @@ ETHERNET_TYPES = {
|
||||||
"IP101": EthernetType.ETHERNET_TYPE_IP101,
|
"IP101": EthernetType.ETHERNET_TYPE_IP101,
|
||||||
"JL1101": EthernetType.ETHERNET_TYPE_JL1101,
|
"JL1101": EthernetType.ETHERNET_TYPE_JL1101,
|
||||||
"KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
|
"KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
|
||||||
|
"KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA,
|
||||||
}
|
}
|
||||||
|
|
||||||
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
|
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
|
||||||
|
|
|
@ -19,7 +19,11 @@
|
||||||
#include <sys/cdefs.h>
|
#include <sys/cdefs.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_eth.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"
|
#include "eth_phy_regs_struct.h"
|
||||||
|
#endif
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
|
@ -170,7 +174,11 @@ static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) {
|
||||||
return ESP_OK;
|
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) {
|
static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) {
|
||||||
|
#endif
|
||||||
phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
|
phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
|
||||||
esp_eth_mediator_t *eth = jl1101->eth;
|
esp_eth_mediator_t *eth = jl1101->eth;
|
||||||
/* in case any link status has changed, let's assume we're in link down status */
|
/* 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;
|
esp_eth_mediator_t *eth = jl1101->eth;
|
||||||
// Detect PHY address
|
// Detect PHY address
|
||||||
if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) {
|
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);
|
PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
/* Power on Ethernet PHY */
|
/* Power on Ethernet PHY */
|
||||||
PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err);
|
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.init = jl1101_init;
|
||||||
jl1101->parent.deinit = jl1101_deinit;
|
jl1101->parent.deinit = jl1101_deinit;
|
||||||
jl1101->parent.set_mediator = jl1101_set_mediator;
|
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;
|
jl1101->parent.negotiate = jl1101_negotiate;
|
||||||
|
#endif
|
||||||
jl1101->parent.get_link = jl1101_get_link;
|
jl1101->parent.get_link = jl1101_get_link;
|
||||||
jl1101->parent.pwrctl = jl1101_pwrctl;
|
jl1101->parent.pwrctl = jl1101_pwrctl;
|
||||||
jl1101->parent.get_addr = jl1101_get_addr;
|
jl1101->parent.get_addr = jl1101_get_addr;
|
||||||
|
|
|
@ -41,18 +41,27 @@ void EthernetComponent::setup() {
|
||||||
this->eth_netif_ = esp_netif_new(&cfg);
|
this->eth_netif_ = esp_netif_new(&cfg);
|
||||||
|
|
||||||
// Init MAC and PHY configs to default
|
// 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();
|
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
|
||||||
|
|
||||||
phy_config.phy_addr = this->phy_addr_;
|
phy_config.phy_addr = this->phy_addr_;
|
||||||
phy_config.reset_gpio_num = this->power_pin_;
|
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_mdc_gpio_num = this->mdc_pin_;
|
||||||
mac_config.smi_mdio_gpio_num = this->mdio_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_mode = this->clk_mode_;
|
||||||
mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_;
|
mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_;
|
||||||
|
|
||||||
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config);
|
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config);
|
||||||
|
#endif
|
||||||
|
|
||||||
switch (this->type_) {
|
switch (this->type_) {
|
||||||
case ETHERNET_TYPE_LAN8720: {
|
case ETHERNET_TYPE_LAN8720: {
|
||||||
|
@ -75,8 +84,13 @@ void EthernetComponent::setup() {
|
||||||
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
|
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
|
||||||
break;
|
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);
|
this->phy_ = esp_eth_phy_new_ksz8081(&phy_config);
|
||||||
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
@ -89,6 +103,12 @@ void EthernetComponent::setup() {
|
||||||
this->eth_handle_ = nullptr;
|
this->eth_handle_ = nullptr;
|
||||||
err = esp_eth_driver_install(ð_config, &this->eth_handle_);
|
err = esp_eth_driver_install(ð_config, &this->eth_handle_);
|
||||||
ESPHL_ERROR_CHECK(err, "ETH driver install error");
|
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 */
|
/* attach Ethernet driver to TCP/IP stack */
|
||||||
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
|
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
|
||||||
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
|
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
|
||||||
|
@ -171,6 +191,10 @@ void EthernetComponent::dump_config() {
|
||||||
eth_type = "KSZ8081";
|
eth_type = "KSZ8081";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ETHERNET_TYPE_KSZ8081RNA:
|
||||||
|
eth_type = "KSZ8081RNA";
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
eth_type = "Unknown";
|
eth_type = "Unknown";
|
||||||
break;
|
break;
|
||||||
|
@ -221,13 +245,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
|
||||||
return;
|
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 EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
|
||||||
void *event_data) {
|
void *event_data) {
|
||||||
global_eth_component->connected_ = true;
|
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_() {
|
void EthernetComponent::start_connect_() {
|
||||||
|
@ -372,6 +396,37 @@ bool EthernetComponent::powerdown() {
|
||||||
return true;
|
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 ethernet
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ enum EthernetType {
|
||||||
ETHERNET_TYPE_IP101,
|
ETHERNET_TYPE_IP101,
|
||||||
ETHERNET_TYPE_JL1101,
|
ETHERNET_TYPE_JL1101,
|
||||||
ETHERNET_TYPE_KSZ8081,
|
ETHERNET_TYPE_KSZ8081,
|
||||||
|
ETHERNET_TYPE_KSZ8081RNA,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ManualIP {
|
struct ManualIP {
|
||||||
|
@ -67,6 +68,8 @@ class EthernetComponent : public Component {
|
||||||
|
|
||||||
void start_connect_();
|
void start_connect_();
|
||||||
void dump_connect_params_();
|
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_;
|
std::string use_address_;
|
||||||
uint8_t phy_addr_{0};
|
uint8_t phy_addr_{0};
|
||||||
|
|
|
@ -3,11 +3,11 @@ from pathlib import Path
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from packaging import version
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from esphome import core
|
from esphome import core
|
||||||
from esphome.components import display
|
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.helpers import copy_file_if_changed
|
from esphome.helpers import copy_file_if_changed
|
||||||
|
@ -29,9 +29,11 @@ DOMAIN = "font"
|
||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
Font = display.display_ns.class_("Font")
|
font_ns = cg.esphome_ns.namespace("font")
|
||||||
Glyph = display.display_ns.class_("Glyph")
|
|
||||||
GlyphData = display.display_ns.struct("GlyphData")
|
Font = font_ns.class_("Font")
|
||||||
|
Glyph = font_ns.class_("Glyph")
|
||||||
|
GlyphData = font_ns.struct("GlyphData")
|
||||||
|
|
||||||
|
|
||||||
def validate_glyphs(value):
|
def validate_glyphs(value):
|
||||||
|
@ -65,13 +67,18 @@ def validate_pillow_installed(value):
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
"Please install the pillow python package to use this feature. "
|
"Please install the pillow python package to use this feature. "
|
||||||
"(pip install pillow)"
|
'(pip install pillow">4.0.0,<10.0.0")'
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
if PIL.__version__[0] < "4":
|
if version.parse(PIL.__version__) < version.parse("4.0.0"):
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
"Please update your pillow installation to at least 4.0.x. "
|
"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
|
return value
|
||||||
|
|
|
@ -1,18 +1,35 @@
|
||||||
#include "font.h"
|
#include "font.h"
|
||||||
|
|
||||||
#include "esphome/core/hal.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 esphome {
|
||||||
namespace display {
|
namespace font {
|
||||||
|
|
||||||
bool Glyph::get_pixel(int x, int y) const {
|
static const char *const TAG = "font";
|
||||||
const int x_data = x - this->glyph_data_->offset_x;
|
|
||||||
const int y_data = y - this->glyph_data_->offset_y;
|
void Glyph::draw(int x_at, int y_start, display::Display *display, Color color) const {
|
||||||
if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height)
|
int scan_x1, scan_y1, scan_width, scan_height;
|
||||||
return false;
|
this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
|
||||||
const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u;
|
|
||||||
const uint32_t pos = x_data + y_data * width_8;
|
const unsigned char *data = this->glyph_data_->data;
|
||||||
return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u));
|
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; }
|
const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
|
||||||
bool Glyph::compare_to(const char *str) const {
|
bool Glyph::compare_to(const char *str) const {
|
||||||
|
@ -47,6 +64,12 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
|
||||||
*width = this->glyph_data_->width;
|
*width = this->glyph_data_->width;
|
||||||
*height = this->glyph_data_->height;
|
*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 Font::match_next_glyph(const char *str, int *match_length) {
|
||||||
int lo = 0;
|
int lo = 0;
|
||||||
int hi = this->glyphs_.size() - 1;
|
int hi = this->glyphs_.size() - 1;
|
||||||
|
@ -95,11 +118,32 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
|
||||||
*x_offset = min_x;
|
*x_offset = min_x;
|
||||||
*width = x - min_x;
|
*width = x - min_x;
|
||||||
}
|
}
|
||||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
|
void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text) {
|
||||||
glyphs_.reserve(data_nr);
|
int i = 0;
|
||||||
for (int i = 0; i < data_nr; ++i)
|
int x_at = x_start;
|
||||||
glyphs_.emplace_back(&data[i]);
|
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 display
|
} // namespace font
|
||||||
} // namespace esphome
|
} // namespace esphome
|
|
@ -1,11 +1,12 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/datatypes.h"
|
#include "esphome/core/datatypes.h"
|
||||||
|
#include "esphome/core/color.h"
|
||||||
|
#include "esphome/components/display/display_buffer.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display {
|
namespace font {
|
||||||
|
|
||||||
class DisplayBuffer;
|
|
||||||
class Font;
|
class Font;
|
||||||
|
|
||||||
struct GlyphData {
|
struct GlyphData {
|
||||||
|
@ -21,7 +22,7 @@ class Glyph {
|
||||||
public:
|
public:
|
||||||
Glyph(const GlyphData *data) : glyph_data_(data) {}
|
Glyph(const GlyphData *data) : glyph_data_(data) {}
|
||||||
|
|
||||||
bool get_pixel(int x, int y) const;
|
void draw(int x, int y, display::Display *display, Color color) const;
|
||||||
|
|
||||||
const char *get_char() const;
|
const char *get_char() const;
|
||||||
|
|
||||||
|
@ -33,12 +34,11 @@ class Glyph {
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend Font;
|
friend Font;
|
||||||
friend DisplayBuffer;
|
|
||||||
|
|
||||||
const GlyphData *glyph_data_;
|
const GlyphData *glyph_data_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Font {
|
class Font : public display::BaseFont {
|
||||||
public:
|
public:
|
||||||
/** Construct the font with the given glyphs.
|
/** Construct the font with the given glyphs.
|
||||||
*
|
*
|
||||||
|
@ -50,7 +50,8 @@ class Font {
|
||||||
|
|
||||||
int match_next_glyph(const char *str, int *match_length);
|
int match_next_glyph(const char *str, int *match_length);
|
||||||
|
|
||||||
void measure(const char *str, int *width, int *x_offset, int *baseline, int *height);
|
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_baseline() { return this->baseline_; }
|
||||||
inline int get_height() { return this->height_; }
|
inline int get_height() { return this->height_; }
|
||||||
|
|
||||||
|
@ -62,5 +63,5 @@ class Font {
|
||||||
int height_;
|
int height_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace display
|
} // namespace font
|
||||||
} // namespace esphome
|
} // namespace esphome
|
|
@ -1,5 +1,5 @@
|
||||||
#include "graph.h"
|
#include "graph.h"
|
||||||
#include "esphome/components/display/display_buffer.h"
|
#include "esphome/components/display/display.h"
|
||||||
#include "esphome/core/color.h"
|
#include "esphome/core/color.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/hal.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());
|
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
|
/// Plot border
|
||||||
if (this->border_) {
|
if (this->border_) {
|
||||||
buff->horizontal_line(x_offset, y_offset, this->width_, color);
|
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_)
|
if (!legend_)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,10 @@
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
// forward declare DisplayBuffer
|
// forward declare Display
|
||||||
namespace display {
|
namespace display {
|
||||||
class DisplayBuffer;
|
class Display;
|
||||||
class Font;
|
class BaseFont;
|
||||||
} // namespace display
|
} // namespace display
|
||||||
|
|
||||||
namespace graph {
|
namespace graph {
|
||||||
|
@ -45,8 +45,8 @@ enum ValuePositionType {
|
||||||
class GraphLegend {
|
class GraphLegend {
|
||||||
public:
|
public:
|
||||||
void init(Graph *g);
|
void init(Graph *g);
|
||||||
void set_name_font(display::Font *font) { this->font_label_ = font; }
|
void set_name_font(display::BaseFont *font) { this->font_label_ = font; }
|
||||||
void set_value_font(display::Font *font) { this->font_value_ = font; }
|
void set_value_font(display::BaseFont *font) { this->font_value_ = font; }
|
||||||
void set_width(uint32_t width) { this->width_ = width; }
|
void set_width(uint32_t width) { this->width_ = width; }
|
||||||
void set_height(uint32_t height) { this->height_ = height; }
|
void set_height(uint32_t height) { this->height_ = height; }
|
||||||
void set_border(bool val) { this->border_ = val; }
|
void set_border(bool val) { this->border_ = val; }
|
||||||
|
@ -63,8 +63,8 @@ class GraphLegend {
|
||||||
ValuePositionType values_{VALUE_POSITION_TYPE_AUTO};
|
ValuePositionType values_{VALUE_POSITION_TYPE_AUTO};
|
||||||
bool units_{true};
|
bool units_{true};
|
||||||
DirectionType direction_{DIRECTION_TYPE_AUTO};
|
DirectionType direction_{DIRECTION_TYPE_AUTO};
|
||||||
display::Font *font_label_{nullptr};
|
display::BaseFont *font_label_{nullptr};
|
||||||
display::Font *font_value_{nullptr};
|
display::BaseFont *font_value_{nullptr};
|
||||||
// Calculated values
|
// Calculated values
|
||||||
Graph *parent_{nullptr};
|
Graph *parent_{nullptr};
|
||||||
// (x0) (xs,ys) (xs,ys)
|
// (x0) (xs,ys) (xs,ys)
|
||||||
|
@ -133,8 +133,8 @@ class GraphTrace {
|
||||||
|
|
||||||
class Graph : public Component {
|
class Graph : public Component {
|
||||||
public:
|
public:
|
||||||
void draw(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::DisplayBuffer *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;
|
void setup() override;
|
||||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
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
|
|
@ -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.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.components import uart
|
import esphome.final_validate as fv
|
||||||
from esphome.components.climate import ClimateSwingMode
|
from esphome.components import uart, sensor, climate, logger
|
||||||
from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES
|
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")
|
haier_ns = cg.esphome_ns.namespace("haier")
|
||||||
HaierClimate = haier_ns.class_(
|
HaierClimateBase = haier_ns.class_(
|
||||||
"HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice
|
"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,
|
AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection")
|
||||||
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
|
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
|
||||||
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
|
"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(
|
climate.CLIMATE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(HaierClimate),
|
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||||
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
|
cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
|
||||||
validate_swing_modes
|
|
||||||
),
|
),
|
||||||
|
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):
|
async def to_code(config):
|
||||||
|
cg.add(haier_ns.init_haier_protocol_logging())
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await climate.register_climate(var, config)
|
|
||||||
await uart.register_uart_device(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:
|
if CONF_SUPPORTED_SWING_MODES in config:
|
||||||
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
|
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")
|
||||||
|
|
|
@ -1,302 +0,0 @@
|
||||||
#include <cmath>
|
|
||||||
#include "haier.h"
|
|
||||||
#include "esphome/core/macros.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace haier {
|
|
||||||
|
|
||||||
static const char *const TAG = "haier";
|
|
||||||
|
|
||||||
static const uint8_t TEMPERATURE = 13;
|
|
||||||
static const uint8_t HUMIDITY = 15;
|
|
||||||
|
|
||||||
static const uint8_t MODE = 23;
|
|
||||||
|
|
||||||
static const uint8_t FAN_SPEED = 25;
|
|
||||||
|
|
||||||
static const uint8_t SWING = 27;
|
|
||||||
|
|
||||||
static const uint8_t POWER = 29;
|
|
||||||
static const uint8_t POWER_MASK = 1;
|
|
||||||
|
|
||||||
static const uint8_t SET_TEMPERATURE = 35;
|
|
||||||
static const uint8_t DECIMAL_MASK = (1 << 5);
|
|
||||||
|
|
||||||
static const uint8_t CRC = 36;
|
|
||||||
|
|
||||||
static const uint8_t COMFORT_PRESET_MASK = (1 << 3);
|
|
||||||
|
|
||||||
static const uint8_t MIN_VALID_TEMPERATURE = 16;
|
|
||||||
static const uint8_t MAX_VALID_TEMPERATURE = 50;
|
|
||||||
static const float TEMPERATURE_STEP = 0.5f;
|
|
||||||
|
|
||||||
static const uint8_t POLL_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 1, 90};
|
|
||||||
static const uint8_t OFF_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 3, 92};
|
|
||||||
|
|
||||||
void HaierClimate::dump_config() {
|
|
||||||
ESP_LOGCONFIG(TAG, "Haier:");
|
|
||||||
ESP_LOGCONFIG(TAG, " Update interval: %u", this->get_update_interval());
|
|
||||||
this->dump_traits_(TAG);
|
|
||||||
this->check_uart_settings(9600);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::loop() {
|
|
||||||
if (this->available() >= sizeof(this->data_)) {
|
|
||||||
this->read_array(this->data_, sizeof(this->data_));
|
|
||||||
if (this->data_[0] != 255 || this->data_[1] != 255)
|
|
||||||
return;
|
|
||||||
|
|
||||||
read_state_(this->data_, sizeof(this->data_));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::update() {
|
|
||||||
this->write_array(POLL_REQ, sizeof(POLL_REQ));
|
|
||||||
dump_message_("Poll sent", POLL_REQ, sizeof(POLL_REQ));
|
|
||||||
}
|
|
||||||
|
|
||||||
climate::ClimateTraits HaierClimate::traits() {
|
|
||||||
auto traits = climate::ClimateTraits();
|
|
||||||
|
|
||||||
traits.set_visual_min_temperature(MIN_VALID_TEMPERATURE);
|
|
||||||
traits.set_visual_max_temperature(MAX_VALID_TEMPERATURE);
|
|
||||||
traits.set_visual_temperature_step(TEMPERATURE_STEP);
|
|
||||||
|
|
||||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL, climate::CLIMATE_MODE_COOL,
|
|
||||||
climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY});
|
|
||||||
|
|
||||||
traits.set_supported_fan_modes({
|
|
||||||
climate::CLIMATE_FAN_AUTO,
|
|
||||||
climate::CLIMATE_FAN_LOW,
|
|
||||||
climate::CLIMATE_FAN_MEDIUM,
|
|
||||||
climate::CLIMATE_FAN_HIGH,
|
|
||||||
});
|
|
||||||
|
|
||||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
|
||||||
traits.set_supports_current_temperature(true);
|
|
||||||
traits.set_supports_two_point_target_temperature(false);
|
|
||||||
|
|
||||||
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
|
|
||||||
traits.add_supported_preset(climate::CLIMATE_PRESET_COMFORT);
|
|
||||||
|
|
||||||
return traits;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::read_state_(const uint8_t *data, uint8_t size) {
|
|
||||||
dump_message_("Received state", data, size);
|
|
||||||
|
|
||||||
uint8_t check = data[CRC];
|
|
||||||
|
|
||||||
uint8_t crc = get_checksum_(data, size);
|
|
||||||
|
|
||||||
if (check != crc) {
|
|
||||||
ESP_LOGW(TAG, "Invalid checksum");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->current_temperature = data[TEMPERATURE];
|
|
||||||
|
|
||||||
this->target_temperature = data[SET_TEMPERATURE] + MIN_VALID_TEMPERATURE;
|
|
||||||
|
|
||||||
if (data[POWER] & DECIMAL_MASK) {
|
|
||||||
this->target_temperature += 0.5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data[MODE]) {
|
|
||||||
case MODE_SMART:
|
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
|
||||||
break;
|
|
||||||
case MODE_COOL:
|
|
||||||
this->mode = climate::CLIMATE_MODE_COOL;
|
|
||||||
break;
|
|
||||||
case MODE_HEAT:
|
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
|
||||||
break;
|
|
||||||
case MODE_ONLY_FAN:
|
|
||||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
|
||||||
break;
|
|
||||||
case MODE_DRY:
|
|
||||||
this->mode = climate::CLIMATE_MODE_DRY;
|
|
||||||
break;
|
|
||||||
default: // other modes are unsupported
|
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data[FAN_SPEED]) {
|
|
||||||
case FAN_AUTO:
|
|
||||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FAN_MIN:
|
|
||||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FAN_MIDDLE:
|
|
||||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FAN_MAX:
|
|
||||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data[SWING]) {
|
|
||||||
case SWING_OFF:
|
|
||||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SWING_VERTICAL:
|
|
||||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SWING_HORIZONTAL:
|
|
||||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SWING_BOTH:
|
|
||||||
this->swing_mode = climate::CLIMATE_SWING_BOTH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data[POWER] & COMFORT_PRESET_MASK) {
|
|
||||||
this->preset = climate::CLIMATE_PRESET_COMFORT;
|
|
||||||
} else {
|
|
||||||
this->preset = climate::CLIMATE_PRESET_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((data[POWER] & POWER_MASK) == 0) {
|
|
||||||
this->mode = climate::CLIMATE_MODE_OFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->publish_state();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::control(const climate::ClimateCall &call) {
|
|
||||||
if (call.get_mode().has_value()) {
|
|
||||||
switch (call.get_mode().value()) {
|
|
||||||
case climate::CLIMATE_MODE_OFF:
|
|
||||||
send_data_(OFF_REQ, sizeof(OFF_REQ));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
|
||||||
case climate::CLIMATE_MODE_AUTO:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_SMART;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_MODE_HEAT:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_HEAT;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_MODE_COOL:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_COOL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_ONLY_FAN;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case climate::CLIMATE_MODE_DRY:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_DRY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.get_preset().has_value()) {
|
|
||||||
if (call.get_preset().value() == climate::CLIMATE_PRESET_COMFORT) {
|
|
||||||
data_[POWER] |= COMFORT_PRESET_MASK;
|
|
||||||
} else {
|
|
||||||
data_[POWER] &= ~COMFORT_PRESET_MASK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.get_target_temperature().has_value()) {
|
|
||||||
float target = call.get_target_temperature().value() - MIN_VALID_TEMPERATURE;
|
|
||||||
|
|
||||||
data_[SET_TEMPERATURE] = (uint8_t) target;
|
|
||||||
|
|
||||||
if ((int) target == std::lroundf(target)) {
|
|
||||||
data_[POWER] &= ~DECIMAL_MASK;
|
|
||||||
} else {
|
|
||||||
data_[POWER] |= DECIMAL_MASK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.get_fan_mode().has_value()) {
|
|
||||||
switch (call.get_fan_mode().value()) {
|
|
||||||
case climate::CLIMATE_FAN_AUTO:
|
|
||||||
data_[FAN_SPEED] = FAN_AUTO;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_FAN_LOW:
|
|
||||||
data_[FAN_SPEED] = FAN_MIN;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_FAN_MEDIUM:
|
|
||||||
data_[FAN_SPEED] = FAN_MIDDLE;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_FAN_HIGH:
|
|
||||||
data_[FAN_SPEED] = FAN_MAX;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: // other modes are unsupported
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.get_swing_mode().has_value()) {
|
|
||||||
switch (call.get_swing_mode().value()) {
|
|
||||||
case climate::CLIMATE_SWING_OFF:
|
|
||||||
data_[SWING] = SWING_OFF;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_SWING_VERTICAL:
|
|
||||||
data_[SWING] = SWING_VERTICAL;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
|
||||||
data_[SWING] = SWING_HORIZONTAL;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_SWING_BOTH:
|
|
||||||
data_[SWING] = SWING_BOTH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parts of the message that must have specific values for "send" command.
|
|
||||||
// The meaning of those values is unknown at the moment.
|
|
||||||
data_[9] = 1;
|
|
||||||
data_[10] = 77;
|
|
||||||
data_[11] = 95;
|
|
||||||
data_[17] = 0;
|
|
||||||
|
|
||||||
// Compute checksum
|
|
||||||
uint8_t crc = get_checksum_(data_, sizeof(data_));
|
|
||||||
data_[CRC] = crc;
|
|
||||||
|
|
||||||
send_data_(data_, sizeof(data_));
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::send_data_(const uint8_t *message, uint8_t size) {
|
|
||||||
this->write_array(message, size);
|
|
||||||
|
|
||||||
dump_message_("Sent message", message, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::dump_message_(const char *title, const uint8_t *message, uint8_t size) {
|
|
||||||
ESP_LOGV(TAG, "%s:", title);
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
ESP_LOGV(TAG, " byte %02d - %d", i, message[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t HaierClimate::get_checksum_(const uint8_t *message, size_t size) {
|
|
||||||
uint8_t position = size - 1;
|
|
||||||
uint8_t crc = 0;
|
|
||||||
|
|
||||||
for (int i = 2; i < position; i++)
|
|
||||||
crc += message[i];
|
|
||||||
|
|
||||||
return crc;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace haier
|
|
||||||
} // namespace esphome
|
|
|
@ -1,37 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include "esphome/components/climate/climate.h"
|
|
||||||
#include "esphome/components/uart/uart.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace haier {
|
|
||||||
|
|
||||||
enum Mode : uint8_t { MODE_SMART = 0, MODE_COOL = 1, MODE_HEAT = 2, MODE_ONLY_FAN = 3, MODE_DRY = 4 };
|
|
||||||
enum FanSpeed : uint8_t { FAN_MAX = 0, FAN_MIDDLE = 1, FAN_MIN = 2, FAN_AUTO = 3 };
|
|
||||||
enum SwingMode : uint8_t { SWING_OFF = 0, SWING_VERTICAL = 1, SWING_HORIZONTAL = 2, SWING_BOTH = 3 };
|
|
||||||
|
|
||||||
class HaierClimate : public climate::Climate, public uart::UARTDevice, public PollingComponent {
|
|
||||||
public:
|
|
||||||
void loop() override;
|
|
||||||
void update() override;
|
|
||||||
void dump_config() override;
|
|
||||||
void control(const climate::ClimateCall &call) override;
|
|
||||||
void set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
|
|
||||||
this->supported_swing_modes_ = modes;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
climate::ClimateTraits traits() override;
|
|
||||||
void read_state_(const uint8_t *data, uint8_t size);
|
|
||||||
void send_data_(const uint8_t *message, uint8_t size);
|
|
||||||
void dump_message_(const char *title, const uint8_t *message, uint8_t size);
|
|
||||||
uint8_t get_checksum_(const uint8_t *message, size_t size);
|
|
||||||
|
|
||||||
private:
|
|
||||||
uint8_t data_[37];
|
|
||||||
std::set<climate::ClimateSwingMode> supported_swing_modes_{};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace haier
|
|
||||||
} // namespace esphome
|
|
311
esphome/components/haier/haier_base.cpp
Normal file
311
esphome/components/haier/haier_base.cpp
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
#include "haier_base.h"
|
||||||
|
|
||||||
|
using namespace esphome::climate;
|
||||||
|
using namespace esphome::uart;
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
static const char *const TAG = "haier.climate";
|
||||||
|
constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000;
|
||||||
|
constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000;
|
||||||
|
constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
|
||||||
|
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
|
||||||
|
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
|
||||||
|
constexpr size_t CONTROL_TIMEOUT_MS = 7000;
|
||||||
|
constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied
|
||||||
|
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
// To reduce size of binary this function only available when log level is Verbose
|
||||||
|
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
|
||||||
|
static const char *phase_names[] = {
|
||||||
|
"SENDING_INIT_1",
|
||||||
|
"WAITING_ANSWER_INIT_1",
|
||||||
|
"SENDING_INIT_2",
|
||||||
|
"WAITING_ANSWER_INIT_2",
|
||||||
|
"SENDING_FIRST_STATUS_REQUEST",
|
||||||
|
"WAITING_FIRST_STATUS_ANSWER",
|
||||||
|
"SENDING_ALARM_STATUS_REQUEST",
|
||||||
|
"WAITING_ALARM_STATUS_ANSWER",
|
||||||
|
"IDLE",
|
||||||
|
"SENDING_STATUS_REQUEST",
|
||||||
|
"WAITING_STATUS_ANSWER",
|
||||||
|
"SENDING_UPDATE_SIGNAL_REQUEST",
|
||||||
|
"WAITING_UPDATE_SIGNAL_ANSWER",
|
||||||
|
"SENDING_SIGNAL_LEVEL",
|
||||||
|
"WAITING_SIGNAL_LEVEL_ANSWER",
|
||||||
|
"SENDING_CONTROL",
|
||||||
|
"WAITING_CONTROL_ANSWER",
|
||||||
|
"SENDING_POWER_ON_COMMAND",
|
||||||
|
"WAITING_POWER_ON_ANSWER",
|
||||||
|
"SENDING_POWER_OFF_COMMAND",
|
||||||
|
"WAITING_POWER_OFF_ANSWER",
|
||||||
|
"UNKNOWN" // Should be the last!
|
||||||
|
};
|
||||||
|
int phase_index = (int) phase;
|
||||||
|
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
|
||||||
|
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
|
||||||
|
return phase_names[phase_index];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HaierClimateBase::HaierClimateBase()
|
||||||
|
: haier_protocol_(*this),
|
||||||
|
protocol_phase_(ProtocolPhases::SENDING_INIT_1),
|
||||||
|
action_request_(ActionRequest::NO_ACTION),
|
||||||
|
display_status_(true),
|
||||||
|
health_mode_(false),
|
||||||
|
force_send_control_(false),
|
||||||
|
forced_publish_(false),
|
||||||
|
forced_request_status_(false),
|
||||||
|
first_control_attempt_(false),
|
||||||
|
reset_protocol_request_(false) {
|
||||||
|
this->traits_ = climate::ClimateTraits();
|
||||||
|
this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT,
|
||||||
|
climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY,
|
||||||
|
climate::CLIMATE_MODE_AUTO});
|
||||||
|
this->traits_.set_supported_fan_modes(
|
||||||
|
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
|
||||||
|
this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
|
||||||
|
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
|
||||||
|
this->traits_.set_supports_current_temperature(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
HaierClimateBase::~HaierClimateBase() {}
|
||||||
|
|
||||||
|
void HaierClimateBase::set_phase_(ProtocolPhases phase) {
|
||||||
|
if (this->protocol_phase_ != phase) {
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
|
||||||
|
#else
|
||||||
|
ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase);
|
||||||
|
#endif
|
||||||
|
this->protocol_phase_ = phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now,
|
||||||
|
std::chrono::steady_clock::time_point tpoint, size_t timeout) {
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
|
||||||
|
return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) {
|
||||||
|
return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) {
|
||||||
|
return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
|
||||||
|
return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now) {
|
||||||
|
return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::get_display_state() const { return this->display_status_; }
|
||||||
|
|
||||||
|
void HaierClimateBase::set_display_state(bool state) {
|
||||||
|
if (this->display_status_ != state) {
|
||||||
|
this->display_status_ = state;
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
|
||||||
|
|
||||||
|
void HaierClimateBase::set_health_mode(bool state) {
|
||||||
|
if (this->health_mode_ != state) {
|
||||||
|
this->health_mode_ = state;
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; }
|
||||||
|
|
||||||
|
void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; }
|
||||||
|
|
||||||
|
void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; }
|
||||||
|
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
|
||||||
|
this->traits_.set_supported_swing_modes(modes);
|
||||||
|
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); // Always available
|
||||||
|
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); // Always available
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
|
||||||
|
this->traits_.set_supported_modes(modes);
|
||||||
|
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
|
||||||
|
this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Always available
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type,
|
||||||
|
uint8_t expected_request_message_type,
|
||||||
|
uint8_t answer_message_type,
|
||||||
|
uint8_t expected_answer_message_type,
|
||||||
|
ProtocolPhases expected_phase) {
|
||||||
|
haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
|
||||||
|
if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type))
|
||||||
|
result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
|
||||||
|
if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type))
|
||||||
|
result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
|
||||||
|
if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))
|
||||||
|
result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
|
||||||
|
if (is_message_invalid(answer_message_type))
|
||||||
|
result = haier_protocol::HandlerError::INVALID_ANSWER;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) {
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_));
|
||||||
|
#else
|
||||||
|
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_);
|
||||||
|
#endif
|
||||||
|
if (this->protocol_phase_ > ProtocolPhases::IDLE) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
}
|
||||||
|
return haier_protocol::HandlerError::HANDLER_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::setup() {
|
||||||
|
ESP_LOGI(TAG, "Haier initialization...");
|
||||||
|
// Set timestamp here to give AC time to boot
|
||||||
|
this->last_request_timestamp_ = std::chrono::steady_clock::now();
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
this->set_answers_handlers();
|
||||||
|
this->haier_protocol_.set_default_timeout_handler(
|
||||||
|
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::dump_config() {
|
||||||
|
LOG_CLIMATE("", "Haier Climate", this);
|
||||||
|
ESP_LOGCONFIG(TAG, " Device communication status: %s",
|
||||||
|
(this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::loop() {
|
||||||
|
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||||
|
if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() >
|
||||||
|
COMMUNICATION_TIMEOUT_MS) ||
|
||||||
|
(this->reset_protocol_request_)) {
|
||||||
|
if (this->protocol_phase_ >= ProtocolPhases::IDLE) {
|
||||||
|
// No status too long, reseting protocol
|
||||||
|
if (this->reset_protocol_request_) {
|
||||||
|
this->reset_protocol_request_ = false;
|
||||||
|
ESP_LOGW(TAG, "Protocol reset requested");
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Communication timeout, reseting protocol");
|
||||||
|
}
|
||||||
|
this->last_valid_status_timestamp_ = now;
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
if (this->hvac_settings_.valid)
|
||||||
|
this->hvac_settings_.reset();
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// No need to reset protocol if we didn't pass initialization phase
|
||||||
|
this->last_valid_status_timestamp_ = now;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if ((this->protocol_phase_ == ProtocolPhases::IDLE) ||
|
||||||
|
(this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
|
||||||
|
(this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) ||
|
||||||
|
(this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) {
|
||||||
|
// If control message or action is pending we should send it ASAP unless we are in initialisation
|
||||||
|
// procedure or waiting for an answer
|
||||||
|
if (this->action_request_ != ActionRequest::NO_ACTION) {
|
||||||
|
this->process_pending_action();
|
||||||
|
} else if (this->hvac_settings_.valid || this->force_send_control_) {
|
||||||
|
ESP_LOGV(TAG, "Control packet is pending...");
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_CONTROL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->process_phase(now);
|
||||||
|
this->haier_protocol_.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::process_pending_action() {
|
||||||
|
ActionRequest request = this->action_request_;
|
||||||
|
if (this->action_request_ == ActionRequest::TOGGLE_POWER) {
|
||||||
|
request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF;
|
||||||
|
}
|
||||||
|
switch (request) {
|
||||||
|
case ActionRequest::TURN_POWER_ON:
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_POWER_ON_COMMAND);
|
||||||
|
break;
|
||||||
|
case ActionRequest::TURN_POWER_OFF:
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_POWER_OFF_COMMAND);
|
||||||
|
break;
|
||||||
|
case ActionRequest::TOGGLE_POWER:
|
||||||
|
case ActionRequest::NO_ACTION:
|
||||||
|
// shouldn't get here, do nothing
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->action_request_ = ActionRequest::NO_ACTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClimateTraits HaierClimateBase::traits() { return traits_; }
|
||||||
|
|
||||||
|
void HaierClimateBase::control(const ClimateCall &call) {
|
||||||
|
ESP_LOGD("Control", "Control call");
|
||||||
|
if (this->protocol_phase_ < ProtocolPhases::IDLE) {
|
||||||
|
ESP_LOGW(TAG, "Can't send control packet, first poll answer not received");
|
||||||
|
return; // cancel the control, we cant do it without a poll answer.
|
||||||
|
}
|
||||||
|
if (this->hvac_settings_.valid) {
|
||||||
|
ESP_LOGW(TAG, "Overriding old valid settings before they were applied!");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
if (call.get_mode().has_value())
|
||||||
|
this->hvac_settings_.mode = call.get_mode();
|
||||||
|
if (call.get_fan_mode().has_value())
|
||||||
|
this->hvac_settings_.fan_mode = call.get_fan_mode();
|
||||||
|
if (call.get_swing_mode().has_value())
|
||||||
|
this->hvac_settings_.swing_mode = call.get_swing_mode();
|
||||||
|
if (call.get_target_temperature().has_value())
|
||||||
|
this->hvac_settings_.target_temperature = call.get_target_temperature();
|
||||||
|
if (call.get_preset().has_value())
|
||||||
|
this->hvac_settings_.preset = call.get_preset();
|
||||||
|
this->hvac_settings_.valid = true;
|
||||||
|
}
|
||||||
|
this->first_control_attempt_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::HvacSettings::reset() {
|
||||||
|
this->valid = false;
|
||||||
|
this->mode.reset();
|
||||||
|
this->fan_mode.reset();
|
||||||
|
this->swing_mode.reset();
|
||||||
|
this->target_temperature.reset();
|
||||||
|
this->preset.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::set_force_send_control_(bool status) {
|
||||||
|
this->force_send_control_ = status;
|
||||||
|
if (status) {
|
||||||
|
this->first_control_attempt_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) {
|
||||||
|
this->haier_protocol_.send_message(command, use_crc);
|
||||||
|
this->last_request_timestamp_ = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
142
esphome/components/haier/haier_base.h
Normal file
142
esphome/components/haier/haier_base.h
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <set>
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
// HaierProtocol
|
||||||
|
#include <protocol/haier_protocol.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
enum class ActionRequest : uint8_t {
|
||||||
|
NO_ACTION = 0,
|
||||||
|
TURN_POWER_ON = 1,
|
||||||
|
TURN_POWER_OFF = 2,
|
||||||
|
TOGGLE_POWER = 3,
|
||||||
|
START_SELF_CLEAN = 4, // only hOn
|
||||||
|
START_STERI_CLEAN = 5, // only hOn
|
||||||
|
};
|
||||||
|
|
||||||
|
class HaierClimateBase : public esphome::Component,
|
||||||
|
public esphome::climate::Climate,
|
||||||
|
public esphome::uart::UARTDevice,
|
||||||
|
public haier_protocol::ProtocolStream {
|
||||||
|
public:
|
||||||
|
HaierClimateBase();
|
||||||
|
HaierClimateBase(const HaierClimateBase &) = delete;
|
||||||
|
HaierClimateBase &operator=(const HaierClimateBase &) = delete;
|
||||||
|
~HaierClimateBase();
|
||||||
|
void setup() override;
|
||||||
|
void loop() override;
|
||||||
|
void control(const esphome::climate::ClimateCall &call) override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
|
||||||
|
void set_fahrenheit(bool fahrenheit);
|
||||||
|
void set_display_state(bool state);
|
||||||
|
bool get_display_state() const;
|
||||||
|
void set_health_mode(bool state);
|
||||||
|
bool get_health_mode() const;
|
||||||
|
void send_power_on_command();
|
||||||
|
void send_power_off_command();
|
||||||
|
void toggle_power();
|
||||||
|
void reset_protocol() { this->reset_protocol_request_ = true; };
|
||||||
|
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
|
||||||
|
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
|
||||||
|
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
|
||||||
|
size_t read_array(uint8_t *data, size_t len) noexcept override {
|
||||||
|
return esphome::uart::UARTDevice::read_array(data, len) ? len : 0;
|
||||||
|
};
|
||||||
|
void write_array(const uint8_t *data, size_t len) noexcept override {
|
||||||
|
esphome::uart::UARTDevice::write_array(data, len);
|
||||||
|
};
|
||||||
|
bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
|
||||||
|
|
||||||
|
protected:
|
||||||
|
enum class ProtocolPhases {
|
||||||
|
UNKNOWN = -1,
|
||||||
|
// INITIALIZATION
|
||||||
|
SENDING_INIT_1 = 0,
|
||||||
|
WAITING_ANSWER_INIT_1 = 1,
|
||||||
|
SENDING_INIT_2 = 2,
|
||||||
|
WAITING_ANSWER_INIT_2 = 3,
|
||||||
|
SENDING_FIRST_STATUS_REQUEST = 4,
|
||||||
|
WAITING_FIRST_STATUS_ANSWER = 5,
|
||||||
|
SENDING_ALARM_STATUS_REQUEST = 6,
|
||||||
|
WAITING_ALARM_STATUS_ANSWER = 7,
|
||||||
|
// FUNCTIONAL STATE
|
||||||
|
IDLE = 8,
|
||||||
|
SENDING_STATUS_REQUEST = 9,
|
||||||
|
WAITING_STATUS_ANSWER = 10,
|
||||||
|
SENDING_UPDATE_SIGNAL_REQUEST = 11,
|
||||||
|
WAITING_UPDATE_SIGNAL_ANSWER = 12,
|
||||||
|
SENDING_SIGNAL_LEVEL = 13,
|
||||||
|
WAITING_SIGNAL_LEVEL_ANSWER = 14,
|
||||||
|
SENDING_CONTROL = 15,
|
||||||
|
WAITING_CONTROL_ANSWER = 16,
|
||||||
|
SENDING_POWER_ON_COMMAND = 17,
|
||||||
|
WAITING_POWER_ON_ANSWER = 18,
|
||||||
|
SENDING_POWER_OFF_COMMAND = 19,
|
||||||
|
WAITING_POWER_OFF_ANSWER = 20,
|
||||||
|
NUM_PROTOCOL_PHASES
|
||||||
|
};
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
const char *phase_to_string_(ProtocolPhases phase);
|
||||||
|
#endif
|
||||||
|
virtual void set_answers_handlers() = 0;
|
||||||
|
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
|
||||||
|
virtual haier_protocol::HaierMessage get_control_message() = 0;
|
||||||
|
virtual bool is_message_invalid(uint8_t message_type) = 0;
|
||||||
|
virtual void process_pending_action();
|
||||||
|
esphome::climate::ClimateTraits traits() override;
|
||||||
|
// Answers handlers
|
||||||
|
haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type,
|
||||||
|
uint8_t answer_message_type, uint8_t expected_answer_message_type,
|
||||||
|
ProtocolPhases expected_phase);
|
||||||
|
// Timeout handler
|
||||||
|
haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type);
|
||||||
|
// Helper functions
|
||||||
|
void set_force_send_control_(bool status);
|
||||||
|
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc);
|
||||||
|
void set_phase_(ProtocolPhases phase);
|
||||||
|
bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
|
||||||
|
size_t timeout);
|
||||||
|
bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
|
||||||
|
bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now);
|
||||||
|
bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now);
|
||||||
|
bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
|
||||||
|
bool is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now);
|
||||||
|
|
||||||
|
struct HvacSettings {
|
||||||
|
esphome::optional<esphome::climate::ClimateMode> mode;
|
||||||
|
esphome::optional<esphome::climate::ClimateFanMode> fan_mode;
|
||||||
|
esphome::optional<esphome::climate::ClimateSwingMode> swing_mode;
|
||||||
|
esphome::optional<float> target_temperature;
|
||||||
|
esphome::optional<esphome::climate::ClimatePreset> preset;
|
||||||
|
bool valid;
|
||||||
|
HvacSettings() : valid(false){};
|
||||||
|
void reset();
|
||||||
|
};
|
||||||
|
haier_protocol::ProtocolHandler haier_protocol_;
|
||||||
|
ProtocolPhases protocol_phase_;
|
||||||
|
ActionRequest action_request_;
|
||||||
|
uint8_t fan_mode_speed_;
|
||||||
|
uint8_t other_modes_fan_speed_;
|
||||||
|
bool display_status_;
|
||||||
|
bool health_mode_;
|
||||||
|
bool force_send_control_;
|
||||||
|
bool forced_publish_;
|
||||||
|
bool forced_request_status_;
|
||||||
|
bool first_control_attempt_;
|
||||||
|
bool reset_protocol_request_;
|
||||||
|
esphome::climate::ClimateTraits traits_;
|
||||||
|
HvacSettings hvac_settings_;
|
||||||
|
std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages
|
||||||
|
std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout
|
||||||
|
std::chrono::steady_clock::time_point last_status_request_; // To request AC status
|
||||||
|
std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
857
esphome/components/haier/hon_climate.cpp
Normal file
857
esphome/components/haier/hon_climate.cpp
Normal file
|
@ -0,0 +1,857 @@
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
#ifdef USE_WIFI
|
||||||
|
#include "esphome/components/wifi/wifi_component.h"
|
||||||
|
#endif
|
||||||
|
#include "hon_climate.h"
|
||||||
|
#include "hon_packet.h"
|
||||||
|
|
||||||
|
using namespace esphome::climate;
|
||||||
|
using namespace esphome::uart;
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
static const char *const TAG = "haier.climate";
|
||||||
|
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
|
||||||
|
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
|
||||||
|
|
||||||
|
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
|
||||||
|
switch (direction) {
|
||||||
|
case AirflowVerticalDirection::HEALTH_UP:
|
||||||
|
return hon_protocol::VerticalSwingMode::HEALTH_UP;
|
||||||
|
case AirflowVerticalDirection::MAX_UP:
|
||||||
|
return hon_protocol::VerticalSwingMode::MAX_UP;
|
||||||
|
case AirflowVerticalDirection::UP:
|
||||||
|
return hon_protocol::VerticalSwingMode::UP;
|
||||||
|
case AirflowVerticalDirection::DOWN:
|
||||||
|
return hon_protocol::VerticalSwingMode::DOWN;
|
||||||
|
case AirflowVerticalDirection::HEALTH_DOWN:
|
||||||
|
return hon_protocol::VerticalSwingMode::HEALTH_DOWN;
|
||||||
|
default:
|
||||||
|
return hon_protocol::VerticalSwingMode::CENTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) {
|
||||||
|
switch (direction) {
|
||||||
|
case AirflowHorizontalDirection::MAX_LEFT:
|
||||||
|
return hon_protocol::HorizontalSwingMode::MAX_LEFT;
|
||||||
|
case AirflowHorizontalDirection::LEFT:
|
||||||
|
return hon_protocol::HorizontalSwingMode::LEFT;
|
||||||
|
case AirflowHorizontalDirection::RIGHT:
|
||||||
|
return hon_protocol::HorizontalSwingMode::RIGHT;
|
||||||
|
case AirflowHorizontalDirection::MAX_RIGHT:
|
||||||
|
return hon_protocol::HorizontalSwingMode::MAX_RIGHT;
|
||||||
|
default:
|
||||||
|
return hon_protocol::HorizontalSwingMode::CENTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HonClimate::HonClimate()
|
||||||
|
: last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]),
|
||||||
|
cleaning_status_(CleaningState::NO_CLEANING),
|
||||||
|
got_valid_outdoor_temp_(false),
|
||||||
|
hvac_hardware_info_available_(false),
|
||||||
|
hvac_functions_{false, false, false, false, false},
|
||||||
|
use_crc_(hvac_functions_[2]),
|
||||||
|
active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||||
|
outdoor_sensor_(nullptr),
|
||||||
|
send_wifi_signal_(true) {
|
||||||
|
this->traits_.set_supported_presets({
|
||||||
|
climate::CLIMATE_PRESET_NONE,
|
||||||
|
climate::CLIMATE_PRESET_ECO,
|
||||||
|
climate::CLIMATE_PRESET_BOOST,
|
||||||
|
climate::CLIMATE_PRESET_SLEEP,
|
||||||
|
});
|
||||||
|
this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID;
|
||||||
|
this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
HonClimate::~HonClimate() {}
|
||||||
|
|
||||||
|
void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
|
||||||
|
|
||||||
|
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
|
||||||
|
|
||||||
|
void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
|
||||||
|
|
||||||
|
AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; };
|
||||||
|
|
||||||
|
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
|
||||||
|
if (direction > AirflowVerticalDirection::DOWN) {
|
||||||
|
this->vertical_direction_ = AirflowVerticalDirection::CENTER;
|
||||||
|
} else {
|
||||||
|
this->vertical_direction_ = direction;
|
||||||
|
}
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
|
||||||
|
|
||||||
|
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
|
||||||
|
if (direction > AirflowHorizontalDirection::RIGHT) {
|
||||||
|
this->horizontal_direction_ = AirflowHorizontalDirection::CENTER;
|
||||||
|
} else {
|
||||||
|
this->horizontal_direction_ = direction;
|
||||||
|
}
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HonClimate::get_cleaning_status_text() const {
|
||||||
|
switch (this->cleaning_status_) {
|
||||||
|
case CleaningState::SELF_CLEAN:
|
||||||
|
return "Self clean";
|
||||||
|
case CleaningState::STERI_CLEAN:
|
||||||
|
return "56°C Steri-Clean";
|
||||||
|
default:
|
||||||
|
return "No cleaning";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; }
|
||||||
|
|
||||||
|
void HonClimate::start_self_cleaning() {
|
||||||
|
if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
|
||||||
|
ESP_LOGI(TAG, "Sending self cleaning start request");
|
||||||
|
this->action_request_ = ActionRequest::START_SELF_CLEAN;
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::start_steri_cleaning() {
|
||||||
|
if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
|
||||||
|
ESP_LOGI(TAG, "Sending steri cleaning start request");
|
||||||
|
this->action_request_ = ActionRequest::START_STERI_CLEAN;
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result = this->answer_preprocess_(
|
||||||
|
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type,
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_1);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) {
|
||||||
|
// Wrong structure
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||||
|
}
|
||||||
|
// All OK
|
||||||
|
hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
|
||||||
|
char tmp[9];
|
||||||
|
tmp[8] = 0;
|
||||||
|
strncpy(tmp, answr->protocol_version, 8);
|
||||||
|
this->hvac_protocol_version_ = std::string(tmp);
|
||||||
|
strncpy(tmp, answr->software_version, 8);
|
||||||
|
this->hvac_software_version_ = std::string(tmp);
|
||||||
|
strncpy(tmp, answr->hardware_version, 8);
|
||||||
|
this->hvac_hardware_version_ = std::string(tmp);
|
||||||
|
strncpy(tmp, answr->device_name, 8);
|
||||||
|
this->hvac_device_name_ = std::string(tmp);
|
||||||
|
this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
|
||||||
|
this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support
|
||||||
|
this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
|
||||||
|
this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
|
||||||
|
this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
|
||||||
|
this->hvac_hardware_info_available_ = true;
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_2);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result = this->answer_preprocess_(
|
||||||
|
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type,
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_2);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result =
|
||||||
|
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type,
|
||||||
|
(uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
result = this->process_status_message_(data, data_size);
|
||||||
|
if (result != haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_INIT_1);
|
||||||
|
} else {
|
||||||
|
if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
|
||||||
|
memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
|
||||||
|
sizeof(hon_protocol::HaierPacketControl));
|
||||||
|
}
|
||||||
|
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) {
|
||||||
|
ESP_LOGI(TAG, "First HVAC status received");
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
|
||||||
|
} else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) ||
|
||||||
|
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) ||
|
||||||
|
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
if (this->hvac_settings_.valid)
|
||||||
|
this->hvac_settings_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type,
|
||||||
|
uint8_t message_type,
|
||||||
|
const uint8_t *data,
|
||||||
|
size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result =
|
||||||
|
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
|
||||||
|
message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE,
|
||||||
|
ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_SIGNAL_LEVEL);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type,
|
||||||
|
uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result =
|
||||||
|
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
|
||||||
|
(uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) {
|
||||||
|
if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
|
||||||
|
// Unexpected answer to request
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
|
||||||
|
}
|
||||||
|
if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) {
|
||||||
|
// Don't expect this answer now
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
|
||||||
|
}
|
||||||
|
memcpy(this->active_alarms_, data + 2, 8);
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return haier_protocol::HandlerError::HANDLER_OK;
|
||||||
|
} else {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::set_answers_handlers() {
|
||||||
|
// Set handlers
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION),
|
||||||
|
std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID),
|
||||||
|
std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::CONTROL),
|
||||||
|
std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
|
||||||
|
std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION),
|
||||||
|
std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
|
||||||
|
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS),
|
||||||
|
std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS),
|
||||||
|
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::dump_config() {
|
||||||
|
HaierClimateBase::dump_config();
|
||||||
|
ESP_LOGCONFIG(TAG, " Protocol version: hOn");
|
||||||
|
if (this->hvac_hardware_info_available_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""),
|
||||||
|
(this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""),
|
||||||
|
(this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : ""));
|
||||||
|
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
|
||||||
|
switch (this->protocol_phase_) {
|
||||||
|
case ProtocolPhases::SENDING_INIT_1:
|
||||||
|
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) {
|
||||||
|
this->hvac_hardware_info_available_ = false;
|
||||||
|
// Indicate device capabilities:
|
||||||
|
// bit 0 - if 1 module support interactive mode
|
||||||
|
// bit 1 - if 1 module support controller-device mode
|
||||||
|
// bit 2 - if 1 module support crc
|
||||||
|
// bit 3 - if 1 module support multiple devices
|
||||||
|
// bit 4..bit 15 - not used
|
||||||
|
uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
|
||||||
|
static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
|
||||||
|
this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_);
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_INIT_2:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID);
|
||||||
|
this->send_message_(DEVICEID_REQUEST, this->use_crc_);
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
||||||
|
case ProtocolPhases::SENDING_STATUS_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage STATUS_REQUEST(
|
||||||
|
(uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcomandsControl::GET_USER_DATA);
|
||||||
|
this->send_message_(STATUS_REQUEST, this->use_crc_);
|
||||||
|
this->last_status_request_ = now;
|
||||||
|
this->set_phase_((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
#ifdef USE_WIFI
|
||||||
|
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST(
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION);
|
||||||
|
this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_);
|
||||||
|
this->last_signal_request_ = now;
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00};
|
||||||
|
if (wifi::global_wifi_component->is_connected()) {
|
||||||
|
wifi_status_data[1] = 0;
|
||||||
|
int8_t rssi = wifi::global_wifi_component->wifi_rssi();
|
||||||
|
wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f);
|
||||||
|
ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]);
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "WiFi is not connected");
|
||||||
|
wifi_status_data[1] = 1;
|
||||||
|
wifi_status_data[3] = 0;
|
||||||
|
}
|
||||||
|
haier_protocol::HaierMessage wifi_status_request((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS,
|
||||||
|
wifi_status_data, sizeof(wifi_status_data));
|
||||||
|
this->send_message_(wifi_status_request, this->use_crc_);
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||||
|
break;
|
||||||
|
#else
|
||||||
|
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||||
|
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||||
|
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS);
|
||||||
|
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_CONTROL:
|
||||||
|
if (this->first_control_attempt_) {
|
||||||
|
this->control_request_timestamp_ = now;
|
||||||
|
this->first_control_attempt_ = false;
|
||||||
|
}
|
||||||
|
if (this->is_control_message_timeout_exceeded_(now)) {
|
||||||
|
ESP_LOGW(TAG, "Sending control packet timeout!");
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
if (this->hvac_settings_.valid)
|
||||||
|
this->hvac_settings_.reset();
|
||||||
|
this->forced_request_status_ = true;
|
||||||
|
this->forced_publish_ = true;
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
|
||||||
|
haier_protocol::HaierMessage control_message = get_control_message();
|
||||||
|
this->send_message_(control_message, this->use_crc_);
|
||||||
|
ESP_LOGI(TAG, "Control packet sent");
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_POWER_ON_COMMAND:
|
||||||
|
case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
uint8_t pwr_cmd_buf[2] = {0x00, 0x00};
|
||||||
|
if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND)
|
||||||
|
pwr_cmd_buf[1] = 0x01;
|
||||||
|
haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL,
|
||||||
|
((uint16_t) hon_protocol::SubcomandsControl::SET_SINGLE_PARAMETER) + 1,
|
||||||
|
pwr_cmd_buf, sizeof(pwr_cmd_buf));
|
||||||
|
this->send_message_(power_cmd, this->use_crc_);
|
||||||
|
this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
|
||||||
|
? ProtocolPhases::WAITING_POWER_ON_ANSWER
|
||||||
|
: ProtocolPhases::WAITING_POWER_OFF_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ProtocolPhases::WAITING_ANSWER_INIT_1:
|
||||||
|
case ProtocolPhases::WAITING_ANSWER_INIT_2:
|
||||||
|
case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_STATUS_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_CONTROL_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_POWER_ON_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::IDLE: {
|
||||||
|
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST);
|
||||||
|
this->forced_request_status_ = false;
|
||||||
|
}
|
||||||
|
#ifdef USE_WIFI
|
||||||
|
else if (this->send_wifi_signal_ &&
|
||||||
|
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
|
||||||
|
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
|
||||||
|
#endif
|
||||||
|
} break;
|
||||||
|
default:
|
||||||
|
// Shouldn't get here
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
|
||||||
|
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
|
||||||
|
#else
|
||||||
|
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
|
||||||
|
#endif
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||||
|
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
|
||||||
|
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
|
||||||
|
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
|
||||||
|
bool has_hvac_settings = false;
|
||||||
|
if (this->hvac_settings_.valid) {
|
||||||
|
has_hvac_settings = true;
|
||||||
|
HvacSettings climate_control;
|
||||||
|
climate_control = this->hvac_settings_;
|
||||||
|
if (climate_control.mode.has_value()) {
|
||||||
|
switch (climate_control.mode.value()) {
|
||||||
|
case CLIMATE_MODE_OFF:
|
||||||
|
out_data->ac_power = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_AUTO:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_HEAT:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::HEAT;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_DRY:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_FAN_ONLY:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN;
|
||||||
|
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
|
||||||
|
// Disabling boost and eco mode for Fan only
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_COOL:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported climate mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set fan speed, if we are in fan mode, reject auto in fan mode
|
||||||
|
if (climate_control.fan_mode.has_value()) {
|
||||||
|
switch (climate_control.fan_mode.value()) {
|
||||||
|
case CLIMATE_FAN_LOW:
|
||||||
|
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_MEDIUM:
|
||||||
|
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_HIGH:
|
||||||
|
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_HIGH;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_AUTO:
|
||||||
|
if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode
|
||||||
|
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported fan mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set swing mode
|
||||||
|
if (climate_control.swing_mode.has_value()) {
|
||||||
|
switch (climate_control.swing_mode.value()) {
|
||||||
|
case CLIMATE_SWING_OFF:
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_VERTICAL:
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_HORIZONTAL:
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_BOTH:
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (climate_control.target_temperature.has_value()) {
|
||||||
|
out_data->set_point =
|
||||||
|
climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16.
|
||||||
|
}
|
||||||
|
if (out_data->ac_power == 0) {
|
||||||
|
// If AC is off - no presets alowed
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
} else if (climate_control.preset.has_value()) {
|
||||||
|
switch (climate_control.preset.value()) {
|
||||||
|
case CLIMATE_PRESET_NONE:
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_ECO:
|
||||||
|
// Eco is not supported in Fan only mode
|
||||||
|
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_BOOST:
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
// Boost is not supported in Fan only mode
|
||||||
|
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_AWAY:
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_SLEEP:
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported preset");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO)
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
|
||||||
|
if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
|
||||||
|
}
|
||||||
|
out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0;
|
||||||
|
control_out_buffer[4] = 0; // This byte should be cleared before setting values
|
||||||
|
out_data->display_status = this->display_status_ ? 1 : 0;
|
||||||
|
out_data->health_mode = this->health_mode_ ? 1 : 0;
|
||||||
|
switch (this->action_request_) {
|
||||||
|
case ActionRequest::START_SELF_CLEAN:
|
||||||
|
this->action_request_ = ActionRequest::NO_ACTION;
|
||||||
|
out_data->self_cleaning_status = 1;
|
||||||
|
out_data->steri_clean = 0;
|
||||||
|
out_data->set_point = 0x06;
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
|
||||||
|
out_data->light_status = 0;
|
||||||
|
break;
|
||||||
|
case ActionRequest::START_STERI_CLEAN:
|
||||||
|
this->action_request_ = ActionRequest::NO_ACTION;
|
||||||
|
out_data->self_cleaning_status = 0;
|
||||||
|
out_data->steri_clean = 1;
|
||||||
|
out_data->set_point = 0x06;
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
|
||||||
|
out_data->light_status = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// No change
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL,
|
||||||
|
(uint16_t) hon_protocol::SubcomandsControl::SET_GROUP_PARAMETERS,
|
||||||
|
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
|
||||||
|
if (size < sizeof(hon_protocol::HaierStatus))
|
||||||
|
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||||
|
hon_protocol::HaierStatus packet;
|
||||||
|
if (size < sizeof(hon_protocol::HaierStatus))
|
||||||
|
size = sizeof(hon_protocol::HaierStatus);
|
||||||
|
memcpy(&packet, packet_buffer, size);
|
||||||
|
if (packet.sensors.error_status != 0) {
|
||||||
|
ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status);
|
||||||
|
}
|
||||||
|
if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) {
|
||||||
|
got_valid_outdoor_temp_ = true;
|
||||||
|
float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET);
|
||||||
|
if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp))
|
||||||
|
this->outdoor_sensor_->publish_state(otemp);
|
||||||
|
}
|
||||||
|
bool should_publish = false;
|
||||||
|
{
|
||||||
|
// Extra modes/presets
|
||||||
|
optional<ClimatePreset> old_preset = this->preset;
|
||||||
|
if (packet.control.quiet_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_ECO;
|
||||||
|
} else if (packet.control.fast_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_BOOST;
|
||||||
|
} else if (packet.control.sleep_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_SLEEP;
|
||||||
|
} else {
|
||||||
|
this->preset = CLIMATE_PRESET_NONE;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Target temperature
|
||||||
|
float old_target_temperature = this->target_temperature;
|
||||||
|
this->target_temperature = packet.control.set_point + 16.0f;
|
||||||
|
should_publish = should_publish || (old_target_temperature != this->target_temperature);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Current temperature
|
||||||
|
float old_current_temperature = this->current_temperature;
|
||||||
|
this->current_temperature = packet.sensors.room_temperature / 2.0f;
|
||||||
|
should_publish = should_publish || (old_current_temperature != this->current_temperature);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Fan mode
|
||||||
|
optional<ClimateFanMode> old_fan_mode = this->fan_mode;
|
||||||
|
// remember the fan speed we last had for climate vs fan
|
||||||
|
if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) {
|
||||||
|
if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO)
|
||||||
|
this->fan_mode_speed_ = packet.control.fan_mode;
|
||||||
|
} else {
|
||||||
|
this->other_modes_fan_speed_ = packet.control.fan_mode;
|
||||||
|
}
|
||||||
|
switch (packet.control.fan_mode) {
|
||||||
|
case (uint8_t) hon_protocol::FanMode::FAN_AUTO:
|
||||||
|
if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) {
|
||||||
|
this->fan_mode = CLIMATE_FAN_AUTO;
|
||||||
|
} else {
|
||||||
|
// Shouldn't accept fan speed auto in fan-only mode even if AC reports it
|
||||||
|
ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::FanMode::FAN_MID:
|
||||||
|
this->fan_mode = CLIMATE_FAN_MEDIUM;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::FanMode::FAN_LOW:
|
||||||
|
this->fan_mode = CLIMATE_FAN_LOW;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
|
||||||
|
this->fan_mode = CLIMATE_FAN_HIGH;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Display status
|
||||||
|
// should be before "Climate mode" because it is changing this->mode
|
||||||
|
if (packet.control.ac_power != 0) {
|
||||||
|
// if AC is off display status always ON so process it only when AC is on
|
||||||
|
bool disp_status = packet.control.display_status != 0;
|
||||||
|
if (disp_status != this->display_status_) {
|
||||||
|
// Do something only if display status changed
|
||||||
|
if (this->mode == CLIMATE_MODE_OFF) {
|
||||||
|
// AC just turned on from remote need to turn off display
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
} else {
|
||||||
|
this->display_status_ = disp_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Health mode
|
||||||
|
bool old_health_mode = this->health_mode_;
|
||||||
|
this->health_mode_ = packet.control.health_mode == 1;
|
||||||
|
should_publish = should_publish || (old_health_mode != this->health_mode_);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
CleaningState new_cleaning;
|
||||||
|
if (packet.control.steri_clean == 1) {
|
||||||
|
// Steri-cleaning
|
||||||
|
new_cleaning = CleaningState::STERI_CLEAN;
|
||||||
|
} else if (packet.control.self_cleaning_status == 1) {
|
||||||
|
// Self-cleaning
|
||||||
|
new_cleaning = CleaningState::SELF_CLEAN;
|
||||||
|
} else {
|
||||||
|
// No cleaning
|
||||||
|
new_cleaning = CleaningState::NO_CLEANING;
|
||||||
|
}
|
||||||
|
if (new_cleaning != this->cleaning_status_) {
|
||||||
|
ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning);
|
||||||
|
if (new_cleaning == CleaningState::NO_CLEANING) {
|
||||||
|
// Turnuin AC off after cleaning
|
||||||
|
this->action_request_ = ActionRequest::TURN_POWER_OFF;
|
||||||
|
}
|
||||||
|
this->cleaning_status_ = new_cleaning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Climate mode
|
||||||
|
ClimateMode old_mode = this->mode;
|
||||||
|
if (packet.control.ac_power == 0) {
|
||||||
|
this->mode = CLIMATE_MODE_OFF;
|
||||||
|
} else {
|
||||||
|
// Check current hvac mode
|
||||||
|
switch (packet.control.ac_mode) {
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::COOL:
|
||||||
|
this->mode = CLIMATE_MODE_COOL;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::HEAT:
|
||||||
|
this->mode = CLIMATE_MODE_HEAT;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::DRY:
|
||||||
|
this->mode = CLIMATE_MODE_DRY;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::FAN:
|
||||||
|
this->mode = CLIMATE_MODE_FAN_ONLY;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::AUTO:
|
||||||
|
this->mode = CLIMATE_MODE_AUTO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (old_mode != this->mode);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Swing mode
|
||||||
|
ClimateSwingMode old_swing_mode = this->swing_mode;
|
||||||
|
if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) {
|
||||||
|
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
|
||||||
|
this->swing_mode = CLIMATE_SWING_BOTH;
|
||||||
|
} else {
|
||||||
|
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
|
||||||
|
this->swing_mode = CLIMATE_SWING_VERTICAL;
|
||||||
|
} else {
|
||||||
|
this->swing_mode = CLIMATE_SWING_OFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (old_swing_mode != this->swing_mode);
|
||||||
|
}
|
||||||
|
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
|
||||||
|
if (this->forced_publish_ || should_publish) {
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
|
||||||
|
#endif
|
||||||
|
this->publish_state();
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
ESP_LOGV(TAG, "Publish delay: %lld ms",
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
|
||||||
|
_publish_start)
|
||||||
|
.count());
|
||||||
|
#endif
|
||||||
|
this->forced_publish_ = false;
|
||||||
|
}
|
||||||
|
if (should_publish) {
|
||||||
|
ESP_LOGI(TAG, "HVAC values changed");
|
||||||
|
}
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"HVAC Mode = 0x%X", packet.control.ac_mode);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"Fan speed Status = 0x%X", packet.control.fan_mode);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"Set Point Status = 0x%X", packet.control.set_point);
|
||||||
|
return haier_protocol::HandlerError::HANDLER_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HonClimate::is_message_invalid(uint8_t message_type) {
|
||||||
|
return message_type == (uint8_t) hon_protocol::FrameType::INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::process_pending_action() {
|
||||||
|
switch (this->action_request_) {
|
||||||
|
case ActionRequest::START_SELF_CLEAN:
|
||||||
|
case ActionRequest::START_STERI_CLEAN:
|
||||||
|
// Will reset action with control message sending
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_CONTROL);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
HaierClimateBase::process_pending_action();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
95
esphome/components/haier/hon_climate.h
Normal file
95
esphome/components/haier/hon_climate.h
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "haier_base.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
enum class AirflowVerticalDirection : uint8_t {
|
||||||
|
HEALTH_UP = 0,
|
||||||
|
MAX_UP = 1,
|
||||||
|
UP = 2,
|
||||||
|
CENTER = 3,
|
||||||
|
DOWN = 4,
|
||||||
|
HEALTH_DOWN = 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AirflowHorizontalDirection : uint8_t {
|
||||||
|
MAX_LEFT = 0,
|
||||||
|
LEFT = 1,
|
||||||
|
CENTER = 2,
|
||||||
|
RIGHT = 3,
|
||||||
|
MAX_RIGHT = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CleaningState : uint8_t {
|
||||||
|
NO_CLEANING = 0,
|
||||||
|
SELF_CLEAN = 1,
|
||||||
|
STERI_CLEAN = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
class HonClimate : public HaierClimateBase {
|
||||||
|
public:
|
||||||
|
HonClimate();
|
||||||
|
HonClimate(const HonClimate &) = delete;
|
||||||
|
HonClimate &operator=(const HonClimate &) = delete;
|
||||||
|
~HonClimate();
|
||||||
|
void dump_config() override;
|
||||||
|
void set_beeper_state(bool state);
|
||||||
|
bool get_beeper_state() const;
|
||||||
|
void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor);
|
||||||
|
AirflowVerticalDirection get_vertical_airflow() const;
|
||||||
|
void set_vertical_airflow(AirflowVerticalDirection direction);
|
||||||
|
AirflowHorizontalDirection get_horizontal_airflow() const;
|
||||||
|
void set_horizontal_airflow(AirflowHorizontalDirection direction);
|
||||||
|
std::string get_cleaning_status_text() const;
|
||||||
|
CleaningState get_cleaning_status() const;
|
||||||
|
void start_self_cleaning();
|
||||||
|
void start_steri_cleaning();
|
||||||
|
void set_send_wifi(bool send_wifi);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void set_answers_handlers() override;
|
||||||
|
void process_phase(std::chrono::steady_clock::time_point now) override;
|
||||||
|
haier_protocol::HaierMessage get_control_message() override;
|
||||||
|
bool is_message_invalid(uint8_t message_type) override;
|
||||||
|
void process_pending_action() override;
|
||||||
|
|
||||||
|
// Answers handlers
|
||||||
|
haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
|
||||||
|
size_t data_size);
|
||||||
|
haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
// Helper functions
|
||||||
|
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
|
||||||
|
std::unique_ptr<uint8_t[]> last_status_message_;
|
||||||
|
bool beeper_status_;
|
||||||
|
CleaningState cleaning_status_;
|
||||||
|
bool got_valid_outdoor_temp_;
|
||||||
|
AirflowVerticalDirection vertical_direction_;
|
||||||
|
AirflowHorizontalDirection horizontal_direction_;
|
||||||
|
bool hvac_hardware_info_available_;
|
||||||
|
std::string hvac_protocol_version_;
|
||||||
|
std::string hvac_software_version_;
|
||||||
|
std::string hvac_hardware_version_;
|
||||||
|
std::string hvac_device_name_;
|
||||||
|
bool hvac_functions_[5];
|
||||||
|
bool &use_crc_;
|
||||||
|
uint8_t active_alarms_[8];
|
||||||
|
esphome::sensor::Sensor *outdoor_sensor_;
|
||||||
|
bool send_wifi_signal_;
|
||||||
|
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
228
esphome/components/haier/hon_packet.h
Normal file
228
esphome/components/haier/hon_packet.h
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
namespace hon_protocol {
|
||||||
|
|
||||||
|
enum class VerticalSwingMode : uint8_t {
|
||||||
|
HEALTH_UP = 0x01,
|
||||||
|
MAX_UP = 0x02,
|
||||||
|
HEALTH_DOWN = 0x03,
|
||||||
|
UP = 0x04,
|
||||||
|
CENTER = 0x06,
|
||||||
|
DOWN = 0x08,
|
||||||
|
AUTO = 0x0C
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HorizontalSwingMode : uint8_t {
|
||||||
|
CENTER = 0x00,
|
||||||
|
MAX_LEFT = 0x03,
|
||||||
|
LEFT = 0x04,
|
||||||
|
RIGHT = 0x05,
|
||||||
|
MAX_RIGHT = 0x06,
|
||||||
|
AUTO = 0x07
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ConditioningMode : uint8_t {
|
||||||
|
AUTO = 0x00,
|
||||||
|
COOL = 0x01,
|
||||||
|
DRY = 0x02,
|
||||||
|
HEALTHY_DRY = 0x03,
|
||||||
|
HEAT = 0x04,
|
||||||
|
ENERGY_SAVING = 0x05,
|
||||||
|
FAN = 0x06
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 };
|
||||||
|
|
||||||
|
enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 };
|
||||||
|
|
||||||
|
struct HaierPacketControl {
|
||||||
|
// Control bytes starts here
|
||||||
|
// 10
|
||||||
|
uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C)
|
||||||
|
// 11
|
||||||
|
uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode
|
||||||
|
uint8_t : 0;
|
||||||
|
// 12
|
||||||
|
uint8_t fan_mode : 3; // See enum FanMode
|
||||||
|
uint8_t special_mode : 2; // See enum SpecialMode
|
||||||
|
uint8_t ac_mode : 3; // See enum ConditioningMode
|
||||||
|
// 13
|
||||||
|
uint8_t : 8;
|
||||||
|
// 14
|
||||||
|
uint8_t ten_degree : 1; // 10 degree status
|
||||||
|
uint8_t display_status : 1; // If 0 disables AC's display
|
||||||
|
uint8_t half_degree : 1; // Use half degree
|
||||||
|
uint8_t intelegence_status : 1; // Intelligence status
|
||||||
|
uint8_t pmv_status : 1; // Comfort/PMV status
|
||||||
|
uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius
|
||||||
|
uint8_t : 1;
|
||||||
|
uint8_t steri_clean : 1;
|
||||||
|
// 15
|
||||||
|
uint8_t ac_power : 1; // Is ac on or off
|
||||||
|
uint8_t health_mode : 1; // Health mode (negative ions) on or off
|
||||||
|
uint8_t electric_heating_status : 1; // Electric heating status
|
||||||
|
uint8_t fast_mode : 1; // Fast mode
|
||||||
|
uint8_t quiet_mode : 1; // Quiet mode
|
||||||
|
uint8_t sleep_mode : 1; // Sleep mode
|
||||||
|
uint8_t lock_remote : 1; // Disable remote
|
||||||
|
uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command)
|
||||||
|
// 16
|
||||||
|
uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%)
|
||||||
|
// 17
|
||||||
|
uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode
|
||||||
|
uint8_t : 3;
|
||||||
|
uint8_t human_sensing_status : 2; // Human sensing status
|
||||||
|
// 18
|
||||||
|
uint8_t change_filter : 1; // Filter need replacement
|
||||||
|
uint8_t : 0;
|
||||||
|
// 19
|
||||||
|
uint8_t fresh_air_status : 1; // Fresh air status
|
||||||
|
uint8_t humidification_status : 1; // Humidification status
|
||||||
|
uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status
|
||||||
|
uint8_t ch2o_cleaning_status : 1; // CH2O cleaning status
|
||||||
|
uint8_t self_cleaning_status : 1; // Self cleaning status
|
||||||
|
uint8_t light_status : 1; // Light status
|
||||||
|
uint8_t energy_saving_status : 1; // Energy saving status
|
||||||
|
uint8_t cleaning_time_status : 1; // Cleaning time (0 - accumulation, 1 - clear)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HaierPacketSensors {
|
||||||
|
// 20
|
||||||
|
uint8_t room_temperature; // 0.5°C step
|
||||||
|
// 21
|
||||||
|
uint8_t room_humidity; // 0%-100% with 1% step
|
||||||
|
// 22
|
||||||
|
uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C)
|
||||||
|
// 23
|
||||||
|
uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
|
||||||
|
uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
|
||||||
|
uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple)
|
||||||
|
uint8_t : 1;
|
||||||
|
uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only)
|
||||||
|
// 24
|
||||||
|
uint8_t error_status; // See enum ErrorStatus
|
||||||
|
// 25
|
||||||
|
uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP)
|
||||||
|
uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan)
|
||||||
|
uint8_t : 3;
|
||||||
|
uint8_t err_confirmation : 1; // If 1 clear error status
|
||||||
|
// 26
|
||||||
|
uint16_t total_cleaning_time; // Cleaning cumulative time (1h step)
|
||||||
|
// 28
|
||||||
|
uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step)
|
||||||
|
// 30
|
||||||
|
uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step)
|
||||||
|
// 32
|
||||||
|
uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step)
|
||||||
|
// 34
|
||||||
|
uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step)
|
||||||
|
// 36
|
||||||
|
uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HaierStatus {
|
||||||
|
uint16_t subcommand;
|
||||||
|
HaierPacketControl control;
|
||||||
|
HaierPacketSensors sensors;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeviceVersionAnswer {
|
||||||
|
char protocol_version[8];
|
||||||
|
char software_version[8];
|
||||||
|
uint8_t encryption[3];
|
||||||
|
char hardware_version[8];
|
||||||
|
uint8_t : 8;
|
||||||
|
char device_name[8];
|
||||||
|
uint8_t functions[2];
|
||||||
|
};
|
||||||
|
|
||||||
|
// In this section comments:
|
||||||
|
// - module is the ESP32 control module (communication module in Haier protocol document)
|
||||||
|
// - device is the conditioner control board (network appliances in Haier protocol document)
|
||||||
|
enum class FrameType : uint8_t {
|
||||||
|
CONTROL = 0x01, // Requests or sets one or multiple parameters (module <-> device, required)
|
||||||
|
STATUS = 0x02, // Contains one or multiple parameters values, usually answer to control frame (module <-> device,
|
||||||
|
// required)
|
||||||
|
INVALID = 0x03, // Communication error indication (module <-> device, required)
|
||||||
|
ALARM_STATUS = 0x04, // Alarm status report (module <-> device, interactive, required)
|
||||||
|
CONFIRM = 0x05, // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module
|
||||||
|
// <-> device, required)
|
||||||
|
REPORT = 0x06, // Report frame (module <-> device, interactive, required)
|
||||||
|
STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required)
|
||||||
|
SYSTEM_DOWNLIK = 0x11, // System downlink frame (module -> device, optional)
|
||||||
|
DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional)
|
||||||
|
SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional)
|
||||||
|
SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional)
|
||||||
|
DEVICE_QUERY = 0x15, // Device query frame (module <- device, optional)
|
||||||
|
DEVICE_QUERY_RESPONSE = 0x16, // Device query response frame (module -> device, optional)
|
||||||
|
GROUP_COMMAND = 0x60, // Group command frame (module -> device, interactive, optional)
|
||||||
|
GET_DEVICE_VERSION = 0x61, // Requests device version (module -> device, required)
|
||||||
|
GET_DEVICE_VERSION_RESPONSE = 0x62, // Device version answer (module <- device, required_
|
||||||
|
GET_ALL_ADDRESSES = 0x67, // Requests all devices addresses (module -> device, interactive, optional)
|
||||||
|
GET_ALL_ADDRESSES_RESPONSE =
|
||||||
|
0x68, // Answer to request of all devices addresses (module <- device , interactive, optional)
|
||||||
|
HANDSET_CHANGE_NOTIFICATION = 0x69, // Handset change notification frame (module <- device , interactive, optional)
|
||||||
|
GET_DEVICE_ID = 0x70, // Requests Device ID (module -> device, required)
|
||||||
|
GET_DEVICE_ID_RESPONSE = 0x71, // Response to device ID request (module <- device , required)
|
||||||
|
GET_ALARM_STATUS = 0x73, // Alarm status request (module -> device, required)
|
||||||
|
GET_ALARM_STATUS_RESPONSE = 0x74, // Response to alarm status request (module <- device, required)
|
||||||
|
GET_DEVICE_CONFIGURATION = 0x7C, // Requests device configuration (module -> device, interactive, required)
|
||||||
|
GET_DEVICE_CONFIGURATION_RESPONSE =
|
||||||
|
0x7D, // Response to device configuration request (module <- device, interactive, required)
|
||||||
|
DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C, // Downlink transparent transmission (proxy data Haier cloud -> device)
|
||||||
|
// (module -> device, interactive, optional)
|
||||||
|
UPLINK_TRANSPARENT_TRANSMISSION = 0x8D, // Uplink transparent transmission (proxy data device -> Haier cloud) (module
|
||||||
|
// <- device, interactive, optional)
|
||||||
|
START_DEVICE_UPGRADE = 0xE1, // Initiate device OTA upgrade (module -> device, OTA required)
|
||||||
|
START_DEVICE_UPGRADE_RESPONSE = 0xE2, // Response to initiate device upgrade command (module <- device, OTA required)
|
||||||
|
GET_FIRMWARE_CONTENT = 0xE5, // Requests to send firmware (module <- device, OTA required)
|
||||||
|
GET_FIRMWARE_CONTENT_RESPONSE =
|
||||||
|
0xE6, // Response to send firmware request (module -> device, OTA required) (multipacket?)
|
||||||
|
CHANGE_BAUD_RATE = 0xE7, // Requests to change port baud rate (module <- device, OTA required)
|
||||||
|
CHANGE_BAUD_RATE_RESPONSE = 0xE8, // Response to change port baud rate request (module -> device, OTA required)
|
||||||
|
GET_SUBBOARD_INFO = 0xE9, // Requests subboard information (module -> device, required)
|
||||||
|
GET_SUBBOARD_INFO_RESPONSE = 0xEA, // Response to subboard information request (module <- device, required)
|
||||||
|
GET_HARDWARE_INFO = 0xEB, // Requests information about device and subboard (module -> device, required)
|
||||||
|
GET_HARDWARE_INFO_RESPONSE = 0xEC, // Response to hardware information request (module <- device, required)
|
||||||
|
GET_UPGRADE_RESULT = 0xED, // Requests result of the firmware update (module <- device, OTA required)
|
||||||
|
GET_UPGRADE_RESULT_RESPONSE = 0xEF, // Response to firmware update results request (module -> device, OTA required)
|
||||||
|
GET_NETWORK_STATUS = 0xF0, // Requests network status (module <- device, interactive, optional)
|
||||||
|
GET_NETWORK_STATUS_RESPONSE = 0xF1, // Response to network status request (module -> device, interactive, optional)
|
||||||
|
START_WIFI_CONFIGURATION = 0xF2, // Starts WiFi configuration procedure (module <- device, interactive, required)
|
||||||
|
START_WIFI_CONFIGURATION_RESPONSE =
|
||||||
|
0xF3, // Response to start WiFi configuration request (module -> device, interactive, required)
|
||||||
|
STOP_WIFI_CONFIGURATION = 0xF4, // Stop WiFi configuration procedure (module <- device, interactive, required)
|
||||||
|
STOP_WIFI_CONFIGURATION_RESPONSE =
|
||||||
|
0xF5, // Response to stop WiFi configuration request (module -> device, interactive, required)
|
||||||
|
REPORT_NETWORK_STATUS = 0xF7, // Reports network status (module -> device, required)
|
||||||
|
CLEAR_CONFIGURATION = 0xF8, // Request to clear module configuration (module <- device, interactive, optional)
|
||||||
|
BIG_DATA_REPORT_CONFIGURATION =
|
||||||
|
0xFA, // Configuration for autoreport device full status (module -> device, interactive, optional)
|
||||||
|
BIG_DATA_REPORT_CONFIGURATION_RESPONSE =
|
||||||
|
0xFB, // Response to set big data configuration (module <- device, interactive, optional)
|
||||||
|
GET_MANAGEMENT_INFORMATION = 0xFC, // Request management information from device (module -> device, required)
|
||||||
|
GET_MANAGEMENT_INFORMATION_RESPONSE =
|
||||||
|
0xFD, // Response to management information request (module <- device, required)
|
||||||
|
WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional)
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SubcomandsControl : uint16_t {
|
||||||
|
GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...)
|
||||||
|
GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None)
|
||||||
|
GET_BIG_DATA = 0x4DFE, // Request big data information from device (packet content: None)
|
||||||
|
SET_PARAMETERS = 0x5C01, // Set parameters of the device and device return parameters (packet content: parameter ID1
|
||||||
|
// + parameter data1 + parameter ID2 + parameter data 2 + ...)
|
||||||
|
SET_SINGLE_PARAMETER = 0x5D00, // Set single parameter (0x5DXX second byte define parameter ID) and return all user
|
||||||
|
// data (packet content: ???)
|
||||||
|
SET_GROUP_PARAMETERS = 0x6001, // Set group parameters to device (0x60XX second byte define parameter is group ID,
|
||||||
|
// the only group mentioned in document is 1) and return all user data (packet
|
||||||
|
// content: all values like in status packet)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace hon_protocol
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
33
esphome/components/haier/logger_handler.cpp
Normal file
33
esphome/components/haier/logger_handler.cpp
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#include "logger_handler.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) {
|
||||||
|
switch (level) {
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_ERROR:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_WARNING:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_INFO:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_DEBUG:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_VERBOSE:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Just ignore everything else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); };
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
14
esphome/components/haier/logger_handler.h
Normal file
14
esphome/components/haier/logger_handler.h
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// HaierProtocol
|
||||||
|
#include <utils/haier_log.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
// This file is called in the code generated by python script
|
||||||
|
// Do not use it directly!
|
||||||
|
void init_haier_protocol_logging();
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
457
esphome/components/haier/smartair2_climate.cpp
Normal file
457
esphome/components/haier/smartair2_climate.cpp
Normal file
|
@ -0,0 +1,457 @@
|
||||||
|
#include <chrono>
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
#include "smartair2_climate.h"
|
||||||
|
#include "smartair2_packet.h"
|
||||||
|
|
||||||
|
using namespace esphome::climate;
|
||||||
|
using namespace esphome::uart;
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
static const char *const TAG = "haier.climate";
|
||||||
|
|
||||||
|
Smartair2Climate::Smartair2Climate()
|
||||||
|
: last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]) {
|
||||||
|
this->traits_.set_supported_presets({
|
||||||
|
climate::CLIMATE_PRESET_NONE,
|
||||||
|
climate::CLIMATE_PRESET_BOOST,
|
||||||
|
climate::CLIMATE_PRESET_COMFORT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result =
|
||||||
|
this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type,
|
||||||
|
(uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
result = this->process_status_message_(data, data_size);
|
||||||
|
if (result != haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
} else {
|
||||||
|
if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
|
||||||
|
memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl));
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
|
||||||
|
sizeof(smartair2_protocol::HaierPacketControl));
|
||||||
|
}
|
||||||
|
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) {
|
||||||
|
ESP_LOGI(TAG, "First HVAC status received");
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
if (this->hvac_settings_.valid)
|
||||||
|
this->hvac_settings_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Smartair2Climate::set_answers_handlers() {
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (smartair2_protocol::FrameType::CONTROL),
|
||||||
|
std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Smartair2Climate::dump_config() {
|
||||||
|
HaierClimateBase::dump_config();
|
||||||
|
ESP_LOGCONFIG(TAG, " Protocol version: smartAir2");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) {
|
||||||
|
switch (this->protocol_phase_) {
|
||||||
|
case ProtocolPhases::SENDING_INIT_1:
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::WAITING_ANSWER_INIT_1:
|
||||||
|
case ProtocolPhases::SENDING_INIT_2:
|
||||||
|
case ProtocolPhases::WAITING_ANSWER_INIT_2:
|
||||||
|
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
|
||||||
|
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||||
|
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||||
|
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||||
|
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL,
|
||||||
|
0x4D01);
|
||||||
|
this->send_message_(STATUS_REQUEST, false);
|
||||||
|
this->last_status_request_ = now;
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_FIRST_STATUS_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_STATUS_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL,
|
||||||
|
0x4D01);
|
||||||
|
this->send_message_(STATUS_REQUEST, false);
|
||||||
|
this->last_status_request_ = now;
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_STATUS_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_CONTROL:
|
||||||
|
if (this->first_control_attempt_) {
|
||||||
|
this->control_request_timestamp_ = now;
|
||||||
|
this->first_control_attempt_ = false;
|
||||||
|
}
|
||||||
|
if (this->is_control_message_timeout_exceeded_(now)) {
|
||||||
|
ESP_LOGW(TAG, "Sending control packet timeout!");
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
if (this->hvac_settings_.valid)
|
||||||
|
this->hvac_settings_.reset();
|
||||||
|
this->forced_request_status_ = true;
|
||||||
|
this->forced_publish_ = true;
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else if (this->can_send_message() && this->is_control_message_interval_exceeded_(
|
||||||
|
now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests
|
||||||
|
{
|
||||||
|
haier_protocol::HaierMessage control_message = get_control_message();
|
||||||
|
this->send_message_(control_message, false);
|
||||||
|
ESP_LOGI(TAG, "Control packet sent");
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_POWER_ON_COMMAND:
|
||||||
|
case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
haier_protocol::HaierMessage power_cmd(
|
||||||
|
(uint8_t) smartair2_protocol::FrameType::CONTROL,
|
||||||
|
this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03);
|
||||||
|
this->send_message_(power_cmd, false);
|
||||||
|
this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
|
||||||
|
? ProtocolPhases::WAITING_POWER_ON_ANSWER
|
||||||
|
: ProtocolPhases::WAITING_POWER_OFF_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_STATUS_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_CONTROL_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_POWER_ON_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::IDLE: {
|
||||||
|
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST);
|
||||||
|
this->forced_request_status_ = false;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
default:
|
||||||
|
// Shouldn't get here
|
||||||
|
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
|
||||||
|
uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)];
|
||||||
|
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl));
|
||||||
|
smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer;
|
||||||
|
out_data->cntrl = 0;
|
||||||
|
if (this->hvac_settings_.valid) {
|
||||||
|
HvacSettings climate_control;
|
||||||
|
climate_control = this->hvac_settings_;
|
||||||
|
if (climate_control.mode.has_value()) {
|
||||||
|
switch (climate_control.mode.value()) {
|
||||||
|
case CLIMATE_MODE_OFF:
|
||||||
|
out_data->ac_power = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_AUTO:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_HEAT:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_DRY:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_FAN_ONLY:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN;
|
||||||
|
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_COOL:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported climate mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set fan speed, if we are in fan mode, reject auto in fan mode
|
||||||
|
if (climate_control.fan_mode.has_value()) {
|
||||||
|
switch (climate_control.fan_mode.value()) {
|
||||||
|
case CLIMATE_FAN_LOW:
|
||||||
|
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_LOW;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_MEDIUM:
|
||||||
|
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_MID;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_HIGH:
|
||||||
|
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_HIGH;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_AUTO:
|
||||||
|
if (this->mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode
|
||||||
|
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_AUTO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported fan mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set swing mode
|
||||||
|
if (climate_control.swing_mode.has_value()) {
|
||||||
|
switch (climate_control.swing_mode.value()) {
|
||||||
|
case CLIMATE_SWING_OFF:
|
||||||
|
out_data->use_swing_bits = 0;
|
||||||
|
out_data->swing_both = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_VERTICAL:
|
||||||
|
out_data->swing_both = 0;
|
||||||
|
out_data->vertical_swing = 1;
|
||||||
|
out_data->horizontal_swing = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_HORIZONTAL:
|
||||||
|
out_data->swing_both = 0;
|
||||||
|
out_data->vertical_swing = 0;
|
||||||
|
out_data->horizontal_swing = 1;
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_BOTH:
|
||||||
|
out_data->swing_both = 1;
|
||||||
|
out_data->use_swing_bits = 0;
|
||||||
|
out_data->vertical_swing = 0;
|
||||||
|
out_data->horizontal_swing = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (climate_control.target_temperature.has_value()) {
|
||||||
|
out_data->set_point =
|
||||||
|
climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16.
|
||||||
|
}
|
||||||
|
if (out_data->ac_power == 0) {
|
||||||
|
// If AC is off - no presets alowed
|
||||||
|
out_data->turbo_mode = 0;
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
} else if (climate_control.preset.has_value()) {
|
||||||
|
switch (climate_control.preset.value()) {
|
||||||
|
case CLIMATE_PRESET_NONE:
|
||||||
|
out_data->turbo_mode = 0;
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_BOOST:
|
||||||
|
out_data->turbo_mode = 1;
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_COMFORT:
|
||||||
|
out_data->turbo_mode = 0;
|
||||||
|
out_data->quiet_mode = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported preset");
|
||||||
|
out_data->turbo_mode = 0;
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out_data->display_status = this->display_status_ ? 0 : 1;
|
||||||
|
out_data->health_mode = this->health_mode_ ? 1 : 0;
|
||||||
|
return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer,
|
||||||
|
sizeof(smartair2_protocol::HaierPacketControl));
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
|
||||||
|
if (size < sizeof(smartair2_protocol::HaierStatus))
|
||||||
|
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||||
|
smartair2_protocol::HaierStatus packet;
|
||||||
|
memcpy(&packet, packet_buffer, size);
|
||||||
|
bool should_publish = false;
|
||||||
|
{
|
||||||
|
// Extra modes/presets
|
||||||
|
optional<ClimatePreset> old_preset = this->preset;
|
||||||
|
if (packet.control.turbo_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_BOOST;
|
||||||
|
} else if (packet.control.quiet_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_COMFORT;
|
||||||
|
} else {
|
||||||
|
this->preset = CLIMATE_PRESET_NONE;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Target temperature
|
||||||
|
float old_target_temperature = this->target_temperature;
|
||||||
|
this->target_temperature = packet.control.set_point + 16.0f;
|
||||||
|
should_publish = should_publish || (old_target_temperature != this->target_temperature);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Current temperature
|
||||||
|
float old_current_temperature = this->current_temperature;
|
||||||
|
this->current_temperature = packet.control.room_temperature;
|
||||||
|
should_publish = should_publish || (old_current_temperature != this->current_temperature);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Fan mode
|
||||||
|
optional<ClimateFanMode> old_fan_mode = this->fan_mode;
|
||||||
|
// remember the fan speed we last had for climate vs fan
|
||||||
|
if (packet.control.ac_mode == (uint8_t) smartair2_protocol::ConditioningMode::FAN) {
|
||||||
|
if (packet.control.fan_mode != (uint8_t) smartair2_protocol::FanMode::FAN_AUTO)
|
||||||
|
this->fan_mode_speed_ = packet.control.fan_mode;
|
||||||
|
} else {
|
||||||
|
this->other_modes_fan_speed_ = packet.control.fan_mode;
|
||||||
|
}
|
||||||
|
switch (packet.control.fan_mode) {
|
||||||
|
case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO:
|
||||||
|
// Somtimes AC reports in fan only mode that fan speed is auto
|
||||||
|
// but never accept this value back
|
||||||
|
if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) {
|
||||||
|
this->fan_mode = CLIMATE_FAN_AUTO;
|
||||||
|
} else {
|
||||||
|
should_publish = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::FanMode::FAN_MID:
|
||||||
|
this->fan_mode = CLIMATE_FAN_MEDIUM;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::FanMode::FAN_LOW:
|
||||||
|
this->fan_mode = CLIMATE_FAN_LOW;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::FanMode::FAN_HIGH:
|
||||||
|
this->fan_mode = CLIMATE_FAN_HIGH;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Display status
|
||||||
|
// should be before "Climate mode" because it is changing this->mode
|
||||||
|
if (packet.control.ac_power != 0) {
|
||||||
|
// if AC is off display status always ON so process it only when AC is on
|
||||||
|
bool disp_status = packet.control.display_status == 0;
|
||||||
|
if (disp_status != this->display_status_) {
|
||||||
|
// Do something only if display status changed
|
||||||
|
if (this->mode == CLIMATE_MODE_OFF) {
|
||||||
|
// AC just turned on from remote need to turn off display
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
} else {
|
||||||
|
this->display_status_ = disp_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Climate mode
|
||||||
|
ClimateMode old_mode = this->mode;
|
||||||
|
if (packet.control.ac_power == 0) {
|
||||||
|
this->mode = CLIMATE_MODE_OFF;
|
||||||
|
} else {
|
||||||
|
// Check current hvac mode
|
||||||
|
switch (packet.control.ac_mode) {
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::COOL:
|
||||||
|
this->mode = CLIMATE_MODE_COOL;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::HEAT:
|
||||||
|
this->mode = CLIMATE_MODE_HEAT;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::DRY:
|
||||||
|
this->mode = CLIMATE_MODE_DRY;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::FAN:
|
||||||
|
this->mode = CLIMATE_MODE_FAN_ONLY;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::AUTO:
|
||||||
|
this->mode = CLIMATE_MODE_AUTO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (old_mode != this->mode);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Health mode
|
||||||
|
bool old_health_mode = this->health_mode_;
|
||||||
|
this->health_mode_ = packet.control.health_mode == 1;
|
||||||
|
should_publish = should_publish || (old_health_mode != this->health_mode_);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Swing mode
|
||||||
|
ClimateSwingMode old_swing_mode = this->swing_mode;
|
||||||
|
if (packet.control.swing_both == 0) {
|
||||||
|
if (packet.control.vertical_swing != 0) {
|
||||||
|
this->swing_mode = CLIMATE_SWING_VERTICAL;
|
||||||
|
} else if (packet.control.horizontal_swing != 0) {
|
||||||
|
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
|
||||||
|
} else {
|
||||||
|
this->swing_mode = CLIMATE_SWING_OFF;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
swing_mode = CLIMATE_SWING_BOTH;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (old_swing_mode != this->swing_mode);
|
||||||
|
}
|
||||||
|
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
|
||||||
|
if (this->forced_publish_ || should_publish) {
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
|
||||||
|
#endif
|
||||||
|
this->publish_state();
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
ESP_LOGV(TAG, "Publish delay: %lld ms",
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
|
||||||
|
_publish_start)
|
||||||
|
.count());
|
||||||
|
#endif
|
||||||
|
this->forced_publish_ = false;
|
||||||
|
}
|
||||||
|
if (should_publish) {
|
||||||
|
ESP_LOGI(TAG, "HVAC values changed");
|
||||||
|
}
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"HVAC Mode = 0x%X", packet.control.ac_mode);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"Fan speed Status = 0x%X", packet.control.fan_mode);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"Horizontal Swing Status = 0x%X", packet.control.horizontal_swing);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"Vertical Swing Status = 0x%X", packet.control.vertical_swing);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
|
||||||
|
"Set Point Status = 0x%X", packet.control.set_point);
|
||||||
|
return haier_protocol::HandlerError::HANDLER_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Smartair2Climate::is_message_invalid(uint8_t message_type) {
|
||||||
|
return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
31
esphome/components/haier/smartair2_climate.h
Normal file
31
esphome/components/haier/smartair2_climate.h
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include "haier_base.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
class Smartair2Climate : public HaierClimateBase {
|
||||||
|
public:
|
||||||
|
Smartair2Climate();
|
||||||
|
Smartair2Climate(const Smartair2Climate &) = delete;
|
||||||
|
Smartair2Climate &operator=(const Smartair2Climate &) = delete;
|
||||||
|
~Smartair2Climate();
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void set_answers_handlers() override;
|
||||||
|
void process_phase(std::chrono::steady_clock::time_point now) override;
|
||||||
|
haier_protocol::HaierMessage get_control_message() override;
|
||||||
|
bool is_message_invalid(uint8_t message_type) override;
|
||||||
|
// Answers handlers
|
||||||
|
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
|
||||||
|
size_t data_size);
|
||||||
|
// Helper functions
|
||||||
|
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
|
||||||
|
std::unique_ptr<uint8_t[]> last_status_message_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
97
esphome/components/haier/smartair2_packet.h
Normal file
97
esphome/components/haier/smartair2_packet.h
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
namespace smartair2_protocol {
|
||||||
|
|
||||||
|
enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 };
|
||||||
|
|
||||||
|
enum class FanMode : uint8_t { FAN_HIGH = 0x00, FAN_MID = 0x01, FAN_LOW = 0x02, FAN_AUTO = 0x03 };
|
||||||
|
|
||||||
|
struct HaierPacketControl {
|
||||||
|
// Control bytes starts here
|
||||||
|
// 10
|
||||||
|
uint8_t : 8; // Temperature high byte
|
||||||
|
// 11
|
||||||
|
uint8_t room_temperature; // current room temperature 1°C step
|
||||||
|
// 12
|
||||||
|
uint8_t : 8; // Humidity high byte
|
||||||
|
// 13
|
||||||
|
uint8_t room_humidity; // Humidity 0%-100% with 1% step
|
||||||
|
// 14
|
||||||
|
uint8_t : 8;
|
||||||
|
// 15
|
||||||
|
uint8_t cntrl; // In AC => ESP packets - 0x7F, in ESP => AC packets - 0x00
|
||||||
|
// 16
|
||||||
|
uint8_t : 8;
|
||||||
|
// 17
|
||||||
|
uint8_t : 8;
|
||||||
|
// 18
|
||||||
|
uint8_t : 8;
|
||||||
|
// 19
|
||||||
|
uint8_t : 8;
|
||||||
|
// 20
|
||||||
|
uint8_t : 8;
|
||||||
|
// 21
|
||||||
|
uint8_t ac_mode; // See enum ConditioningMode
|
||||||
|
// 22
|
||||||
|
uint8_t : 8;
|
||||||
|
// 23
|
||||||
|
uint8_t fan_mode; // See enum FanMode
|
||||||
|
// 24
|
||||||
|
uint8_t : 8;
|
||||||
|
// 25
|
||||||
|
uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define
|
||||||
|
// vertical/horizontal/off
|
||||||
|
// 26
|
||||||
|
uint8_t : 3;
|
||||||
|
uint8_t use_fahrenheit : 1;
|
||||||
|
uint8_t : 3;
|
||||||
|
uint8_t lock_remote : 1; // Disable remote
|
||||||
|
// 27
|
||||||
|
uint8_t ac_power : 1; // Is ac on or off
|
||||||
|
uint8_t : 2;
|
||||||
|
uint8_t health_mode : 1; // Health mode on or off
|
||||||
|
uint8_t compressor : 1; // Compressor on or off ???
|
||||||
|
uint8_t : 1;
|
||||||
|
uint8_t ten_degree : 1; // 10 degree status (only work in heat mode)
|
||||||
|
uint8_t : 0;
|
||||||
|
// 28
|
||||||
|
uint8_t : 8;
|
||||||
|
// 29
|
||||||
|
uint8_t use_swing_bits : 1; // Indicate if horizontal_swing and vertical_swing should be used
|
||||||
|
uint8_t turbo_mode : 1; // Turbo mode
|
||||||
|
uint8_t quiet_mode : 1; // Sleep mode
|
||||||
|
uint8_t horizontal_swing : 1; // Horizontal swing (if swing_both == 0)
|
||||||
|
uint8_t vertical_swing : 1; // Vertical swing (if swing_both == 0) if vertical_swing and horizontal_swing both 0 =>
|
||||||
|
// swing off
|
||||||
|
uint8_t display_status : 1; // Led on or off
|
||||||
|
uint8_t : 0;
|
||||||
|
// 30
|
||||||
|
uint8_t : 8;
|
||||||
|
// 31
|
||||||
|
uint8_t : 8;
|
||||||
|
// 32
|
||||||
|
uint8_t : 8; // Target temperature high byte
|
||||||
|
// 33
|
||||||
|
uint8_t set_point; // Target temperature with 16°C offset, 1°C step
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HaierStatus {
|
||||||
|
uint16_t subcommand;
|
||||||
|
HaierPacketControl control;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class FrameType : uint8_t {
|
||||||
|
CONTROL = 0x01,
|
||||||
|
STATUS = 0x02,
|
||||||
|
INVALID = 0x03,
|
||||||
|
CONFIRM = 0x05,
|
||||||
|
GET_DEVICE_VERSION = 0x61,
|
||||||
|
REPORT_NETWORK_STATUS = 0xF7,
|
||||||
|
NO_COMMAND = 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace smartair2_protocol
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
|
@ -14,6 +14,14 @@ ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len
|
||||||
return bus_->read(address_, data, len);
|
return bus_->read(address_, data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) {
|
||||||
|
a_register = convert_big_endian(a_register);
|
||||||
|
ErrorCode const err = this->write(reinterpret_cast<const uint8_t *>(&a_register), 2, stop);
|
||||||
|
if (err != ERROR_OK)
|
||||||
|
return err;
|
||||||
|
return bus_->read(address_, data, len);
|
||||||
|
}
|
||||||
|
|
||||||
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) {
|
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) {
|
||||||
WriteBuffer buffers[2];
|
WriteBuffer buffers[2];
|
||||||
buffers[0].data = &a_register;
|
buffers[0].data = &a_register;
|
||||||
|
@ -23,6 +31,16 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
|
||||||
return bus_->writev(address_, buffers, 2, stop);
|
return bus_->writev(address_, buffers, 2, stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) {
|
||||||
|
a_register = convert_big_endian(a_register);
|
||||||
|
WriteBuffer buffers[2];
|
||||||
|
buffers[0].data = reinterpret_cast<const uint8_t *>(&a_register);
|
||||||
|
buffers[0].len = 2;
|
||||||
|
buffers[1].data = data;
|
||||||
|
buffers[1].len = len;
|
||||||
|
return bus_->writev(address_, buffers, 2, stop);
|
||||||
|
}
|
||||||
|
|
||||||
bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) {
|
bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) {
|
||||||
if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK)
|
if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK)
|
||||||
return false;
|
return false;
|
||||||
|
@ -60,5 +78,26 @@ uint8_t I2CRegister::get() const {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
I2CRegister16 &I2CRegister16::operator=(uint8_t value) {
|
||||||
|
this->parent_->write_register16(this->register_, &value, 1);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
I2CRegister16 &I2CRegister16::operator&=(uint8_t value) {
|
||||||
|
value &= get();
|
||||||
|
this->parent_->write_register16(this->register_, &value, 1);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
I2CRegister16 &I2CRegister16::operator|=(uint8_t value) {
|
||||||
|
value |= get();
|
||||||
|
this->parent_->write_register16(this->register_, &value, 1);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t I2CRegister16::get() const {
|
||||||
|
uint8_t value = 0x00;
|
||||||
|
this->parent_->read_register16(this->register_, &value, 1);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace i2c
|
} // namespace i2c
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -31,6 +31,25 @@ class I2CRegister {
|
||||||
uint8_t register_;
|
uint8_t register_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class I2CRegister16 {
|
||||||
|
public:
|
||||||
|
I2CRegister16 &operator=(uint8_t value);
|
||||||
|
I2CRegister16 &operator&=(uint8_t value);
|
||||||
|
I2CRegister16 &operator|=(uint8_t value);
|
||||||
|
|
||||||
|
explicit operator uint8_t() const { return get(); }
|
||||||
|
|
||||||
|
uint8_t get() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
friend class I2CDevice;
|
||||||
|
|
||||||
|
I2CRegister16(I2CDevice *parent, uint16_t a_register) : parent_(parent), register_(a_register) {}
|
||||||
|
|
||||||
|
I2CDevice *parent_;
|
||||||
|
uint16_t register_;
|
||||||
|
};
|
||||||
|
|
||||||
// like ntohs/htons but without including networking headers.
|
// like ntohs/htons but without including networking headers.
|
||||||
// ("i2c" byte order is big-endian)
|
// ("i2c" byte order is big-endian)
|
||||||
inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); }
|
inline uint16_t i2ctohs(uint16_t i2cshort) { return convert_big_endian(i2cshort); }
|
||||||
|
@ -44,12 +63,15 @@ class I2CDevice {
|
||||||
void set_i2c_bus(I2CBus *bus) { bus_ = bus; }
|
void set_i2c_bus(I2CBus *bus) { bus_ = bus; }
|
||||||
|
|
||||||
I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
|
I2CRegister reg(uint8_t a_register) { return {this, a_register}; }
|
||||||
|
I2CRegister16 reg16(uint16_t a_register) { return {this, a_register}; }
|
||||||
|
|
||||||
ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
|
ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
|
||||||
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true);
|
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true);
|
||||||
|
ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true);
|
||||||
|
|
||||||
ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
|
ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
|
||||||
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true);
|
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true);
|
||||||
|
ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true);
|
||||||
|
|
||||||
// Compat APIs
|
// Compat APIs
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ namespace i2c {
|
||||||
static const char *const TAG = "i2c.idf";
|
static const char *const TAG = "i2c.idf";
|
||||||
|
|
||||||
void IDFI2CBus::setup() {
|
void IDFI2CBus::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Setting up I2C bus...");
|
||||||
static i2c_port_t next_port = 0;
|
static i2c_port_t next_port = 0;
|
||||||
port_ = next_port++;
|
port_ = next_port++;
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ from esphome.const import (
|
||||||
CONF_PAGES,
|
CONF_PAGES,
|
||||||
CONF_RESET_PIN,
|
CONF_RESET_PIN,
|
||||||
CONF_DIMENSIONS,
|
CONF_DIMENSIONS,
|
||||||
|
CONF_DATA_RATE,
|
||||||
)
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ["spi"]
|
DEPENDENCIES = ["spi"]
|
||||||
|
@ -43,6 +44,7 @@ MODELS = {
|
||||||
"ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI),
|
"ILI9481": ili9XXX_ns.class_("ILI9XXXILI9481", ili9XXXSPI),
|
||||||
"ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI),
|
"ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI),
|
||||||
"ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI),
|
"ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI),
|
||||||
|
"ILI9488_A": ili9XXX_ns.class_("ILI9XXXILI9488A", ili9XXXSPI),
|
||||||
"ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI),
|
"ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI),
|
||||||
"S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI),
|
"S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI),
|
||||||
"S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI),
|
"S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI),
|
||||||
|
@ -97,6 +99,7 @@ CONFIG_SCHEMA = cv.All(
|
||||||
cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list(
|
cv.Optional(CONF_COLOR_PALETTE_IMAGES, default=[]): cv.ensure_list(
|
||||||
cv.file_
|
cv.file_
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_DATA_RATE, default="40MHz"): spi.SPI_DATA_RATE_SCHEMA,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(cv.polling_component_schema("1s"))
|
.extend(cv.polling_component_schema("1s"))
|
||||||
|
@ -118,7 +121,7 @@ async def to_code(config):
|
||||||
|
|
||||||
if CONF_LAMBDA in config:
|
if CONF_LAMBDA in config:
|
||||||
lambda_ = await cg.process_lambda(
|
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_))
|
cg.add(var.set_writer(lambda_))
|
||||||
|
|
||||||
|
@ -175,3 +178,6 @@ async def to_code(config):
|
||||||
if rhs is not None:
|
if rhs is not None:
|
||||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||||
cg.add(var.set_palette(prog_arr))
|
cg.add(var.set_palette(prog_arr))
|
||||||
|
|
||||||
|
spi_data_rate = str(spi.SPI_DATA_RATE_OPTIONS[config[CONF_DATA_RATE]])
|
||||||
|
cg.add_define("ILI9XXXDisplay_DATA_RATE", cg.RawExpression(spi_data_rate))
|
||||||
|
|
|
@ -152,12 +152,10 @@ void ILI9XXXDisplay::update() {
|
||||||
this->need_update_ = true;
|
this->need_update_ = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
do {
|
|
||||||
this->prossing_update_ = true;
|
this->prossing_update_ = true;
|
||||||
|
do {
|
||||||
this->need_update_ = false;
|
this->need_update_ = false;
|
||||||
if (!this->need_update_) {
|
|
||||||
this->do_update_();
|
this->do_update_();
|
||||||
}
|
|
||||||
} while (this->need_update_);
|
} while (this->need_update_);
|
||||||
this->prossing_update_ = false;
|
this->prossing_update_ = false;
|
||||||
this->display_();
|
this->display_();
|
||||||
|
@ -411,6 +409,17 @@ void ILI9XXXILI9488::initialize() {
|
||||||
this->is_18bitdisplay_ = true;
|
this->is_18bitdisplay_ = true;
|
||||||
}
|
}
|
||||||
// 40_TFT display
|
// 40_TFT display
|
||||||
|
void ILI9XXXILI9488A::initialize() {
|
||||||
|
this->init_lcd_(INITCMD_ILI9488_A);
|
||||||
|
if (this->width_ == 0) {
|
||||||
|
this->width_ = 480;
|
||||||
|
}
|
||||||
|
if (this->height_ == 0) {
|
||||||
|
this->height_ = 320;
|
||||||
|
}
|
||||||
|
this->is_18bitdisplay_ = true;
|
||||||
|
}
|
||||||
|
// 40_TFT display
|
||||||
void ILI9XXXST7796::initialize() {
|
void ILI9XXXST7796::initialize() {
|
||||||
this->init_lcd_(INITCMD_ST7796);
|
this->init_lcd_(INITCMD_ST7796);
|
||||||
if (this->width_ == 0) {
|
if (this->width_ == 0) {
|
||||||
|
|
|
@ -15,10 +15,14 @@ enum ILI9XXXColorMode {
|
||||||
BITS_16 = 0x10,
|
BITS_16 = 0x10,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef ILI9XXXDisplay_DATA_RATE
|
||||||
|
#define ILI9XXXDisplay_DATA_RATE spi::DATA_RATE_40MHZ
|
||||||
|
#endif // ILI9XXXDisplay_DATA_RATE
|
||||||
|
|
||||||
class ILI9XXXDisplay : public PollingComponent,
|
class ILI9XXXDisplay : public PollingComponent,
|
||||||
public display::DisplayBuffer,
|
public display::DisplayBuffer,
|
||||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_40MHZ> {
|
spi::CLOCK_PHASE_LEADING, ILI9XXXDisplay_DATA_RATE> {
|
||||||
public:
|
public:
|
||||||
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
|
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
|
||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
|
@ -128,6 +132,12 @@ class ILI9XXXILI9488 : public ILI9XXXDisplay {
|
||||||
void initialize() override;
|
void initialize() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//----------- ILI9XXX_35_TFT origin colors rotated display --------------
|
||||||
|
class ILI9XXXILI9488A : public ILI9XXXDisplay {
|
||||||
|
protected:
|
||||||
|
void initialize() override;
|
||||||
|
};
|
||||||
|
|
||||||
//----------- ILI9XXX_35_TFT rotated display --------------
|
//----------- ILI9XXX_35_TFT rotated display --------------
|
||||||
class ILI9XXXST7796 : public ILI9XXXDisplay {
|
class ILI9XXXST7796 : public ILI9XXXDisplay {
|
||||||
protected:
|
protected:
|
||||||
|
|
|
@ -139,6 +139,40 @@ static const uint8_t PROGMEM INITCMD_ILI9488[] = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 5 frames
|
||||||
|
//ILI9XXX_ETMOD, 1, 0xC6, //
|
||||||
|
|
||||||
|
|
||||||
|
ILI9XXX_SLPOUT, 0x80, // Exit sleep mode
|
||||||
|
//ILI9XXX_INVON , 0,
|
||||||
|
ILI9XXX_DISPON, 0x80, // Set display on
|
||||||
|
0x00 // end
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t PROGMEM INITCMD_ILI9488_A[] = {
|
||||||
|
ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F,
|
||||||
|
ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F,
|
||||||
|
|
||||||
|
ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2
|
||||||
|
ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL
|
||||||
|
ILI9XXX_VMCTR1, 3, 0x00, 0x12, 0x80, // nVM VCM_REG VCM_REG_EN
|
||||||
|
|
||||||
|
ILI9XXX_IFMODE, 1, 0x00,
|
||||||
|
ILI9XXX_FRMCTR1, 1, 0xA0, // Frame rate = 60Hz
|
||||||
|
ILI9XXX_INVCTR, 1, 0x02, // Display Inversion Control = 2dot
|
||||||
|
|
||||||
|
ILI9XXX_DFUNCTR, 2, 0x02, 0x02, // Nomal scan
|
||||||
|
|
||||||
|
0xE9, 1, 0x00, // Set Image Functio. Disable 24 bit data
|
||||||
|
|
||||||
|
ILI9XXX_ADJCTL3, 4, 0xA9, 0x51, 0x2C, 0x82, // Adjust Control 3
|
||||||
|
|
||||||
|
ILI9XXX_MADCTL, 1, 0x28,
|
||||||
|
//ILI9XXX_PIXFMT, 1, 0x55, // Interface Pixel Format = 16bit
|
||||||
|
ILI9XXX_PIXFMT, 1, 0x66, //ILI9488 only supports 18-bit pixel format in 4/3 wire SPI mode
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 5 frames
|
// 5 frames
|
||||||
//ILI9XXX_ETMOD, 1, 0xC6, //
|
//ILI9XXX_ETMOD, 1, 0xC6, //
|
||||||
|
|
||||||
|
@ -218,12 +252,12 @@ static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = {
|
||||||
ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control
|
ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control
|
||||||
0xF2, 1, 0x00, // 3Gamma Function Disable
|
0xF2, 1, 0x00, // 3Gamma Function Disable
|
||||||
ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected
|
ILI9XXX_GAMMASET , 1, 0x01, // Gamma curve selected
|
||||||
ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma
|
ILI9XXX_GMCTRP1 , 14, 0xF0, 0x09, 0x0B, 0x06, 0x04, 0x15, // Set Gamma
|
||||||
0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03,
|
0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14,
|
||||||
0x0E, 0x09, 0x00,
|
0x18, 0x1B,
|
||||||
ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma
|
ILI9XXX_GMCTRN1 , 14, 0xE0, 0x09, 0x0B, 0x06, 0x04, 0x03, // Set Gamma
|
||||||
0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C,
|
0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14,
|
||||||
0x31, 0x36, 0x0F,
|
0x17, 0x1B,
|
||||||
ILI9XXX_SLPOUT , 0x80, // Exit Sleep
|
ILI9XXX_SLPOUT , 0x80, // Exit Sleep
|
||||||
ILI9XXX_DISPON , 0x80, // Display on
|
ILI9XXX_DISPON , 0x80, // Display on
|
||||||
0x00 // End of list
|
0x00 // End of list
|
||||||
|
|
|
@ -6,7 +6,7 @@ import re
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from esphome import core
|
from esphome import core
|
||||||
from esphome.components import display, font
|
from esphome.components import font
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
@ -28,7 +28,9 @@ DOMAIN = "image"
|
||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
MULTI_CONF = True
|
MULTI_CONF = True
|
||||||
|
|
||||||
ImageType = display.display_ns.enum("ImageType")
|
image_ns = cg.esphome_ns.namespace("image")
|
||||||
|
|
||||||
|
ImageType = image_ns.enum("ImageType")
|
||||||
IMAGE_TYPE = {
|
IMAGE_TYPE = {
|
||||||
"BINARY": ImageType.IMAGE_TYPE_BINARY,
|
"BINARY": ImageType.IMAGE_TYPE_BINARY,
|
||||||
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
|
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
|
||||||
|
@ -46,7 +48,7 @@ MDI_DOWNLOAD_TIMEOUT = 30 # seconds
|
||||||
SOURCE_LOCAL = "local"
|
SOURCE_LOCAL = "local"
|
||||||
SOURCE_MDI = "mdi"
|
SOURCE_MDI = "mdi"
|
||||||
|
|
||||||
Image_ = display.display_ns.class_("Image")
|
Image_ = image_ns.class_("Image")
|
||||||
|
|
||||||
|
|
||||||
def _compute_local_icon_path(value) -> Path:
|
def _compute_local_icon_path(value) -> Path:
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
#include "image.h"
|
#include "image.h"
|
||||||
|
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "display_buffer.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display {
|
namespace image {
|
||||||
|
|
||||||
void Image::draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) {
|
void Image::draw(int x, int y, display::Display *display, Color color_on, Color color_off) {
|
||||||
switch (type_) {
|
switch (type_) {
|
||||||
case IMAGE_TYPE_BINARY: {
|
case IMAGE_TYPE_BINARY: {
|
||||||
for (int img_x = 0; img_x < width_; img_x++) {
|
for (int img_x = 0; img_x < width_; img_x++) {
|
||||||
|
@ -131,5 +130,5 @@ ImageType Image::get_type() const { return this->type_; }
|
||||||
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
|
Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
|
||||||
: width_(width), height_(height), type_(type), data_start_(data_start) {}
|
: width_(width), height_(height), type_(type), data_start_(data_start) {}
|
||||||
|
|
||||||
} // namespace display
|
} // namespace image
|
||||||
} // namespace esphome
|
} // namespace esphome
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue