Merge branch 'esphome:dev' into weekly_schedules

This commit is contained in:
j0ta29 2024-02-18 10:28:57 +01:00 committed by GitHub
commit 814ae8be8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
708 changed files with 20104 additions and 2079 deletions

View file

@ -28,11 +28,20 @@ runs:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-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' && runner.os != 'Windows'
shell: bash shell: bash
run: | run: |
python -m venv venv python -m venv venv
. venv/bin/activate source venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
shell: bash
run: |
python -m venv venv
./venv/Scripts/activate
python --version python --version
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e . pip install -e .

View file

@ -45,7 +45,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.3.2 uses: actions/cache@v4.0.0
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@ -166,7 +166,35 @@ jobs:
pytest: pytest:
name: Run pytest name: Run pytest
runs-on: ubuntu-latest strategy:
fail-fast: false
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
os:
- ubuntu-latest
- macOS-latest
- windows-latest
exclude:
# Minimize CI resource usage
# by only running the Python version
# version used for docker images on Windows and macOS
- python-version: "3.12"
os: windows-latest
- python-version: "3.10"
os: windows-latest
- python-version: "3.9"
os: windows-latest
- python-version: "3.12"
os: macOS-latest
- python-version: "3.10"
os: macOS-latest
- python-version: "3.9"
os: macOS-latest
runs-on: ${{ matrix.os }}
needs: needs:
- common - common
steps: steps:
@ -175,14 +203,24 @@ jobs:
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ matrix.python-version }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- 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
if: matrix.os == 'windows-latest'
run: |
./venv/Scripts/activate
pytest -vv --cov-report=xml --tb=native tests
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: | run: |
. venv/bin/activate . venv/bin/activate
pytest -vv --tb=native tests pytest -vv --cov-report=xml --tb=native tests
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
clang-format: clang-format:
name: Check clang-format name: Check clang-format
@ -327,7 +365,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio - name: Cache platformio
uses: actions/cache@v3.3.2 uses: actions/cache@v4.0.0
with: with:
path: ~/.platformio path: ~/.platformio
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@ -354,6 +392,62 @@ jobs:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() if: always()
list-components:
runs-on: ubuntu-latest
needs:
- common
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
with:
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
fetch-depth: 500
- name: Fetch dev branch
run: |
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/dev*:refs/remotes/origin/dev* +refs/tags/dev*:refs/tags/dev*
git merge-base refs/remotes/origin/dev HEAD
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Find changed components
id: set-matrix
run: |
. venv/bin/activate
echo "matrix=$(script/list-components.py --changed | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
test-build-components:
name: Component test ${{ matrix.file }}
runs-on: ubuntu-latest
needs:
- common
- list-components
if: ${{ needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }}
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.list-components.outputs.matrix) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: test_build_components -e config -c ${{ matrix.file }}
run: |
. venv/bin/activate
./script/test_build_components -e config -c ${{ matrix.file }}
- name: test_build_components -e compile -c ${{ matrix.file }}
run: |
. venv/bin/activate
./script/test_build_components -e compile -c ${{ matrix.file }}
ci-status: ci-status:
name: CI Status name: CI Status
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -368,6 +462,7 @@ jobs:
- pyupgrade - pyupgrade
- compile-tests - compile-tests
- clang-tidy - clang-tidy
- test-build-components
if: always() if: always()
steps: steps:
- name: Success - name: Success

View file

@ -3,7 +3,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.0 rev: 23.12.1
hooks: hooks:
- id: black - id: black
args: args:

View file

@ -25,7 +25,7 @@ esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @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 @hwstar
esphome/components/alpha3/* @jan-hofmeier esphome/components/alpha3/* @jan-hofmeier
esphome/components/am43/* @buxtronix esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix esphome/components/am43/cover/* @buxtronix
@ -34,6 +34,8 @@ esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
esphome/components/api/* @OttoWinter esphome/components/api/* @OttoWinter
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
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
@ -50,8 +52,10 @@ esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0939/* @ziceva esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @tobias- esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas esphome/components/bl0942/* @dbuezas
esphome/components/ble_client/* @buxtronix esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bluetooth_proxy/* @jesserockz
esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov
esphome/components/bme680_bsec/* @trvrnrth esphome/components/bme680_bsec/* @trvrnrth
esphome/components/bmi160/* @flaviut esphome/components/bmi160/* @flaviut
esphome/components/bmp3xx/* @martgras esphome/components/bmp3xx/* @martgras
@ -67,6 +71,7 @@ esphome/components/cd74hc4067/* @asoehlke
esphome/components/climate/* @esphome/core esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz esphome/components/color_temperature/* @jesserockz
esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/coolix/* @glmnet esphome/components/coolix/* @glmnet
esphome/components/copy/* @OttoWinter esphome/components/copy/* @OttoWinter
esphome/components/cover/* @esphome/core esphome/components/cover/* @esphome/core
@ -133,6 +138,7 @@ esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode esphome/components/hm3301/* @freekode
esphome/components/homeassistant/* @OttoWinter esphome/components/homeassistant/* @OttoWinter
esphome/components/honeywell_hih_i2c/* @Benichou34
esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp/* @RubyBailey
esphome/components/honeywellabp2_i2c/* @jpfaff esphome/components/honeywellabp2_i2c/* @jpfaff
esphome/components/host/* @esphome/core esphome/components/host/* @esphome/core
@ -156,7 +162,6 @@ esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931 esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core esphome/components/interval/* @esphome/core
esphome/components/json/* @OttoWinter esphome/components/json/* @OttoWinter
esphome/components/kalman_combinator/* @Cat-Ion
esphome/components/key_collector/* @ssieb esphome/components/key_collector/* @ssieb
esphome/components/key_provider/* @ssieb esphome/components/key_provider/* @ssieb
esphome/components/kuntze/* @ssieb esphome/components/kuntze/* @ssieb
@ -194,6 +199,7 @@ esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @jorre05 esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz esphome/components/microphone/* @jesserockz
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
@ -223,7 +229,7 @@ esphome/components/nextion/binary_sensor/* @senexcrenshaw
esphome/components/nextion/sensor/* @senexcrenshaw esphome/components/nextion/sensor/* @senexcrenshaw
esphome/components/nextion/switch/* @senexcrenshaw esphome/components/nextion/switch/* @senexcrenshaw
esphome/components/nextion/text_sensor/* @senexcrenshaw esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra esphome/components/noblex/* @AGalfra
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/optolink/* @j0ta29 esphome/components/optolink/* @j0ta29
@ -315,6 +321,9 @@ esphome/components/ssd1331_base/* @kbx81
esphome/components/ssd1331_spi/* @kbx81 esphome/components/ssd1331_spi/* @kbx81
esphome/components/ssd1351_base/* @kbx81 esphome/components/ssd1351_base/* @kbx81
esphome/components/ssd1351_spi/* @kbx81 esphome/components/ssd1351_spi/* @kbx81
esphome/components/st7567_base/* @latonita
esphome/components/st7567_i2c/* @latonita
esphome/components/st7567_spi/* @latonita
esphome/components/st7735/* @SenexCrenshaw esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81 esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155 esphome/components/st7920/* @marsjan155
@ -326,7 +335,7 @@ esphome/components/tca9548a/* @andreashergert1984
esphome/components/tcl112/* @glmnet esphome/components/tcl112/* @glmnet
esphome/components/tee501/* @Stock-M esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax esphome/components/teleinfo/* @0hax
esphome/components/template/alarm_control_panel/* @grahambrown11 esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/text/* @mauritskorse esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81 esphome/components/thermostat/* @kbx81
esphome/components/time/* @OttoWinter esphome/components/time/* @OttoWinter
@ -356,9 +365,11 @@ esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter esphome/components/ultrasonic/* @OttoWinter
esphome/components/vbus/* @ssieb esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81
esphome/components/version/* @esphome/core 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/waveshare_epaper/* @clydebarrow
esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_base/* @OttoWinter
esphome/components/web_server_idf/* @dentra esphome/components/web_server_idf/* @dentra
esphome/components/whirlpool/* @glmnet esphome/components/whirlpool/* @glmnet

View file

@ -34,8 +34,8 @@ RUN \
python3-wheel=0.38.4-2 \ python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1 \ iputils-ping=3:20221126-1 \
git=1:2.39.2-1.1 \ git=1:2.39.2-1.1 \
curl=7.88.1-10+deb12u4 \ curl=7.88.1-10+deb12u5 \
openssh-client=1:9.2p1-2+deb12u1 \ openssh-client=1:9.2p1-2+deb12u2 \
python3-cffi=1.15.1-5 \ python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \ libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \ libmagic1=1:5.44-3 \
@ -50,7 +50,7 @@ RUN \
libssl-dev=3.0.11-1~deb12u2 \ libssl-dev=3.0.11-1~deb12u2 \
libffi-dev=3.4.4-1 \ libffi-dev=3.4.4-1 \
libopenjp2-7=2.5.0-2 \ libopenjp2-7=2.5.0-2 \
libtiff6=4.5.0-6 \ libtiff6=4.5.0-6+deb12u1 \
cargo=0.66.0+ds1-1 \ cargo=0.66.0+ds1-1 \
pkg-config=1.8.1-1 \ pkg-config=1.8.1-1 \
gcc-arm-linux-gnueabihf=4:12.2.0-3; \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \
@ -81,7 +81,7 @@ RUN \
fi; \ fi; \
pip3 install \ pip3 install \
--break-system-packages --no-cache-dir \ --break-system-packages --no-cache-dir \
platformio==6.1.11 \ platformio==6.1.13 \
# Change some platformio settings # Change some platformio settings
&& platformio settings set enable_telemetry No \ && platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \ && platformio settings set check_platformio_interval 1000000 \

View file

@ -12,7 +12,7 @@ import argcomplete
from esphome import const, writer, yaml_util from esphome import const, writer, yaml_util
import esphome.codegen as cg import esphome.codegen as cg
from esphome.config import iter_components, read_config, strip_default_ids from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import ( from esphome.const import (
ALLOWED_NAME_CHARS, ALLOWED_NAME_CHARS,
CONF_BAUD_RATE, CONF_BAUD_RATE,
@ -196,7 +196,7 @@ def write_cpp(config):
def generate_cpp_contents(config): def generate_cpp_contents(config):
_LOGGER.info("Generating C++ source...") _LOGGER.info("Generating C++ source...")
for name, component, conf in iter_components(CORE.config): for name, component, conf in iter_component_configs(CORE.config):
if component.to_code is not None: if component.to_code is not None:
coro = wrap_to_code(name, component) coro = wrap_to_code(name, component)
CORE.add_job(coro, conf) CORE.add_job(coro, conf)

View file

@ -139,6 +139,9 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
VARIANT_ESP32C3: { VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0, 5: adc2_channel_t.ADC2_CHANNEL_0,
}, },
VARIANT_ESP32C2: {},
VARIANT_ESP32C6: {},
VARIANT_ESP32H2: {},
} }

View file

@ -11,7 +11,7 @@ from esphome.const import (
) )
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
CONF_ON_TRIGGERED = "on_triggered" CONF_ON_TRIGGERED = "on_triggered"
@ -22,6 +22,8 @@ CONF_ON_ARMED_HOME = "on_armed_home"
CONF_ON_ARMED_NIGHT = "on_armed_night" CONF_ON_ARMED_NIGHT = "on_armed_night"
CONF_ON_ARMED_AWAY = "on_armed_away" CONF_ON_ARMED_AWAY = "on_armed_away"
CONF_ON_DISARMED = "on_disarmed" CONF_ON_DISARMED = "on_disarmed"
CONF_ON_CHIME = "on_chime"
CONF_ON_READY = "on_ready"
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
@ -53,12 +55,22 @@ ArmedAwayTrigger = alarm_control_panel_ns.class_(
DisarmedTrigger = alarm_control_panel_ns.class_( DisarmedTrigger = alarm_control_panel_ns.class_(
"DisarmedTrigger", automation.Trigger.template() "DisarmedTrigger", automation.Trigger.template()
) )
ChimeTrigger = alarm_control_panel_ns.class_(
"ChimeTrigger", automation.Trigger.template()
)
ReadyTrigger = alarm_control_panel_ns.class_(
"ReadyTrigger", automation.Trigger.template()
)
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action) ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action) DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action) PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action) TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action)
ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action)
AlarmControlPanelCondition = alarm_control_panel_ns.class_( AlarmControlPanelCondition = alarm_control_panel_ns.class_(
"AlarmControlPanelCondition", automation.Condition "AlarmControlPanelCondition", automation.Condition
) )
@ -111,6 +123,16 @@ ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
} }
), ),
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
}
),
cv.Optional(CONF_ON_READY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
}
),
} }
) )
@ -157,6 +179,12 @@ async def setup_alarm_control_panel_core_(var, config):
for conf in config.get(CONF_ON_CLEARED, []): for conf in config.get(CONF_ON_CLEARED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_CHIME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_READY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
async def register_alarm_control_panel(var, config): async def register_alarm_control_panel(var, config):
@ -232,6 +260,29 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
return var return var
@automation.register_action(
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
async def alarm_action_chime_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(
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
)
@automation.register_condition(
"alarm_control_panel.ready",
AlarmControlPanelCondition,
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
)
async def alarm_action_ready_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_condition( @automation.register_condition(
"alarm_control_panel.is_armed", "alarm_control_panel.is_armed",
AlarmControlPanelCondition, AlarmControlPanelCondition,

View file

@ -96,6 +96,14 @@ void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback
this->cleared_callback_.add(std::move(callback)); this->cleared_callback_.add(std::move(callback));
} }
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
this->chime_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_away(optional<std::string> code) { void AlarmControlPanel::arm_away(optional<std::string> code) {
auto call = this->make_call(); auto call = this->make_call();
call.arm_away(); call.arm_away();

View file

@ -89,6 +89,18 @@ class AlarmControlPanel : public EntityBase {
*/ */
void add_on_cleared_callback(std::function<void()> &&callback); void add_on_cleared_callback(std::function<void()> &&callback);
/** Add a callback for when a chime zone goes from closed to open
*
* @param callback The callback function
*/
void add_on_chime_callback(std::function<void()> &&callback);
/** Add a callback for when a ready state changes
*
* @param callback The callback function
*/
void add_on_ready_callback(std::function<void()> &&callback);
/** A numeric representation of the supported features as per HomeAssistant /** A numeric representation of the supported features as per HomeAssistant
* *
*/ */
@ -178,6 +190,10 @@ class AlarmControlPanel : public EntityBase {
CallbackManager<void()> disarmed_callback_{}; CallbackManager<void()> disarmed_callback_{};
// clear callback // clear callback
CallbackManager<void()> cleared_callback_{}; CallbackManager<void()> cleared_callback_{};
// chime callback
CallbackManager<void()> chime_callback_{};
// ready callback
CallbackManager<void()> ready_callback_{};
}; };
} // namespace alarm_control_panel } // namespace alarm_control_panel

View file

@ -69,6 +69,20 @@ class ClearedTrigger : public Trigger<> {
} }
}; };
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
}
};
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
}
};
template<typename... Ts> class ArmAwayAction : public Action<Ts...> { template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
public: public:
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}

View file

@ -118,7 +118,9 @@ void APIConnection::loop() {
this->list_entities_iterator_.advance(); this->list_entities_iterator_.advance();
this->initial_state_iterator_.advance(); this->initial_state_iterator_.advance();
const uint32_t keepalive = 60000; static uint32_t keepalive = 60000;
static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000;
const uint32_t now = millis(); const uint32_t now = millis();
if (this->sent_ping_) { if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
@ -126,10 +128,24 @@ void APIConnection::loop() {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str()); ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str());
} }
} else if (now - this->last_traffic_ > keepalive) { } else if (now - this->last_traffic_ > keepalive && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING..."); ESP_LOGVV(TAG, "Sending keepalive PING...");
this->sent_ping_ = true; this->sent_ping_ = this->send_ping_request(PingRequest());
this->send_ping_request(PingRequest()); if (!this->sent_ping_) {
this->next_ping_retry_ = now + ping_retry_interval;
this->ping_retries_++;
if (this->ping_retries_ >= max_ping_retries) {
on_fatal_error();
ESP_LOGE(TAG, "%s: Sending keepalive failed %d time(s). Disconnecting...", this->client_combined_info_.c_str(),
this->ping_retries_);
} else if (this->ping_retries_ >= 10) {
ESP_LOGW(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms",
this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval);
} else {
ESP_LOGD(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms",
this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval);
}
}
} }
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA

View file

@ -140,6 +140,7 @@ class APIConnection : public APIServerConnection {
void on_disconnect_response(const DisconnectResponse &value) override; void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override { void on_ping_response(const PingResponse &value) override {
// we initiated ping // we initiated ping
this->ping_retries_ = 0;
this->sent_ping_ = false; this->sent_ping_ = false;
} }
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
@ -217,6 +218,8 @@ class APIConnection : public APIServerConnection {
bool state_subscription_{false}; bool state_subscription_{false};
int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
uint32_t last_traffic_; uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
uint8_t ping_retries_{0};
bool sent_ping_{false}; bool sent_ping_{false};
bool service_call_subscription_{false}; bool service_call_subscription_{false};
bool next_close_ = false; bool next_close_ = false;

View file

@ -3848,6 +3848,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%g", this->visual_max_humidity); sprintf(buffer, "%g", this->visual_max_humidity);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append("}");
} }
#endif #endif
bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
@ -4015,6 +4016,7 @@ void ClimateStateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%g", this->target_humidity); sprintf(buffer, "%g", this->target_humidity);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append("}");
} }
#endif #endif
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {

View file

@ -319,7 +319,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() { void APIServer::request_time() {
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
if (!client->remove_ && client->connection_state_ == APIConnection::ConnectionState::CONNECTED) if (!client->remove_ && client->is_authenticated())
client->send_time_request(); client->send_time_request();
} }
} }

View file

@ -160,8 +160,7 @@ class ProtoWriteBuffer {
this->encode_field_raw(field_id, 2); this->encode_field_raw(field_id, 2);
this->encode_varint_raw(len); this->encode_varint_raw(len);
auto *data = reinterpret_cast<const uint8_t *>(string); auto *data = reinterpret_cast<const uint8_t *>(string);
for (size_t i = 0; i < len; i++) this->buffer_->insert(this->buffer_->end(), data, data + len);
this->write(data[i]);
} }
void encode_string(uint32_t field_id, const std::string &value, bool force = false) { void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size()); this->encode_string(field_id, value.data(), value.size());

View file

@ -0,0 +1,228 @@
from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c
from esphome.const import (
CONF_ID,
CONF_DIR_PIN,
CONF_DIRECTION,
CONF_HYSTERESIS,
CONF_RANGE,
)
CODEOWNERS = ["@ammmze"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
as5600_ns = cg.esphome_ns.namespace("as5600")
AS5600Component = as5600_ns.class_("AS5600Component", cg.Component, i2c.I2CDevice)
DIRECTION = {
"CLOCKWISE": 0,
"COUNTERCLOCKWISE": 1,
}
POWER_MODE = {
"NOMINAL": 0,
"LOW1": 1,
"LOW2": 2,
"LOW3": 3,
}
HYSTERESIS = {
"NONE": 0,
"LSB1": 1,
"LSB2": 2,
"LSB3": 3,
}
SLOW_FILTER = {
"16X": 0,
"8X": 1,
"4X": 2,
"2X": 3,
}
FAST_FILTER = {
"NONE": 0,
"LSB6": 1,
"LSB7": 2,
"LSB9": 3,
"LSB18": 4,
"LSB21": 5,
"LSB24": 6,
"LSB10": 7,
}
CONF_ANGLE = "angle"
CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_POWER_MODE = "power_mode"
CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter"
CONF_START_POSITION = "start_position"
CONF_END_POSITION = "end_position"
RESOLUTION = 4096
MAX_POSITION = RESOLUTION - 1
ANGLE_TO_POSITION = RESOLUTION / 360
POSITION_TO_ANGLE = 360 / RESOLUTION
# validate min range of 18deg (per datasheet) ... though i seem to get valid values down to a range of 192steps (16.875deg)
MIN_RANGE = round(18 * ANGLE_TO_POSITION)
def angle(min=-360, max=360):
return cv.All(
cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max)
)
def angle_to_position(value, min=-360, max=360):
try:
value = angle(min=min, max=max)(value)
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
except cv.Invalid as e:
raise cv.Invalid(f"When using angle, {e.error_message}")
def percent_to_position(value):
value = cv.possibly_negative_percentage(value)
return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION
def position(min=-MAX_POSITION, max=MAX_POSITION):
"""Validate that the config option is a position.
Accepts integers, degrees, or percentage (of 360 degrees).
"""
def validator(value):
if isinstance(value, str) and value.endswith("%"):
value = percent_to_position(value)
if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")):
return angle_to_position(
value,
min=round(min * POSITION_TO_ANGLE),
max=round(max * POSITION_TO_ANGLE),
)
return cv.int_range(min=min, max=max)(value)
return validator
def position_range():
"""Validate that value given is a valid range for the device.
A valid range is one of the following:
- a value of 0 (meaning full range)
- 18 thru 360 degrees
- negative 360 thru negative 18 degrees (notes: these are normalized to their positive values, accepting negatives is for convenience)
"""
zero_validator = position(min=0, max=0)
negative_validator = cv.Any(
position(min=-MAX_POSITION, max=-MIN_RANGE),
zero_validator,
)
positive_validator = cv.Any(
position(min=MIN_RANGE, max=MAX_POSITION),
zero_validator,
)
def validator(value):
is_negative_str = isinstance(value, str) and value.startswith("-")
is_negative_num = isinstance(value, (float, int)) and value < 0
if is_negative_str or is_negative_num:
return negative_validator(value)
return positive_validator(value)
return validator
def has_valid_range_config():
"""Validate that that the config start + end position results in a valid
positional range, which must be >= 18degrees
"""
range_validator = position_range()
def validator(config):
# if we don't have an end position, then there is nothing to do
if CONF_END_POSITION not in config:
return config
# determine the range by taking the difference from the end and start
range = config[CONF_END_POSITION] - config[CONF_START_POSITION]
# but need to account for start position being greater than end position
# where the range rolls back around the 0 position
if config[CONF_END_POSITION] < config[CONF_START_POSITION]:
range = RESOLUTION + config[CONF_END_POSITION] - config[CONF_START_POSITION]
try:
range_validator(range)
return config
except cv.Invalid as e:
raise cv.Invalid(
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
)
return validator
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AS5600Component),
cv.Optional(CONF_DIR_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_DIRECTION, default="CLOCKWISE"): cv.enum(
DIRECTION, upper=True
),
cv.Optional(CONF_WATCHDOG, default=False): cv.boolean,
cv.Optional(CONF_POWER_MODE, default="NOMINAL"): cv.enum(
POWER_MODE, upper=True, space=""
),
cv.Optional(CONF_HYSTERESIS, default="NONE"): cv.enum(
HYSTERESIS, upper=True, space=""
),
cv.Optional(CONF_SLOW_FILTER, default="16X"): cv.enum(
SLOW_FILTER, upper=True, space=""
),
cv.Optional(CONF_FAST_FILTER, default="NONE"): cv.enum(
FAST_FILTER, upper=True, space=""
),
cv.Optional(CONF_START_POSITION, default=0): position(),
cv.Optional(CONF_END_POSITION): position(),
cv.Optional(CONF_RANGE): position_range(),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x36)),
# ensure end_position and range are mutually exclusive
cv.has_at_most_one_key(CONF_END_POSITION, CONF_RANGE),
has_valid_range_config(),
)
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)
cg.add(var.set_direction(config[CONF_DIRECTION]))
cg.add(var.set_watchdog(config[CONF_WATCHDOG]))
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
cg.add(var.set_hysteresis(config[CONF_HYSTERESIS]))
cg.add(var.set_slow_filter(config[CONF_SLOW_FILTER]))
cg.add(var.set_fast_filter(config[CONF_FAST_FILTER]))
cg.add(var.set_start_position(config[CONF_START_POSITION]))
if dir_pin_config := config.get(CONF_DIR_PIN):
pin = await cg.gpio_pin_expression(dir_pin_config)
cg.add(var.set_dir_pin(pin))
if (end_position_config := config.get(CONF_END_POSITION, None)) is not None:
cg.add(var.set_end_position(end_position_config))
if (range_config := config.get(CONF_RANGE, None)) is not None:
cg.add(var.set_range(range_config))

View file

@ -0,0 +1,138 @@
#include "as5600.h"
#include "esphome/core/log.h"
namespace esphome {
namespace as5600 {
static const char *const TAG = "as5600";
// Configuration registers
static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R
static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW
static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW
static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW
static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW
// Output registers
static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R
static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R
// Status registers
static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R
static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
void AS5600Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up AS5600...");
if (!this->read_byte(REGISTER_STATUS).has_value()) {
this->mark_failed();
return;
}
// configuration direction pin, if given
// the dir pin on the chip should be low for clockwise
// and high for counterclockwise. If the pin is left floating
// the reported positions will be erratic.
if (this->dir_pin_ != nullptr) {
this->dir_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->dir_pin_->digital_write(this->direction_ == 1);
}
// build config register
// take the value, shift it left, and add mask to it to ensure we
// are only changing the bits appropriate for that setting in the
// off chance we somehow have bad value in there and it makes for
// a nice visual for the bit positions.
uint16_t config = 0;
// clang-format off
config |= (this->watchdog_ << 13) & 0b0010000000000000;
config |= (this->fast_filter_ << 10) & 0b0001110000000000;
config |= (this->slow_filter_ << 8) & 0b0000001100000000;
config |= (this->pwm_frequency_ << 6) & 0b0000000011000000;
config |= (this->output_mode_ << 4) & 0b0000000000110000;
config |= (this->hysteresis_ << 2) & 0b0000000000001100;
config |= (this->power_mode_ << 0) & 0b0000000000000011;
// clang-format on
// write config to config register
if (!this->write_byte_16(REGISTER_CONF, config)) {
this->mark_failed();
return;
}
// configure the start position
this->write_byte_16(REGISTER_ZPOS, this->start_position_);
// configure either end position or max angle
if (this->end_mode_ == END_MODE_POSITION) {
this->write_byte_16(REGISTER_MPOS, this->end_position_);
} else {
this->write_byte_16(REGISTER_MANG, this->end_position_);
}
// calculate the raw max from end position or start + range
this->raw_max_ = this->end_mode_ == END_MODE_POSITION ? this->end_position_ & 4095
: (this->start_position_ + this->end_position_) & 4095;
// calculate allowed range of motion by taking the start from the end
// but only if the end is greater than the start. If the start is greater
// than the end position, then that means we take the start all the way to
// reset point (i.e. 0 deg raw) and then we that with the end position
uint16_t range = this->raw_max_ > this->start_position_ ? this->raw_max_ - this->start_position_
: (4095 - this->start_position_) + this->raw_max_;
// range scale is ratio of actual allowed range to the full range
this->range_scale_ = range / 4095.0f;
}
void AS5600Component::dump_config() {
ESP_LOGCONFIG(TAG, "AS5600:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with AS5600 failed!");
return;
}
ESP_LOGCONFIG(TAG, " Watchdog: %d", this->watchdog_);
ESP_LOGCONFIG(TAG, " Fast Filter: %d", this->fast_filter_);
ESP_LOGCONFIG(TAG, " Slow Filter: %d", this->slow_filter_);
ESP_LOGCONFIG(TAG, " Hysteresis: %d", this->hysteresis_);
ESP_LOGCONFIG(TAG, " Start Position: %d", this->start_position_);
if (this->end_mode_ == END_MODE_POSITION) {
ESP_LOGCONFIG(TAG, " End Position: %d", this->end_position_);
} else {
ESP_LOGCONFIG(TAG, " Range: %d", this->end_position_);
}
}
bool AS5600Component::in_range(uint16_t raw_position) {
return this->raw_max_ > this->start_position_
? raw_position >= this->start_position_ && raw_position <= this->raw_max_
: raw_position >= this->start_position_ || raw_position <= this->raw_max_;
}
AS5600MagnetStatus AS5600Component::read_magnet_status() {
uint8_t status = this->reg(REGISTER_STATUS).get() >> 3 & 0b000111;
return static_cast<AS5600MagnetStatus>(status);
}
optional<uint16_t> AS5600Component::read_position() {
uint16_t pos = 0;
if (!this->read_byte_16(REGISTER_ANGLE, &pos)) {
return {};
}
return pos;
}
optional<uint16_t> AS5600Component::read_raw_position() {
uint16_t pos = 0;
if (!this->read_byte_16(REGISTER_ANGLE_RAW, &pos)) {
return {};
}
return pos;
}
} // namespace as5600
} // namespace esphome

View file

@ -0,0 +1,105 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/preferences.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace as5600 {
static const uint16_t POSITION_COUNT = 4096;
static const float RAW_TO_DEGREES = 360.0 / POSITION_COUNT;
static const float DEGREES_TO_RAW = POSITION_COUNT / 360.0;
enum EndPositionMode : uint8_t {
// In this mode, the end position is calculated by taking the start position
// and adding the range/positions. For example, you could say start at 90deg,
// and have a range of 180deg and effectively the sensor will report values
// from the physical 90deg thru 270deg.
END_MODE_RANGE,
// In this mode, the end position is explicitly set, and changing the start
// position will NOT change the end position.
END_MODE_POSITION,
};
enum OutRangeMode : uint8_t {
// In this mode, the AS5600 chip itself actually reports these values, but
// effectively it splits the out-of-range values in half, and when positioned
// over the half closest to the min/start position, it will report 0 and when
// positioned over the half closes to the max/end position, it will report the
// max/end value.
OUT_RANGE_MODE_MIN_MAX,
// In this mode, when the magnet is positioned outside the configured
// range, the sensor will report NAN, which translates to "Unknown"
// in Home Assistant.
OUT_RANGE_MODE_NAN,
};
enum AS5600MagnetStatus : uint8_t {
MAGNET_GONE = 2, // 0b010 / magnet not detected
MAGNET_OK = 4, // 0b100 / magnet just right
MAGNET_STRONG = 5, // 0b101 / magnet too strong
MAGNET_WEAK = 6, // 0b110 / magnet too weak
};
class AS5600Component : public Component, public i2c::I2CDevice {
public:
/// Set up the internal sensor array.
void setup() override;
void dump_config() override;
/// HARDWARE_LATE setup priority
float get_setup_priority() const override { return setup_priority::DATA; }
// configuration setters
void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; }
void set_direction(uint8_t direction) { this->direction_ = direction; }
void set_fast_filter(uint8_t fast_filter) { this->fast_filter_ = fast_filter; }
void set_hysteresis(uint8_t hysteresis) { this->hysteresis_ = hysteresis; }
void set_power_mode(uint8_t power_mode) { this->power_mode_ = power_mode; }
void set_slow_filter(uint8_t slow_filter) { this->slow_filter_ = slow_filter; }
void set_watchdog(bool watchdog) { this->watchdog_ = watchdog; }
bool get_watchdog() { return this->watchdog_; }
void set_start_position(uint16_t start_position) { this->start_position_ = start_position % POSITION_COUNT; }
void set_end_position(uint16_t end_position) {
this->end_position_ = end_position % POSITION_COUNT;
this->end_mode_ = END_MODE_POSITION;
}
void set_range(uint16_t range) {
this->end_position_ = range % POSITION_COUNT;
this->end_mode_ = END_MODE_RANGE;
}
// Gets the scale value for the configured range.
// For example, if configured to start at 0deg and end at 180deg, the
// range is 50% of the native/raw range, so the range scale would be 0.5.
// If configured to use the full 360deg, the range scale would be 1.0.
float get_range_scale() { return this->range_scale_; }
// Indicates whether the given *raw* position is within the configured range
bool in_range(uint16_t raw_position);
AS5600MagnetStatus read_magnet_status();
optional<uint16_t> read_position();
optional<uint16_t> read_raw_position();
protected:
InternalGPIOPin *dir_pin_{nullptr};
uint8_t direction_;
uint8_t fast_filter_;
uint8_t hysteresis_;
uint8_t power_mode_;
uint8_t slow_filter_;
uint8_t pwm_frequency_{0};
uint8_t output_mode_{0};
bool watchdog_;
uint16_t start_position_;
uint16_t end_position_{0};
uint16_t raw_max_;
EndPositionMode end_mode_{END_MODE_RANGE};
float range_scale_{1.0};
};
} // namespace as5600
} // namespace esphome

View file

@ -0,0 +1,119 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_ID,
STATE_CLASS_MEASUREMENT,
ICON_MAGNET,
ICON_ROTATE_RIGHT,
CONF_GAIN,
ENTITY_CATEGORY_DIAGNOSTIC,
CONF_MAGNITUDE,
CONF_STATUS,
CONF_POSITION,
)
from .. import as5600_ns, AS5600Component
CODEOWNERS = ["@ammmze"]
DEPENDENCIES = ["as5600"]
AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent)
CONF_ANGLE = "angle"
CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_POWER_MODE = "power_mode"
CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter"
CONF_PWM_FREQUENCY = "pwm_frequency"
CONF_BURN_COUNT = "burn_count"
CONF_START_POSITION = "start_position"
CONF_END_POSITION = "end_position"
CONF_OUT_OF_RANGE_MODE = "out_of_range_mode"
OutOfRangeMode = as5600_ns.enum("OutRangeMode")
OUT_OF_RANGE_MODES = {
"MIN_MAX": OutOfRangeMode.OUT_RANGE_MODE_MIN_MAX,
"NAN": OutOfRangeMode.OUT_RANGE_MODE_NAN,
}
CONF_AS5600_ID = "as5600_id"
CONFIG_SCHEMA = (
sensor.sensor_schema(
AS5600Sensor,
accuracy_decimals=0,
icon=ICON_ROTATE_RIGHT,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.GenerateID(CONF_AS5600_ID): cv.use_id(AS5600Component),
cv.Optional(CONF_OUT_OF_RANGE_MODE): cv.enum(
OUT_OF_RANGE_MODES, upper=True, space="_"
),
cv.Optional(CONF_RAW_POSITION): sensor.sensor_schema(
accuracy_decimals=0,
icon=ICON_ROTATE_RIGHT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_GAIN): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_MAGNITUDE): sensor.sensor_schema(
accuracy_decimals=0,
icon=ICON_MAGNET,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_STATUS): sensor.sensor_schema(
accuracy_decimals=0,
icon=ICON_MAGNET,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_AS5600_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE):
cg.add(var.set_out_of_range_mode(out_of_range_mode_config))
if angle_config := config.get(CONF_ANGLE):
sens = await sensor.new_sensor(angle_config)
cg.add(var.set_angle_sensor(sens))
if raw_angle_config := config.get(CONF_RAW_ANGLE):
sens = await sensor.new_sensor(raw_angle_config)
cg.add(var.set_raw_angle_sensor(sens))
if position_config := config.get(CONF_POSITION):
sens = await sensor.new_sensor(position_config)
cg.add(var.set_position_sensor(sens))
if raw_position_config := config.get(CONF_RAW_POSITION):
sens = await sensor.new_sensor(raw_position_config)
cg.add(var.set_raw_position_sensor(sens))
if gain_config := config.get(CONF_GAIN):
sens = await sensor.new_sensor(gain_config)
cg.add(var.set_gain_sensor(sens))
if magnitude_config := config.get(CONF_MAGNITUDE):
sens = await sensor.new_sensor(magnitude_config)
cg.add(var.set_magnitude_sensor(sens))
if status_config := config.get(CONF_STATUS):
sens = await sensor.new_sensor(status_config)
cg.add(var.set_status_sensor(sens))

View file

@ -0,0 +1,98 @@
#include "as5600_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace as5600 {
static const char *const TAG = "as5600.sensor";
// Configuration registers
static const uint8_t REGISTER_ZMCO = 0x00; // 8 bytes / R
static const uint8_t REGISTER_ZPOS = 0x01; // 16 bytes / RW
static const uint8_t REGISTER_MPOS = 0x03; // 16 bytes / RW
static const uint8_t REGISTER_MANG = 0x05; // 16 bytes / RW
static const uint8_t REGISTER_CONF = 0x07; // 16 bytes / RW
// Output registers
static const uint8_t REGISTER_ANGLE_RAW = 0x0C; // 16 bytes / R
static const uint8_t REGISTER_ANGLE = 0x0E; // 16 bytes / R
// Status registers
static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R
static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; }
void AS5600Sensor::dump_config() {
LOG_SENSOR("", "AS5600 Sensor", this);
ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_);
if (this->angle_sensor_ != nullptr) {
LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_);
}
if (this->raw_angle_sensor_ != nullptr) {
LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_);
}
if (this->position_sensor_ != nullptr) {
LOG_SENSOR(" ", "Position Sensor", this->position_sensor_);
}
if (this->raw_position_sensor_ != nullptr) {
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
}
if (this->gain_sensor_ != nullptr) {
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
}
if (this->magnitude_sensor_ != nullptr) {
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
}
if (this->status_sensor_ != nullptr) {
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
}
LOG_UPDATE_INTERVAL(this);
}
void AS5600Sensor::update() {
if (this->gain_sensor_ != nullptr) {
this->gain_sensor_->publish_state(this->parent_->reg(REGISTER_AGC).get());
}
if (this->magnitude_sensor_ != nullptr) {
uint16_t value = 0;
this->parent_->read_byte_16(REGISTER_MAGNITUDE, &value);
this->magnitude_sensor_->publish_state(value);
}
// 2 = magnet not detected
// 4 = magnet just right
// 5 = magnet too strong
// 6 = magnet too weak
if (this->status_sensor_ != nullptr) {
this->status_sensor_->publish_state(this->parent_->read_magnet_status());
}
auto pos = this->parent_->read_position();
if (!pos.has_value()) {
this->status_set_warning();
return;
}
auto raw = this->parent_->read_raw_position();
if (!raw.has_value()) {
this->status_set_warning();
return;
}
if (this->out_of_range_mode_ == OUT_RANGE_MODE_NAN) {
this->publish_state(this->parent_->in_range(raw.value()) ? pos.value() : NAN);
} else {
this->publish_state(pos.value());
}
if (this->raw_position_sensor_ != nullptr) {
this->raw_position_sensor_->publish_state(raw.value());
}
this->status_clear_warning();
}
} // namespace as5600
} // namespace esphome

View file

@ -0,0 +1,43 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/preferences.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/as5600/as5600.h"
namespace esphome {
namespace as5600 {
class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>, public sensor::Sensor {
public:
void update() override;
void dump_config() override;
float get_setup_priority() const override;
void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; }
void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; }
void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; }
void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) {
this->raw_position_sensor_ = raw_position_sensor;
}
void set_gain_sensor(sensor::Sensor *gain_sensor) { this->gain_sensor_ = gain_sensor; }
void set_magnitude_sensor(sensor::Sensor *magnitude_sensor) { this->magnitude_sensor_ = magnitude_sensor; }
void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
void set_out_of_range_mode(OutRangeMode oor_mode) { this->out_of_range_mode_ = oor_mode; }
OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; }
protected:
sensor::Sensor *angle_sensor_{nullptr};
sensor::Sensor *raw_angle_sensor_{nullptr};
sensor::Sensor *position_sensor_{nullptr};
sensor::Sensor *raw_position_sensor_{nullptr};
sensor::Sensor *gain_sensor_{nullptr};
sensor::Sensor *magnitude_sensor_{nullptr};
sensor::Sensor *status_sensor_{nullptr};
OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX};
};
} // namespace as5600
} // namespace esphome

View file

@ -242,7 +242,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
this->set_notify_(true); this->set_notify_(true);
#ifdef USE_TIME #ifdef USE_TIME
if (this->time_id_.has_value()) { if (this->time_id_ != nullptr) {
this->send_local_time(); this->send_local_time();
} }
#endif #endif
@ -441,9 +441,8 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) {
#ifdef USE_TIME #ifdef USE_TIME
void BedJetHub::send_local_time() { void BedJetHub::send_local_time() {
if (this->time_id_.has_value()) { if (this->time_id_ != nullptr) {
auto *time_id = *this->time_id_; ESPTime now = this->time_id_->now();
ESPTime now = time_id->now();
if (now.is_valid()) { if (now.is_valid()) {
this->set_clock(now.hour, now.minute); this->set_clock(now.hour, now.minute);
ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
@ -454,10 +453,9 @@ void BedJetHub::send_local_time() {
} }
void BedJetHub::setup_time_() { void BedJetHub::setup_time_() {
if (this->time_id_.has_value()) { if (this->time_id_ != nullptr) {
this->send_local_time(); this->send_local_time();
auto *time_id = *this->time_id_; this->time_id_->add_on_time_sync_callback([this] { this->send_local_time(); });
time_id->add_on_time_sync_callback([this] { this->send_local_time(); });
} else { } else {
ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
} }

View file

@ -141,7 +141,7 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo
#ifdef USE_TIME #ifdef USE_TIME
/** Initializes time sync callbacks to support syncing current time to the BedJet. */ /** Initializes time sync callbacks to support syncing current time to the BedJet. */
void setup_time_(); void setup_time_();
optional<time::RealTimeClock *> time_id_{}; time::RealTimeClock *time_id_{nullptr};
#endif #endif
uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};

View file

@ -141,6 +141,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
FILTER_REGISTRY = Registry() FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
@ -259,6 +260,19 @@ async def lambda_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id, lambda_) return cg.new_Pvariable(filter_id, lambda_)
@register_filter(
"settle",
SettleFilter,
cv.templatable(cv.positive_time_period_milliseconds),
)
async def settle_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id)
await cg.register_component(var, {})
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_delay(template_))
return var
MULTI_CLICK_TIMING_SCHEMA = cv.Schema( MULTI_CLICK_TIMING_SCHEMA = cv.Schema(
{ {
cv.Optional(CONF_STATE): cv.boolean, cv.Optional(CONF_STATE): cv.boolean,

View file

@ -111,6 +111,23 @@ 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> SettleFilter::new_value(bool value, bool is_initial) {
if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->steady_ = true;
this->output(value, is_initial);
});
return {};
} else {
this->steady_ = false;
this->output(value, is_initial);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value;
}
}
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace binary_sensor } // namespace binary_sensor
} // namespace esphome } // namespace esphome

View file

@ -108,6 +108,19 @@ class LambdaFilter : public Filter {
std::function<optional<bool>(bool)> f_; std::function<optional<bool>(bool)> f_;
}; };
class SettleFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableValue<uint32_t> delay_{};
bool steady_{true};
};
} // namespace binary_sensor } // namespace binary_sensor
} // namespace esphome } // namespace esphome

View file

@ -18,6 +18,7 @@ from esphome.const import (
UNIT_KILOWATT_HOURS, UNIT_KILOWATT_HOURS,
UNIT_VOLT, UNIT_VOLT,
UNIT_WATT, UNIT_WATT,
STATE_CLASS_TOTAL_INCREASING,
) )
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@ -54,6 +55,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_KILOWATT_HOURS, unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY, device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema( cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS, unit_of_measurement=UNIT_CELSIUS,

View file

@ -19,6 +19,7 @@ from esphome.const import (
UNIT_VOLT, UNIT_VOLT,
UNIT_WATT, UNIT_WATT,
UNIT_HERTZ, UNIT_HERTZ,
STATE_CLASS_TOTAL_INCREASING,
) )
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@ -52,6 +53,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_KILOWATT_HOURS, unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY, device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ, unit_of_measurement=UNIT_HERTZ,

View file

@ -1,5 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.automation import maybe_simple_id
from esphome.components import esp32_ble_tracker, esp32_ble_client from esphome.components import esp32_ble_tracker, esp32_ble_client
from esphome.const import ( from esphome.const import (
CONF_CHARACTERISTIC_UUID, CONF_CHARACTERISTIC_UUID,
@ -15,7 +16,7 @@ from esphome.const import (
from esphome import automation from esphome import automation
AUTO_LOAD = ["esp32_ble_client"] AUTO_LOAD = ["esp32_ble_client"]
CODEOWNERS = ["@buxtronix"] CODEOWNERS = ["@buxtronix", "@clydebarrow"]
DEPENDENCIES = ["esp32_ble_tracker"] DEPENDENCIES = ["esp32_ble_tracker"]
ble_client_ns = cg.esphome_ns.namespace("ble_client") ble_client_ns = cg.esphome_ns.namespace("ble_client")
@ -43,6 +44,10 @@ BLEClientNumericComparisonRequestTrigger = ble_client_ns.class_(
# Actions # Actions
BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action)
BLEConnectAction = ble_client_ns.class_("BLEClientConnectAction", automation.Action)
BLEDisconnectAction = ble_client_ns.class_(
"BLEClientDisconnectAction", automation.Action
)
BLEPasskeyReplyAction = ble_client_ns.class_( BLEPasskeyReplyAction = ble_client_ns.class_(
"BLEClientPasskeyReplyAction", automation.Action "BLEClientPasskeyReplyAction", automation.Action
) )
@ -58,6 +63,7 @@ CONF_ACCEPT = "accept"
CONF_ON_PASSKEY_REQUEST = "on_passkey_request" CONF_ON_PASSKEY_REQUEST = "on_passkey_request"
CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification" CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification"
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request" CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
CONF_AUTO_CONNECT = "auto_connect"
# Espressif platformio framework is built with MAX_BLE_CONN to 3, so # Espressif platformio framework is built with MAX_BLE_CONN to 3, so
# enforce this in yaml checks. # enforce this in yaml checks.
@ -69,6 +75,7 @@ CONFIG_SCHEMA = (
cv.GenerateID(): cv.declare_id(BLEClient), cv.GenerateID(): cv.declare_id(BLEClient),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_NAME): cv.string, cv.Optional(CONF_NAME): cv.string,
cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean,
cv.Optional(CONF_ON_CONNECT): automation.validate_automation( cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@ -135,6 +142,12 @@ BLE_WRITE_ACTION_SCHEMA = cv.Schema(
} }
) )
BLE_CONNECT_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
}
)
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema( BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
{ {
cv.GenerateID(CONF_ID): cv.use_id(BLEClient), cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
@ -157,6 +170,24 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
) )
@automation.register_action(
"ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA
)
async def ble_disconnect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent)
return var
@automation.register_action(
"ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA
)
async def ble_connect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent)
return var
@automation.register_action( @automation.register_action(
"ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA
) )
@ -261,6 +292,7 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await esp32_ble_tracker.register_client(var, config) await esp32_ble_tracker.register_client(var, config)
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
cg.add(var.set_auto_connect(config[CONF_AUTO_CONNECT]))
for conf in config.get(CONF_ON_CONNECT, []): for conf in config.get(CONF_ON_CONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)

View file

@ -2,76 +2,10 @@
#include "automation.h" #include "automation.h"
#include <esp_bt_defs.h>
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
#include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace ble_client { namespace ble_client {
static const char *const TAG = "ble_client.automation";
void BLEWriterClientNode::write(const std::vector<uint8_t> &value) { const char *const Automation::TAG = "ble_client.automation";
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected");
return;
} else if (this->ble_char_handle_ == 0) {
ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found");
return;
}
esp_gatt_write_type_t write_type;
if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
write_type = ESP_GATT_WRITE_TYPE_RSP;
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
} else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
write_type = ESP_GATT_WRITE_TYPE_NO_RSP;
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
} else {
ESP_LOGE(TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str());
return;
}
ESP_LOGVV(TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
esp_err_t err =
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->ble_char_handle_,
value.size(), const_cast<uint8_t *>(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
}
}
void BLEWriterClientNode::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_REG_EVT:
break;
case ESP_GATTC_OPEN_EVT:
this->node_state = espbt::ClientState::ESTABLISHED;
ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str());
break;
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
if (chr == nullptr) {
ESP_LOGW("ble_write_action", "Characteristic %s was not found in service %s",
this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str());
break;
}
this->ble_char_handle_ = chr->handle;
this->char_props_ = chr->properties;
this->node_state = espbt::ClientState::ESTABLISHED;
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
ble_client_->address_str().c_str());
break;
}
case ESP_GATTC_DISCONNECT_EVT:
this->node_state = espbt::ClientState::IDLE;
this->ble_char_handle_ = 0;
ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str());
break;
default:
break;
}
}
} // namespace ble_client } // namespace ble_client
} // namespace esphome } // namespace esphome

View file

@ -7,9 +7,19 @@
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/components/ble_client/ble_client.h" #include "esphome/components/ble_client/ble_client.h"
#include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace ble_client { namespace ble_client {
// placeholder class for static TAG .
class Automation {
public:
// could be made inline with C++17
static const char *const TAG;
};
// implement on_connect automation.
class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode {
public: public:
explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
@ -23,17 +33,28 @@ class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode {
} }
}; };
// on_disconnect automation
class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode {
public: public:
explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {} void loop() override {}
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override { esp_ble_gattc_cb_param_t *param) override {
if (event == ESP_GATTC_DISCONNECT_EVT && // test for CLOSE and not DISCONNECT - DISCONNECT can occur even if no virtual connection (OPEN event) occurred.
memcmp(param->disconnect.remote_bda, this->parent_->get_remote_bda(), 6) == 0) // So this will not trigger unless a complete open has previously succeeded.
this->trigger(); switch (event) {
if (event == ESP_GATTC_SEARCH_CMPL_EVT) case ESP_GATTC_SEARCH_CMPL_EVT: {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
break;
}
case ESP_GATTC_CLOSE_EVT: {
this->trigger();
break;
}
default: {
break;
}
}
} }
}; };
@ -42,11 +63,9 @@ class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode {
explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {} void loop() override {}
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr))
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
this->trigger(); this->trigger();
} }
}
}; };
class BLEClientPasskeyNotificationTrigger : public Trigger<uint32_t>, public BLEClientNode { class BLEClientPasskeyNotificationTrigger : public Trigger<uint32_t>, public BLEClientNode {
@ -54,10 +73,8 @@ class BLEClientPasskeyNotificationTrigger : public Trigger<uint32_t>, public BLE
explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {} void loop() override {}
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) {
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { this->trigger(param->ble_security.key_notif.passkey);
uint32_t passkey = param->ble_security.key_notif.passkey;
this->trigger(passkey);
} }
} }
}; };
@ -67,24 +84,20 @@ class BLEClientNumericComparisonRequestTrigger : public Trigger<uint32_t>, publi
explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {} void loop() override {}
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
if (event == ESP_GAP_BLE_NC_REQ_EVT && if (event == ESP_GAP_BLE_NC_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) {
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { this->trigger(param->ble_security.key_notif.passkey);
uint32_t passkey = param->ble_security.key_notif.passkey;
this->trigger(passkey);
} }
} }
}; };
class BLEWriterClientNode : public BLEClientNode { // implement the ble_client.ble_write action.
template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, public BLEClientNode {
public: public:
BLEWriterClientNode(BLEClient *ble_client) { BLEClientWriteAction(BLEClient *ble_client) {
ble_client->register_ble_node(this); ble_client->register_ble_node(this);
ble_client_ = ble_client; ble_client_ = ble_client;
} }
// Attempts to write the contents of value to char_uuid_.
void write(const std::vector<uint8_t> &value);
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
@ -93,29 +106,6 @@ class BLEWriterClientNode : public BLEClientNode {
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
private:
BLEClient *ble_client_;
int ble_char_handle_ = 0;
esp_gatt_char_prop_t char_props_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;
};
template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, public BLEWriterClientNode {
public:
BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {}
void play(Ts... x) override {
if (has_simple_value_) {
return write(this->value_simple_);
} else {
return write(this->value_template_(x...));
}
}
void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) { void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->value_template_ = std::move(func); this->value_template_ = std::move(func);
has_simple_value_ = false; has_simple_value_ = false;
@ -126,10 +116,94 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
has_simple_value_ = true; has_simple_value_ = true;
} }
void play(Ts... x) override {}
void play_complex(Ts... x) override {
this->num_running_++;
this->var_ = std::make_tuple(x...);
auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...);
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!write(value))
this->play_next_(x...);
}
/**
* Note about logging: the esph_log_X macros are used here because the CI checks complain about use of the ESP LOG
* macros in header files (Can't even write it in a comment!)
* Not sure why, because they seem to work just fine.
* The problem is that the implementation of a templated class can't be placed in a .cpp file when using C++ less than
* 17, so the methods have to be here. The esph_log_X macros are equivalent in function, but don't trigger the CI
* errors.
*/
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
bool write(const std::vector<uint8_t> &value) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
return false;
}
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_OK) {
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
return false;
}
return true;
}
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
switch (event) {
case ESP_GATTC_WRITE_CHAR_EVT:
// upstream code checked the MAC address, verify the characteristic.
if (param->write.handle == this->char_handle_)
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
break;
case ESP_GATTC_DISCONNECT_EVT:
if (this->num_running_ != 0)
this->stop_complex();
break;
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
if (chr == nullptr) {
esph_log_w("ble_write_action", "Characteristic %s was not found in service %s",
this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str());
break;
}
this->char_handle_ = chr->handle;
this->char_props_ = chr->properties;
if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
this->write_type_ = ESP_GATT_WRITE_TYPE_RSP;
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
} else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
} else {
esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str());
break;
}
this->node_state = espbt::ClientState::ESTABLISHED;
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
ble_client_->address_str().c_str());
break;
}
default:
break;
}
}
private: private:
BLEClient *ble_client_;
bool has_simple_value_ = true; bool has_simple_value_ = true;
std::vector<uint8_t> value_simple_; std::vector<uint8_t> value_simple_;
std::function<std::vector<uint8_t>(Ts...)> value_template_{}; std::function<std::vector<uint8_t>(Ts...)> value_template_{};
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;
std::tuple<Ts...> var_{};
uint16_t char_handle_{};
esp_gatt_char_prop_t char_props_{};
esp_gatt_write_type_t write_type_{};
}; };
template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...> { template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...> {
@ -212,6 +286,92 @@ template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...>
BLEClient *parent_{nullptr}; BLEClient *parent_{nullptr};
}; };
template<typename... Ts> class BLEClientConnectAction : public Action<Ts...>, public BLEClientNode {
public:
BLEClientConnectAction(BLEClient *ble_client) {
ble_client->register_ble_node(this);
ble_client_ = ble_client;
}
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
if (this->num_running_ == 0)
return;
switch (event) {
case ESP_GATTC_SEARCH_CMPL_EVT:
this->node_state = espbt::ClientState::ESTABLISHED;
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
break;
// if the connection is closed, terminate the automation chain.
case ESP_GATTC_DISCONNECT_EVT:
this->stop_complex();
break;
default:
break;
}
}
// not used since we override play_complex_
void play(Ts... x) override {}
void play_complex(Ts... x) override {
// it makes no sense to have multiple instances of this running at the same time.
// this would occur only if the same automation was re-triggered while still
// running. So just cancel the second chain if this is detected.
if (this->num_running_ != 0) {
this->stop_complex();
return;
}
this->num_running_++;
if (this->node_state == espbt::ClientState::ESTABLISHED) {
this->play_next_(x...);
} else {
this->var_ = std::make_tuple(x...);
this->ble_client_->connect();
}
}
private:
BLEClient *ble_client_;
std::tuple<Ts...> var_{};
};
template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>, public BLEClientNode {
public:
BLEClientDisconnectAction(BLEClient *ble_client) {
ble_client->register_ble_node(this);
ble_client_ = ble_client;
}
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
if (this->num_running_ == 0)
return;
switch (event) {
case ESP_GATTC_CLOSE_EVT:
case ESP_GATTC_DISCONNECT_EVT:
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
break;
default:
break;
}
}
// not used since we override play_complex_
void play(Ts... x) override {}
void play_complex(Ts... x) override {
this->num_running_++;
if (this->node_state == espbt::ClientState::IDLE) {
this->play_next_(x...);
} else {
this->var_ = std::make_tuple(x...);
this->ble_client_->disconnect();
}
}
private:
BLEClient *ble_client_;
std::tuple<Ts...> var_{};
};
} // namespace ble_client } // namespace ble_client
} // namespace esphome } // namespace esphome

View file

@ -26,6 +26,7 @@ void BLEClient::loop() {
void BLEClient::dump_config() { void BLEClient::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Client:"); ESP_LOGCONFIG(TAG, "BLE Client:");
ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str());
ESP_LOGCONFIG(TAG, " Auto-Connect: %s", TRUEFALSE(this->auto_connect_));
} }
bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
@ -37,31 +38,24 @@ bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
void BLEClient::set_enabled(bool enabled) { void BLEClient::set_enabled(bool enabled) {
if (enabled == this->enabled) if (enabled == this->enabled)
return; return;
if (!enabled && this->state() != espbt::ClientState::IDLE) {
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
auto ret = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
if (ret) {
ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret);
}
}
this->enabled = enabled; this->enabled = enabled;
if (!enabled) {
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
this->disconnect();
}
} }
bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
bool all_established = this->all_nodes_established_();
if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param)) if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param))
return false; return false;
for (auto *node : this->nodes_) for (auto *node : this->nodes_)
node->gattc_event_handler(event, esp_gattc_if, param); node->gattc_event_handler(event, esp_gattc_if, param);
// Delete characteristics after clients have used them to save RAM. if (!this->services_.empty() && this->all_nodes_established_()) {
if (!all_established && this->all_nodes_established_()) { this->release_services();
for (auto &svc : this->services_) ESP_LOGD(TAG, "All clients established, services released");
delete svc; // NOLINT(cppcoreguidelines-owning-memory)
this->services_.clear();
} }
return true; return true;
} }

View file

@ -19,25 +19,35 @@ void BLEBinaryOutput::dump_config() {
void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GATTC_OPEN_EVT: case ESP_GATTC_SEARCH_CMPL_EVT: {
this->client_state_ = espbt::ClientState::ESTABLISHED;
ESP_LOGW(TAG, "[%s] Connected successfully!", this->char_uuid_.to_string().c_str());
break;
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGW(TAG, "[%s] Disconnected", this->char_uuid_.to_string().c_str());
this->client_state_ = espbt::ClientState::IDLE;
break;
case ESP_GATTC_WRITE_CHAR_EVT: {
if (param->write.status == 0) {
break;
}
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
if (chr == nullptr) { if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] Characteristic not found.", this->char_uuid_.to_string().c_str()); ESP_LOGW(TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_string().c_str(),
this->service_uuid_.to_string().c_str());
break; break;
} }
if (param->write.handle == chr->handle) { this->char_handle_ = chr->handle;
this->char_props_ = chr->properties;
if (this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
this->write_type_ = ESP_GATT_WRITE_TYPE_RSP;
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
} else if (!this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
} else {
ESP_LOGE(TAG, "Characteristic %s does not allow writing with%s response", this->char_uuid_.to_string().c_str(),
this->require_response_ ? "" : "out");
break;
}
this->node_state = espbt::ClientState::ESTABLISHED;
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
this->parent()->address_str().c_str());
this->node_state = espbt::ClientState::ESTABLISHED;
break;
}
case ESP_GATTC_WRITE_CHAR_EVT: {
if (param->write.handle == this->char_handle_) {
if (param->write.status != 0)
ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status); ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status);
} }
break; break;
@ -48,26 +58,18 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
} }
void BLEBinaryOutput::write_state(bool state) { void BLEBinaryOutput::write_state(bool state) {
if (this->client_state_ != espbt::ClientState::ESTABLISHED) { if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.",
this->char_uuid_.to_string().c_str()); this->char_uuid_.to_string().c_str());
return; return;
} }
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] Characteristic not found. State update can not be written.",
this->char_uuid_.to_string().c_str());
return;
}
uint8_t state_as_uint = (uint8_t) state; uint8_t state_as_uint = (uint8_t) state;
ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint); ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint);
if (this->require_response_) { esp_err_t err =
chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_RSP); esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
} else { sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_NO_RSP); if (err != ESP_GATT_OK)
} ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err);
} }
} // namespace ble_client } // namespace ble_client

View file

@ -32,7 +32,9 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi
bool require_response_; bool require_response_;
espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_; espbt::ESPBTUUID char_uuid_;
espbt::ClientState client_state_; uint16_t char_handle_{};
esp_gatt_char_prop_t char_props_{};
esp_gatt_write_type_t write_type_{};
}; };
} // namespace ble_client } // namespace ble_client

View file

@ -14,15 +14,17 @@ class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override { esp_ble_gattc_cb_param_t *param) override {
switch (event) { switch (event) {
case ESP_GATTC_SEARCH_CMPL_EVT: { case ESP_GATTC_NOTIFY_EVT: {
this->sensor_->node_state = espbt::ClientState::ESTABLISHED; if (param->notify.handle == this->sensor_->handle)
this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len));
break; break;
} }
case ESP_GATTC_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() || // confirms notifications are being listened for. While enabling of notifications may still be in
param->notify.handle != this->sensor_->handle) // progress by the parent, we assume it will happen.
if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->sensor_->handle)
this->node_state = espbt::ClientState::ESTABLISHED;
break; break;
this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len));
} }
default: default:
break; break;

View file

@ -22,26 +22,19 @@ void BLEClientRSSISensor::dump_config() {
void BLEClientRSSISensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void BLEClientRSSISensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GATTC_OPEN_EVT: { case ESP_GATTC_CLOSE_EVT: {
if (param->open.status == ESP_GATT_OK) {
ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str());
break;
}
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
this->status_set_warning(); this->status_set_warning();
this->publish_state(NAN); this->publish_state(NAN);
break; break;
} }
case ESP_GATTC_SEARCH_CMPL_EVT: case ESP_GATTC_SEARCH_CMPL_EVT: {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
if (this->should_update_) { if (this->should_update_) {
this->should_update_ = false; this->should_update_ = false;
this->get_rssi_(); this->get_rssi_();
} }
break; break;
}
default: default:
break; break;
} }

View file

@ -33,7 +33,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
} }
break; break;
} }
case ESP_GATTC_DISCONNECT_EVT: { case ESP_GATTC_CLOSE_EVT: {
ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
this->status_set_warning(); this->status_set_warning();
this->publish_state(NAN); this->publish_state(NAN);
@ -74,8 +74,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
break; break;
} }
case ESP_GATTC_READ_CHAR_EVT: { case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id())
break;
if (param->read.status != ESP_GATT_OK) { if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break; break;
@ -87,15 +85,23 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
break; break;
} }
case ESP_GATTC_NOTIFY_EVT: { case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
break;
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
param->notify.handle, param->notify.value[0]); param->notify.handle, param->notify.value[0]);
if (param->notify.handle != this->handle)
break;
this->publish_state(this->parse_data_(param->notify.value, param->notify.value_len)); this->publish_state(this->parse_data_(param->notify.value, param->notify.value_len));
break; break;
} }
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (param->reg_for_notify.handle == this->handle) {
if (param->reg_for_notify.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error registering for notifications at handle %d, status=%d", param->reg_for_notify.handle,
param->reg_for_notify.status);
break;
}
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_string().c_str());
}
break; break;
} }
default: default:

View file

@ -17,14 +17,11 @@ void BLEClientSwitch::write_state(bool state) {
void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GATTC_REG_EVT: case ESP_GATTC_CLOSE_EVT:
this->publish_state(this->parent_->enabled); this->publish_state(this->parent_->enabled);
break; break;
case ESP_GATTC_OPEN_EVT: case ESP_GATTC_SEARCH_CMPL_EVT:
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
break;
case ESP_GATTC_DISCONNECT_EVT:
this->node_state = espbt::ClientState::IDLE;
this->publish_state(this->parent_->enabled); this->publish_state(this->parent_->enabled);
break; break;
default: default:

View file

@ -36,8 +36,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} }
break; break;
} }
case ESP_GATTC_DISCONNECT_EVT: { case ESP_GATTC_CLOSE_EVT: {
ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
this->status_set_warning(); this->status_set_warning();
this->publish_state(EMPTY); this->publish_state(EMPTY);
break; break;
@ -77,20 +76,18 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break; break;
} }
case ESP_GATTC_READ_CHAR_EVT: { case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.conn_id != this->parent()->get_conn_id()) if (param->read.handle == this->handle) {
break;
if (param->read.status != ESP_GATT_OK) { if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
break; break;
} }
if (param->read.handle == this->handle) {
this->status_clear_warning(); this->status_clear_warning();
this->publish_state(this->parse_data(param->read.value, param->read.value_len)); this->publish_state(this->parse_data(param->read.value, param->read.value_len));
} }
break; break;
} }
case ESP_GATTC_NOTIFY_EVT: { case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle) if (param->notify.handle != this->handle)
break; break;
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
param->notify.handle, param->notify.value[0]); param->notify.handle, param->notify.value[0]);
@ -98,6 +95,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break; break;
} }
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->handle)
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
break; break;
} }

View file

@ -1,116 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
UNIT_PERCENT,
)
DEPENDENCIES = ["i2c"]
bme280_ns = cg.esphome_ns.namespace("bme280")
BME280Oversampling = bme280_ns.enum("BME280Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE,
"1X": BME280Oversampling.BME280_OVERSAMPLING_1X,
"2X": BME280Oversampling.BME280_OVERSAMPLING_2X,
"4X": BME280Oversampling.BME280_OVERSAMPLING_4X,
"8X": BME280Oversampling.BME280_OVERSAMPLING_8X,
"16X": BME280Oversampling.BME280_OVERSAMPLING_16X,
}
BME280IIRFilter = bme280_ns.enum("BME280IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF,
"2X": BME280IIRFilter.BME280_IIR_FILTER_2X,
"4X": BME280IIRFilter.BME280_IIR_FILTER_4X,
"8X": BME280IIRFilter.BME280_IIR_FILTER_8X,
"16X": BME280IIRFilter.BME280_IIR_FILTER_16X,
}
BME280Component = bme280_ns.class_(
"BME280Component", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BME280Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x77))
)
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)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))

View file

@ -0,0 +1 @@
CODEOWNERS = ["@esphome/core"]

View file

@ -1,9 +1,14 @@
#include "bme280.h" #include <cmath>
#include <cstdint>
#include "bme280_base.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <esphome/components/sensor/sensor.h>
#include <esphome/core/component.h>
namespace esphome { namespace esphome {
namespace bme280 { namespace bme280_base {
static const char *const TAG = "bme280.sensor"; static const char *const TAG = "bme280.sensor";
@ -46,7 +51,24 @@ static const uint8_t BME280_STATUS_IM_UPDATE = 0b01;
inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); }
static const char *oversampling_to_str(BME280Oversampling oversampling) { const char *iir_filter_to_str(BME280IIRFilter filter) { // NOLINT
switch (filter) {
case BME280_IIR_FILTER_OFF:
return "OFF";
case BME280_IIR_FILTER_2X:
return "2x";
case BME280_IIR_FILTER_4X:
return "4x";
case BME280_IIR_FILTER_8X:
return "8x";
case BME280_IIR_FILTER_16X:
return "16x";
default:
return "UNKNOWN";
}
}
const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT
switch (oversampling) { switch (oversampling) {
case BME280_OVERSAMPLING_NONE: case BME280_OVERSAMPLING_NONE:
return "None"; return "None";
@ -65,23 +87,6 @@ static const char *oversampling_to_str(BME280Oversampling oversampling) {
} }
} }
static const char *iir_filter_to_str(BME280IIRFilter filter) {
switch (filter) {
case BME280_IIR_FILTER_OFF:
return "OFF";
case BME280_IIR_FILTER_2X:
return "2x";
case BME280_IIR_FILTER_4X:
return "4x";
case BME280_IIR_FILTER_8X:
return "8x";
case BME280_IIR_FILTER_16X:
return "16x";
default:
return "UNKNOWN";
}
}
void BME280Component::setup() { void BME280Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up BME280..."); ESP_LOGCONFIG(TAG, "Setting up BME280...");
uint8_t chip_id = 0; uint8_t chip_id = 0;
@ -112,7 +117,7 @@ void BME280Component::setup() {
// Wait until the NVM data has finished loading. // Wait until the NVM data has finished loading.
uint8_t status; uint8_t status;
uint8_t retry = 5; uint8_t retry = 5;
do { do { // NOLINT
delay(2); delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
ESP_LOGW(TAG, "Error reading status register."); ESP_LOGW(TAG, "Error reading status register.");
@ -175,7 +180,6 @@ void BME280Component::setup() {
} }
void BME280Component::dump_config() { void BME280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BME280:"); ESP_LOGCONFIG(TAG, "BME280:");
LOG_I2C_DEVICE(this);
switch (this->error_code_) { switch (this->error_code_) {
case COMMUNICATION_FAILED: case COMMUNICATION_FAILED:
ESP_LOGE(TAG, "Communication with BME280 failed!"); ESP_LOGE(TAG, "Communication with BME280 failed!");
@ -226,14 +230,14 @@ void BME280Component::update() {
return; return;
} }
int32_t t_fine = 0; int32_t t_fine = 0;
float temperature = this->read_temperature_(data, &t_fine); float const temperature = this->read_temperature_(data, &t_fine);
if (std::isnan(temperature)) { if (std::isnan(temperature)) {
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
this->status_set_warning(); this->status_set_warning();
return; return;
} }
float pressure = this->read_pressure_(data, t_fine); float const pressure = this->read_pressure_(data, t_fine);
float humidity = this->read_humidity_(data, t_fine); float const humidity = this->read_humidity_(data, t_fine);
ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity);
if (this->temperature_sensor_ != nullptr) if (this->temperature_sensor_ != nullptr)
@ -257,12 +261,12 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) {
const int32_t t2 = this->calibration_.t2; const int32_t t2 = this->calibration_.t2;
const int32_t t3 = this->calibration_.t3; const int32_t t3 = this->calibration_.t3;
int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; int32_t const var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
*t_fine = var1 + var2; *t_fine = var1 + var2;
float temperature = (*t_fine * 5 + 128) >> 8; float const temperature = (*t_fine * 5 + 128);
return temperature / 100.0f; return temperature / 25600.0f;
} }
float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) { float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
@ -303,11 +307,11 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
} }
float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) { float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
uint16_t raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); uint16_t const raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
if (raw_adc == 0x8000) if (raw_adc == 0x8000)
return NAN; return NAN;
int32_t adc = raw_adc; int32_t const adc = raw_adc;
const int32_t h1 = this->calibration_.h1; const int32_t h1 = this->calibration_.h1;
const int32_t h2 = this->calibration_.h2; const int32_t h2 = this->calibration_.h2;
@ -325,7 +329,7 @@ float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r; v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r;
v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r; v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r;
float h = v_x1_u32r >> 12; float const h = v_x1_u32r >> 12;
return h / 1024.0f; return h / 1024.0f;
} }
@ -351,5 +355,5 @@ uint16_t BME280Component::read_u16_le_(uint8_t a_register) {
} }
int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }
} // namespace bme280 } // namespace bme280_base
} // namespace esphome } // namespace esphome

View file

@ -2,10 +2,9 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome { namespace esphome {
namespace bme280 { namespace bme280_base {
/// Internal struct storing the calibration values of an BME280. /// Internal struct storing the calibration values of an BME280.
struct BME280CalibrationData { struct BME280CalibrationData {
@ -57,8 +56,8 @@ enum BME280IIRFilter {
BME280_IIR_FILTER_16X = 0b100, BME280_IIR_FILTER_16X = 0b100,
}; };
/// This class implements support for the BME280 Temperature+Pressure+Humidity i2c sensor. /// This class implements support for the BME280 Temperature+Pressure+Humidity sensor.
class BME280Component : public PollingComponent, public i2c::I2CDevice { class BME280Component : public PollingComponent {
public: public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
@ -91,6 +90,11 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
uint16_t read_u16_le_(uint8_t a_register); uint16_t read_u16_le_(uint8_t a_register);
int16_t read_s16_le_(uint8_t a_register); int16_t read_s16_le_(uint8_t a_register);
virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0;
BME280CalibrationData calibration_; BME280CalibrationData calibration_;
BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X}; BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X};
BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X}; BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X};
@ -106,5 +110,5 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
} error_code_{NONE}; } error_code_{NONE};
}; };
} // namespace bme280 } // namespace bme280_base
} // namespace esphome } // namespace esphome

View file

@ -0,0 +1,106 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HECTOPASCAL,
UNIT_PERCENT,
)
bme280_ns = cg.esphome_ns.namespace("bme280_base")
BME280Oversampling = bme280_ns.enum("BME280Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE,
"1X": BME280Oversampling.BME280_OVERSAMPLING_1X,
"2X": BME280Oversampling.BME280_OVERSAMPLING_2X,
"4X": BME280Oversampling.BME280_OVERSAMPLING_4X,
"8X": BME280Oversampling.BME280_OVERSAMPLING_8X,
"16X": BME280Oversampling.BME280_OVERSAMPLING_16X,
}
BME280IIRFilter = bme280_ns.enum("BME280IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF,
"2X": BME280IIRFilter.BME280_IIR_FILTER_2X,
"4X": BME280IIRFilter.BME280_IIR_FILTER_4X,
"8X": BME280IIRFilter.BME280_IIR_FILTER_8X,
"16X": BME280IIRFilter.BME280_IIR_FILTER_16X,
}
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
).extend(cv.polling_component_schema("60s"))
async def to_code(config, func=None):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if func is not None:
await func(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
if humidity_config := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity_config)
cg.add(var.set_humidity_sensor(sens))
cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))

View file

@ -0,0 +1,30 @@
#include <cstddef>
#include <cstdint>
#include "bme280_i2c.h"
#include "esphome/components/i2c/i2c.h"
#include "../bme280_base/bme280_base.h"
namespace esphome {
namespace bme280_i2c {
bool BME280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
return I2CDevice::read_byte(a_register, data);
};
bool BME280I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
return I2CDevice::write_byte(a_register, data);
};
bool BME280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
return I2CDevice::read_bytes(a_register, data, len);
};
bool BME280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
return I2CDevice::read_byte_16(a_register, data);
};
void BME280I2CComponent::dump_config() {
LOG_I2C_DEVICE(this);
BME280Component::dump_config();
}
} // namespace bme280_i2c
} // namespace esphome

View file

@ -0,0 +1,20 @@
#pragma once
#include "esphome/components/bme280_base/bme280_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bme280_i2c {
static const char *const TAG = "bme280_i2c.sensor";
class BME280I2CComponent : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
bool read_byte(uint8_t a_register, uint8_t *data) override;
bool write_byte(uint8_t a_register, uint8_t data) override;
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
void dump_config() override;
};
} // namespace bme280_i2c
} // namespace esphome

View file

@ -0,0 +1,19 @@
import esphome.codegen as cg
from esphome.components import i2c
from ..bme280_base.sensor import to_code as to_code_base, cv, CONFIG_SCHEMA_BASE
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["bme280_base"]
bme280_ns = cg.esphome_ns.namespace("bme280_i2c")
BME280I2CComponent = bme280_ns.class_(
"BME280I2CComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
i2c.i2c_device_schema(default_address=0x77)
).extend({cv.GenerateID(): cv.declare_id(BME280I2CComponent)})
async def to_code(config):
await to_code_base(config, func=i2c.register_i2c_device)

View file

@ -0,0 +1 @@
CODEOWNERS = ["@apbodrov"]

View file

@ -0,0 +1,66 @@
#include <cstdint>
#include <cstddef>
#include "bme280_spi.h"
#include <esphome/components/bme280_base/bme280_base.h>
int set_bit(uint8_t num, int position) {
int mask = 1 << position;
return num | mask;
}
int clear_bit(uint8_t num, int position) {
int mask = 1 << position;
return num & ~mask;
}
namespace esphome {
namespace bme280_spi {
void BME280SPIComponent::setup() {
this->spi_setup();
BME280Component::setup();
};
// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used
// and replaced by a read/write bit (RW = 0 for write and RW = 1 for read).
// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte
// 0x77 is transferred, for read access, the byte 0xF7 is transferred.
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf
bool BME280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
this->enable();
// cause: *data = this->delegate_->transfer(tmp) doesnt work
this->delegate_->transfer(set_bit(a_register, 7));
*data = this->delegate_->transfer(0);
this->disable();
return true;
}
bool BME280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
this->enable();
this->delegate_->transfer(clear_bit(a_register, 7));
this->delegate_->transfer(data);
this->disable();
return true;
}
bool BME280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
this->enable();
this->delegate_->transfer(set_bit(a_register, 7));
this->delegate_->read_array(data, len);
this->disable();
return true;
}
bool BME280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
this->enable();
this->delegate_->transfer(set_bit(a_register, 7));
((uint8_t *) data)[1] = this->delegate_->transfer(0);
((uint8_t *) data)[0] = this->delegate_->transfer(0);
this->disable();
return true;
}
} // namespace bme280_spi
} // namespace esphome

View file

@ -0,0 +1,20 @@
#pragma once
#include "esphome/components/bme280_base/bme280_base.h"
#include "esphome/components/spi/spi.h"
namespace esphome {
namespace bme280_spi {
class BME280SPIComponent : public esphome::bme280_base::BME280Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
void setup() override;
bool read_byte(uint8_t a_register, uint8_t *data) override;
bool write_byte(uint8_t a_register, uint8_t data) override;
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
};
} // namespace bme280_spi
} // namespace esphome

View file

@ -0,0 +1,24 @@
import esphome.codegen as cg
from esphome.components import spi
from esphome.components.bme280_base.sensor import (
to_code as to_code_base,
cv,
CONFIG_SCHEMA_BASE,
)
DEPENDENCIES = ["spi"]
AUTO_LOAD = ["bme280_base"]
bme280_spi_ns = cg.esphome_ns.namespace("bme280_spi")
BME280SPIComponent = bme280_spi_ns.class_(
"BME280SPIComponent", cg.PollingComponent, spi.SPIDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend(
{cv.GenerateID(): cv.declare_id(BME280SPIComponent)}
)
async def to_code(config):
await to_code_base(config, func=spi.register_spi_device)

View file

@ -200,8 +200,8 @@ float BMP280Component::read_temperature_(int32_t *t_fine) {
int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
*t_fine = var1 + var2; *t_fine = var1 + var2;
float temperature = (*t_fine * 5 + 128) >> 8; float temperature = (*t_fine * 5 + 128);
return temperature / 100.0f; return temperature / 25600.0f;
} }
float BMP280Component::read_pressure_(int32_t t_fine) { float BMP280Component::read_pressure_(int32_t t_fine) {

View file

@ -0,0 +1,262 @@
#include "combination.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cmath>
#include <functional>
#include <vector>
namespace esphome {
namespace combination {
static const char *const TAG = "combination";
void CombinationComponent::log_config_(const LogString *combo_type) {
LOG_SENSOR("", "Combination Sensor:", this);
ESP_LOGCONFIG(TAG, " Combination Type: %s", LOG_STR_ARG(combo_type));
this->log_source_sensors();
}
void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); }
void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function<float(float)> const &stddev) {
this->sensor_pairs_.emplace_back(sensor, stddev);
}
void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) {
this->add_source(sensor, std::function<float(float)>{[stddev](float x) -> float { return stddev; }});
}
void CombinationNoParameterComponent::log_source_sensors() {
ESP_LOGCONFIG(TAG, " Source Sensors:");
for (const auto &sensor : this->sensors_) {
ESP_LOGCONFIG(TAG, " - %s", sensor->get_name().c_str());
}
}
void CombinationOneParameterComponent::log_source_sensors() {
ESP_LOGCONFIG(TAG, " Source Sensors:");
for (const auto &sensor : this->sensor_pairs_) {
auto &entity = *sensor.first;
ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str());
}
}
void CombinationNoParameterComponent::setup() {
for (const auto &sensor : this->sensors_) {
// All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
// repeatedly in the same loop if multiple source senors update.
sensor->add_on_state_callback(
[this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); });
}
}
void KalmanCombinationComponent::dump_config() {
this->log_config_(LOG_STR("kalman"));
ESP_LOGCONFIG(TAG, " Update variance: %f per ms", this->update_variance_value_);
if (this->std_dev_sensor_ != nullptr) {
LOG_SENSOR(" ", "Standard Deviation Sensor:", this->std_dev_sensor_);
}
}
void KalmanCombinationComponent::setup() {
for (const auto &sensor : this->sensor_pairs_) {
const auto stddev = sensor.second;
sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); });
}
}
void KalmanCombinationComponent::update_variance_() {
uint32_t now = millis();
// Variance increases by update_variance_ each millisecond
auto dt = now - this->last_update_;
auto dv = this->update_variance_value_ * dt;
this->variance_ += dv;
this->last_update_ = now;
}
void KalmanCombinationComponent::correct_(float value, float stddev) {
if (std::isnan(value) || std::isinf(stddev)) {
return;
}
if (std::isnan(this->state_) || std::isinf(this->variance_)) {
this->state_ = value;
this->variance_ = stddev * stddev;
if (this->std_dev_sensor_ != nullptr) {
this->std_dev_sensor_->publish_state(stddev);
}
return;
}
this->update_variance_();
// Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu
// Use the value with the smaller variance as mu1 to prevent precision errors
const bool this_first = this->variance_ < (stddev * stddev);
const float mu1 = this_first ? this->state_ : value;
const float mu2 = this_first ? value : this->state_;
const float var1 = this_first ? this->variance_ : stddev * stddev;
const float var2 = this_first ? stddev * stddev : this->variance_;
const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2);
const float var = var1 - (var1 * var1) / (var1 + var2);
// Update and publish state
this->state_ = mu;
this->variance_ = var;
this->publish_state(mu);
if (this->std_dev_sensor_ != nullptr) {
this->std_dev_sensor_->publish_state(std::sqrt(var));
}
}
void LinearCombinationComponent::setup() {
for (const auto &sensor : this->sensor_pairs_) {
// All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
// repeatedly in the same loop if multiple source senors update.
sensor.first->add_on_state_callback(
[this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); });
}
}
void LinearCombinationComponent::handle_new_value(float value) {
// Multiplies each sensor state by a configured coeffecient and then sums
if (!std::isfinite(value))
return;
float sum = 0.0;
for (const auto &sensor : this->sensor_pairs_) {
const float sensor_state = sensor.first->state;
if (std::isfinite(sensor_state)) {
sum += sensor_state * sensor.second(sensor_state);
}
}
this->publish_state(sum);
};
void MaximumCombinationComponent::handle_new_value(float value) {
if (!std::isfinite(value))
return;
float max_value = (-1) * std::numeric_limits<float>::infinity(); // note x = max(x, -infinity)
for (const auto &sensor : this->sensors_) {
if (std::isfinite(sensor->state)) {
max_value = std::max(max_value, sensor->state);
}
}
this->publish_state(max_value);
}
void MeanCombinationComponent::handle_new_value(float value) {
if (!std::isfinite(value))
return;
float sum = 0.0;
size_t count = 0.0;
for (const auto &sensor : this->sensors_) {
if (std::isfinite(sensor->state)) {
++count;
sum += sensor->state;
}
}
float mean = sum / count;
this->publish_state(mean);
}
void MedianCombinationComponent::handle_new_value(float value) {
// Sorts sensor states in ascending order and determines the middle value
if (!std::isfinite(value))
return;
std::vector<float> sensor_states;
for (const auto &sensor : this->sensors_) {
if (std::isfinite(sensor->state)) {
sensor_states.push_back(sensor->state);
}
}
sort(sensor_states.begin(), sensor_states.end());
size_t sensor_states_size = sensor_states.size();
float median = NAN;
if (sensor_states_size) {
if (sensor_states_size % 2) {
// Odd number of measurements, use middle measurement
median = sensor_states[sensor_states_size / 2];
} else {
// Even number of measurements, use the average of the two middle measurements
median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0;
}
}
this->publish_state(median);
}
void MinimumCombinationComponent::handle_new_value(float value) {
if (!std::isfinite(value))
return;
float min_value = std::numeric_limits<float>::infinity(); // note x = min(x, infinity)
for (const auto &sensor : this->sensors_) {
if (std::isfinite(sensor->state)) {
min_value = std::min(min_value, sensor->state);
}
}
this->publish_state(min_value);
}
void MostRecentCombinationComponent::handle_new_value(float value) { this->publish_state(value); }
void RangeCombinationComponent::handle_new_value(float value) {
// Sorts sensor states then takes difference between largest and smallest states
if (!std::isfinite(value))
return;
std::vector<float> sensor_states;
for (const auto &sensor : this->sensors_) {
if (std::isfinite(sensor->state)) {
sensor_states.push_back(sensor->state);
}
}
sort(sensor_states.begin(), sensor_states.end());
float range = sensor_states.back() - sensor_states.front();
this->publish_state(range);
}
void SumCombinationComponent::handle_new_value(float value) {
if (!std::isfinite(value))
return;
float sum = 0.0;
for (const auto &sensor : this->sensors_) {
if (std::isfinite(sensor->state)) {
sum += sensor->state;
}
}
this->publish_state(sum);
}
} // namespace combination
} // namespace esphome

View file

@ -0,0 +1,141 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include <vector>
namespace esphome {
namespace combination {
class CombinationComponent : public Component, public sensor::Sensor {
public:
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
/// @brief Logs all source sensor's names
virtual void log_source_sensors() = 0;
protected:
/// @brief Logs the sensor for use in dump_config
/// @param combo_type Name of the combination operation
void log_config_(const LogString *combo_type);
};
/// @brief Base class for operations that do not require an extra parameter to compute the combination
class CombinationNoParameterComponent : public CombinationComponent {
public:
/// @brief Adds a callback to each source sensor
void setup() override;
void add_source(Sensor *sensor);
/// @brief Computes the combination
/// @param value Newest sensor measurement
virtual void handle_new_value(float value) = 0;
/// @brief Logs all source sensor's names in sensors_
void log_source_sensors() override;
protected:
std::vector<Sensor *> sensors_;
};
// Base class for opertions that require one parameter to compute the combination
class CombinationOneParameterComponent : public CombinationComponent {
public:
void add_source(Sensor *sensor, std::function<float(float)> const &stddev);
void add_source(Sensor *sensor, float stddev);
/// @brief Logs all source sensor's names in sensor_pairs_
void log_source_sensors() override;
protected:
std::vector<std::pair<Sensor *, std::function<float(float)>>> sensor_pairs_;
};
class KalmanCombinationComponent : public CombinationOneParameterComponent {
public:
void dump_config() override;
void setup() override;
void set_process_std_dev(float process_std_dev) {
this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f;
}
void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; }
protected:
void update_variance_();
void correct_(float value, float stddev);
// Optional sensor for publishing the current error
sensor::Sensor *std_dev_sensor_{nullptr};
// Tick of the last update
uint32_t last_update_{0};
// Change of the variance, per ms
float update_variance_value_{0.f};
// Best guess for the state and its variance
float state_{NAN};
float variance_{INFINITY};
};
class LinearCombinationComponent : public CombinationOneParameterComponent {
public:
void dump_config() override { this->log_config_(LOG_STR("linear")); }
void setup() override;
void handle_new_value(float value);
};
class MaximumCombinationComponent : public CombinationNoParameterComponent {
public:
void dump_config() override { this->log_config_(LOG_STR("max")); }
void handle_new_value(float value) override;
};
class MeanCombinationComponent : public CombinationNoParameterComponent {
public:
void dump_config() override { this->log_config_(LOG_STR("mean")); }
void handle_new_value(float value) override;
};
class MedianCombinationComponent : public CombinationNoParameterComponent {
public:
void dump_config() override { this->log_config_(LOG_STR("median")); }
void handle_new_value(float value) override;
};
class MinimumCombinationComponent : public CombinationNoParameterComponent {
public:
void dump_config() override { this->log_config_(LOG_STR("min")); }
void handle_new_value(float value) override;
};
class MostRecentCombinationComponent : public CombinationNoParameterComponent {
public:
void dump_config() override { this->log_config_(LOG_STR("most_recently_updated")); }
void handle_new_value(float value) override;
};
class RangeCombinationComponent : public CombinationNoParameterComponent {
public:
void dump_config() override { this->log_config_(LOG_STR("range")); }
void handle_new_value(float value) override;
};
class SumCombinationComponent : public CombinationNoParameterComponent {
public:
void dump_config() override { this->log_config_(LOG_STR("sum")); }
void handle_new_value(float value) override;
};
} // namespace combination
} // namespace esphome

View file

@ -0,0 +1,176 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID,
CONF_RANGE,
CONF_SOURCE,
CONF_SUM,
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
)
from esphome.core.entity_helpers import inherit_property_from
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
combination_ns = cg.esphome_ns.namespace("combination")
KalmanCombinationComponent = combination_ns.class_(
"KalmanCombinationComponent", cg.Component, sensor.Sensor
)
LinearCombinationComponent = combination_ns.class_(
"LinearCombinationComponent", cg.Component, sensor.Sensor
)
MaximumCombinationComponent = combination_ns.class_(
"MaximumCombinationComponent", cg.Component, sensor.Sensor
)
MeanCombinationComponent = combination_ns.class_(
"MeanCombinationComponent", cg.Component, sensor.Sensor
)
MedianCombinationComponent = combination_ns.class_(
"MedianCombinationComponent", cg.Component, sensor.Sensor
)
MinimumCombinationComponent = combination_ns.class_(
"MinimumCombinationComponent", cg.Component, sensor.Sensor
)
MostRecentCombinationComponent = combination_ns.class_(
"MostRecentCombinationComponent", cg.Component, sensor.Sensor
)
RangeCombinationComponent = combination_ns.class_(
"RangeCombinationComponent", cg.Component, sensor.Sensor
)
SumCombinationComponent = combination_ns.class_(
"SumCombinationComponent", cg.Component, sensor.Sensor
)
CONF_COEFFECIENT = "coeffecient"
CONF_ERROR = "error"
CONF_KALMAN = "kalman"
CONF_LINEAR = "linear"
CONF_MAX = "max"
CONF_MEAN = "mean"
CONF_MEDIAN = "median"
CONF_MIN = "min"
CONF_MOST_RECENTLY_UPDATED = "most_recently_updated"
CONF_PROCESS_STD_DEV = "process_std_dev"
CONF_SOURCES = "sources"
CONF_STD_DEV = "std_dev"
KALMAN_SOURCE_SCHEMA = cv.Schema(
{
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_ERROR): cv.templatable(cv.positive_float),
}
)
LINEAR_SOURCE_SCHEMA = cv.Schema(
{
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_),
}
)
SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema(
{
cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
}
)
CONFIG_SCHEMA = cv.typed_schema(
{
CONF_KALMAN: sensor.sensor_schema(KalmanCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{
cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float,
cv.Required(CONF_SOURCES): cv.ensure_list(KALMAN_SOURCE_SCHEMA),
cv.Optional(CONF_STD_DEV): sensor.sensor_schema(),
}
),
CONF_LINEAR: sensor.sensor_schema(LinearCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(LINEAR_SOURCE_SCHEMA)}),
CONF_MAX: sensor.sensor_schema(MaximumCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
CONF_MEAN: sensor.sensor_schema(MeanCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
CONF_MEDIAN: sensor.sensor_schema(MedianCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
CONF_MIN: sensor.sensor_schema(MinimumCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
CONF_MOST_RECENTLY_UPDATED: sensor.sensor_schema(MostRecentCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
CONF_RANGE: sensor.sensor_schema(RangeCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
CONF_SUM: sensor.sensor_schema(SumCombinationComponent)
.extend(cv.COMPONENT_SCHEMA)
.extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
}
)
# Inherit some sensor values from the first source, for both the state and the error value
# CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing"
properties_to_inherit = [
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_UNIT_OF_MEASUREMENT,
]
inherit_schema_for_state = [
inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE])
for property in properties_to_inherit
]
inherit_schema_for_std_dev = [
inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE])
for property in properties_to_inherit
]
FINAL_VALIDATE_SCHEMA = cv.All(
*inherit_schema_for_state,
*inherit_schema_for_std_dev,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
if proces_std_dev := config.get(CONF_PROCESS_STD_DEV):
cg.add(var.set_process_std_dev(proces_std_dev))
for source_conf in config[CONF_SOURCES]:
source = await cg.get_variable(source_conf[CONF_SOURCE])
if config[CONF_TYPE] == CONF_KALMAN:
error = await cg.templatable(
source_conf[CONF_ERROR],
[(float, "x")],
cg.float_,
)
cg.add(var.add_source(source, error))
elif config[CONF_TYPE] == CONF_LINEAR:
coeffecient = await cg.templatable(
source_conf[CONF_COEFFECIENT],
[(float, "x")],
cg.float_,
)
cg.add(var.add_source(source, coeffecient))
else:
cg.add(var.add_source(source))
if CONF_STD_DEV in config:
sens = await sensor.new_sensor(config[CONF_STD_DEV])
cg.add(var.set_std_dev_sensor(sens))

View file

@ -1,6 +1,8 @@
#include "cse7766.h" #include "cse7766.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes> #include <cinttypes>
#include <iomanip>
#include <sstream>
namespace esphome { namespace esphome {
namespace cse7766 { namespace cse7766 {
@ -68,20 +70,26 @@ bool CSE7766Component::check_byte_() {
return true; return true;
} }
void CSE7766Component::parse_data_() { void CSE7766Component::parse_data_() {
ESP_LOGVV(TAG, "CSE7766 Data: "); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::stringstream ss;
ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0');
for (uint8_t i = 0; i < 23; i++) { for (uint8_t i = 0; i < 23; i++) {
ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]), ss << ' ' << std::setw(2) << static_cast<unsigned>(this->raw_data_[i]);
this->raw_data_[i]);
} }
ESP_LOGVV(TAG, "%s", ss.str().c_str());
}
#endif
// Parse header
uint8_t header1 = this->raw_data_[0]; uint8_t header1 = this->raw_data_[0];
if (header1 == 0xAA) { if (header1 == 0xAA) {
ESP_LOGE(TAG, "CSE7766 not calibrated!"); ESP_LOGE(TAG, "CSE7766 not calibrated!");
return; return;
} }
bool power_cycle_exceeds_range = false; bool power_cycle_exceeds_range = false;
if ((header1 & 0xF0) == 0xF0) { if ((header1 & 0xF0) == 0xF0) {
if (header1 & 0xD) { if (header1 & 0xD) {
ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1); ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1);
@ -94,99 +102,106 @@ void CSE7766Component::parse_data_() {
if (header1 & (1 << 0)) { if (header1 & (1 << 0)) {
ESP_LOGE(TAG, " Coefficient storage area is abnormal."); ESP_LOGE(TAG, " Coefficient storage area is abnormal.");
} }
// Datasheet: voltage or current cycle exceeding range means invalid values
return; return;
} }
power_cycle_exceeds_range = header1 & (1 << 1); power_cycle_exceeds_range = header1 & (1 << 1);
} }
uint32_t voltage_calib = this->get_24_bit_uint_(2); // Parse data frame
uint32_t voltage_coeff = this->get_24_bit_uint_(2);
uint32_t voltage_cycle = this->get_24_bit_uint_(5); uint32_t voltage_cycle = this->get_24_bit_uint_(5);
uint32_t current_calib = this->get_24_bit_uint_(8); uint32_t current_coeff = this->get_24_bit_uint_(8);
uint32_t current_cycle = this->get_24_bit_uint_(11); uint32_t current_cycle = this->get_24_bit_uint_(11);
uint32_t power_calib = this->get_24_bit_uint_(14); uint32_t power_coeff = this->get_24_bit_uint_(14);
uint32_t power_cycle = this->get_24_bit_uint_(17); uint32_t power_cycle = this->get_24_bit_uint_(17);
uint8_t adj = this->raw_data_[20]; uint8_t adj = this->raw_data_[20];
uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
bool have_voltage = adj & 0x40;
if (have_voltage) {
// voltage cycle of serial port outputted is a complete cycle;
this->voltage_acc_ += voltage_calib / float(voltage_cycle);
this->voltage_counts_ += 1;
}
bool have_power = adj & 0x10; bool have_power = adj & 0x10;
float power = 0.0f; bool have_current = adj & 0x20;
bool have_voltage = adj & 0x40;
if (have_power) { float voltage = 0.0f;
// power cycle of serial port outputted is a complete cycle; if (have_voltage) {
// According to the user manual, power cycle exceeding range means the measured power is 0 voltage = voltage_coeff / float(voltage_cycle);
if (!power_cycle_exceeds_range) { if (this->voltage_sensor_ != nullptr) {
power = power_calib / float(power_cycle); this->voltage_sensor_->publish_state(voltage);
}
} }
this->power_acc_ += power;
this->power_counts_ += 1;
uint32_t difference; float power = 0.0f;
float energy = 0.0f;
if (power_cycle_exceeds_range) {
// Datasheet: power cycle exceeding range means active power is 0
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
} else if (have_power) {
power = power_coeff / float(power_cycle);
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
// Add CF pulses to the total energy only if we have Power coefficient to multiply by
if (this->cf_pulses_last_ == 0) { if (this->cf_pulses_last_ == 0) {
this->cf_pulses_last_ = cf_pulses; this->cf_pulses_last_ = cf_pulses;
} }
uint32_t cf_diff;
if (cf_pulses < this->cf_pulses_last_) { if (cf_pulses < this->cf_pulses_last_) {
difference = cf_pulses + (0x10000 - this->cf_pulses_last_); cf_diff = cf_pulses + (0x10000 - this->cf_pulses_last_);
} else { } else {
difference = cf_pulses - this->cf_pulses_last_; cf_diff = cf_pulses - this->cf_pulses_last_;
} }
this->cf_pulses_last_ = cf_pulses; this->cf_pulses_last_ = cf_pulses;
this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f;
this->energy_total_counts_ += 1;
}
if (adj & 0x20) { energy = cf_diff * float(power_coeff) / 1000000.0f / 3600.0f;
// indicates current cycle of serial port outputted is a complete cycle; this->energy_total_ += energy;
float current = 0.0f; if (this->energy_sensor_ != nullptr)
if (have_voltage && !have_power) {
// Testing has shown that when we have voltage and current but not power, that means the power is 0.
// We report a power of 0, which in turn means we should report a current of 0.
this->power_counts_ += 1;
} else if (power != 0.0f) {
current = current_calib / float(current_cycle);
}
this->current_acc_ += current;
this->current_counts_ += 1;
}
}
void CSE7766Component::update() {
const auto publish_state = [](const char *name, sensor::Sensor *sensor, float &acc, uint32_t &counts) {
if (counts != 0) {
const auto avg = acc / counts;
ESP_LOGV(TAG, "Got %s_acc=%.2f %s_counts=%" PRIu32 " %s=%.1f", name, acc, name, counts, name, avg);
if (sensor != nullptr) {
sensor->publish_state(avg);
}
acc = 0.0f;
counts = 0;
}
};
publish_state("voltage", this->voltage_sensor_, this->voltage_acc_, this->voltage_counts_);
publish_state("current", this->current_sensor_, this->current_acc_, this->current_counts_);
publish_state("power", this->power_sensor_, this->power_acc_, this->power_counts_);
if (this->energy_total_counts_ != 0) {
ESP_LOGV(TAG, "Got energy_total=%.2f energy_total_counts=%" PRIu32, this->energy_total_,
this->energy_total_counts_);
if (this->energy_sensor_ != nullptr) {
this->energy_sensor_->publish_state(this->energy_total_); this->energy_sensor_->publish_state(this->energy_total_);
} else if ((this->energy_sensor_ != nullptr) && !this->energy_sensor_->has_state()) {
this->energy_sensor_->publish_state(0);
} }
this->energy_total_counts_ = 0;
float current = 0.0f;
float calculated_current = 0.0f;
if (have_current) {
// Assumption: if we don't have power measurement, then current is likely below 50mA
if (have_power && voltage > 1.0f) {
calculated_current = power / voltage;
} }
// Datasheet: minimum measured current is 50mA
if (calculated_current > 0.05f) {
current = current_coeff / float(current_cycle);
}
if (this->current_sensor_ != nullptr) {
this->current_sensor_->publish_state(current);
}
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::stringstream ss;
ss << "Parsed:";
if (have_voltage) {
ss << " V=" << voltage << "V";
}
if (have_current) {
ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)";
}
if (have_power) {
ss << " P=" << power << "W";
}
if (energy != 0.0f) {
ss << " E=" << energy << "kWh (" << cf_pulses << ")";
}
ESP_LOGVV(TAG, "%s", ss.str().c_str());
}
#endif
} }
uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) { uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) {
@ -196,7 +211,6 @@ uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) {
void CSE7766Component::dump_config() { void CSE7766Component::dump_config() {
ESP_LOGCONFIG(TAG, "CSE7766:"); ESP_LOGCONFIG(TAG, "CSE7766:");
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_);
LOG_SENSOR(" ", "Power", this->power_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_);

View file

@ -7,7 +7,7 @@
namespace esphome { namespace esphome {
namespace cse7766 { namespace cse7766 {
class CSE7766Component : public PollingComponent, public uart::UARTDevice { class CSE7766Component : public Component, public uart::UARTDevice {
public: public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
@ -16,7 +16,6 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice {
void loop() override; void loop() override;
float get_setup_priority() const override; float get_setup_priority() const override;
void update() override;
void dump_config() override; void dump_config() override;
protected: protected:
@ -31,16 +30,8 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice {
sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr};
sensor::Sensor *energy_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr};
float voltage_acc_{0.0f};
float current_acc_{0.0f};
float power_acc_{0.0f};
float energy_total_{0.0f}; float energy_total_{0.0f};
uint32_t cf_pulses_last_{0}; uint32_t cf_pulses_last_{0};
uint32_t voltage_counts_{0};
uint32_t current_counts_{0};
uint32_t power_counts_{0};
// Setting this to 1 means it will always publish 0 once at startup
uint32_t energy_total_counts_{1};
}; };
} // namespace cse7766 } // namespace cse7766

View file

@ -22,12 +22,9 @@ from esphome.const import (
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
cse7766_ns = cg.esphome_ns.namespace("cse7766") cse7766_ns = cg.esphome_ns.namespace("cse7766")
CSE7766Component = cse7766_ns.class_( CSE7766Component = cse7766_ns.class_("CSE7766Component", cg.Component, uart.UARTDevice)
"CSE7766Component", cg.PollingComponent, uart.UARTDevice
)
CONFIG_SCHEMA = ( CONFIG_SCHEMA = cv.Schema(
cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(CSE7766Component), cv.GenerateID(): cv.declare_id(CSE7766Component),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
@ -55,10 +52,7 @@ CONFIG_SCHEMA = (
state_class=STATE_CLASS_TOTAL_INCREASING, state_class=STATE_CLASS_TOTAL_INCREASING,
), ),
} }
) ).extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7766", baud_rate=4800, require_rx=True "cse7766", baud_rate=4800, require_rx=True
) )

View file

@ -1,7 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome import core
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.components import uart from esphome.components import uart
@ -101,7 +100,7 @@ def range_segment_list(input):
largest_distance = -1 largest_distance = -1
for distance in input: for distance in input:
if isinstance(distance, core.Lambda): if isinstance(distance, cv.Lambda):
continue continue
m = cv.distance(distance) m = cv.distance(distance)
if m > 9: if m > 9:
@ -128,14 +127,14 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema(
cv.Optional(CONF_OUTPUT_LATENCY): { cv.Optional(CONF_OUTPUT_LATENCY): {
cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable( cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable(
cv.All( cv.All(
cv.positive_time_period, cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(seconds=1638.375)), cv.Range(max=cv.TimePeriod(seconds=1638.375)),
) )
), ),
cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable( cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable(
cv.All( cv.All(
cv.positive_time_period, cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(seconds=1638.375)), cv.Range(max=cv.TimePeriod(seconds=1638.375)),
) )
), ),
}, },

View file

@ -50,7 +50,7 @@ class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<Dfrob
float detect = this->delay_after_detect_.value(x...); float detect = this->delay_after_detect_.value(x...);
float disappear = this->delay_after_disappear_.value(x...); float disappear = this->delay_after_disappear_.value(x...);
if (detect >= 0 && disappear >= 0) { if (detect >= 0 && disappear >= 0) {
this->parent_->enqueue(make_unique<OutputLatencyCommand>(detect, disappear)); this->parent_->enqueue(make_unique<SetLatencyCommand>(detect, disappear));
} }
} }
if (this->start_after_power_on_.has_value()) { if (this->start_after_power_on_.has_value()) {

View file

@ -1,5 +1,7 @@
#include "commands.h" #include "commands.h"
#include <cmath>
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "dfrobot_sen0395.h" #include "dfrobot_sen0395.h"
@ -194,32 +196,22 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) {
return 0; // Command not done yet. return 0; // Command not done yet.
} }
OutputLatencyCommand::OutputLatencyCommand(float delay_after_detection, float delay_after_disappear) { SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) {
delay_after_detection = round(delay_after_detection / 0.025) * 0.025; delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f;
delay_after_disappear = round(delay_after_disappear / 0.025) * 0.025; delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
if (delay_after_detection < 0) this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
delay_after_detection = 0; this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
if (delay_after_detection > 1638.375) this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
delay_after_detection = 1638.375;
if (delay_after_disappear < 0)
delay_after_disappear = 0;
if (delay_after_disappear > 1638.375)
delay_after_disappear = 1638.375;
this->delay_after_detection_ = delay_after_detection;
this->delay_after_disappear_ = delay_after_disappear;
this->cmd_ = str_sprintf("outputLatency -1 %.0f %.0f", delay_after_detection / 0.025, delay_after_disappear / 0.025);
}; };
uint8_t OutputLatencyCommand::on_message(std::string &message) { uint8_t SetLatencyCommand::on_message(std::string &message) {
if (message == "sensor is not stopped") { if (message == "sensor is not stopped") {
ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!"); ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
return 1; // Command done return 1; // Command done
} else if (message == "Done") { } else if (message == "Done") {
ESP_LOGI(TAG, "Updated output latency config:"); ESP_LOGI(TAG, "Updated output latency config:");
ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.02fs.", this->delay_after_detection_); ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.03f s.", this->delay_after_detection_);
ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.02fs.", this->delay_after_disappear_); ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_disappear_);
ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str()); ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
return 1; // Command done return 1; // Command done
} }

View file

@ -62,9 +62,9 @@ class DetRangeCfgCommand : public Command {
// TODO: Set min max values in component, so they can be published as sensor. // TODO: Set min max values in component, so they can be published as sensor.
}; };
class OutputLatencyCommand : public Command { class SetLatencyCommand : public Command {
public: public:
OutputLatencyCommand(float delay_after_detection, float delay_after_disappear); SetLatencyCommand(float delay_after_detection, float delay_after_disappear);
uint8_t on_message(std::string &message) override; uint8_t on_message(std::string &message) override;
protected: protected:

View file

@ -91,7 +91,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
delayMicroseconds(40); delayMicroseconds(40);
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000); delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2302) { } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
delayMicroseconds(1000); delayMicroseconds(1000);
} else { } else {
delayMicroseconds(800); delayMicroseconds(800);
@ -217,8 +217,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF); uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF);
if (this->model_ != DHT_MODEL_DHT22_TYPE2 && (raw_temperature & 0x8000) != 0) if (raw_temperature & 0x8000) {
if (!(raw_temperature & 0x4000))
raw_temperature = ~(raw_temperature & 0x7FFF); raw_temperature = ~(raw_temperature & 0x7FFF);
} else if (raw_temperature & 0x800) {
raw_temperature |= 0xf000;
}
if (raw_temperature == 1 && raw_humidity == 10) { if (raw_temperature == 1 && raw_humidity == 10) {
if (report_errors) { if (report_errors) {

View file

@ -11,6 +11,7 @@ enum DHTModel {
DHT_MODEL_AUTO_DETECT = 0, DHT_MODEL_AUTO_DETECT = 0,
DHT_MODEL_DHT11, DHT_MODEL_DHT11,
DHT_MODEL_DHT22, DHT_MODEL_DHT22,
DHT_MODEL_AM2120,
DHT_MODEL_AM2302, DHT_MODEL_AM2302,
DHT_MODEL_RHT03, DHT_MODEL_RHT03,
DHT_MODEL_SI7021, DHT_MODEL_SI7021,
@ -27,6 +28,7 @@ class DHT : public PollingComponent {
* - DHT_MODEL_AUTO_DETECT (default) * - DHT_MODEL_AUTO_DETECT (default)
* - DHT_MODEL_DHT11 * - DHT_MODEL_DHT11
* - DHT_MODEL_DHT22 * - DHT_MODEL_DHT22
* - DHT_MODEL_AM2120
* - DHT_MODEL_AM2302 * - DHT_MODEL_AM2302
* - DHT_MODEL_RHT03 * - DHT_MODEL_RHT03
* - DHT_MODEL_SI7021 * - DHT_MODEL_SI7021

View file

@ -23,6 +23,7 @@ DHT_MODELS = {
"AUTO_DETECT": DHTModel.DHT_MODEL_AUTO_DETECT, "AUTO_DETECT": DHTModel.DHT_MODEL_AUTO_DETECT,
"DHT11": DHTModel.DHT_MODEL_DHT11, "DHT11": DHTModel.DHT_MODEL_DHT11,
"DHT22": DHTModel.DHT_MODEL_DHT22, "DHT22": DHTModel.DHT_MODEL_DHT22,
"AM2120": DHTModel.DHT_MODEL_AM2120,
"AM2302": DHTModel.DHT_MODEL_AM2302, "AM2302": DHTModel.DHT_MODEL_AM2302,
"RHT03": DHTModel.DHT_MODEL_RHT03, "RHT03": DHTModel.DHT_MODEL_RHT03,
"SI7021": DHTModel.DHT_MODEL_SI7021, "SI7021": DHTModel.DHT_MODEL_SI7021,

View file

@ -18,8 +18,8 @@ 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") Display = display_ns.class_("Display", cg.PollingComponent)
DisplayBuffer = display_ns.class_("DisplayBuffer") DisplayBuffer = display_ns.class_("DisplayBuffer", Display)
DisplayPage = display_ns.class_("DisplayPage") DisplayPage = display_ns.class_("DisplayPage")
DisplayPagePtr = DisplayPage.operator("ptr") DisplayPagePtr = DisplayPage.operator("ptr")
DisplayRef = Display.operator("ref") DisplayRef = Display.operator("ref")
@ -145,7 +145,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args):
DisplayPageShowNextAction, DisplayPageShowNextAction,
maybe_simple_id( maybe_simple_id(
{ {
cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)),
} }
), ),
) )
@ -159,7 +159,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args):
DisplayPageShowPrevAction, DisplayPageShowPrevAction,
maybe_simple_id( maybe_simple_id(
{ {
cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)), cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)),
} }
), ),
) )
@ -173,7 +173,7 @@ async def display_page_show_previous_to_code(config, action_id, template_arg, ar
DisplayIsDisplayingPageCondition, DisplayIsDisplayingPageCondition,
cv.maybe_simple_value( cv.maybe_simple_value(
{ {
cv.GenerateID(CONF_ID): cv.use_id(DisplayBuffer), cv.GenerateID(CONF_ID): cv.use_id(Display),
cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage), cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage),
}, },
key=CONF_PAGE_ID, key=CONF_PAGE_ID,

View file

@ -35,6 +35,41 @@ void HOT Display::line(int x1, int y1, int x2, int y2, Color color) {
} }
} }
} }
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
uint32_t color_value;
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
size_t source_idx_mod;
for (int x = 0; x != w; x++, source_idx++) {
switch (bitness) {
default:
color_value = ptr[source_idx];
break;
case COLOR_BITNESS_565:
source_idx_mod = source_idx * 2;
if (big_endian) {
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
} else {
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
}
break;
case COLOR_BITNESS_888:
source_idx_mod = source_idx * 3;
if (big_endian) {
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
} else {
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
}
break;
}
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
}
}
}
void HOT Display::horizontal_line(int x, int y, int width, Color color) { 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. // Future: Could be made more efficient by manipulating buffer directly in certain rotations.
for (int i = x; i < x + width; i++) for (int i = x; i < x + width; i++)
@ -106,6 +141,122 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color)
} }
} while (dx <= 0); } while (dx <= 0);
} }
void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
this->line(x1, y1, x2, y2, color);
this->line(x1, y1, x3, y3, color);
this->line(x2, y2, x3, y3, color);
}
void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) {
if (*y1 > *y2) {
int x_temp = *x1, y_temp = *y1;
*x1 = *x2, *y1 = *y2;
*x2 = x_temp, *y2 = y_temp;
}
if (*y1 > *y3) {
int x_temp = *x1, y_temp = *y1;
*x1 = *x3, *y1 = *y3;
*x3 = x_temp, *y3 = y_temp;
}
if (*y2 > *y3) {
int x_temp = *x2, y_temp = *y2;
*x2 = *x3, *y2 = *y3;
*x3 = x_temp, *y3 = y_temp;
}
}
void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
// y2 must be equal to y3 (same horizontal line)
// Initialize Bresenham's algorithm for side 1
int s1_current_x = x1;
int s1_current_y = y1;
bool s1_axis_swap = false;
int s1_dx = abs(x2 - x1);
int s1_dy = abs(y2 - y1);
int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1;
int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1;
if (s1_dy > s1_dx) { // swap values
int tmp = s1_dx;
s1_dx = s1_dy;
s1_dy = tmp;
s1_axis_swap = true;
}
int s1_error = 2 * s1_dy - s1_dx;
// Initialize Bresenham's algorithm for side 2
int s2_current_x = x1;
int s2_current_y = y1;
bool s2_axis_swap = false;
int s2_dx = abs(x3 - x1);
int s2_dy = abs(y3 - y1);
int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1;
int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1;
if (s2_dy > s2_dx) { // swap values
int tmp = s2_dx;
s2_dx = s2_dy;
s2_dy = tmp;
s2_axis_swap = true;
}
int s2_error = 2 * s2_dy - s2_dx;
// Iterate on side 1 and allow side 2 to be processed to match the advance of the y-axis.
for (int i = 0; i <= s1_dx; i++) {
if (s1_current_x <= s2_current_x) {
this->horizontal_line(s1_current_x, s1_current_y, s2_current_x - s1_current_x + 1, color);
} else {
this->horizontal_line(s2_current_x, s2_current_y, s1_current_x - s2_current_x + 1, color);
}
// Bresenham's #1
// Side 1 s1_current_x and s1_current_y calculation
while (s1_error >= 0) {
if (s1_axis_swap) {
s1_current_x += s1_sign_x;
} else {
s1_current_y += s1_sign_y;
}
s1_error = s1_error - 2 * s1_dx;
}
if (s1_axis_swap) {
s1_current_y += s1_sign_y;
} else {
s1_current_x += s1_sign_x;
}
s1_error = s1_error + 2 * s1_dy;
// Bresenham's #2
// Side 2 s2_current_x and s2_current_y calculation
while (s2_current_y != s1_current_y) {
while (s2_error >= 0) {
if (s2_axis_swap) {
s2_current_x += s2_sign_x;
} else {
s2_current_y += s2_sign_y;
}
s2_error = s2_error - 2 * s2_dx;
}
if (s2_axis_swap) {
s2_current_y += s2_sign_y;
} else {
s2_current_x += s2_sign_x;
}
s2_error = s2_error + 2 * s2_dy;
}
}
}
void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
// Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point
this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3);
if (y2 == y3) { // Check for special case of a bottom-flat triangle
this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color);
} else if (y1 == y2) { // Check for special case of a top-flat triangle
this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color);
} else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle
int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2;
this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color);
this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color);
}
}
void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) { void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
int x_start, y_start; int x_start, y_start;

View file

@ -8,6 +8,7 @@
#include "esphome/core/color.h" #include "esphome/core/color.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/time.h" #include "esphome/core/time.h"
#include "display_color_utils.h"
#ifdef USE_GRAPH #ifdef USE_GRAPH
#include "esphome/components/graph/graph.h" #include "esphome/components/graph/graph.h"
@ -185,6 +186,34 @@ class Display : public PollingComponent {
/// Set a single pixel at the specified coordinates to the given color. /// Set a single pixel at the specified coordinates to the given color.
virtual void draw_pixel_at(int x, int y, Color color) = 0; virtual void draw_pixel_at(int x, int y, Color color) = 0;
/** Given an array of pixels encoded in the nominated format, draw these into the display's buffer.
* The naive implementation here will work in all cases, but can be overridden by sub-classes
* in order to optimise the procedure.
* The parameters describe a rectangular block of pixels, potentially within a larger buffer.
*
* \param x_start The starting destination x position
* \param y_start The starting destination y position
* \param w the width of the pixel block
* \param h the height of the pixel block
* \param ptr A pointer to the start of the data to be copied
* \param order The ordering of the colors
* \param bitness Defines the number of bits and their format for each pixel
* \param big_endian True if 16 bit values are stored big-endian
* \param x_offset The initial x-offset into the source buffer.
* \param y_offset The initial y-offset into the source buffer.
* \param x_pad How many pixels are in each line after the end of the pixels to be copied.
*
* The length of each source buffer line (stride) will be x_offset + w + x_pad.
*/
virtual void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad);
/// Convenience overload for base case where the pixels are packed into the buffer with no gaps (e.g. suits LVGL.)
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian) {
this->draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, 0, 0, 0);
}
/// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. /// 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); void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
@ -207,6 +236,12 @@ class Display : public PollingComponent {
/// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. /// 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); void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON);
/// Draw the outline of a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
void triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
/// Fill a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
void filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
/** Print `text` with the anchor point at [x,y] with `font`. /** Print `text` with the anchor point at [x,y] with `font`.
* *
* @param x The x coordinate of the text alignment anchor point. * @param x The x coordinate of the text alignment anchor point.
@ -503,6 +538,15 @@ class Display : public PollingComponent {
void do_update_(); void do_update_();
void clear_clipping_(); void clear_clipping_();
/**
* This method fills a triangle using only integer variables by using a
* modified bresenham algorithm.
* It is mandatory that [x2,y2] and [x3,y3] lie on the same horizontal line,
* so y2 must be equal to y3.
*/
void filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color);
void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
optional<display_writer_t> writer_{}; optional<display_writer_t> writer_{};
DisplayPage *page_{nullptr}; DisplayPage *page_{nullptr};

View file

@ -74,7 +74,7 @@ void EKTF2232Touchscreen::update_touches() {
uint8_t *d = raw + 1 + (i * 3); uint8_t *d = raw + 1 + (i * 3);
x_raw = (d[0] & 0xF0) << 4 | d[1]; x_raw = (d[0] & 0xF0) << 4 | d[1];
y_raw = (d[0] & 0x0F) << 8 | d[2]; y_raw = (d[0] & 0x0F) << 8 | d[2];
this->set_raw_touch_position_(i, x_raw, y_raw); this->add_raw_touch_position_(i, x_raw, y_raw);
} }
} }

View file

@ -226,7 +226,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 5) RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 6)
# The platformio/espressif32 version to use for esp-idf frameworks # The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases # - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
@ -271,8 +271,8 @@ def _arduino_check_versions(value):
def _esp_idf_check_versions(value): def _esp_idf_check_versions(value):
value = value.copy() value = value.copy()
lookups = { lookups = {
"dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"), "dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 1, 0), None), "latest": (cv.Version(5, 1, 2), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
} }

View file

@ -42,6 +42,34 @@ ESP32_BASE_PINS = {
} }
ESP32_BOARD_PINS = { ESP32_BOARD_PINS = {
"adafruit_feather_esp32_v2": {
"A0": 26,
"A1": 25,
"A2": 34,
"A3": 39,
"A4": 36,
"A5": 4,
"SCK": 5,
"MOSI": 19,
"MISO": 21,
"RX": 7,
"TX": 8,
"D37": 37,
"LED": 13,
"LED_BUILTIN": 13,
"D12": 12,
"D27": 27,
"D33": 33,
"D15": 15,
"D32": 32,
"D14": 14,
"SCL": 20,
"SDA": 22,
"BUTTON": 38,
"NEOPIXEL": 0,
"PIN_NEOPIXEL": 0,
"NEOPIXEL_POWER": 2,
},
"adafruit_feather_esp32s2_tft": { "adafruit_feather_esp32s2_tft": {
"BUTTON": 0, "BUTTON": 0,
"A0": 18, "A0": 18,
@ -133,6 +161,10 @@ ESP32_BOARD_PINS = {
"BUTTON": 0, "BUTTON": 0,
"SWITCH": 0, "SWITCH": 0,
}, },
"airm2m_core_esp32c3": {
"LED1_BUILTIN": 12,
"LED2_BUILTIN": 13,
},
"alksesp32": { "alksesp32": {
"A0": 32, "A0": 32,
"A1": 33, "A1": 33,

View file

@ -45,21 +45,19 @@ void BLEClientBase::loop() {
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
if (!this->auto_connect_)
return false;
if (this->address_ == 0 || device.address_uint64() != this->address_) if (this->address_ == 0 || device.address_uint64() != this->address_)
return false; return false;
if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING)
return false; return false;
ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str()); this->log_event_("Found device");
this->set_state(espbt::ClientState::DISCOVERED); if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
esp32_ble_tracker::global_esp32_ble_tracker->print_bt_device_info(device);
auto addr = device.address_uint64(); this->set_state(espbt::ClientState::DISCOVERED);
this->remote_bda_[0] = (addr >> 40) & 0xFF; this->set_address(device.address_uint64());
this->remote_bda_[1] = (addr >> 32) & 0xFF;
this->remote_bda_[2] = (addr >> 24) & 0xFF;
this->remote_bda_[3] = (addr >> 16) & 0xFF;
this->remote_bda_[4] = (addr >> 8) & 0xFF;
this->remote_bda_[5] = (addr >> 0) & 0xFF;
this->remote_addr_type_ = device.get_address_type(); this->remote_addr_type_ = device.get_address_type();
return true; return true;
} }
@ -108,6 +106,10 @@ void BLEClientBase::release_services() {
#endif #endif
} }
void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
}
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
@ -131,51 +133,73 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break; break;
} }
case ESP_GATTC_OPEN_EVT: { case ESP_GATTC_OPEN_EVT: {
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str()); if (!this->check_addr(param->open.remote_bda))
return false;
this->log_event_("ESP_GATTC_OPEN_EVT");
this->conn_id_ = param->open.conn_id; this->conn_id_ = param->open.conn_id;
this->service_count_ = 0; this->service_count_ = 0;
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(), ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(),
param->open.status); param->open.status);
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
break; return false;
} }
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
if (ret) { if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_, ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
this->address_str_.c_str(), ret); this->address_str_.c_str(), ret);
} }
this->set_state(espbt::ClientState::CONNECTED);
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
this->set_state(espbt::ClientState::CONNECTED); // only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED; this->state_ = espbt::ClientState::ESTABLISHED;
break; break;
} }
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
break; break;
} }
case ESP_GATTC_CFG_MTU_EVT: { case ESP_GATTC_CONNECT_EVT: {
if (param->cfg_mtu.status != ESP_GATT_OK) { if (!this->check_addr(param->connect.remote_bda))
ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, return false;
this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status); this->log_event_("ESP_GATTC_CONNECT_EVT");
this->set_state(espbt::ClientState::IDLE);
break;
}
ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
param->cfg_mtu.status, param->cfg_mtu.mtu);
this->mtu_ = param->cfg_mtu.mtu;
break; break;
} }
case ESP_GATTC_DISCONNECT_EVT: { case ESP_GATTC_DISCONNECT_EVT: {
if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0) if (!this->check_addr(param->disconnect.remote_bda))
return false; return false;
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_,
this->address_str_.c_str(), param->disconnect.reason); this->address_str_.c_str(), param->disconnect.reason);
this->release_services(); this->release_services();
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
break; break;
} }
case ESP_GATTC_CFG_MTU_EVT: {
if (this->conn_id_ != param->cfg_mtu.conn_id)
return false;
if (param->cfg_mtu.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_,
this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status);
// No state change required here - disconnect event will follow if needed.
break;
}
ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
param->cfg_mtu.status, param->cfg_mtu.mtu);
this->mtu_ = param->cfg_mtu.mtu;
break;
}
case ESP_GATTC_CLOSE_EVT: {
if (this->conn_id_ != param->close.conn_id)
return false;
this->log_event_("ESP_GATTC_CLOSE_EVT");
this->release_services();
this->set_state(espbt::ClientState::IDLE);
break;
}
case ESP_GATTC_SEARCH_RES_EVT: { case ESP_GATTC_SEARCH_RES_EVT: {
if (this->conn_id_ != param->search_res.conn_id)
return false;
this->service_count_++; this->service_count_++;
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// V3 clients don't need services initialized since // V3 clients don't need services initialized since
@ -191,7 +215,9 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break; break;
} }
case ESP_GATTC_SEARCH_CMPL_EVT: { case ESP_GATTC_SEARCH_CMPL_EVT: {
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str()); if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT");
for (auto &svc : this->services_) { for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
svc->uuid.to_string().c_str()); svc->uuid.to_string().c_str());
@ -199,11 +225,41 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->address_str_.c_str(), svc->start_handle, svc->end_handle); this->address_str_.c_str(), svc->start_handle, svc->end_handle);
} }
ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
this->set_state(espbt::ClientState::CONNECTED);
this->state_ = espbt::ClientState::ESTABLISHED; this->state_ = espbt::ClientState::ESTABLISHED;
break; break;
} }
case ESP_GATTC_READ_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_event_("ESP_GATTC_READ_DESCR_EVT");
break;
}
case ESP_GATTC_WRITE_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_event_("ESP_GATTC_WRITE_DESCR_EVT");
break;
}
case ESP_GATTC_WRITE_CHAR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_event_("ESP_GATTC_WRITE_CHAR_EVT");
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (this->conn_id_ != param->read.conn_id)
return false;
this->log_event_("ESP_GATTC_READ_CHAR_EVT");
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (this->conn_id_ != param->notify.conn_id)
return false;
this->log_event_("ESP_GATTC_NOTIFY_EVT");
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT");
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// Client is responsible for flipping the descriptor value // Client is responsible for flipping the descriptor value
@ -212,9 +268,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} }
esp_gattc_descr_elem_t desc_result; esp_gattc_descr_elem_t desc_result;
uint16_t count = 1; uint16_t count = 1;
esp_gatt_status_t descr_status = esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle(
esp_ble_gattc_get_descr_by_char_handle(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count);
NOTIFY_DESC_UUID, &desc_result, &count);
if (descr_status != ESP_GATT_OK) { if (descr_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_, ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_,
this->address_str_.c_str(), descr_status); this->address_str_.c_str(), descr_status);
@ -222,7 +277,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} }
esp_gattc_char_elem_t char_result; esp_gattc_char_elem_t char_result;
esp_gatt_status_t char_status = esp_gatt_status_t char_status =
esp_ble_gattc_get_all_char(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle, esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle,
param->reg_for_notify.handle, &char_result, &count, 0); param->reg_for_notify.handle, &char_result, &count, 0);
if (char_status != ESP_GATT_OK) { if (char_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
@ -238,6 +293,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_err_t status = esp_err_t status =
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en), esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); (uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
if (status) { if (status) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_, ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_,
this->address_str_.c_str(), status); this->address_str_.c_str(), status);
@ -246,24 +302,31 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} }
default: default:
// ideally would check all other events for matching conn_id
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event);
break; break;
} }
return true; return true;
} }
// clients can't call defer() directly since it's protected.
void BLEClientBase::run_later(std::function<void()> &&f) { // NOLINT
this->defer(std::move(f));
}
void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) { switch (event) {
// This event is sent by the server when it requests security // This event is sent by the server when it requests security
case ESP_GAP_BLE_SEC_REQ_EVT: case ESP_GAP_BLE_SEC_REQ_EVT:
if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
break; return;
ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event); ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event);
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break; break;
// This event is sent once authentication has completed // This event is sent once authentication has completed
case ESP_GAP_BLE_AUTH_CMPL_EVT: case ESP_GAP_BLE_AUTH_CMPL_EVT:
if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
break; return;
esp_bd_addr_t bd_addr; esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(),
@ -273,11 +336,12 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
param->ble_security.auth_cmpl.fail_reason); param->ble_security.auth_cmpl.fail_reason);
} else { } else {
this->paired_ = true; this->paired_ = true;
ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type,
param->ble_security.auth_cmpl.auth_mode); param->ble_security.auth_cmpl.auth_mode);
} }
break; break;
// There are other events we'll want to implement at some point to support things like pass key // There are other events we'll want to implement at some point to support things like pass key
// https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md
default: default:

View file

@ -27,6 +27,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void loop() override; void loop() override;
float get_setup_priority() const override; float get_setup_priority() const override;
void run_later(std::function<void()> &&f); // NOLINT
bool parse_device(const espbt::ESPBTDevice &device) override; bool parse_device(const espbt::ESPBTDevice &device) override;
void on_scan_end() override {} void on_scan_end() override {}
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@ -39,10 +40,17 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; } bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
void set_address(uint64_t address) { void set_address(uint64_t address) {
this->address_ = address; this->address_ = address;
this->remote_bda_[0] = (address >> 40) & 0xFF;
this->remote_bda_[1] = (address >> 32) & 0xFF;
this->remote_bda_[2] = (address >> 24) & 0xFF;
this->remote_bda_[3] = (address >> 16) & 0xFF;
this->remote_bda_[4] = (address >> 8) & 0xFF;
this->remote_bda_[5] = (address >> 0) & 0xFF;
if (address == 0) { if (address == 0) {
memset(this->remote_bda_, 0, sizeof(this->remote_bda_));
this->address_str_ = ""; this->address_str_ = "";
} else { } else {
this->address_str_ = this->address_str_ =
@ -79,20 +87,24 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; } virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; }
bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; }
protected: protected:
int gattc_if_; int gattc_if_;
esp_bd_addr_t remote_bda_; esp_bd_addr_t remote_bda_;
esp_ble_addr_type_t remote_addr_type_; esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
uint16_t conn_id_{0xFFFF}; uint16_t conn_id_{0xFFFF};
uint64_t address_{0}; uint64_t address_{0};
bool auto_connect_{false};
std::string address_str_{}; std::string address_str_{};
uint8_t connection_index_; uint8_t connection_index_;
int16_t service_count_{0}; int16_t service_count_{0};
uint16_t mtu_{23}; uint16_t mtu_{23};
bool paired_{false}; bool paired_{false};
espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
std::vector<BLEService *> services_; std::vector<BLEService *> services_;
void log_event_(const char *name);
}; };
} // namespace esp32_ble_client } // namespace esp32_ble_client

View file

@ -1,23 +1,26 @@
import re import re
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.const import ( from esphome.const import (
CONF_ACTIVE, CONF_ACTIVE,
CONF_DURATION,
CONF_ID, CONF_ID,
CONF_INTERVAL, CONF_INTERVAL,
CONF_DURATION,
CONF_TRIGGER_ID,
CONF_MAC_ADDRESS, CONF_MAC_ADDRESS,
CONF_SERVICE_UUID,
CONF_MANUFACTURER_ID, CONF_MANUFACTURER_ID,
CONF_ON_BLE_ADVERTISE, CONF_ON_BLE_ADVERTISE,
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
CONF_SERVICE_UUID,
CONF_TRIGGER_ID,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
) )
from esphome.components import esp32_ble
from esphome.core import CORE from esphome.core import CORE
from esphome.components.esp32 import add_idf_sdkconfig_option
AUTO_LOAD = ["esp32_ble"] AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
@ -263,6 +266,9 @@ async def to_code(config):
# https://github.com/espressif/esp-idf/issues/2503 # https://github.com/espressif/esp-idf/issues/2503
# Match arduino CONFIG_BTU_TASK_STACK_SIZE # Match arduino CONFIG_BTU_TASK_STACK_SIZE
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6):
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
else:
add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)

View file

@ -37,7 +37,7 @@ void ESP32Camera::setup() {
"framebuffer_task", // name "framebuffer_task", // name
1024, // stack size 1024, // stack size
nullptr, // task pv params nullptr, // task pv params
0, // priority 1, // priority
nullptr, // handle nullptr, // handle
1 // core 1 // core
); );

View file

@ -13,6 +13,8 @@ namespace esp32_rmt_led_strip {
static const char *const TAG = "esp32_rmt_led_strip"; static const char *const TAG = "esp32_rmt_led_strip";
static const uint32_t RMT_CLK_FREQ = 80000000;
static const uint8_t RMT_CLK_DIV = 2; static const uint8_t RMT_CLK_DIV = 2;
void ESP32RMTLEDStripLightOutput::setup() { void ESP32RMTLEDStripLightOutput::setup() {
@ -65,7 +67,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
uint32_t bit1_low) { uint32_t bit1_low) {
float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f; float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
// 0-bit // 0-bit
this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); this->bit0_.duration0 = (uint32_t) (ratio * bit0_high);
@ -159,10 +161,12 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index
break; break;
} }
uint8_t multiplier = this->is_rgbw_ ? 4 : 3; uint8_t multiplier = this->is_rgbw_ ? 4 : 3;
return {this->buf_ + (index * multiplier) + r, uint8_t white = this->is_wrgb_ ? 0 : 3;
this->buf_ + (index * multiplier) + g,
this->buf_ + (index * multiplier) + b, return {this->buf_ + (index * multiplier) + r + this->is_wrgb_,
this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, this->buf_ + (index * multiplier) + g + this->is_wrgb_,
this->buf_ + (index * multiplier) + b + this->is_wrgb_,
this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr,
&this->effect_data_[index], &this->effect_data_[index],
&this->correction_}; &this->correction_};
} }

View file

@ -33,7 +33,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
int32_t size() const override { return this->num_leds_; } int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override { light::LightTraits get_traits() override {
auto traits = light::LightTraits(); auto traits = light::LightTraits();
if (this->is_rgbw_) { if (this->is_rgbw_ || this->is_wrgb_) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}); traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else { } else {
traits.set_supported_color_modes({light::ColorMode::RGB}); traits.set_supported_color_modes({light::ColorMode::RGB});
@ -44,6 +44,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
void set_pin(uint8_t pin) { this->pin_ = pin; } void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; }
/// Set a maximum refresh rate in µs as some lights do not like being updated too often. /// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
@ -72,6 +73,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
uint8_t pin_; uint8_t pin_;
uint16_t num_leds_; uint16_t num_leds_;
bool is_rgbw_; bool is_rgbw_;
bool is_wrgb_;
rmt_item32_t bit0_, bit1_; rmt_item32_t bit0_, bit1_;
RGBOrder rgb_order_; RGBOrder rgb_order_;

View file

@ -52,6 +52,7 @@ CHIPSETS = {
CONF_IS_RGBW = "is_rgbw" CONF_IS_RGBW = "is_rgbw"
CONF_IS_WRGB = "is_wrgb"
CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low" CONF_BIT0_LOW = "bit0_low"
CONF_BIT1_HIGH = "bit1_high" CONF_BIT1_HIGH = "bit1_high"
@ -90,6 +91,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
cv.Inclusive( cv.Inclusive(
CONF_BIT0_HIGH, CONF_BIT0_HIGH,
"custom", "custom",
@ -145,6 +147,7 @@ async def to_code(config):
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
cg.add( cg.add(
var.set_rmt_channel( var.set_rmt_channel(

View file

@ -13,8 +13,11 @@ from esphome.const import (
CONF_ON_ENROLLMENT_DONE, CONF_ON_ENROLLMENT_DONE,
CONF_ON_ENROLLMENT_FAILED, CONF_ON_ENROLLMENT_FAILED,
CONF_ON_ENROLLMENT_SCAN, CONF_ON_ENROLLMENT_SCAN,
CONF_ON_FINGER_SCAN_START,
CONF_ON_FINGER_SCAN_MATCHED, CONF_ON_FINGER_SCAN_MATCHED,
CONF_ON_FINGER_SCAN_UNMATCHED, CONF_ON_FINGER_SCAN_UNMATCHED,
CONF_ON_FINGER_SCAN_MISPLACED,
CONF_ON_FINGER_SCAN_INVALID,
CONF_PASSWORD, CONF_PASSWORD,
CONF_SENSING_PIN, CONF_SENSING_PIN,
CONF_SPEED, CONF_SPEED,
@ -34,6 +37,10 @@ FingerprintGrowComponent = fingerprint_grow_ns.class_(
"FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice
) )
FingerScanStartTrigger = fingerprint_grow_ns.class_(
"FingerScanStartTrigger", automation.Trigger.template()
)
FingerScanMatchedTrigger = fingerprint_grow_ns.class_( FingerScanMatchedTrigger = fingerprint_grow_ns.class_(
"FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16)
) )
@ -42,6 +49,14 @@ FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_(
"FingerScanUnmatchedTrigger", automation.Trigger.template() "FingerScanUnmatchedTrigger", automation.Trigger.template()
) )
FingerScanMisplacedTrigger = fingerprint_grow_ns.class_(
"FingerScanMisplacedTrigger", automation.Trigger.template()
)
FingerScanInvalidTrigger = fingerprint_grow_ns.class_(
"FingerScanInvalidTrigger", automation.Trigger.template()
)
EnrollmentScanTrigger = fingerprint_grow_ns.class_( EnrollmentScanTrigger = fingerprint_grow_ns.class_(
"EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16)
) )
@ -94,6 +109,13 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_PASSWORD): cv.uint32_t, cv.Optional(CONF_PASSWORD): cv.uint32_t,
cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t,
cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanStartTrigger
),
}
),
cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@ -108,6 +130,20 @@ CONFIG_SCHEMA = (
), ),
} }
), ),
cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanMisplacedTrigger
),
}
),
cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
FingerScanInvalidTrigger
),
}
),
cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@ -152,6 +188,10 @@ async def to_code(config):
sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN]) sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN])
cg.add(var.set_sensing_pin(sensing_pin)) cg.add(var.set_sensing_pin(sensing_pin))
for conf in config.get(CONF_ON_FINGER_SCAN_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation( await automation.build_automation(
@ -162,6 +202,14 @@ async def to_code(config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation( await automation.build_automation(

View file

@ -15,16 +15,18 @@ void FingerprintGrowComponent::update() {
return; return;
} }
if (this->sensing_pin_ != nullptr) { if (this->has_sensing_pin_) {
if (this->sensing_pin_->digital_read()) { if (this->sensing_pin_->digital_read()) {
ESP_LOGV(TAG, "No touch sensing"); ESP_LOGV(TAG, "No touch sensing");
this->waiting_removal_ = false; this->waiting_removal_ = false;
return; return;
} else if (!this->waiting_removal_) {
this->finger_scan_start_callback_.call();
} }
} }
if (this->waiting_removal_) { if (this->waiting_removal_) {
if (this->scan_image_(1) == NO_FINGER) { if ((!this->has_sensing_pin_) && (this->scan_image_(1) == NO_FINGER)) {
ESP_LOGD(TAG, "Finger removed"); ESP_LOGD(TAG, "Finger removed");
this->waiting_removal_ = false; this->waiting_removal_ = false;
} }
@ -51,6 +53,7 @@ void FingerprintGrowComponent::update() {
void FingerprintGrowComponent::setup() { void FingerprintGrowComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader...");
this->has_sensing_pin_ = (this->sensing_pin_ != nullptr);
if (this->check_password_()) { if (this->check_password_()) {
if (this->new_password_ != -1) { if (this->new_password_ != -1) {
if (this->set_password_()) if (this->set_password_())
@ -91,7 +94,7 @@ void FingerprintGrowComponent::finish_enrollment(uint8_t result) {
} }
void FingerprintGrowComponent::scan_and_match_() { void FingerprintGrowComponent::scan_and_match_() {
if (this->sensing_pin_ != nullptr) { if (this->has_sensing_pin_) {
ESP_LOGD(TAG, "Scan and match"); ESP_LOGD(TAG, "Scan and match");
} else { } else {
ESP_LOGV(TAG, "Scan and match"); ESP_LOGV(TAG, "Scan and match");
@ -122,43 +125,52 @@ void FingerprintGrowComponent::scan_and_match_() {
} }
uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
if (this->sensing_pin_ != nullptr) { if (this->has_sensing_pin_) {
ESP_LOGD(TAG, "Getting image %d", buffer); ESP_LOGD(TAG, "Getting image %d", buffer);
} else { } else {
ESP_LOGV(TAG, "Getting image %d", buffer); ESP_LOGV(TAG, "Getting image %d", buffer);
} }
this->data_ = {GET_IMAGE}; this->data_ = {GET_IMAGE};
switch (this->send_command_()) { uint8_t send_result = this->send_command_();
switch (send_result) {
case OK: case OK:
break; break;
case NO_FINGER: case NO_FINGER:
if (this->sensing_pin_ != nullptr) { if (this->has_sensing_pin_) {
ESP_LOGD(TAG, "No finger"); this->waiting_removal_ = true;
ESP_LOGD(TAG, "Finger Misplaced");
this->finger_scan_misplaced_callback_.call();
} else { } else {
ESP_LOGV(TAG, "No finger"); ESP_LOGV(TAG, "No finger");
} }
return this->data_[0]; return send_result;
case IMAGE_FAIL: case IMAGE_FAIL:
ESP_LOGE(TAG, "Imaging error"); ESP_LOGE(TAG, "Imaging error");
this->finger_scan_invalid_callback_.call();
return send_result;
default: default:
return this->data_[0]; ESP_LOGD(TAG, "Unknown Scan Error: %d", send_result);
return send_result;
} }
ESP_LOGD(TAG, "Processing image %d", buffer); ESP_LOGD(TAG, "Processing image %d", buffer);
this->data_ = {IMAGE_2_TZ, buffer}; this->data_ = {IMAGE_2_TZ, buffer};
switch (this->send_command_()) { send_result = this->send_command_();
switch (send_result) {
case OK: case OK:
ESP_LOGI(TAG, "Processed image %d", buffer); ESP_LOGI(TAG, "Processed image %d", buffer);
break; break;
case IMAGE_MESS: case IMAGE_MESS:
ESP_LOGE(TAG, "Image too messy"); ESP_LOGE(TAG, "Image too messy");
this->finger_scan_invalid_callback_.call();
break; break;
case FEATURE_FAIL: case FEATURE_FAIL:
case INVALID_IMAGE: case INVALID_IMAGE:
ESP_LOGE(TAG, "Could not find fingerprint features"); ESP_LOGE(TAG, "Could not find fingerprint features");
this->finger_scan_invalid_callback_.call();
break; break;
} }
return this->data_[0]; return send_result;
} }
uint8_t FingerprintGrowComponent::save_fingerprint_() { uint8_t FingerprintGrowComponent::save_fingerprint_() {
@ -221,10 +233,11 @@ bool FingerprintGrowComponent::get_parameters_() {
ESP_LOGD(TAG, "Getting parameters"); ESP_LOGD(TAG, "Getting parameters");
this->data_ = {READ_SYS_PARAM}; this->data_ = {READ_SYS_PARAM};
if (this->send_command_() == OK) { if (this->send_command_() == OK) {
ESP_LOGD(TAG, "Got parameters"); ESP_LOGD(TAG, "Got parameters"); // Bear in mind data_[0] is the transfer status,
if (this->status_sensor_ != nullptr) { if (this->status_sensor_ != nullptr) { // the parameters table start at data_[1]
this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]);
} }
this->system_identifier_code_ = ((uint16_t) this->data_[3] << 8) | this->data_[4];
this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6]; this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6];
if (this->capacity_sensor_ != nullptr) { if (this->capacity_sensor_ != nullptr) {
this->capacity_sensor_->publish_state(this->capacity_); this->capacity_sensor_->publish_state(this->capacity_);
@ -426,13 +439,22 @@ uint8_t FingerprintGrowComponent::send_command_() {
void FingerprintGrowComponent::dump_config() { void FingerprintGrowComponent::dump_config() {
ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:"); ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:");
ESP_LOGCONFIG(TAG, " System Identifier Code: 0x%.4X", this->system_identifier_code_);
ESP_LOGCONFIG(TAG, " Touch Sensing Pin: %s",
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None");
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->fingerprint_count_sensor_->get_state());
LOG_SENSOR(" ", "Status", this->status_sensor_); LOG_SENSOR(" ", "Status", this->status_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->status_sensor_->get_state());
LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); LOG_SENSOR(" ", "Capacity", this->capacity_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint16_t) this->capacity_sensor_->get_state());
LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); LOG_SENSOR(" ", "Security Level", this->security_level_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint8_t) this->security_level_sensor_->get_state());
LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_finger_id_sensor_->get_state());
LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %d", (uint32_t) this->last_confidence_sensor_->get_state());
} }
} // namespace fingerprint_grow } // namespace fingerprint_grow

View file

@ -118,12 +118,21 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) {
this->enrolling_binary_sensor_ = enrolling_binary_sensor; this->enrolling_binary_sensor_ = enrolling_binary_sensor;
} }
void add_on_finger_scan_start_callback(std::function<void()> callback) {
this->finger_scan_start_callback_.add(std::move(callback));
}
void add_on_finger_scan_matched_callback(std::function<void(uint16_t, uint16_t)> callback) { void add_on_finger_scan_matched_callback(std::function<void(uint16_t, uint16_t)> callback) {
this->finger_scan_matched_callback_.add(std::move(callback)); this->finger_scan_matched_callback_.add(std::move(callback));
} }
void add_on_finger_scan_unmatched_callback(std::function<void()> callback) { void add_on_finger_scan_unmatched_callback(std::function<void()> callback) {
this->finger_scan_unmatched_callback_.add(std::move(callback)); this->finger_scan_unmatched_callback_.add(std::move(callback));
} }
void add_on_finger_scan_misplaced_callback(std::function<void()> callback) {
this->finger_scan_misplaced_callback_.add(std::move(callback));
}
void add_on_finger_scan_invalid_callback(std::function<void()> callback) {
this->finger_scan_invalid_callback_.add(std::move(callback));
}
void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) { void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) {
this->enrollment_scan_callback_.add(std::move(callback)); this->enrollment_scan_callback_.add(std::move(callback));
} }
@ -163,8 +172,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED; uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED;
uint8_t enrollment_buffers_ = 5; uint8_t enrollment_buffers_ = 5;
bool waiting_removal_ = false; bool waiting_removal_ = false;
bool has_sensing_pin_ = false;
uint32_t last_aura_led_control_ = 0; uint32_t last_aura_led_control_ = 0;
uint16_t last_aura_led_duration_ = 0; uint16_t last_aura_led_duration_ = 0;
uint16_t system_identifier_code_ = 0;
sensor::Sensor *fingerprint_count_sensor_{nullptr}; sensor::Sensor *fingerprint_count_sensor_{nullptr};
sensor::Sensor *status_sensor_{nullptr}; sensor::Sensor *status_sensor_{nullptr};
sensor::Sensor *capacity_sensor_{nullptr}; sensor::Sensor *capacity_sensor_{nullptr};
@ -172,13 +183,23 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
sensor::Sensor *last_finger_id_sensor_{nullptr}; sensor::Sensor *last_finger_id_sensor_{nullptr};
sensor::Sensor *last_confidence_sensor_{nullptr}; sensor::Sensor *last_confidence_sensor_{nullptr};
binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr};
CallbackManager<void()> finger_scan_invalid_callback_;
CallbackManager<void()> finger_scan_start_callback_;
CallbackManager<void(uint16_t, uint16_t)> finger_scan_matched_callback_; CallbackManager<void(uint16_t, uint16_t)> finger_scan_matched_callback_;
CallbackManager<void()> finger_scan_unmatched_callback_; CallbackManager<void()> finger_scan_unmatched_callback_;
CallbackManager<void()> finger_scan_misplaced_callback_;
CallbackManager<void(uint8_t, uint16_t)> enrollment_scan_callback_; CallbackManager<void(uint8_t, uint16_t)> enrollment_scan_callback_;
CallbackManager<void(uint16_t)> enrollment_done_callback_; CallbackManager<void(uint16_t)> enrollment_done_callback_;
CallbackManager<void(uint16_t)> enrollment_failed_callback_; CallbackManager<void(uint16_t)> enrollment_failed_callback_;
}; };
class FingerScanStartTrigger : public Trigger<> {
public:
explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_start_callback([this]() { this->trigger(); });
}
};
class FingerScanMatchedTrigger : public Trigger<uint16_t, uint16_t> { class FingerScanMatchedTrigger : public Trigger<uint16_t, uint16_t> {
public: public:
explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) {
@ -194,6 +215,20 @@ class FingerScanUnmatchedTrigger : public Trigger<> {
} }
}; };
class FingerScanMisplacedTrigger : public Trigger<> {
public:
explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); });
}
};
class FingerScanInvalidTrigger : public Trigger<> {
public:
explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) {
parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); });
}
};
class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> { class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> {
public: public:
explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) {

View file

@ -67,13 +67,13 @@ 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==10.1.0")' '(pip install "pillow==10.2.0")'
) from err ) from err
if version.parse(PIL.__version__) != version.parse("10.1.0"): if version.parse(PIL.__version__) != version.parse("10.2.0"):
raise cv.Invalid( raise cv.Invalid(
"Please update your pillow installation to 10.1.0. " "Please update your pillow installation to 10.2.0. "
'(pip install "pillow==10.1.0")' '(pip install "pillow==10.2.0")'
) )
return value return value
@ -235,7 +235,7 @@ FILE_SCHEMA = cv.Schema(_file_schema)
DEFAULT_GLYPHS = ( DEFAULT_GLYPHS = (
' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
) )
CONF_RAW_GLYPH_ID = "raw_glyph_id" CONF_RAW_GLYPH_ID = "raw_glyph_id"

View file

@ -94,7 +94,7 @@ class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
if (status == 0 || status == 2) { if (status == 0 || status == 2) {
this->set_raw_touch_position_(id, x, y); this->add_raw_touch_position_(id, x, y);
} }
} }
} }

View file

@ -13,14 +13,14 @@
namespace esphome { namespace esphome {
namespace ft63x6 { namespace ft63x6 {
static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02; static const uint8_t FT63X6_ADDR_TOUCH1_STATE = 0x03;
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03; static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03;
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05; static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05;
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B; static const uint8_t FT63X6_ADDR_TOUCH2_STATE = 0x09;
static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09; static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09;
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B;
static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B; static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B;
static const char *const TAG = "FT63X6Touchscreen"; static const char *const TAG = "FT63X6Touchscreen";
@ -40,26 +40,11 @@ void FT63X6Touchscreen::setup() {
this->hard_reset_(); this->hard_reset_();
// Get touch resolution // Get touch resolution
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = 320; this->x_raw_max_ = 320;
this->y_raw_max_ = 480;
}
void FT63X6Touchscreen::update_touches() {
int touch_count = this->read_touch_count_();
if (touch_count == 0) {
return;
} }
if (this->y_raw_max_ == this->y_raw_min_) {
uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID); // id1 = 0 or 1 this->y_raw_max_ = 480;
int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X);
int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y);
this->set_raw_touch_position_(touch_id, x, y);
if (touch_count >= 2) {
touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID); // id2 = 0 or 1(~id1 & 0x01)
x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X);
y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y);
this->set_raw_touch_position_(touch_id, x, y);
} }
} }
@ -76,23 +61,31 @@ void FT63X6Touchscreen::dump_config() {
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_UPDATE_INTERVAL(this);
} }
uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); } void FT63X6Touchscreen::update_touches() {
uint8_t data[15];
uint16_t touch_id, x, y;
// Touch functions if (!this->read_bytes(0x00, (uint8_t *) data, 15)) {
uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) { ESP_LOGE(TAG, "Failed to read touch data");
uint8_t read_buf[2]; this->skip_update_ = true;
read_buf[0] = this->read_byte_(coordinate); return;
read_buf[1] = this->read_byte_(coordinate + 1); }
return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
}
uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; }
uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { if (((data[FT63X6_ADDR_TOUCH1_STATE] >> 6) & 0x01) == 0) {
uint8_t byte = 0; touch_id = data[FT63X6_ADDR_TOUCH1_ID] >> 4; // id1 = 0 or 1
this->read_byte(addr, &byte); x = encode_uint16(data[FT63X6_ADDR_TOUCH1_X] & 0x0F, data[FT63X6_ADDR_TOUCH1_X + 1]);
return byte; y = encode_uint16(data[FT63X6_ADDR_TOUCH1_Y] & 0x0F, data[FT63X6_ADDR_TOUCH1_Y + 1]);
this->add_raw_touch_position_(touch_id, x, y);
}
if (((data[FT63X6_ADDR_TOUCH2_STATE] >> 6) & 0x01) == 0) {
touch_id = data[FT63X6_ADDR_TOUCH2_ID] >> 4; // id1 = 0 or 1
x = encode_uint16(data[FT63X6_ADDR_TOUCH2_X] & 0x0F, data[FT63X6_ADDR_TOUCH2_X + 1]);
y = encode_uint16(data[FT63X6_ADDR_TOUCH2_Y] & 0x0F, data[FT63X6_ADDR_TOUCH2_Y + 1]);
this->add_raw_touch_position_(touch_id, x, y);
}
} }
} // namespace ft63x6 } // namespace ft63x6

View file

@ -61,6 +61,7 @@ VALUE_POSITION_TYPE = {
"BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW, "BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW,
} }
CONF_CONTINUOUS = "continuous"
GRAPH_TRACE_SCHEMA = cv.Schema( GRAPH_TRACE_SCHEMA = cv.Schema(
{ {
@ -70,6 +71,7 @@ GRAPH_TRACE_SCHEMA = cv.Schema(
cv.Optional(CONF_LINE_THICKNESS): cv.positive_int, cv.Optional(CONF_LINE_THICKNESS): cv.positive_int,
cv.Optional(CONF_LINE_TYPE): cv.enum(LINE_TYPE, upper=True), cv.Optional(CONF_LINE_TYPE): cv.enum(LINE_TYPE, upper=True),
cv.Optional(CONF_COLOR): cv.use_id(color.ColorStruct), cv.Optional(CONF_COLOR): cv.use_id(color.ColorStruct),
cv.Optional(CONF_CONTINUOUS): cv.boolean,
} }
) )
@ -186,6 +188,8 @@ async def to_code(config):
if CONF_COLOR in trace: if CONF_COLOR in trace:
c = await cg.get_variable(trace[CONF_COLOR]) c = await cg.get_variable(trace[CONF_COLOR])
cg.add(tr.set_line_color(c)) cg.add(tr.set_line_color(c))
if CONF_CONTINUOUS in trace:
cg.add(tr.set_continuous(trace[CONF_CONTINUOUS]))
cg.add(var.add_trace(tr)) cg.add(var.add_trace(tr))
# Add legend # Add legend
if CONF_LEGEND in config: if CONF_LEGEND in config:

View file

@ -165,17 +165,42 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
for (auto *trace : traces_) { for (auto *trace : traces_) {
Color c = trace->get_line_color(); Color c = trace->get_line_color();
uint16_t thick = trace->get_line_thickness(); uint16_t thick = trace->get_line_thickness();
bool continuous = trace->get_continuous();
bool has_prev = false;
bool prev_b = false;
int16_t prev_y = 0;
for (uint32_t i = 0; i < this->width_; i++) { for (uint32_t i = 0; i < this->width_; i++) {
float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange; float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange;
if (!std::isnan(v) && (thick > 0)) { if (!std::isnan(v) && (thick > 0)) {
int16_t x = this->width_ - 1 - i; int16_t x = this->width_ - 1 - i + x_offset;
uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick; uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick);
if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) { bool b = (trace->get_line_type() & bit) == bit;
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2; if (b) {
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
for (uint16_t t = 0; t < thick; t++) { for (uint16_t t = 0; t < thick; t++) {
buff->draw_pixel_at(x_offset + x, y_offset + y + t, c); buff->draw_pixel_at(x, y + t, c);
}
} else {
int16_t mid_y = (y + prev_y + thick) / 2;
if (y > prev_y) {
for (uint16_t t = prev_y + thick; t <= mid_y; t++)
buff->draw_pixel_at(x + 1, t, c);
for (uint16_t t = mid_y + 1; t < y + thick; t++)
buff->draw_pixel_at(x, t, c);
} else {
for (uint16_t t = prev_y - 1; t >= mid_y; t--)
buff->draw_pixel_at(x + 1, t, c);
for (uint16_t t = mid_y - 1; t >= y; t--)
buff->draw_pixel_at(x, t, c);
} }
} }
prev_y = y;
}
prev_b = b;
has_prev = true;
} else {
has_prev = false;
} }
} }
} }

View file

@ -116,6 +116,8 @@ class GraphTrace {
void set_line_type(enum LineType val) { this->line_type_ = val; } void set_line_type(enum LineType val) { this->line_type_ = val; }
Color get_line_color() { return this->line_color_; } Color get_line_color() { return this->line_color_; }
void set_line_color(Color val) { this->line_color_ = val; } void set_line_color(Color val) { this->line_color_ = val; }
bool get_continuous() { return this->continuous_; }
void set_continuous(bool continuous) { this->continuous_ = continuous; }
std::string get_name() { return name_; } std::string get_name() { return name_; }
const HistoryData *get_tracedata() { return &data_; } const HistoryData *get_tracedata() { return &data_; }
@ -125,6 +127,7 @@ class GraphTrace {
uint8_t line_thickness_{3}; uint8_t line_thickness_{3};
enum LineType line_type_ { LINE_TYPE_SOLID }; enum LineType line_type_ { LINE_TYPE_SOLID };
Color line_color_{COLOR_ON}; Color line_color_{COLOR_ON};
bool continuous_{false};
HistoryData data_; HistoryData data_;
friend Graph; friend Graph;

View file

@ -14,6 +14,7 @@ static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D};
static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48}; static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48};
static const size_t MAX_TOUCHES = 5; // max number of possible touches reported static const size_t MAX_TOUCHES = 5; // max number of possible touches reported
static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
#define ERROR_CHECK(err) \ #define ERROR_CHECK(err) \
if ((err) != i2c::ERROR_OK) { \ if ((err) != i2c::ERROR_OK) { \
@ -79,9 +80,6 @@ void GT911Touchscreen::update_touches() {
return; return;
} }
if (num_of_touches == 0)
return;
err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false);
ERROR_CHECK(err); ERROR_CHECK(err);
// num_of_touches is guaranteed to be 0..5. Also read the key data // num_of_touches is guaranteed to be 0..5. Also read the key data
@ -92,13 +90,16 @@ void GT911Touchscreen::update_touches() {
uint16_t id = data[i][0]; uint16_t id = data[i][0];
uint16_t x = encode_uint16(data[i][2], data[i][1]); uint16_t x = encode_uint16(data[i][2], data[i][1]);
uint16_t y = encode_uint16(data[i][4], data[i][3]); uint16_t y = encode_uint16(data[i][4], data[i][3]);
this->set_raw_touch_position_(id, x, y); this->add_raw_touch_position_(id, x, y);
} }
auto keys = data[num_of_touches][0]; auto keys = data[num_of_touches][0] & ((1 << MAX_BUTTONS) - 1);
for (size_t i = 0; i != 4; i++) { if (keys != this->button_state_) {
this->button_state_ = keys;
for (size_t i = 0; i != MAX_BUTTONS; i++) {
for (auto *listener : this->button_listeners_) for (auto *listener : this->button_listeners_)
listener->update_button(i, (keys & (1 << i)) != 0); listener->update_button(i, (keys & (1 << i)) != 0);
} }
}
} }
void GT911Touchscreen::dump_config() { void GT911Touchscreen::dump_config() {

View file

@ -26,6 +26,7 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
InternalGPIOPin *interrupt_pin_{}; InternalGPIOPin *interrupt_pin_{};
std::vector<GT911ButtonListener *> button_listeners_; std::vector<GT911ButtonListener *> button_listeners_;
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
}; };
} // namespace gt911 } // namespace gt911

View file

@ -18,6 +18,7 @@ from esphome.const import (
CONF_SUPPORTED_SWING_MODES, CONF_SUPPORTED_SWING_MODES,
CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE,
CONF_TEMPERATURE_STEP, CONF_TEMPERATURE_STEP,
CONF_TRIGGER_ID,
CONF_VISUAL, CONF_VISUAL,
CONF_WIFI, CONF_WIFI,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
@ -49,6 +50,8 @@ CONF_CONTROL_METHOD = "control_method"
CONF_CONTROL_PACKET_SIZE = "control_packet_size" CONF_CONTROL_PACKET_SIZE = "control_packet_size"
CONF_DISPLAY = "display" CONF_DISPLAY = "display"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
CONF_ON_ALARM_START = "on_alarm_start"
CONF_ON_ALARM_END = "on_alarm_end"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_VERTICAL_AIRFLOW = "vertical_airflow" CONF_VERTICAL_AIRFLOW = "vertical_airflow"
CONF_WIFI_SIGNAL = "wifi_signal" CONF_WIFI_SIGNAL = "wifi_signal"
@ -85,8 +88,8 @@ AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
} }
SUPPORTED_SWING_MODES_OPTIONS = { SUPPORTED_SWING_MODES_OPTIONS = {
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available "OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
} }
@ -101,13 +104,15 @@ SUPPORTED_CLIMATE_MODES_OPTIONS = {
} }
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = { SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
"AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
} }
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
"ECO": ClimatePreset.CLIMATE_PRESET_ECO, "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
} }
@ -118,6 +123,16 @@ SUPPORTED_HON_CONTROL_METHODS = {
"SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER,
} }
HaierAlarmStartTrigger = haier_ns.class_(
"HaierAlarmStartTrigger",
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
)
HaierAlarmEndTrigger = haier_ns.class_(
"HaierAlarmEndTrigger",
automation.Trigger.template(cg.uint8, cg.const_char_ptr),
)
def validate_visual(config): def validate_visual(config):
if CONF_VISUAL in config: if CONF_VISUAL in config:
@ -200,9 +215,7 @@ CONFIG_SCHEMA = cv.All(
): cv.boolean, ): cv.boolean,
cv.Optional( cv.Optional(
CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_PRESETS,
default=list( default=list(["BOOST", "COMFORT"]), # No AWAY by default
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS.keys()
),
): cv.ensure_list( ): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True) cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True)
), ),
@ -222,7 +235,7 @@ CONFIG_SCHEMA = cv.All(
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50), ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
cv.Optional( cv.Optional(
CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_PRESETS,
default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), default=list(["BOOST", "ECO", "SLEEP"]), # No AWAY by default
): cv.ensure_list( ): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True) cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
), ),
@ -233,6 +246,20 @@ CONFIG_SCHEMA = cv.All(
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_ON_ALARM_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HaierAlarmStartTrigger
),
}
),
cv.Optional(CONF_ON_ALARM_END): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
HaierAlarmEndTrigger
),
}
),
} }
), ),
}, },
@ -457,5 +484,15 @@ async def to_code(config):
config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE
) )
) )
for conf in config.get(CONF_ON_ALARM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
)
for conf in config.get(CONF_ON_ALARM_END, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
)
# https://github.com/paveldn/HaierProtocol # https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.24") cg.add_library("pavlodn/HaierProtocol", "0.9.25")

View file

@ -25,13 +25,14 @@ const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
"SENDING_INIT_1", "SENDING_INIT_1",
"SENDING_INIT_2", "SENDING_INIT_2",
"SENDING_FIRST_STATUS_REQUEST", "SENDING_FIRST_STATUS_REQUEST",
"SENDING_ALARM_STATUS_REQUEST", "SENDING_FIRST_ALARM_STATUS_REQUEST",
"IDLE", "IDLE",
"SENDING_STATUS_REQUEST", "SENDING_STATUS_REQUEST",
"SENDING_UPDATE_SIGNAL_REQUEST", "SENDING_UPDATE_SIGNAL_REQUEST",
"SENDING_SIGNAL_LEVEL", "SENDING_SIGNAL_LEVEL",
"SENDING_CONTROL", "SENDING_CONTROL",
"SENDING_ACTION_COMMAND", "SENDING_ACTION_COMMAND",
"SENDING_ALARM_STATUS_REQUEST",
"UNKNOWN" // Should be the last! "UNKNOWN" // Should be the last!
}; };
static_assert( static_assert(

View file

@ -64,7 +64,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_INIT_1 = 0, SENDING_INIT_1 = 0,
SENDING_INIT_2, SENDING_INIT_2,
SENDING_FIRST_STATUS_REQUEST, SENDING_FIRST_STATUS_REQUEST,
SENDING_ALARM_STATUS_REQUEST, SENDING_FIRST_ALARM_STATUS_REQUEST,
// FUNCTIONAL STATE // FUNCTIONAL STATE
IDLE, IDLE,
SENDING_STATUS_REQUEST, SENDING_STATUS_REQUEST,
@ -72,6 +72,7 @@ class HaierClimateBase : public esphome::Component,
SENDING_SIGNAL_LEVEL, SENDING_SIGNAL_LEVEL,
SENDING_CONTROL, SENDING_CONTROL,
SENDING_ACTION_COMMAND, SENDING_ACTION_COMMAND,
SENDING_ALARM_STATUS_REQUEST,
NUM_PROTOCOL_PHASES NUM_PROTOCOL_PHASES
}; };
const char *phase_to_string_(ProtocolPhases phase); const char *phase_to_string_(ProtocolPhases phase);

View file

@ -16,6 +16,7 @@ constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
switch (direction) { switch (direction) {
@ -110,6 +111,14 @@ void HonClimate::start_steri_cleaning() {
} }
} }
void HonClimate::add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback) {
this->alarm_start_callback_.add(std::move(callback));
}
void HonClimate::add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback) {
this->alarm_end_callback_.add(std::move(callback));
}
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type, haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type, haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
@ -194,7 +203,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
switch (this->protocol_phase_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
ESP_LOGI(TAG, "First HVAC status received"); ESP_LOGI(TAG, "First HVAC status received");
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST);
break; break;
case ProtocolPhases::SENDING_ACTION_COMMAND: case ProtocolPhases::SENDING_ACTION_COMMAND:
// Do nothing, phase will be changed in process_phase // Do nothing, phase will be changed in process_phase
@ -251,12 +260,15 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
} }
if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) { if ((this->protocol_phase_ != ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST) &&
(this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)) {
// Don't expect this answer now // Don't expect this answer now
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
} }
memcpy(this->active_alarms_, data + 2, 8); if (data_size < sizeof(active_alarms_) + 2)
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
this->process_alarm_message_(data, data_size, this->protocol_phase_ >= ProtocolPhases::IDLE);
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} else { } else {
@ -265,6 +277,19 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
} }
} }
haier_protocol::HandlerError HonClimate::alarm_status_message_handler_(haier_protocol::FrameType type,
const uint8_t *buffer, size_t size) {
haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
if (size < sizeof(this->active_alarms_) + 2) {
// Log error but confirm anyway to avoid to many messages
result = haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
}
this->process_alarm_message_(buffer, size, true);
this->haier_protocol_.send_answer(haier_protocol::HaierMessage(haier_protocol::FrameType::CONFIRM));
this->last_alarm_request_ = std::chrono::steady_clock::now();
return result;
}
void HonClimate::set_handlers() { void HonClimate::set_handlers() {
// Set handlers // Set handlers
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
@ -291,6 +316,10 @@ void HonClimate::set_handlers() {
haier_protocol::FrameType::REPORT_NETWORK_STATUS, haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_message_handler(
haier_protocol::FrameType::ALARM_STATUS,
std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3));
} }
void HonClimate::dump_config() { void HonClimate::dump_config() {
@ -363,10 +392,12 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
break; break;
#endif #endif
case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS); static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS);
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
this->last_alarm_request_ = now;
} }
break; break;
case ProtocolPhases::SENDING_CONTROL: case ProtocolPhases::SENDING_CONTROL:
@ -417,12 +448,16 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
this->forced_request_status_ = false; this->forced_request_status_ = false;
} else if (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_alarm_request_).count() >
ALARM_STATUS_REQUEST_INTERVAL_MS) {
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
} }
#ifdef USE_WIFI #ifdef USE_WIFI
else if (this->send_wifi_signal_ && else if (this->send_wifi_signal_ &&
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() > (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
}
#endif #endif
} break; } break;
default: default:
@ -452,6 +487,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), 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; hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
control_out_buffer[4] = 0; // This byte should be cleared before setting values
bool has_hvac_settings = false; bool has_hvac_settings = false;
if (this->current_hvac_settings_.valid) { if (this->current_hvac_settings_.valid) {
has_hvac_settings = true; has_hvac_settings = true;
@ -552,31 +588,41 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
out_data->fast_mode = 0; out_data->fast_mode = 0;
out_data->sleep_mode = 0; out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break; break;
case CLIMATE_PRESET_ECO: case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode // Eco is not supported in Fan only mode
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->fast_mode = 0; out_data->fast_mode = 0;
out_data->sleep_mode = 0; out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break; break;
case CLIMATE_PRESET_BOOST: case CLIMATE_PRESET_BOOST:
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
// Boost is not supported in Fan only mode // Boost is not supported in Fan only mode
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0; out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
out_data->sleep_mode = 0; out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break; break;
case CLIMATE_PRESET_AWAY: case CLIMATE_PRESET_AWAY:
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
out_data->fast_mode = 0; out_data->fast_mode = 0;
out_data->sleep_mode = 0; out_data->sleep_mode = 0;
// 10 degrees allowed only in heat mode
out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
break; break;
case CLIMATE_PRESET_SLEEP: case CLIMATE_PRESET_SLEEP:
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
out_data->fast_mode = 0; out_data->fast_mode = 0;
out_data->sleep_mode = 1; out_data->sleep_mode = 1;
out_data->ten_degree = 0;
break; break;
default: default:
ESP_LOGE("Control", "Unsupported preset"); ESP_LOGE("Control", "Unsupported preset");
out_data->quiet_mode = 0;
out_data->fast_mode = 0;
out_data->sleep_mode = 0;
out_data->ten_degree = 0;
break; break;
} }
} }
@ -595,6 +641,50 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
} }
void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new) {
constexpr size_t active_alarms_size = sizeof(this->active_alarms_);
if (size >= active_alarms_size + 2) {
if (check_new) {
size_t alarm_code = 0;
for (int i = active_alarms_size - 1; i >= 0; i--) {
if (packet[2 + i] != active_alarms_[i]) {
uint8_t alarm_bit = 1;
for (int b = 0; b < 8; b++) {
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
: "Unknown";
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
alarm_code, alarm_message);
if (alarm_status) {
this->alarm_start_callback_.call(alarm_code, alarm_message);
this->active_alarm_count_ += 1.0f;
} else {
this->alarm_end_callback_.call(alarm_code, alarm_message);
this->active_alarm_count_ -= 1.0f;
}
}
alarm_bit <<= 1;
alarm_code++;
}
active_alarms_[i] = packet[2 + i];
} else
alarm_code += 8;
}
} else {
float alarm_count = 0.0f;
static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
for (size_t i = 0; i < sizeof(this->active_alarms_); i++) {
alarm_count += (float) (nibble_bits_count[packet[2 + i] & 0x0F] + nibble_bits_count[packet[2 + i] >> 4]);
}
this->active_alarm_count_ = alarm_count;
memcpy(this->active_alarms_, packet + 2, sizeof(this->active_alarms_));
}
}
}
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_) if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
@ -626,6 +716,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
this->preset = CLIMATE_PRESET_BOOST; this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.sleep_mode != 0) { } else if (packet.control.sleep_mode != 0) {
this->preset = CLIMATE_PRESET_SLEEP; this->preset = CLIMATE_PRESET_SLEEP;
} else if (packet.control.ten_degree != 0) {
this->preset = CLIMATE_PRESET_AWAY;
} else { } else {
this->preset = CLIMATE_PRESET_NONE; this->preset = CLIMATE_PRESET_NONE;
} }
@ -882,25 +974,35 @@ void HonClimate::fill_control_messages_queue_() {
// CLimate preset // CLimate preset
{ {
uint8_t fast_mode_buf[] = {0x00, 0xFF}; uint8_t fast_mode_buf[] = {0x00, 0xFF};
uint8_t away_mode_buf[] = {0x00, 0xFF};
if (!new_power) { if (!new_power) {
// If AC is off - no presets allowed // If AC is off - no presets allowed
quiet_mode_buf[1] = 0x00; quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
} else if (climate_control.preset.has_value()) { } else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) { switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE: case CLIMATE_PRESET_NONE:
quiet_mode_buf[1] = 0x00; quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break; break;
case CLIMATE_PRESET_ECO: case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode // Eco is not supported in Fan only mode
quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
fast_mode_buf[1] = 0x00; fast_mode_buf[1] = 0x00;
away_mode_buf[1] = 0x00;
break; break;
case CLIMATE_PRESET_BOOST: case CLIMATE_PRESET_BOOST:
quiet_mode_buf[1] = 0x00; quiet_mode_buf[1] = 0x00;
// Boost is not supported in Fan only mode // Boost is not supported in Fan only mode
fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
away_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_AWAY:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00;
break; break;
default: default:
ESP_LOGE("Control", "Unsupported preset"); ESP_LOGE("Control", "Unsupported preset");
@ -921,6 +1023,13 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::FAST_MODE, (uint8_t) hon_protocol::DataParameters::FAST_MODE,
fast_mode_buf, 2)); fast_mode_buf, 2));
} }
if (away_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
away_mode_buf, 2));
}
} }
// Target temperature // Target temperature
if (climate_control.target_temperature.has_value()) { if (climate_control.target_temperature.has_value()) {

View file

@ -2,6 +2,7 @@
#include <chrono> #include <chrono>
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "haier_base.h" #include "haier_base.h"
namespace esphome { namespace esphome {
@ -52,6 +53,9 @@ class HonClimate : public HaierClimateBase {
void start_steri_cleaning(); void start_steri_cleaning();
void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; }; void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; };
void set_control_method(HonControlMethod method) { this->control_method_ = method; }; void set_control_method(HonControlMethod method) { this->control_method_ = method; };
void add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback);
void add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback);
float get_active_alarm_count() const { return this->active_alarm_count_; }
protected: protected:
void set_handlers() override; void set_handlers() override;
@ -77,8 +81,11 @@ class HonClimate : public HaierClimateBase {
haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type, haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError alarm_status_message_handler_(haier_protocol::FrameType type, const uint8_t *buffer,
size_t size);
// Helper functions // Helper functions
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
void process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new);
void fill_control_messages_queue_(); void fill_control_messages_queue_();
void clear_control_messages_queue_(); void clear_control_messages_queue_();
@ -101,6 +108,26 @@ class HonClimate : public HaierClimateBase {
HonControlMethod control_method_; HonControlMethod control_method_;
esphome::sensor::Sensor *outdoor_sensor_; esphome::sensor::Sensor *outdoor_sensor_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_; std::queue<haier_protocol::HaierMessage> control_messages_queue_;
CallbackManager<void(uint8_t, const char *)> alarm_start_callback_{};
CallbackManager<void(uint8_t, const char *)> alarm_end_callback_{};
float active_alarm_count_{NAN};
std::chrono::steady_clock::time_point last_alarm_request_;
};
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {
public:
explicit HaierAlarmStartTrigger(HonClimate *parent) {
parent->add_alarm_start_callback(
[this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
}
};
class HaierAlarmEndTrigger : public Trigger<uint8_t, const char *> {
public:
explicit HaierAlarmEndTrigger(HonClimate *parent) {
parent->add_alarm_end_callback(
[this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
}
}; };
} // namespace haier } // namespace haier

View file

@ -163,6 +163,62 @@ enum class SubcommandsControl : uint16_t {
// content: all values like in status packet) // content: all values like in status packet)
}; };
const std::string HON_ALARM_MESSAGES[] = {
"Outdoor module failure",
"Outdoor defrost sensor failure",
"Outdoor compressor exhaust sensor failure",
"Outdoor EEPROM abnormality",
"Indoor coil sensor failure",
"Indoor-outdoor communication failure",
"Power supply overvoltage protection",
"Communication failure between panel and indoor unit",
"Outdoor compressor overheat protection",
"Outdoor environmental sensor abnormality",
"Full water protection",
"Indoor EEPROM failure",
"Outdoor out air sensor failure",
"CBD and module communication failure",
"Indoor DC fan failure",
"Outdoor DC fan failure",
"Door switch failure",
"Dust filter needs cleaning reminder",
"Water shortage protection",
"Humidity sensor failure",
"Indoor temperature sensor failure",
"Manipulator limit failure",
"Indoor PM2.5 sensor failure",
"Outdoor PM2.5 sensor failure",
"Indoor heating overload/high load alarm",
"Outdoor AC current protection",
"Outdoor compressor operation abnormality",
"Outdoor DC current protection",
"Outdoor no-load failure",
"CT current abnormality",
"Indoor cooling freeze protection",
"High and low pressure protection",
"Compressor out air temperature is too high",
"Outdoor evaporator sensor failure",
"Outdoor cooling overload",
"Water pump drainage failure",
"Three-phase power supply failure",
"Four-way valve failure",
"External alarm/scraper flow switch failure",
"Temperature cutoff protection alarm",
"Different mode operation failure",
"Electronic expansion valve failure",
"Dual heat source sensor Tw failure",
"Communication failure with the wired controller",
"Indoor unit address duplication failure",
"50Hz zero crossing failure",
"Outdoor unit failure",
"Formaldehyde sensor failure",
"VOC sensor failure",
"CO2 sensor failure",
"Firewall failure",
};
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
} // namespace hon_protocol } // namespace hon_protocol
} // namespace haier } // namespace haier
} // namespace esphome } // namespace esphome

View file

@ -95,7 +95,7 @@ haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cyc
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type, ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
phase_to_string_(this->protocol_phase_)); phase_to_string_(this->protocol_phase_));
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST)
new_phase = ProtocolPhases::SENDING_INIT_1; new_phase = ProtocolPhases::SENDING_INIT_1;
this->set_phase(new_phase); this->set_phase(new_phase);
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
@ -170,9 +170,12 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
break; break;
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
break; break;
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
this->set_phase(ProtocolPhases::IDLE);
break;
case ProtocolPhases::SENDING_CONTROL: case ProtocolPhases::SENDING_CONTROL:
if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
ESP_LOGI(TAG, "Sending control packet"); ESP_LOGI(TAG, "Sending control packet");
@ -343,19 +346,29 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
} else if (climate_control.preset.has_value()) { } else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) { switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE: case CLIMATE_PRESET_NONE:
out_data->ten_degree = 0;
out_data->turbo_mode = 0; out_data->turbo_mode = 0;
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
break; break;
case CLIMATE_PRESET_BOOST: case CLIMATE_PRESET_BOOST:
out_data->ten_degree = 0;
out_data->turbo_mode = 1; out_data->turbo_mode = 1;
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
break; break;
case CLIMATE_PRESET_COMFORT: case CLIMATE_PRESET_COMFORT:
out_data->ten_degree = 0;
out_data->turbo_mode = 0; out_data->turbo_mode = 0;
out_data->quiet_mode = 1; out_data->quiet_mode = 1;
break; break;
case CLIMATE_PRESET_AWAY:
// Only allowed in heat mode
out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
out_data->turbo_mode = 0;
out_data->quiet_mode = 0;
break;
default: default:
ESP_LOGE("Control", "Unsupported preset"); ESP_LOGE("Control", "Unsupported preset");
out_data->ten_degree = 0;
out_data->turbo_mode = 0; out_data->turbo_mode = 0;
out_data->quiet_mode = 0; out_data->quiet_mode = 0;
break; break;
@ -381,6 +394,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
this->preset = CLIMATE_PRESET_BOOST; this->preset = CLIMATE_PRESET_BOOST;
} else if (packet.control.quiet_mode != 0) { } else if (packet.control.quiet_mode != 0) {
this->preset = CLIMATE_PRESET_COMFORT; this->preset = CLIMATE_PRESET_COMFORT;
} else if (packet.control.ten_degree != 0) {
this->preset = CLIMATE_PRESET_AWAY;
} else { } else {
this->preset = CLIMATE_PRESET_NONE; this->preset = CLIMATE_PRESET_NONE;
} }

View file

@ -96,7 +96,7 @@ void HLW8012Component::update() {
this->energy_sensor_->publish_state(energy); this->energy_sensor_->publish_state(energy);
} }
if (this->change_mode_at_++ == this->change_mode_every_) { if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) {
this->current_mode_ = !this->current_mode_; this->current_mode_ = !this->current_mode_;
ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE"); ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
this->change_mode_at_ = 0; this->change_mode_at_ = 0;

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