diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index bd9ceb8072..d277ec06c7 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -46,7 +46,10 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6.7.0 + env: + DOCKER_BUILD_SUMMARY: false + DOCKER_BUILD_RECORD_UPLOAD: false with: context: . file: ./docker/Dockerfile @@ -69,7 +72,10 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6.7.0 + env: + DOCKER_BUILD_SUMMARY: false + DOCKER_BUILD_RECORD_UPLOAD: false with: context: . file: ./docker/Dockerfile diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index d3fe2a89dc..c618a5ca97 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,7 +17,7 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.2.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index ee08a0246d..8112c4e0ff 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.2.0 with: python-version: "3.11" diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 91c02b0a17..891367d16a 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.2.0 with: python-version: "3.9" - name: Set up Docker Buildx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8e93248bb..7c4fa65695 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: paths: - "**" - "!.github/workflows/*.yml" + - "!.github/actions/build-image/*" - ".github/workflows/ci.yml" - "!.yamllint" - "!.github/dependabot.yml" @@ -40,7 +41,7 @@ jobs: run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.2.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment @@ -396,7 +397,7 @@ jobs: file: ${{ fromJson(needs.list-components.outputs.components) }} steps: - name: Install dependencies - run: sudo apt-get install libsodium-dev libsdl2-dev + run: sudo apt-get install libsdl2-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 @@ -450,7 +451,7 @@ jobs: run: echo ${{ matrix.components }} - name: Install dependencies - run: sudo apt-get install libsodium-dev libsdl2-dev + run: sudo apt-get install libsdl2-dev - name: Check out code from GitHub uses: actions/checkout@v4.1.7 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..ddeb0a99d2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,91 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + workflow_dispatch: + schedule: + - cron: "30 18 * * 4" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + # - language: c-cpp + # build-mode: autobuild + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d454076c84..522de63360 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.2.0 with: python-version: "3.x" - name: Set up python environment @@ -65,7 +65,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 deploy-docker: name: Build ESPHome ${{ matrix.platform }} @@ -85,7 +85,7 @@ jobs: steps: - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.2.0 with: python-version: "3.9" @@ -141,7 +141,7 @@ jobs: echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT - name: Upload digests - uses: actions/upload-artifact@v4.3.4 + uses: actions/upload-artifact@v4.4.0 with: name: digests-${{ steps.sanitize.outputs.name }} path: /tmp/digests diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 89a3627c64..1418867240 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -22,7 +22,7 @@ jobs: path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.2.0 with: python-version: 3.12 @@ -36,7 +36,7 @@ jobs: python ./script/sync-device_class.py - name: Commit changes - uses: peter-evans/create-pull-request@v6.1.0 + uses: peter-evans/create-pull-request@v7.0.2 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot diff --git a/.gitignore b/.gitignore index 0c9a878400..79820249ac 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ sdkconfig.* .tests/ /components +/managed_components + diff --git a/CODEOWNERS b/CODEOWNERS index cabb76e6fb..1a4dd20689 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -46,6 +46,7 @@ esphome/components/async_tcp/* @OttoWinter esphome/components/at581x/* @X-Ryl669 esphome/components/atc_mithermometer/* @ahpohl esphome/components/atm90e26/* @danieltwagner +esphome/components/atm90e32/* @circuitsetup @descipher esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter @@ -57,15 +58,21 @@ esphome/components/beken_spi_led_strip/* @Mat931 esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/bk72xx/* @kuba2k2 +esphome/components/bl0906/* @athom-tech @jesserockz @tarontop esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @tobias- -esphome/components/bl0942/* @dbuezas +esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bme280_base/* @esphome/core esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth +esphome/components/bme68x_bsec2/* @kbx81 @neffs +esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs esphome/components/bmi160/* @flaviut +esphome/components/bmp280_base/* @ademuri +esphome/components/bmp280_i2c/* @ademuri +esphome/components/bmp280_spi/* @ademuri esphome/components/bmp3xx/* @latonita esphome/components/bmp3xx_base/* @latonita @martgras esphome/components/bmp3xx_i2c/* @latonita @@ -79,6 +86,7 @@ esphome/components/cap1188/* @mreditor97 esphome/components/captive_portal/* @OttoWinter esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke +esphome/components/ch422g/* @jesterret esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz @@ -166,7 +174,10 @@ esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode -esphome/components/homeassistant/* @OttoWinter +esphome/components/hmac_md5/* @dwmw2 +esphome/components/homeassistant/* @OttoWinter @esphome/core +esphome/components/homeassistant/number/* @landonr +esphome/components/homeassistant/switch/* @Links2004 esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp2_i2c/* @jpfaff @@ -216,6 +227,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @latonita @sjtrny +esphome/components/ltr501/* @latonita esphome/components/ltr_als_ps/* @latonita esphome/components/lvgl/* @clydebarrow esphome/components/m5stack_8angle/* @rnauber @@ -276,6 +288,8 @@ esphome/components/nfc/* @jesserockz @kbx81 esphome/components/noblex/* @AGalfra esphome/components/number/* @esphome/core esphome/components/one_wire/* @ssieb +esphome/components/online_image/* @guillempages +esphome/components/opentherm/* @olegtarasov esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pca6416a/* @Mat931 @@ -376,6 +390,7 @@ esphome/components/st7701s/* @clydebarrow esphome/components/st7735/* @SenexCrenshaw esphome/components/st7789v/* @kbx81 esphome/components/st7920/* @marsjan155 +esphome/components/statsd/* @Links2004 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/sun_gtil2/* @Mat931 @@ -416,6 +431,7 @@ esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra esphome/components/uart/* @esphome/core esphome/components/uart/button/* @ssieb +esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli esphome/components/ultrasonic/* @OttoWinter @@ -451,6 +467,7 @@ esphome/components/wl_134/* @hobbypunk90 esphome/components/x9c/* @EtienneMD esphome/components/xgzp68xx/* @gcormier esphome/components/xiaomi_hhccjcy10/* @fariouche +esphome/components/xiaomi_lywsd02mmc/* @juanluss31 esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/docker/Dockerfile b/docker/Dockerfile index 16f37274c6..85823687c2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -33,9 +33,9 @@ RUN \ python3-venv=3.11.2-1+b1 \ python3-wheel=0.38.4-2 \ iputils-ping=3:20221126-1 \ - git=1:2.39.2-1.1 \ - curl=7.88.1-10+deb12u6 \ - openssh-client=1:9.2p1-2+deb12u2 \ + git=1:2.39.5-0+deb12u1 \ + curl=7.88.1-10+deb12u7 \ + openssh-client=1:9.2p1-2+deb12u3 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ libmagic1=1:5.44-3 \ @@ -49,7 +49,7 @@ RUN \ zlib1g-dev=1:1.2.13.dfsg-1 \ libjpeg-dev=1:2.1.5-2 \ libfreetype-dev=2.12.1+dfsg-5+deb12u3 \ - libssl-dev=3.0.13-1~deb12u1 \ + libssl-dev=3.0.14-1~deb12u2 \ libffi-dev=3.4.4-1 \ libopenjp2-7=2.5.0-2 \ libtiff6=4.5.0-6+deb12u1 \ @@ -96,14 +96,19 @@ RUN \ # First install requirements to leverage caching when requirements don't change # tmpfs is for https://github.com/rust-lang/cargo/issues/8719 -COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini / +COPY requirements.txt requirements_optional.txt / RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ - export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ + curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \ + && pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \ + && rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \ + && export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ fi; \ CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \ pip3 install \ - --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ - && /platformio_install_deps.py /platformio.ini --libraries + --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt + +COPY script/platformio_install_deps.py platformio.ini / +RUN /platformio_install_deps.py /platformio.ini --libraries # Avoid unsafe git error when container user and file config volume permissions don't match RUN git config --system --add safe.directory '*' diff --git a/docker/docker_entrypoint.sh b/docker/docker_entrypoint.sh index 397b1528c5..1b9224244c 100755 --- a/docker/docker_entrypoint.sh +++ b/docker/docker_entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # If /cache is mounted, use that as PIO's coredir # otherwise use path in /config (so that PIO packages aren't downloaded on each compile) diff --git a/esphome/__main__.py b/esphome/__main__.py index 7237a04717..cf2741dbdb 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -38,7 +38,7 @@ from esphome.const import ( SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine -from esphome.helpers import indent, is_ip_address +from esphome.helpers import indent, is_ip_address, get_bool_env from esphome.log import Fore, color, setup_log from esphome.util import ( get_serial_ports, @@ -731,7 +731,11 @@ POST_CONFIG_ACTIONS = { def parse_args(argv): options_parser = argparse.ArgumentParser(add_help=False) options_parser.add_argument( - "-v", "--verbose", help="Enable verbose ESPHome logs.", action="store_true" + "-v", + "--verbose", + help="Enable verbose ESPHome logs.", + action="store_true", + default=get_bool_env("ESPHOME_VERBOSE"), ) options_parser.add_argument( "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" @@ -972,13 +976,6 @@ def run_esphome(argv): args.command == "dashboard", ) - if sys.version_info < (3, 8, 0): - _LOGGER.error( - "You're running ESPHome with Python <3.8. ESPHome is no longer compatible " - "with this Python version. Please reinstall ESPHome with Python 3.8+" - ) - return 1 - if args.command in PRE_CONFIG_ACTIONS: try: return PRE_CONFIG_ACTIONS[args.command](args) diff --git a/esphome/components/ade7953_spi/ade7953_spi.cpp b/esphome/components/ade7953_spi/ade7953_spi.cpp index cfd5d71d0a..77a2a8adc7 100644 --- a/esphome/components/ade7953_spi/ade7953_spi.cpp +++ b/esphome/components/ade7953_spi/ade7953_spi.cpp @@ -60,7 +60,7 @@ bool AdE7953Spi::ade_read_16(uint16_t reg, uint16_t *value) { this->write_byte16(reg); this->transfer_byte(0x80); uint8_t recv[2]; - this->read_array(recv, 4); + this->read_array(recv, 2); *value = encode_uint16(recv[0], recv[1]); this->disable(); return false; diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp index a32128e992..8c8c514fdb 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.cpp @@ -14,8 +14,6 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { ESP_LOGD(TAG, "version = %d", value->version); if (value->version == 1) { - ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); - if (this->humidity_sensor_ != nullptr) { this->humidity_sensor_->publish_state(value->humidity / 2.0f); } @@ -43,6 +41,10 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { this->tvoc_sensor_->publish_state(value->voc); } + + if (this->illuminance_sensor_ != nullptr) { + this->illuminance_sensor_->publish_state(value->ambientLight); + } } else { ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); } @@ -68,6 +70,7 @@ void AirthingsWavePlus::dump_config() { LOG_SENSOR(" ", "Radon", this->radon_sensor_); LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); } AirthingsWavePlus::AirthingsWavePlus() { diff --git a/esphome/components/airthings_wave_plus/airthings_wave_plus.h b/esphome/components/airthings_wave_plus/airthings_wave_plus.h index 23c8cbb166..bd7a40ef8b 100644 --- a/esphome/components/airthings_wave_plus/airthings_wave_plus.h +++ b/esphome/components/airthings_wave_plus/airthings_wave_plus.h @@ -22,6 +22,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; } protected: bool is_valid_radon_value_(uint16_t radon); @@ -32,6 +33,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { sensor::Sensor *radon_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *illuminance_sensor_{nullptr}; struct WavePlusReadings { uint8_t version; diff --git a/esphome/components/airthings_wave_plus/sensor.py b/esphome/components/airthings_wave_plus/sensor.py index 643a2bfb68..d28c7e2abc 100644 --- a/esphome/components/airthings_wave_plus/sensor.py +++ b/esphome/components/airthings_wave_plus/sensor.py @@ -12,6 +12,9 @@ from esphome.const import ( CONF_CO2, UNIT_BECQUEREL_PER_CUBIC_METER, UNIT_PARTS_PER_MILLION, + CONF_ILLUMINANCE, + UNIT_LUX, + DEVICE_CLASS_ILLUMINANCE, ) DEPENDENCIES = airthings_wave_base.DEPENDENCIES @@ -45,6 +48,12 @@ CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( device_class=DEVICE_CLASS_CARBON_DIOXIDE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), } ) @@ -62,3 +71,6 @@ async def to_code(config): if config_co2 := config.get(CONF_CO2): sens = await sensor.new_sensor(config_co2) cg.add(var.set_co2(sens)) + if config_illuminance := config.get(CONF_ILLUMINANCE): + sens = await sensor.new_sensor(config_illuminance) + cg.add(var.set_illuminance(sens)) diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 7ad4358011..8987d708fd 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -1,16 +1,17 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import web_server from esphome import automation from esphome.automation import maybe_simple_id -from esphome.core import CORE, coroutine_with_priority +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_CODE, CONF_ID, + CONF_MQTT_ID, CONF_ON_STATE, CONF_TRIGGER_ID, - CONF_CODE, CONF_WEB_SERVER_ID, ) +from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@grahambrown11", "@hwstar"] @@ -77,67 +78,72 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_( "AlarmControlPanelCondition", automation.Condition ) -ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( - web_server.WEBSERVER_SORTING_SCHEMA -).extend( - { - cv.GenerateID(): cv.declare_id(AlarmControlPanel), - cv.Optional(CONF_ON_STATE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), - } - ), - cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), - } - ), - cv.Optional(CONF_ON_ARMING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), - } - ), - cv.Optional(CONF_ON_PENDING): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), - } - ), - cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), - } - ), - cv.Optional(CONF_ON_DISARMED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), - } - ), - cv.Optional(CONF_ON_CLEARED): automation.validate_automation( - { - 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), - } - ), - } +ALARM_CONTROL_PANEL_SCHEMA = ( + cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend( + { + cv.GenerateID(): cv.declare_id(AlarmControlPanel), + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( + mqtt.MQTTAlarmControlPanelComponent + ), + cv.Optional(CONF_ON_STATE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), + } + ), + cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), + } + ), + cv.Optional(CONF_ON_ARMING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger), + } + ), + cv.Optional(CONF_ON_PENDING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger), + } + ), + cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger), + } + ), + cv.Optional(CONF_ON_DISARMED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger), + } + ), + cv.Optional(CONF_ON_CLEARED): automation.validate_automation( + { + 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), + } + ), + } + ) ) ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( @@ -192,6 +198,9 @@ async def setup_alarm_control_panel_core_(var, config): if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None: web_server_ = await cg.get_variable(webserver_id) web_server.add_entity_to_sorting_list(web_server_, var, config) + if mqtt_id := config.get(CONF_MQTT_ID): + mqtt_ = cg.new_Pvariable(mqtt_id, var) + await mqtt.register_mqtt_component(mqtt_, config) async def register_alarm_control_panel(var, config): diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index dbfc82c891..eb3d09ac96 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -1,26 +1,26 @@ import logging from esphome import automation, core +import esphome.codegen as cg from esphome.components import font import esphome.components.image as espImage from esphome.components.image import ( CONF_USE_TRANSPARENCY, LOCAL_SCHEMA, - WEB_SCHEMA, - SOURCE_WEB, SOURCE_LOCAL, + SOURCE_WEB, + WEB_SCHEMA, ) import esphome.config_validation as cv -import esphome.codegen as cg from esphome.const import ( CONF_FILE, CONF_ID, + CONF_PATH, CONF_RAW_DATA_ID, CONF_REPEAT, CONF_RESIZE, - CONF_TYPE, CONF_SOURCE, - CONF_PATH, + CONF_TYPE, CONF_URL, ) from esphome.core import CORE, HexInt @@ -172,6 +172,9 @@ async def to_code(config): path = CORE.relative_config_path(conf_file[CONF_PATH]) elif conf_file[CONF_SOURCE] == SOURCE_WEB: path = espImage.compute_local_image_path(conf_file).as_posix() + else: + raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}") + try: image = Image.open(path) except Exception as e: @@ -183,13 +186,12 @@ async def to_code(config): new_width_max, new_height_max = config[CONF_RESIZE] ratio = min(new_width_max / width, new_height_max / height) width, height = int(width * ratio), int(height * ratio) - else: - if width > 500 or height > 500: - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, - ) + elif width > 500 or height > 500: + _LOGGER.warning( + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, + ) transparent = config[CONF_USE_TRANSPARENCY] @@ -306,6 +308,8 @@ async def to_code(config): if transparent: alpha = image.split()[-1] has_alpha = alpha.getextrema()[0] < 0xFF + else: + has_alpha = False frame = image.convert("1", dither=Image.Dither.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index d6b4416af8..27de5c873b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -1,25 +1,27 @@ import base64 -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_ACTION, + CONF_ACTIONS, CONF_DATA, CONF_DATA_TEMPLATE, + CONF_EVENT, CONF_ID, CONF_KEY, + CONF_ON_CLIENT_CONNECTED, + CONF_ON_CLIENT_DISCONNECTED, CONF_PASSWORD, CONF_PORT, CONF_REBOOT_TIMEOUT, CONF_SERVICE, - CONF_VARIABLES, CONF_SERVICES, - CONF_TRIGGER_ID, - CONF_EVENT, CONF_TAG, - CONF_ON_CLIENT_CONNECTED, - CONF_ON_CLIENT_DISCONNECTED, + CONF_TRIGGER_ID, + CONF_VARIABLES, ) from esphome.core import coroutine_with_priority @@ -63,40 +65,51 @@ def validate_encryption_key(value): return value -CONFIG_SCHEMA = cv.Schema( +ACTIONS_SCHEMA = automation.validate_automation( { - cv.GenerateID(): cv.declare_id(APIServer), - cv.Optional(CONF_PORT, default=6053): cv.port, - cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, - cv.Optional( - CONF_REBOOT_TIMEOUT, default="15min" - ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SERVICES): automation.validate_automation( + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), + cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), - cv.Required(CONF_SERVICE): cv.valid_name, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema( - { - cv.validate_id_name: cv.one_of( - *SERVICE_ARG_NATIVE_TYPES, lower=True - ), - } - ), + cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), } ), - cv.Optional(CONF_ENCRYPTION): cv.Schema( - { - cv.Required(CONF_KEY): validate_encryption_key, - } - ), - cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( - single=True - ), - cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( - single=True - ), - } -).extend(cv.COMPONENT_SCHEMA) + }, + cv.All( + cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), + cv.rename_key(CONF_SERVICE, CONF_ACTION), + ), +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(APIServer), + cv.Optional(CONF_PORT, default=6053): cv.port, + cv.Optional(CONF_PASSWORD, default=""): cv.string_strict, + cv.Optional( + CONF_REBOOT_TIMEOUT, default="15min" + ): cv.positive_time_period_milliseconds, + cv.Exclusive( + CONF_SERVICES, group_of_exclusion=CONF_ACTIONS + ): ACTIONS_SCHEMA, + cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, + cv.Optional(CONF_ENCRYPTION): cv.Schema( + { + cv.Required(CONF_KEY): validate_encryption_key, + } + ), + cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( + single=True + ), + cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation( + single=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.rename_key(CONF_SERVICES, CONF_ACTIONS), +) @coroutine_with_priority(40.0) @@ -108,7 +121,7 @@ async def to_code(config): cg.add(var.set_password(config[CONF_PASSWORD])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - for conf in config.get(CONF_SERVICES, []): + for conf in config.get(CONF_ACTIONS, []): template_args = [] func_args = [] service_arg_names = [] @@ -119,7 +132,7 @@ async def to_code(config): service_arg_names.append(name) templ = cg.TemplateArguments(*template_args) trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], templ, conf[CONF_SERVICE], service_arg_names + conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names ) cg.add(var.register_user_service(trigger)) await automation.build_automation(trigger, func_args, conf) @@ -142,7 +155,7 @@ async def to_code(config): decoded = base64.b64decode(encryption_config[CONF_KEY]) cg.add(var.set_noise_psk(list(decoded))) cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.4") + cg.add_library("esphome/noise-c", "0.1.6") else: cg.add_define("USE_API_PLAINTEXT") @@ -152,28 +165,43 @@ async def to_code(config): KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)}) -HOMEASSISTANT_SERVICE_ACTION_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(APIServer), - cv.Required(CONF_SERVICE): cv.templatable(cv.string), - cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, - cv.Optional(CONF_VARIABLES, default={}): cv.Schema( - {cv.string: cv.returning_lambda} - ), - } + +HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(APIServer), + cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.templatable( + cv.string + ), + cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.templatable( + cv.string + ), + cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA, + cv.Optional(CONF_VARIABLES, default={}): cv.Schema( + {cv.string: cv.returning_lambda} + ), + } + ), + cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), + cv.rename_key(CONF_SERVICE, CONF_ACTION), ) +@automation.register_action( + "homeassistant.action", + HomeAssistantServiceCallAction, + HOMEASSISTANT_ACTION_ACTION_SCHEMA, +) @automation.register_action( "homeassistant.service", HomeAssistantServiceCallAction, - HOMEASSISTANT_SERVICE_ACTION_SCHEMA, + HOMEASSISTANT_ACTION_ACTION_SCHEMA, ) async def homeassistant_service_to_code(config, action_id, template_arg, args): serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, False) - templ = await cg.templatable(config[CONF_SERVICE], args, None) + templ = await cg.templatable(config[CONF_ACTION], args, None) cg.add(var.set_service(templ)) for key, value in config[CONF_DATA].items(): templ = await cg.templatable(value, args, None) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 812a1d74ae..92fb57b711 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -686,6 +686,7 @@ message SubscribeHomeAssistantStateResponse { option (source) = SOURCE_SERVER; string entity_id = 1; string attribute = 2; + bool once = 3; } message HomeAssistantStateResponse { @@ -1106,6 +1107,19 @@ enum MediaPlayerCommand { MEDIA_PLAYER_COMMAND_MUTE = 3; MEDIA_PLAYER_COMMAND_UNMUTE = 4; } +enum MediaPlayerFormatPurpose { + MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0; + MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1; +} +message MediaPlayerSupportedFormat { + option (ifdef) = "USE_MEDIA_PLAYER"; + + string format = 1; + uint32 sample_rate = 2; + uint32 num_channels = 3; + MediaPlayerFormatPurpose purpose = 4; + uint32 sample_bytes = 5; +} message ListEntitiesMediaPlayerResponse { option (id) = 63; option (source) = SOURCE_SERVER; @@ -1121,6 +1135,8 @@ message ListEntitiesMediaPlayerResponse { EntityCategory entity_category = 7; bool supports_pause = 8; + + repeated MediaPlayerSupportedFormat supported_formats = 9; } message MediaPlayerStateResponse { option (id) = 64; @@ -1538,6 +1554,53 @@ message VoiceAssistantTimerEventResponse { bool is_active = 6; } +message VoiceAssistantAnnounceRequest { + option (id) = 119; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + string media_id = 1; + string text = 2; +} + +message VoiceAssistantAnnounceFinished { + option (id) = 120; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + bool success = 1; +} + +message VoiceAssistantWakeWord { + uint32 id = 1; + string wake_word = 2; + repeated string trained_languages = 3; +} + +message VoiceAssistantConfigurationRequest { + option (id) = 121; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; +} + +message VoiceAssistantConfigurationResponse { + option (id) = 122; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + repeated VoiceAssistantWakeWord available_wake_words = 1; + repeated uint32 active_wake_words = 2; + uint32 max_active_wake_words = 3; +} + +message VoiceAssistantSetConfiguration { + option (id) = 123; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + repeated uint32 active_wake_words = 1; +} + // ==================== ALARM CONTROL PANEL ==================== enum AlarmControlPanelState { ALARM_STATE_DISARMED = 0; @@ -1872,6 +1935,11 @@ message UpdateStateResponse { string release_summary = 9; string release_url = 10; } +enum UpdateCommand { + UPDATE_COMMAND_NONE = 0; + UPDATE_COMMAND_UPDATE = 1; + UPDATE_COMMAND_CHECK = 2; +} message UpdateCommandRequest { option (id) = 118; option (source) = SOURCE_CLIENT; @@ -1879,5 +1947,5 @@ message UpdateCommandRequest { option (no_delay) = true; fixed32 key = 1; - bool install = 2; + UpdateCommand command = 2; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 2e73a8336e..20e9a45314 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -179,6 +179,7 @@ void APIConnection::loop() { SubscribeHomeAssistantStateResponse resp; resp.entity_id = it.entity_id; resp.attribute = it.attribute.value(); + resp.once = it.once; if (this->send_subscribe_home_assistant_state_response(resp)) { state_subs_at_++; } @@ -1025,6 +1026,16 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play auto traits = media_player->get_traits(); msg.supports_pause = traits.get_supports_pause(); + for (auto &supported_format : traits.get_supported_formats()) { + MediaPlayerSupportedFormat media_format; + media_format.format = supported_format.format; + media_format.sample_rate = supported_format.sample_rate; + media_format.num_channels = supported_format.num_channels; + media_format.purpose = static_cast(supported_format.purpose); + media_format.sample_bytes = supported_format.sample_bytes; + msg.supported_formats.push_back(media_format); + } + return this->send_list_entities_media_player_response(msg); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { @@ -1203,6 +1214,16 @@ void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistant } }; +void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) { + if (voice_assistant::global_voice_assistant != nullptr) { + if (voice_assistant::global_voice_assistant->get_api_connection() != this) { + return; + } + + voice_assistant::global_voice_assistant->on_announce(msg); + } +} + #endif #ifdef USE_ALARM_CONTROL_PANEL @@ -1328,7 +1349,20 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) { if (update == nullptr) return; - update->perform(); + switch (msg.command) { + case enums::UPDATE_COMMAND_UPDATE: + update->perform(); + break; + case enums::UPDATE_COMMAND_CHECK: + update->check(); + break; + case enums::UPDATE_COMMAND_NONE: + ESP_LOGE(TAG, "UPDATE_COMMAND_NONE not handled. Check client is sending the correct command"); + break; + default: + ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command); + break; + } } #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 714e806470..e8d66a5e07 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -151,6 +151,7 @@ class APIConnection : public APIServerConnection { void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; + void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; #endif #ifdef USE_ALARM_CONTROL_PANEL diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index e6e905c6d1..791de29511 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -387,6 +387,18 @@ template<> const char *proto_enum_to_string(enums::Me } #endif #ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::MediaPlayerFormatPurpose value) { + switch (value) { + case enums::MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT: + return "MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT"; + case enums::MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT: + return "MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP template<> const char *proto_enum_to_string(enums::BluetoothDeviceRequestType value) { switch (value) { @@ -567,6 +579,20 @@ template<> const char *proto_enum_to_string(enums::ValveO } } #endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::UpdateCommand value) { + switch (value) { + case enums::UPDATE_COMMAND_NONE: + return "UPDATE_COMMAND_NONE"; + case enums::UPDATE_COMMAND_UPDATE: + return "UPDATE_COMMAND_UPDATE"; + case enums::UPDATE_COMMAND_CHECK: + return "UPDATE_COMMAND_CHECK"; + default: + return "UNKNOWN"; + } +} +#endif bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -3095,6 +3121,16 @@ void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { out.append("SubscribeHomeAssistantStatesRequest {}"); } #endif +bool SubscribeHomeAssistantStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->once = value.as_bool(); + return true; + } + default: + return false; + } +} bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -3112,6 +3148,7 @@ bool SubscribeHomeAssistantStateResponse::decode_length(uint32_t field_id, Proto void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->entity_id); buffer.encode_string(2, this->attribute); + buffer.encode_bool(3, this->once); } #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { @@ -3124,6 +3161,10 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const { out.append(" attribute: "); out.append("'").append(this->attribute).append("'"); out.append("\n"); + + out.append(" once: "); + out.append(YESNO(this->once)); + out.append("\n"); out.append("}"); } #endif @@ -5094,6 +5135,74 @@ void ButtonCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool MediaPlayerSupportedFormat::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->sample_rate = value.as_uint32(); + return true; + } + case 3: { + this->num_channels = value.as_uint32(); + return true; + } + case 4: { + this->purpose = value.as_enum(); + return true; + } + case 5: { + this->sample_bytes = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool MediaPlayerSupportedFormat::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->format = value.as_string(); + return true; + } + default: + return false; + } +} +void MediaPlayerSupportedFormat::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->format); + buffer.encode_uint32(2, this->sample_rate); + buffer.encode_uint32(3, this->num_channels); + buffer.encode_enum(4, this->purpose); + buffer.encode_uint32(5, this->sample_bytes); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void MediaPlayerSupportedFormat::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerSupportedFormat {\n"); + out.append(" format: "); + out.append("'").append(this->format).append("'"); + out.append("\n"); + + out.append(" sample_rate: "); + sprintf(buffer, "%" PRIu32, this->sample_rate); + out.append(buffer); + out.append("\n"); + + out.append(" num_channels: "); + sprintf(buffer, "%" PRIu32, this->num_channels); + out.append(buffer); + out.append("\n"); + + out.append(" purpose: "); + out.append(proto_enum_to_string(this->purpose)); + out.append("\n"); + + out.append(" sample_bytes: "); + sprintf(buffer, "%" PRIu32, this->sample_bytes); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -5130,6 +5239,10 @@ bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLeng this->icon = value.as_string(); return true; } + case 9: { + this->supported_formats.push_back(value.as_message()); + return true; + } default: return false; } @@ -5153,6 +5266,9 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_enum(7, this->entity_category); buffer.encode_bool(8, this->supports_pause); + for (auto &it : this->supported_formats) { + buffer.encode_message(9, it, true); + } } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { @@ -5190,6 +5306,12 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { out.append(" supports_pause: "); out.append(YESNO(this->supports_pause)); out.append("\n"); + + for (const auto &it : this->supported_formats) { + out.append(" supported_formats: "); + it.dump_to(out); + out.append("\n"); + } out.append("}"); } #endif @@ -6949,6 +7071,202 @@ void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->media_id = value.as_string(); + return true; + } + case 2: { + this->text = value.as_string(); + return true; + } + default: + return false; + } +} +void VoiceAssistantAnnounceRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->media_id); + buffer.encode_string(2, this->text); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAnnounceRequest {\n"); + out.append(" media_id: "); + out.append("'").append(this->media_id).append("'"); + out.append("\n"); + + out.append(" text: "); + out.append("'").append(this->text).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantAnnounceFinished::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->success = value.as_bool(); + return true; + } + default: + return false; + } +} +void VoiceAssistantAnnounceFinished::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->success); } +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantAnnounceFinished {\n"); + out.append(" success: "); + out.append(YESNO(this->success)); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->id = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool VoiceAssistantWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->wake_word = value.as_string(); + return true; + } + case 3: { + this->trained_languages.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +void VoiceAssistantWakeWord::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->id); + buffer.encode_string(2, this->wake_word); + for (auto &it : this->trained_languages) { + buffer.encode_string(3, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantWakeWord::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantWakeWord {\n"); + out.append(" id: "); + sprintf(buffer, "%" PRIu32, this->id); + out.append(buffer); + out.append("\n"); + + out.append(" wake_word: "); + out.append("'").append(this->wake_word).append("'"); + out.append("\n"); + + for (const auto &it : this->trained_languages) { + out.append(" trained_languages: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +#endif +void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { + out.append("VoiceAssistantConfigurationRequest {}"); +} +#endif +bool VoiceAssistantConfigurationResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->active_wake_words.push_back(value.as_uint32()); + return true; + } + case 3: { + this->max_active_wake_words = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool VoiceAssistantConfigurationResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->available_wake_words.push_back(value.as_message()); + return true; + } + default: + return false; + } +} +void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->available_wake_words) { + buffer.encode_message(1, it, true); + } + for (auto &it : this->active_wake_words) { + buffer.encode_uint32(2, it, true); + } + buffer.encode_uint32(3, this->max_active_wake_words); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantConfigurationResponse {\n"); + for (const auto &it : this->available_wake_words) { + out.append(" available_wake_words: "); + it.dump_to(out); + out.append("\n"); + } + + for (const auto &it : this->active_wake_words) { + out.append(" active_wake_words: "); + sprintf(buffer, "%" PRIu32, it); + out.append(buffer); + out.append("\n"); + } + + out.append(" max_active_wake_words: "); + sprintf(buffer, "%" PRIu32, this->max_active_wake_words); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +#endif +bool VoiceAssistantSetConfiguration::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->active_wake_words.push_back(value.as_uint32()); + return true; + } + default: + return false; + } +} +void VoiceAssistantSetConfiguration::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->active_wake_words) { + buffer.encode_uint32(1, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void VoiceAssistantSetConfiguration::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("VoiceAssistantSetConfiguration {\n"); + for (const auto &it : this->active_wake_words) { + out.append(" active_wake_words: "); + sprintf(buffer, "%" PRIu32, it); + out.append(buffer); + out.append("\n"); + } + out.append("}"); +} +#endif bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -8596,7 +8914,7 @@ void UpdateStateResponse::dump_to(std::string &out) const { bool UpdateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { - this->install = value.as_bool(); + this->command = value.as_enum(); return true; } default: @@ -8615,7 +8933,7 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { } void UpdateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); - buffer.encode_bool(2, this->install); + buffer.encode_enum(2, this->command); } #ifdef HAS_PROTO_MESSAGE_DUMP void UpdateCommandRequest::dump_to(std::string &out) const { @@ -8626,8 +8944,8 @@ void UpdateCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" install: "); - out.append(YESNO(this->install)); + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); out.append("\n"); out.append("}"); } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ef051eecf1..8631884f94 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -156,6 +156,10 @@ enum MediaPlayerCommand : uint32_t { MEDIA_PLAYER_COMMAND_MUTE = 3, MEDIA_PLAYER_COMMAND_UNMUTE = 4, }; +enum MediaPlayerFormatPurpose : uint32_t { + MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0, + MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1, +}; enum BluetoothDeviceRequestType : uint32_t { BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0, BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1, @@ -227,6 +231,11 @@ enum ValveOperation : uint32_t { VALVE_OPERATION_IS_OPENING = 1, VALVE_OPERATION_IS_CLOSING = 2, }; +enum UpdateCommand : uint32_t { + UPDATE_COMMAND_NONE = 0, + UPDATE_COMMAND_UPDATE = 1, + UPDATE_COMMAND_CHECK = 2, +}; } // namespace enums @@ -831,6 +840,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { public: std::string entity_id{}; std::string attribute{}; + bool once{false}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -838,6 +848,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; class HomeAssistantStateResponse : public ProtoMessage { public: @@ -1260,6 +1271,22 @@ class ButtonCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +class MediaPlayerSupportedFormat : public ProtoMessage { + public: + std::string format{}; + uint32_t sample_rate{0}; + uint32_t num_channels{0}; + enums::MediaPlayerFormatPurpose purpose{}; + uint32_t sample_bytes{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class ListEntitiesMediaPlayerResponse : public ProtoMessage { public: std::string object_id{}; @@ -1270,6 +1297,7 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage { bool disabled_by_default{false}; enums::EntityCategory entity_category{}; bool supports_pause{false}; + std::vector supported_formats{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; @@ -1798,6 +1826,77 @@ class VoiceAssistantTimerEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class VoiceAssistantAnnounceRequest : public ProtoMessage { + public: + std::string media_id{}; + std::string text{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class VoiceAssistantAnnounceFinished : public ProtoMessage { + public: + bool success{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class VoiceAssistantWakeWord : public ProtoMessage { + public: + uint32_t id{0}; + std::string wake_word{}; + std::vector trained_languages{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class VoiceAssistantConfigurationRequest : public ProtoMessage { + public: + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +class VoiceAssistantConfigurationResponse : public ProtoMessage { + public: + std::vector available_wake_words{}; + std::vector active_wake_words{}; + uint32_t max_active_wake_words{0}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class VoiceAssistantSetConfiguration : public ProtoMessage { + public: + std::vector active_wake_words{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { public: std::string object_id{}; @@ -2175,7 +2274,7 @@ class UpdateStateResponse : public ProtoMessage { class UpdateCommandRequest : public ProtoMessage { public: uint32_t key{0}; - bool install{false}; + enums::UpdateCommand command{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 269a755e9e..454f20d50a 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -486,6 +486,29 @@ bool APIServerConnectionBase::send_voice_assistant_audio(const VoiceAssistantAud #endif #ifdef USE_VOICE_ASSISTANT #endif +#ifdef USE_VOICE_ASSISTANT +#endif +#ifdef USE_VOICE_ASSISTANT +bool APIServerConnectionBase::send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_voice_assistant_announce_finished: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 120); +} +#endif +#ifdef USE_VOICE_ASSISTANT +#endif +#ifdef USE_VOICE_ASSISTANT +bool APIServerConnectionBase::send_voice_assistant_configuration_response( + const VoiceAssistantConfigurationResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_voice_assistant_configuration_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 122); +} +#endif +#ifdef USE_VOICE_ASSISTANT +#endif #ifdef USE_ALARM_CONTROL_PANEL bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response( const ListEntitiesAlarmControlPanelResponse &msg) { @@ -1135,6 +1158,39 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str()); #endif this->on_update_command_request(msg); +#endif + break; + } + case 119: { +#ifdef USE_VOICE_ASSISTANT + VoiceAssistantAnnounceRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str()); +#endif + this->on_voice_assistant_announce_request(msg); +#endif + break; + } + case 121: { +#ifdef USE_VOICE_ASSISTANT + VoiceAssistantConfigurationRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); +#endif + this->on_voice_assistant_configuration_request(msg); +#endif + break; + } + case 123: { +#ifdef USE_VOICE_ASSISTANT + VoiceAssistantSetConfiguration msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str()); +#endif + this->on_voice_assistant_set_configuration(msg); #endif break; } diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 83bfc2ed98..e69954f7ab 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -247,6 +247,21 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_VOICE_ASSISTANT virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){}; #endif +#ifdef USE_VOICE_ASSISTANT + virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){}; +#endif +#ifdef USE_VOICE_ASSISTANT + bool send_voice_assistant_announce_finished(const VoiceAssistantAnnounceFinished &msg); +#endif +#ifdef USE_VOICE_ASSISTANT + virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){}; +#endif +#ifdef USE_VOICE_ASSISTANT + bool send_voice_assistant_configuration_response(const VoiceAssistantConfigurationResponse &msg); +#endif +#ifdef USE_VOICE_ASSISTANT + virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){}; +#endif #ifdef USE_ALARM_CONTROL_PANEL bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg); #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a61ae89243..0fde3e47af 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -359,8 +359,18 @@ void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, + std::function f) { + this->state_subs_.push_back(HomeAssistantStateSubscription{ + .entity_id = std::move(entity_id), + .attribute = std::move(attribute), + .callback = std::move(f), + .once = true, + }); +}; const std::vector &APIServer::get_state_subs() const { return this->state_subs_; } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 43bc8a7348..899eaede49 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -112,10 +112,13 @@ class APIServer : public Component, public Controller { std::string entity_id; optional attribute; std::function callback; + bool once; }; void subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f); + void get_home_assistant_state(std::string entity_id, optional attribute, + std::function f); const std::vector &get_state_subs() const; const std::vector &get_user_services() const { return this->user_services_; } diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index eae8c0e2df..99e250b6fc 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -1,13 +1,13 @@ # Dummy integration to allow relying on AsyncTCP import esphome.codegen as cg import esphome.config_validation as cv -from esphome.core import CORE, coroutine_with_priority from esphome.const import ( + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_BK72XX, PLATFORM_RTL87XX, ) +from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@OttoWinter"] @@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): if CORE.is_esp32 or CORE.is_libretiny: # https://github.com/esphome/AsyncTCP/blob/master/library.json - cg.add_library("esphome/AsyncTCP-esphome", "2.1.3") + cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") elif CORE.is_esp8266: # https://github.com/esphome/ESPAsyncTCP cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py index a702e23cf0..42ef259100 100644 --- a/esphome/components/atm90e26/sensor.py +++ b/esphome/components/atm90e26/sensor.py @@ -1,34 +1,34 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, spi +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_REACTIVE_POWER, - CONF_VOLTAGE, CONF_CURRENT, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_LINE_FREQUENCY, CONF_POWER, CONF_POWER_FACTOR, - CONF_FREQUENCY, - CONF_FORWARD_ACTIVE_ENERGY, + CONF_REACTIVE_POWER, CONF_REVERSE_ACTIVE_ENERGY, + CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, - ICON_LIGHTBULB, ICON_CURRENT_AC, + ICON_LIGHTBULB, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, UNIT_HERTZ, UNIT_VOLT, - UNIT_AMPERE, - UNIT_WATT, UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, UNIT_WATT_HOURS, ) -CONF_LINE_FREQUENCY = "line_frequency" CONF_METER_CONSTANT = "meter_constant" CONF_PL_CONST = "pl_const" CONF_GAIN_PGA = "gain_pga" diff --git a/esphome/components/atm90e32/__init__.py b/esphome/components/atm90e32/__init__.py index e69de29bb2..8ce95be489 100644 --- a/esphome/components/atm90e32/__init__.py +++ b/esphome/components/atm90e32/__init__.py @@ -0,0 +1,7 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@circuitsetup", "@descipher"] + +atm90e32_ns = cg.esphome_ns.namespace("atm90e32") + +CONF_ATM90E32_ID = "atm90e32_id" diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index e27459b18a..43647b1855 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -132,10 +132,77 @@ void ATM90E32Component::update() { this->status_clear_warning(); } +void ATM90E32Component::restore_calibrations_() { + if (enable_offset_calibration_) { + this->pref_.load(&this->offset_phase_); + } +}; + +void ATM90E32Component::run_offset_calibrations() { + // Run the calibrations and + // Setup voltage and current calibration offsets for PHASE A + this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); + this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); + this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset + // Setup voltage and current calibration offsets for PHASE B + this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); + this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); + this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset + // Setup voltage and current calibration offsets for PHASE C + this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); + this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); + this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset + this->pref_.save(&this->offset_phase_); + ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, + this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); + ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, + this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); +} + +void ATM90E32Component::clear_offset_calibrations() { + // Clear the calibrations and + this->offset_phase_[PHASEA].voltage_offset_ = 0; + this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEA].current_offset_ = 0; + this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset + this->offset_phase_[PHASEB].voltage_offset_ = 0; + this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEB].current_offset_ = 0; + this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset + this->offset_phase_[PHASEC].voltage_offset_ = 0; + this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_; + this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset + this->offset_phase_[PHASEC].current_offset_ = 0; + this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_; + this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset + this->pref_.save(&this->offset_phase_); + ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_, + this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_); + ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_, + this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_); +} + void ATM90E32Component::setup() { ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); this->spi_setup(); - + if (this->enable_offset_calibration_) { + uint32_t hash = fnv1_hash(App.get_friendly_name()); + this->pref_ = global_preferences->make_preference(hash, true); + this->restore_calibrations_(); + } uint16_t mmode0 = 0x87; // 3P4W 50Hz if (line_freq_ == 60) { mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz @@ -167,27 +234,12 @@ void ATM90E32Component::setup() { this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% - // Setup voltage and current calibration offsets for PHASE A - this->phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA); - this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // A Voltage offset - this->phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA); - this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // A Current offset // Setup voltage and current gain for PHASE A this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain - // Setup voltage and current calibration offsets for PHASE B - this->phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB); - this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // B Voltage offset - this->phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB); - this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // B Current offset // Setup voltage and current gain for PHASE B this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain - // Setup voltage and current calibration offsets for PHASE C - this->phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC); - this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset - this->phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC); - this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset // Setup voltage and current gain for PHASE C this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 0a334dbe8b..35c61d1e05 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -1,9 +1,12 @@ #pragma once -#include "esphome/core/component.h" +#include "atm90e32_reg.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" -#include "atm90e32_reg.h" +#include "esphome/core/application.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/preferences.h" namespace esphome { namespace atm90e32 { @@ -20,7 +23,6 @@ class ATM90E32Component : public PollingComponent, void dump_config() override; float get_setup_priority() const override; void update() override; - void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; } void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } @@ -48,9 +50,11 @@ class ATM90E32Component : public PollingComponent, void set_line_freq(int freq) { line_freq_ = freq; } void set_current_phases(int phases) { current_phases_ = phases; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } + void run_offset_calibrations(); + void clear_offset_calibrations(); + void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); - int32_t last_periodic_millis = millis(); protected: @@ -83,10 +87,11 @@ class ATM90E32Component : public PollingComponent, float get_chip_temperature_(); bool get_publish_interval_flag_() { return publish_interval_flag_; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; + void restore_calibrations_(); struct ATM90E32Phase { - uint16_t voltage_gain_{7305}; - uint16_t ct_gain_{27961}; + uint16_t voltage_gain_{0}; + uint16_t ct_gain_{0}; uint16_t voltage_offset_{0}; uint16_t current_offset_{0}; float voltage_{0}; @@ -114,13 +119,21 @@ class ATM90E32Component : public PollingComponent, uint32_t cumulative_reverse_active_energy_{0}; } phase_[3]; + struct Calibration { + uint16_t voltage_offset_{0}; + uint16_t current_offset_{0}; + } offset_phase_[3]; + + ESPPreferenceObject pref_; + sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr}; uint16_t pga_gain_{0x15}; int line_freq_{60}; int current_phases_{3}; - bool publish_interval_flag_{true}; + bool publish_interval_flag_{false}; bool peak_current_signed_{false}; + bool enable_offset_calibration_{false}; }; } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index dac62aa6b4..954fb42e79 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -1,5 +1,7 @@ #pragma once +#include + namespace esphome { namespace atm90e32 { diff --git a/esphome/components/atm90e32/button/__init__.py b/esphome/components/atm90e32/button/__init__.py new file mode 100644 index 0000000000..931346b386 --- /dev/null +++ b/esphome/components/atm90e32/button/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE + +from .. import atm90e32_ns +from ..sensor import ATM90E32Component + +CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration" +CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration" + +ATM90E32CalibrationButton = atm90e32_ns.class_( + "ATM90E32CalibrationButton", + button.Button, +) +ATM90E32ClearCalibrationButton = atm90e32_ns.class_( + "ATM90E32ClearCalibrationButton", + button.Button, +) + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), + cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema( + ATM90E32CalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SCALE, + ), + cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema( + ATM90E32ClearCalibrationButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_CHIP, + ), +} + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION): + b = await button.new_button(run_offset) + await cg.register_parented(b, parent) + if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION): + b = await button.new_button(clear_offset) + await cg.register_parented(b, parent) diff --git a/esphome/components/atm90e32/button/atm90e32_button.cpp b/esphome/components/atm90e32/button/atm90e32_button.cpp new file mode 100644 index 0000000000..00715b61dd --- /dev/null +++ b/esphome/components/atm90e32/button/atm90e32_button.cpp @@ -0,0 +1,20 @@ +#include "atm90e32_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace atm90e32 { + +static const char *const TAG = "atm90e32.button"; + +void ATM90E32CalibrationButton::press_action() { + ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process..."); + this->parent_->run_offset_calibrations(); +} + +void ATM90E32ClearCalibrationButton::press_action() { + ESP_LOGI(TAG, "Offset calibrations cleared."); + this->parent_->clear_offset_calibrations(); +} + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/button/atm90e32_button.h b/esphome/components/atm90e32/button/atm90e32_button.h new file mode 100644 index 0000000000..0617099457 --- /dev/null +++ b/esphome/components/atm90e32/button/atm90e32_button.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/atm90e32/atm90e32.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace atm90e32 { + +class ATM90E32CalibrationButton : public button::Button, public Parented { + public: + ATM90E32CalibrationButton() = default; + + protected: + void press_action() override; +}; + +class ATM90E32ClearCalibrationButton : public button::Button, public Parented { + public: + ATM90E32ClearCalibrationButton() = default; + + protected: + void press_action() override; +}; + +} // namespace atm90e32 +} // namespace esphome diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 2bc7f0498d..0dc3bfdc4f 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -1,21 +1,22 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, spi +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_REACTIVE_POWER, - CONF_VOLTAGE, + CONF_APPARENT_POWER, CONF_CURRENT, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_LINE_FREQUENCY, CONF_PHASE_A, + CONF_PHASE_ANGLE, CONF_PHASE_B, CONF_PHASE_C, - CONF_PHASE_ANGLE, CONF_POWER, CONF_POWER_FACTOR, - CONF_APPARENT_POWER, - CONF_FREQUENCY, - CONF_FORWARD_ACTIVE_ENERGY, + CONF_REACTIVE_POWER, CONF_REVERSE_ACTIVE_ENERGY, + CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -23,13 +24,13 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, - ICON_LIGHTBULB, ICON_CURRENT_AC, + ICON_LIGHTBULB, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, - UNIT_DEGREES, UNIT_CELSIUS, + UNIT_DEGREES, UNIT_HERTZ, UNIT_VOLT, UNIT_VOLT_AMPS_REACTIVE, @@ -37,7 +38,8 @@ from esphome.const import ( UNIT_WATT_HOURS, ) -CONF_LINE_FREQUENCY = "line_frequency" +from . import atm90e32_ns + CONF_CHIP_TEMPERATURE = "chip_temperature" CONF_GAIN_PGA = "gain_pga" CONF_CURRENT_PHASES = "current_phases" @@ -46,6 +48,7 @@ CONF_GAIN_CT = "gain_ct" CONF_HARMONIC_POWER = "harmonic_power" CONF_PEAK_CURRENT = "peak_current" CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" +CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration" UNIT_DEG = "degrees" LINE_FREQS = { "50HZ": 50, @@ -61,7 +64,6 @@ PGA_GAINS = { "4X": 0x2A, } -atm90e32_ns = cg.esphome_ns.namespace("atm90e32") ATM90E32Component = atm90e32_ns.class_( "ATM90E32Component", cg.PollingComponent, spi.SPIDevice ) @@ -164,6 +166,7 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, + cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")) @@ -227,3 +230,4 @@ async def to_code(config): cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) + cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION])) diff --git a/esphome/components/bedjet/bedjet_codec.h b/esphome/components/bedjet/bedjet_codec.h index 527e757d7f..07aee32d54 100644 --- a/esphome/components/bedjet/bedjet_codec.h +++ b/esphome/components/bedjet/bedjet_codec.h @@ -90,7 +90,7 @@ struct BedjetStatusPacket { int unused_6 : 1; // 0x4 bool is_dual_zone : 1; /// Is part of a Dual Zone configuration int unused_7 : 1; // 0x1 - } dual_zone_flags; + } dual_zone_flags; // NOLINT(clang-diagnostic-unaligned-access) uint8_t unused_4 : 8; // Unknown 23-24 = 0x1310 uint8_t unused_5 : 8; // Unknown 23-24 = 0x1310 diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 86c83aff5c..8346a82cf0 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -18,10 +18,11 @@ class BinaryLightOutput : public light::LightOutput { void write_state(light::LightState *state) override { bool binary; state->current_values_as_binary(&binary); - if (binary) + if (binary) { this->output_->turn_on(); - else + } else { this->output_->turn_off(); + } } protected: diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 11a1887206..95fd17bcc0 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -1,10 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.cpp_generator import MockObjClass -from esphome.cpp_helpers import setup_entity from esphome import automation, core from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DELAY, CONF_DEVICE_CLASS, @@ -16,6 +14,7 @@ from esphome.const import ( CONF_INVERTED, CONF_MAX_LENGTH, CONF_MIN_LENGTH, + CONF_MQTT_ID, CONF_ON_CLICK, CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, @@ -26,7 +25,6 @@ from esphome.const import ( CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY_CHARGING, @@ -59,6 +57,8 @@ from esphome.const import ( DEVICE_CLASS_WINDOW, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity from esphome.util import Registry CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/bl0906/__init__.py b/esphome/components/bl0906/__init__.py new file mode 100644 index 0000000000..b077792604 --- /dev/null +++ b/esphome/components/bl0906/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@athom-tech", "@tarontop", "@jesserockz"] diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp new file mode 100644 index 0000000000..bddb62ff64 --- /dev/null +++ b/esphome/components/bl0906/bl0906.cpp @@ -0,0 +1,238 @@ +#include "bl0906.h" +#include "constants.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0906 { + +static const char *const TAG = "bl0906"; + +constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +// The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; +constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) { + return (address + data->l + data->m + data->h) ^ 0xFF; +} + +void BL0906::loop() { + if (this->current_channel_ == UINT8_MAX) { + return; + } + + while (this->available()) + this->flush(); + + if (this->current_channel_ == 0) { + // Temperature + this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_); + } else if (this->current_channel_ == 1) { + this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_); + this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_); + this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_); + } else if (this->current_channel_ == 2) { + this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_); + this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_); + this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_); + } else if (this->current_channel_ == 3) { + this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_); + this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_); + this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_); + } else if (this->current_channel_ == 4) { + this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_); + this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_); + this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_); + } else if (this->current_channel_ == 5) { + this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_); + this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_); + this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_); + } else if (this->current_channel_ == 6) { + this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_); + this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_); + this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_); + } else if (this->current_channel_ == UINT8_MAX - 2) { + // Frequency + this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_); + // Voltage + this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_); + } else if (this->current_channel_ == UINT8_MAX - 1) { + // Total power + this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_); + // Total Energy + this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_); + } else { + this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage + return; + } + this->current_channel_++; + this->handle_actions_(); +} + +void BL0906::setup() { + while (this->available()) + this->flush(); + this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE)); + // Calibration (1: register address; 2: value before calibration; 3: value after calibration) + this->bias_correction_(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1 + this->bias_correction_(BL0906_RMSOS_2, 0.01500, 0); + this->bias_correction_(BL0906_RMSOS_3, 0.01400, 0); + this->bias_correction_(BL0906_RMSOS_4, 0.01300, 0); + this->bias_correction_(BL0906_RMSOS_5, 0.01200, 0); + this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6 + + this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD)); +} + +void BL0906::update() { this->current_channel_ = 0; } + +size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) { + this->action_queue_.push_back(function); + return this->action_queue_.size(); +} + +void BL0906::handle_actions_() { + if (this->action_queue_.empty()) { + return; + } + ActionCallbackFuncPtr ptr_func = nullptr; + for (int i = 0; i < this->action_queue_.size(); i++) { + ptr_func = this->action_queue_[i]; + if (ptr_func) { + ESP_LOGI(TAG, "HandleActionCallback[%d]...", i); + (this->*ptr_func)(); + } + } + + while (this->available()) { + this->read(); + } + + this->action_queue_.clear(); +} + +// Reset energy +void BL0906::reset_energy_() { + this->write_array(BL0906_INIT[0], 6); + delay(1); + this->flush(); + + ESP_LOGW(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_INIT[0][0], BL0906_INIT[0][1], BL0906_INIT[0][2], + BL0906_INIT[0][3], BL0906_INIT[0][4], BL0906_INIT[0][5]); +} + +// Read data +void BL0906::read_data_(const uint8_t address, const float reference, sensor::Sensor *sensor) { + if (sensor == nullptr) { + return; + } + DataPacket buffer; + ube24_t data_u24; + sbe24_t data_s24; + float value = 0; + + bool signed_result = reference == BL0906_TREF || reference == BL0906_WATT || reference == BL0906_PREF; + + this->write_byte(BL0906_READ_COMMAND); + this->write_byte(address); + if (this->read_array((uint8_t *) &buffer, sizeof(buffer) - 1)) { + if (bl0906_checksum(address, &buffer) == buffer.checksum) { + if (signed_result) { + data_s24.l = buffer.l; + data_s24.m = buffer.m; + data_s24.h = buffer.h; + } else { + data_u24.l = buffer.l; + data_u24.m = buffer.m; + data_u24.h = buffer.h; + } + } else { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + return; + } + } + // Power + if (reference == BL0906_PREF) { + value = (float) to_int32_t(data_s24) * reference; + } + + // Total power + if (reference == BL0906_WATT) { + value = (float) to_int32_t(data_s24) * reference; + } + + // Voltage, current, power, total power + if (reference == BL0906_UREF || reference == BL0906_IREF || reference == BL0906_EREF || reference == BL0906_CF) { + value = (float) to_uint32_t(data_u24) * reference; + } + + // Frequency + if (reference == BL0906_FREF) { + value = reference / (float) to_uint32_t(data_u24); + } + // Chip temperature + if (reference == BL0906_TREF) { + value = (float) to_int32_t(data_s24); + value = (value - 64) * 12.5 / 59 - 40; + } + sensor->publish_state(value); +} + +// RMS offset correction +void BL0906::bias_correction_(uint8_t address, float measurements, float correction) { + DataPacket data; + float ki = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097; // Current coefficient + float i_rms0 = measurements * ki; + float i_rms = correction * ki; + int32_t value = (i_rms * i_rms - i_rms0 * i_rms0) / 256; + data.l = value << 24 >> 24; + data.m = value << 16 >> 24; + if (value < 0) { + data.h = (value << 8 >> 24) | 0b10000000; + } + data.address = bl0906_checksum(address, &data); + ESP_LOGV(TAG, "RMSOS:%02X%02X%02X%02X%02X%02X", BL0906_WRITE_COMMAND, address, data.l, data.m, data.h, data.address); + this->write_byte(BL0906_WRITE_COMMAND); + this->write_byte(address); + this->write_byte(data.l); + this->write_byte(data.m); + this->write_byte(data.h); + this->write_byte(data.address); +} + +void BL0906::dump_config() { + ESP_LOGCONFIG(TAG, "BL0906:"); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + + LOG_SENSOR(" ", "Current1", this->current_1_sensor_); + LOG_SENSOR(" ", "Current2", this->current_2_sensor_); + LOG_SENSOR(" ", "Current3", this->current_3_sensor_); + LOG_SENSOR(" ", "Current4", this->current_4_sensor_); + LOG_SENSOR(" ", "Current5", this->current_5_sensor_); + LOG_SENSOR(" ", "Current6", this->current_6_sensor_); + + LOG_SENSOR(" ", "Power1", this->power_1_sensor_); + LOG_SENSOR(" ", "Power2", this->power_2_sensor_); + LOG_SENSOR(" ", "Power3", this->power_3_sensor_); + LOG_SENSOR(" ", "Power4", this->power_4_sensor_); + LOG_SENSOR(" ", "Power5", this->power_5_sensor_); + LOG_SENSOR(" ", "Power6", this->power_6_sensor_); + + LOG_SENSOR(" ", "Energy1", this->energy_1_sensor_); + LOG_SENSOR(" ", "Energy2", this->energy_2_sensor_); + LOG_SENSOR(" ", "Energy3", this->energy_3_sensor_); + LOG_SENSOR(" ", "Energy4", this->energy_4_sensor_); + LOG_SENSOR(" ", "Energy5", this->energy_5_sensor_); + LOG_SENSOR(" ", "Energy6", this->energy_6_sensor_); + + LOG_SENSOR(" ", "Total Power", this->total_power_sensor_); + LOG_SENSOR(" ", "Total Energy", this->total_energy_sensor_); + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); +} + +} // namespace bl0906 +} // namespace esphome diff --git a/esphome/components/bl0906/bl0906.h b/esphome/components/bl0906/bl0906.h new file mode 100644 index 0000000000..5a9ad0f028 --- /dev/null +++ b/esphome/components/bl0906/bl0906.h @@ -0,0 +1,96 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/datatypes.h" + +// https://www.belling.com.cn/media/file_object/bel_product/BL0906/datasheet/BL0906_V1.02_cn.pdf +// https://www.belling.com.cn/media/file_object/bel_product/BL0906/guide/BL0906%20APP%20Note_V1.02.pdf + +namespace esphome { +namespace bl0906 { + +struct DataPacket { // NOLINT(altera-struct-pack-align) + uint8_t l{0}; + uint8_t m{0}; + uint8_t h{0}; + uint8_t checksum; // checksum + uint8_t address; +} __attribute__((packed)); + +struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l{0}; + uint8_t m{0}; + uint8_t h{0}; +} __attribute__((packed)); + +struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l{0}; + uint8_t m{0}; + int8_t h{0}; +} __attribute__((packed)); + +template class ResetEnergyAction; + +class BL0906; + +using ActionCallbackFuncPtr = void (BL0906::*)(); + +class BL0906 : public PollingComponent, public uart::UARTDevice { + SUB_SENSOR(voltage) + SUB_SENSOR(current_1) + SUB_SENSOR(current_2) + SUB_SENSOR(current_3) + SUB_SENSOR(current_4) + SUB_SENSOR(current_5) + SUB_SENSOR(current_6) + SUB_SENSOR(power_1) + SUB_SENSOR(power_2) + SUB_SENSOR(power_3) + SUB_SENSOR(power_4) + SUB_SENSOR(power_5) + SUB_SENSOR(power_6) + SUB_SENSOR(total_power) + SUB_SENSOR(energy_1) + SUB_SENSOR(energy_2) + SUB_SENSOR(energy_3) + SUB_SENSOR(energy_4) + SUB_SENSOR(energy_5) + SUB_SENSOR(energy_6) + SUB_SENSOR(total_energy) + SUB_SENSOR(frequency) + SUB_SENSOR(temperature) + + public: + void loop() override; + + void update() override; + void setup() override; + void dump_config() override; + + protected: + template friend class ResetEnergyAction; + + void reset_energy_(); + + void read_data_(uint8_t address, float reference, sensor::Sensor *sensor); + + void bias_correction_(uint8_t address, float measurements, float correction); + + uint8_t current_channel_{0}; + size_t enqueue_action_(ActionCallbackFuncPtr function); + void handle_actions_(); + + private: + std::vector action_queue_{}; +}; + +template class ResetEnergyAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } +}; + +} // namespace bl0906 +} // namespace esphome diff --git a/esphome/components/bl0906/const.py b/esphome/components/bl0906/const.py new file mode 100644 index 0000000000..67f21d35b0 --- /dev/null +++ b/esphome/components/bl0906/const.py @@ -0,0 +1,4 @@ +# const.py +ICON_ENERGY = "mdi:lightning-bolt" +ICON_FREQUENCY = "mdi:cosine-wave" +ICON_VOLTAGE = "mdi:sine-wave" diff --git a/esphome/components/bl0906/constants.h b/esphome/components/bl0906/constants.h new file mode 100644 index 0000000000..546916aa3c --- /dev/null +++ b/esphome/components/bl0906/constants.h @@ -0,0 +1,122 @@ +#pragma once +#include + +namespace esphome { +namespace bl0906 { + +// Total power conversion +static const float BL0906_WATT = 16 * 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / + (40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000); +// Total Energy conversion +static const float BL0906_CF = 16 * 4194304 * 0.032768 * 16 / + (3600000 * 16 * + (40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / + (1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000)))); +// Frequency conversion +static const float BL0906_FREF = 10000000; +// Temperature conversion +static const float BL0906_TREF = 12.5 / 59 - 40; +// Current conversion +static const float BL0906_IREF = 1.097 / (12875 * 1 * (5.1 + 5.1) * 1000 / 2000); +// Voltage conversion +static const float BL0906_UREF = 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / (13162 * 1 * 100 * 1000); +// Power conversion +static const float BL0906_PREF = 1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000) / + (40.41259 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000); +// Energy conversion +static const float BL0906_EREF = 4194304 * 0.032768 * 16 / + (3600000 * 16 * + (40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / + (1.097 * 1.097 * (20000 + 20000 + 20000 + 20000 + 20000)))); +// Current coefficient +static const float BL0906_KI = 12875 * 1 * (5.1 + 5.1) * 1000 / 2000 / 1.097; +// Power coefficient +static const float BL0906_KP = 40.4125 * ((5.1 + 5.1) * 1000 / 2000) * 1 * 100 * 1 * 1000 / 1.097 / 1.097 / + (20000 + 20000 + 20000 + 20000 + 20000); + +static const uint8_t USR_WRPROT_WITABLE[6] = {0xCA, 0x9E, 0x55, 0x55, 0x00, 0xB7}; +static const uint8_t USR_WRPROT_ONLYREAD[6] = {0xCA, 0x9E, 0x00, 0x00, 0x00, 0x61}; + +static const uint8_t BL0906_READ_COMMAND = 0x35; +static const uint8_t BL0906_WRITE_COMMAND = 0xCA; + +// Register address +// Voltage +static const uint8_t BL0906_V_RMS = 0x16; + +// Total power +static const uint8_t BL0906_WATT_SUM = 0X2C; + +// Current1~6 +static const uint8_t BL0906_I_1_RMS = 0x0D; // current_1 +static const uint8_t BL0906_I_2_RMS = 0x0E; +static const uint8_t BL0906_I_3_RMS = 0x0F; +static const uint8_t BL0906_I_4_RMS = 0x10; +static const uint8_t BL0906_I_5_RMS = 0x13; +static const uint8_t BL0906_I_6_RMS = 0x14; // current_6 + +// Power1~6 +static const uint8_t BL0906_WATT_1 = 0X23; // power_1 +static const uint8_t BL0906_WATT_2 = 0X24; +static const uint8_t BL0906_WATT_3 = 0X25; +static const uint8_t BL0906_WATT_4 = 0X26; +static const uint8_t BL0906_WATT_5 = 0X29; +static const uint8_t BL0906_WATT_6 = 0X2A; // power_6 + +// Active pulse count, unsigned +static const uint8_t BL0906_CF_1_CNT = 0X30; // Channel_1 +static const uint8_t BL0906_CF_2_CNT = 0X31; +static const uint8_t BL0906_CF_3_CNT = 0X32; +static const uint8_t BL0906_CF_4_CNT = 0X33; +static const uint8_t BL0906_CF_5_CNT = 0X36; +static const uint8_t BL0906_CF_6_CNT = 0X37; // Channel_6 + +// Total active pulse count, unsigned +static const uint8_t BL0906_CF_SUM_CNT = 0X39; + +// Voltage frequency cycle +static const uint8_t BL0906_FREQUENCY = 0X4E; + +// Internal temperature +static const uint8_t BL0906_TEMPERATURE = 0X5E; + +// Calibration register +// RMS gain adjustment register +static const uint8_t BL0906_RMSGN_1 = 0x6D; // Channel_1 +static const uint8_t BL0906_RMSGN_2 = 0x6E; +static const uint8_t BL0906_RMSGN_3 = 0x6F; +static const uint8_t BL0906_RMSGN_4 = 0x70; +static const uint8_t BL0906_RMSGN_5 = 0x73; +static const uint8_t BL0906_RMSGN_6 = 0x74; // Channel_6 + +// RMS offset correction register +static const uint8_t BL0906_RMSOS_1 = 0x78; // Channel_1 +static const uint8_t BL0906_RMSOS_2 = 0x79; +static const uint8_t BL0906_RMSOS_3 = 0x7A; +static const uint8_t BL0906_RMSOS_4 = 0x7B; +static const uint8_t BL0906_RMSOS_5 = 0x7E; +static const uint8_t BL0906_RMSOS_6 = 0x7F; // Channel_6 + +// Active power gain adjustment register +static const uint8_t BL0906_WATTGN_1 = 0xB7; // Channel_1 +static const uint8_t BL0906_WATTGN_2 = 0xB8; +static const uint8_t BL0906_WATTGN_3 = 0xB9; +static const uint8_t BL0906_WATTGN_4 = 0xBA; +static const uint8_t BL0906_WATTGN_5 = 0xBD; +static const uint8_t BL0906_WATTGN_6 = 0xBE; // Channel_6 + +// User write protection setting register, +// You must first write 0x5555 to the write protection setting register before writing to other registers. +static const uint8_t BL0906_USR_WRPROT = 0x9E; + +// Reset Register +static const uint8_t BL0906_SOFT_RESET = 0x9F; + +const uint8_t BL0906_INIT[2][6] = { + // Reset to default + {BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52}, + // Enable User Operation Write + {BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}}; + +} // namespace bl0906 +} // namespace esphome diff --git a/esphome/components/bl0906/sensor.py b/esphome/components/bl0906/sensor.py new file mode 100644 index 0000000000..bc370c9252 --- /dev/null +++ b/esphome/components/bl0906/sensor.py @@ -0,0 +1,184 @@ +from esphome import automation +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + CONF_CHANNEL, + CONF_CURRENT, + CONF_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_NAME, + CONF_POWER, + CONF_TEMPERATURE, + CONF_TOTAL_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_POWER, + ICON_THERMOMETER, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_CELSIUS, + UNIT_HERTZ, + UNIT_KILOWATT_HOURS, + UNIT_VOLT, + UNIT_WATT, +) + +# Import ICONS not included in esphome's const.py, from the local components const.py +from .const import ICON_ENERGY, ICON_FREQUENCY, ICON_VOLTAGE + +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["bl0906"] +CONF_TOTAL_ENERGY = "total_energy" + +bl0906_ns = cg.esphome_ns.namespace("bl0906") +BL0906 = bl0906_ns.class_("BL0906", cg.PollingComponent, uart.UARTDevice) +ResetEnergyAction = bl0906_ns.class_("ResetEnergyAction", automation.Action) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0906), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + icon=ICON_FREQUENCY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_FREQUENCY, + unit_of_measurement=UNIT_HERTZ, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=UNIT_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + icon=ICON_VOLTAGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=UNIT_VOLT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TOTAL_POWER): sensor.sensor_schema( + icon=ICON_POWER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=UNIT_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TOTAL_ENERGY): sensor.sensor_schema( + icon=ICON_ENERGY, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + ), + } + ) + .extend( + cv.Schema( + { + cv.Optional(f"{CONF_CHANNEL}_{i + 1}"): cv.Schema( + { + cv.Optional(CONF_CURRENT): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_CURRENT_AC, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + unit_of_measurement=UNIT_AMPERE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_POWER): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_POWER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=UNIT_WATT, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ENERGY): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_ENERGY, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + unit_of_measurement=UNIT_KILOWATT_HOURS, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + key=CONF_NAME, + ), + } + ) + for i in range(6) + } + ) + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.polling_component_schema("60s")) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "bl0906", baud_rate=19200, require_tx=True, require_rx=True +) + + +@automation.register_action( + "bl0906.reset_energy", + ResetEnergyAction, + maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(BL0906), + } + ), +) +async def reset_energy_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + if frequency_config := config.get(CONF_FREQUENCY): + sens = await sensor.new_sensor(frequency_config) + cg.add(var.set_frequency_sensor(sens)) + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + if voltage_config := config.get(CONF_VOLTAGE): + sens = await sensor.new_sensor(voltage_config) + cg.add(var.set_voltage_sensor(sens)) + + for i in range(6): + if channel_config := config.get(f"{CONF_CHANNEL}_{i + 1}"): + if current_config := channel_config.get(CONF_CURRENT): + sens = await sensor.new_sensor(current_config) + cg.add(getattr(var, f"set_current_{i + 1}_sensor")(sens)) + if power_config := channel_config.get(CONF_POWER): + sens = await sensor.new_sensor(power_config) + cg.add(getattr(var, f"set_power_{i + 1}_sensor")(sens)) + if energy_config := channel_config.get(CONF_ENERGY): + sens = await sensor.new_sensor(energy_config) + cg.add(getattr(var, f"set_energy_{i + 1}_sensor")(sens)) + + if total_power_config := config.get(CONF_TOTAL_POWER): + sens = await sensor.new_sensor(total_power_config) + cg.add(var.set_total_power_sensor(sens)) + + if total_energy_config := config.get(CONF_TOTAL_ENERGY): + sens = await sensor.new_sensor(total_energy_config) + cg.add(var.set_total_energy_sensor(sens)) diff --git a/esphome/components/bl0942/__init__.py b/esphome/components/bl0942/__init__.py index 8ef7857b7b..38b68d84b5 100644 --- a/esphome/components/bl0942/__init__.py +++ b/esphome/components/bl0942/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@dbuezas"] +CODEOWNERS = ["@dbuezas", "@dwmw2"] diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 38b1c89036..e6f96c1b19 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -2,6 +2,8 @@ #include "esphome/core/log.h" #include +// Datasheet: https://www.belling.com.cn/media/file_object/bel_product/BL0942/datasheet/BL0942_V1.06_en.pdf + namespace esphome { namespace bl0942 { @@ -12,43 +14,64 @@ static const uint8_t BL0942_FULL_PACKET = 0xAA; static const uint8_t BL0942_PACKET_HEADER = 0x55; static const uint8_t BL0942_WRITE_COMMAND = 0xA8; -static const uint8_t BL0942_REG_I_FAST_RMS_CTRL = 0x10; -static const uint8_t BL0942_REG_MODE = 0x18; -static const uint8_t BL0942_REG_SOFT_RESET = 0x19; -static const uint8_t BL0942_REG_USR_WRPROT = 0x1A; + +static const uint8_t BL0942_REG_I_RMSOS = 0x12; +static const uint8_t BL0942_REG_WA_CREEP = 0x14; +static const uint8_t BL0942_REG_I_FAST_RMS_TH = 0x15; +static const uint8_t BL0942_REG_I_FAST_RMS_CYC = 0x16; +static const uint8_t BL0942_REG_FREQ_CYC = 0x17; +static const uint8_t BL0942_REG_OT_FUNX = 0x18; +static const uint8_t BL0942_REG_MODE = 0x19; +static const uint8_t BL0942_REG_SOFT_RESET = 0x1C; +static const uint8_t BL0942_REG_USR_WRPROT = 0x1D; static const uint8_t BL0942_REG_TPS_CTRL = 0x1B; -// TODO: Confirm insialisation works as intended -const uint8_t BL0942_INIT[5][6] = { - // Reset to default - {BL0942_WRITE_COMMAND, BL0942_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, - // Enable User Operation Write - {BL0942_WRITE_COMMAND, BL0942_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, - // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS - {BL0942_WRITE_COMMAND, BL0942_REG_MODE, 0x00, 0x10, 0x00, 0x37}, - // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS - {BL0942_WRITE_COMMAND, BL0942_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, - // 0x181C = Half cycle, Fast RMS threshold 6172 - {BL0942_WRITE_COMMAND, BL0942_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; +static const uint32_t BL0942_REG_MODE_RESV = 0x03; +static const uint32_t BL0942_REG_MODE_CF_EN = 0x04; +static const uint32_t BL0942_REG_MODE_RMS_UPDATE_SEL = 0x08; +static const uint32_t BL0942_REG_MODE_FAST_RMS_SEL = 0x10; +static const uint32_t BL0942_REG_MODE_AC_FREQ_SEL = 0x20; +static const uint32_t BL0942_REG_MODE_CF_CNT_CLR_SEL = 0x40; +static const uint32_t BL0942_REG_MODE_CF_CNT_ADD_SEL = 0x80; +static const uint32_t BL0942_REG_MODE_UART_RATE_19200 = 0x200; +static const uint32_t BL0942_REG_MODE_UART_RATE_38400 = 0x300; +static const uint32_t BL0942_REG_MODE_DEFAULT = + BL0942_REG_MODE_RESV | BL0942_REG_MODE_CF_EN | BL0942_REG_MODE_CF_CNT_ADD_SEL; + +static const uint32_t BL0942_REG_SOFT_RESET_MAGIC = 0x5a5a5a; +static const uint32_t BL0942_REG_USR_WRPROT_MAGIC = 0x55; + +// 23-byte packet, 11 bits per byte, 2400 baud: about 105ms +static const uint32_t PKT_TIMEOUT_MS = 200; void BL0942::loop() { DataPacket buffer; - if (!this->available()) { + int avail = this->available(); + + if (!avail) { return; } - if (read_array((uint8_t *) &buffer, sizeof(buffer))) { - if (validate_checksum(&buffer)) { - received_package_(&buffer); + if (avail < sizeof(buffer)) { + if (!this->rx_start_) { + this->rx_start_ = millis(); + } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail); + this->read_array((uint8_t *) &buffer, avail); + this->rx_start_ = 0; } - } else { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); - while (read() >= 0) - ; + return; } + + if (this->read_array((uint8_t *) &buffer, sizeof(buffer))) { + if (this->validate_checksum_(&buffer)) { + this->received_package_(&buffer); + } + } + this->rx_start_ = 0; } -bool BL0942::validate_checksum(DataPacket *data) { - uint8_t checksum = BL0942_READ_COMMAND; +bool BL0942::validate_checksum_(DataPacket *data) { + uint8_t checksum = BL0942_READ_COMMAND | this->address_; // Whole package but checksum uint8_t *raw = (uint8_t *) data; for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { @@ -61,17 +84,73 @@ bool BL0942::validate_checksum(DataPacket *data) { return checksum == data->checksum; } -void BL0942::update() { +void BL0942::write_reg_(uint8_t reg, uint32_t val) { + uint8_t pkt[6]; + this->flush(); - this->write_byte(BL0942_READ_COMMAND); + pkt[0] = BL0942_WRITE_COMMAND | this->address_; + pkt[1] = reg; + pkt[2] = (val & 0xff); + pkt[3] = (val >> 8) & 0xff; + pkt[4] = (val >> 16) & 0xff; + pkt[5] = (pkt[0] + pkt[1] + pkt[2] + pkt[3] + pkt[4]) ^ 0xff; + this->write_array(pkt, 6); + delay(1); +} + +int BL0942::read_reg_(uint8_t reg) { + union { + uint8_t b[4]; + uint32_le_t le32; + } resp; + + this->write_byte(BL0942_READ_COMMAND | this->address_); + this->write_byte(reg); + this->flush(); + if (this->read_array(resp.b, 4) && + resp.b[3] == + (uint8_t) ((BL0942_READ_COMMAND + this->address_ + reg + resp.b[0] + resp.b[1] + resp.b[2]) ^ 0xff)) { + resp.b[3] = 0; + return resp.le32; + } + return -1; +} + +void BL0942::update() { + this->write_byte(BL0942_READ_COMMAND | this->address_); this->write_byte(BL0942_FULL_PACKET); } void BL0942::setup() { - for (auto *i : BL0942_INIT) { - this->write_array(i, 6); - delay(1); + // If either current or voltage references are set explicitly by the user, + // calculate the power reference from it unless that is also explicitly set. + if ((this->current_reference_set_ || this->voltage_reference_set_) && !this->power_reference_set_) { + this->power_reference_ = (this->voltage_reference_ * this->current_reference_ * 3537.0 / 305978.0) / 73989.0; + this->power_reference_set_ = true; } + + // Similarly for energy reference, if the power reference was set by the user + // either implicitly or explicitly. + if (this->power_reference_set_ && !this->energy_reference_set_) { + this->energy_reference_ = this->power_reference_ * 3600000 / 419430.4; + this->energy_reference_set_ = true; + } + + this->write_reg_(BL0942_REG_USR_WRPROT, BL0942_REG_USR_WRPROT_MAGIC); + if (this->reset_) + this->write_reg_(BL0942_REG_SOFT_RESET, BL0942_REG_SOFT_RESET_MAGIC); + + uint32_t mode = BL0942_REG_MODE_DEFAULT; + mode |= BL0942_REG_MODE_RMS_UPDATE_SEL; /* 800ms refresh time */ + if (this->line_freq_ == LINE_FREQUENCY_60HZ) + mode |= BL0942_REG_MODE_AC_FREQ_SEL; + this->write_reg_(BL0942_REG_MODE, mode); + + this->write_reg_(BL0942_REG_USR_WRPROT, 0); + + if (this->read_reg_(BL0942_REG_MODE) != mode) + this->status_set_warning("BL0942 setup failed!"); + this->flush(); } @@ -82,10 +161,17 @@ void BL0942::received_package_(DataPacket *data) { return; } + // cf_cnt is only 24 bits, so track overflows + uint32_t cf_cnt = (uint24_t) data->cf_cnt; + cf_cnt |= this->prev_cf_cnt_ & 0xff000000; + if (cf_cnt < this->prev_cf_cnt_) { + cf_cnt += 0x1000000; + } + this->prev_cf_cnt_ = cf_cnt; + float v_rms = (uint24_t) data->v_rms / voltage_reference_; float i_rms = (uint24_t) data->i_rms / current_reference_; float watt = (int24_t) data->watt / power_reference_; - uint32_t cf_cnt = (uint24_t) data->cf_cnt; float total_energy_consumption = cf_cnt / energy_reference_; float frequency = 1000000.0f / data->frequency; @@ -104,18 +190,25 @@ void BL0942::received_package_(DataPacket *data) { if (frequency_sensor_ != nullptr) { frequency_sensor_->publish_state(frequency); } - + this->status_clear_warning(); ESP_LOGV(TAG, "BL0942: U %fV, I %fA, P %fW, Cnt %" PRId32 ", âˆĢP %fkWh, frequency %fHz, status 0x%08X", v_rms, i_rms, watt, cf_cnt, total_energy_consumption, frequency, data->status); } void BL0942::dump_config() { // NOLINT(readability-function-cognitive-complexity) ESP_LOGCONFIG(TAG, "BL0942:"); + ESP_LOGCONFIG(TAG, " Reset: %s", TRUEFALSE(this->reset_)); + ESP_LOGCONFIG(TAG, " Address: %d", this->address_); + ESP_LOGCONFIG(TAG, " Nominal line frequency: %d Hz", this->line_freq_); + ESP_LOGCONFIG(TAG, " Current reference: %f", this->current_reference_); + ESP_LOGCONFIG(TAG, " Energy reference: %f", this->energy_reference_); + ESP_LOGCONFIG(TAG, " Power reference: %f", this->power_reference_); + ESP_LOGCONFIG(TAG, " Voltage reference: %f", this->voltage_reference_); LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Power", this->power_sensor_); LOG_SENSOR("", "Energy", this->energy_sensor_); - LOG_SENSOR("", "frequency", this->frequency_sensor_); + LOG_SENSOR("", "Frequency", this->frequency_sensor_); } } // namespace bl0942 diff --git a/esphome/components/bl0942/bl0942.h b/esphome/components/bl0942/bl0942.h index 12489915e1..37b884e6ca 100644 --- a/esphome/components/bl0942/bl0942.h +++ b/esphome/components/bl0942/bl0942.h @@ -8,6 +8,57 @@ namespace esphome { namespace bl0942 { +// The BL0942 IC is "calibration-free", which means that it doesn't care +// at all about calibration, and that's left to software. It measures a +// voltage differential on its IP/IN pins which linearly proportional to +// the current flow, and another on its VP pin which is proportional to +// the line voltage. It never knows the actual calibration; the values +// it reports are solely in terms of those inputs. +// +// The datasheet refers to the input voltages as I(A) and V(V), both +// in millivolts. It measures them against a reference voltage Vref, +// which is typically 1.218V (but that absolute value is meaningless +// without the actual calibration anyway). +// +// The reported I_RMS value is 305978 I(A)/Vref, and the reported V_RMS +// value is 73989 V(V)/Vref. So we can calibrate those by applying a +// simple meter with a resistive load. +// +// The chip also measures the phase difference between voltage and +// current, and uses it to calculate the power factor (cos Ī†). It +// reports the WATT value of 3537 * I_RMS * V_RMS * cos Ī†). +// +// It also integrates total energy based on the WATT value. The time for +// one CF_CNT pulse is 1638.4*256 / WATT. +// +// So... how do we calibrate that? +// +// Using a simple resistive load and an external meter, we can measure +// the true voltage and current for a given V_RMS and I_RMS reading, +// to calculate BL0942_UREF and BL0942_IREF. Those are in units of +// "305978 counts per amp" or "73989 counts per volt" respectively. +// +// We can derive BL0942_PREF from those. Let's eliminate the weird +// factors and express the calibration in plain counts per volt/amp: +// UREF1 = UREF/73989, IREF1 = IREF/305978. +// +// Next... the true power in Watts is V * I * cos Ī†, so that's equal +// to WATT/3537 * IREF1 * UREF1. Which means +// BL0942_PREF = BL0942_UREF * BL0942_IREF * 3537 / 305978 / 73989. +// +// Finally the accumulated energy. The period of a CF_CNT count is +// 1638.4*256 / WATT seconds, or 419230.4 / WATT seconds. Which means +// the energy represented by a CN_CNT pulse is 419230.4 WATT-seconds. +// Factoring in the calibration, that's 419230.4 / BL0942_PREF actual +// Watt-seconds (or Joules, as the physicists like to call them). +// +// But we're not being physicists today; we we're being engineers, so +// we want to convert to kWh instead. Which we do by dividing by 1000 +// and then by 3600, so the energy in kWh is +// CF_CNT * 419230.4 / BL0942_PREF / 3600000 +// +// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4 + static const float BL0942_PREF = 596; // taken from tasmota static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218 static const float BL0942_IREF = 251213.46469622; // 305978/1.218 @@ -28,6 +79,11 @@ struct DataPacket { uint8_t checksum; } __attribute__((packed)); +enum LineFrequency : uint8_t { + LINE_FREQUENCY_50HZ = 50, + LINE_FREQUENCY_60HZ = 60, +}; + class BL0942 : public PollingComponent, public uart::UARTDevice { public: void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } @@ -35,9 +91,27 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; } + void set_address(uint8_t address) { this->address_ = address; } + void set_reset(bool reset) { this->reset_ = reset; } + void set_current_reference(float current_ref) { + this->current_reference_ = current_ref; + this->current_reference_set_ = true; + } + void set_energy_reference(float energy_ref) { + this->energy_reference_ = energy_ref; + this->energy_reference_set_ = true; + } + void set_power_reference(float power_ref) { + this->power_reference_ = power_ref; + this->power_reference_set_ = true; + } + void set_voltage_reference(float voltage_ref) { + this->voltage_reference_ = voltage_ref; + this->voltage_reference_set_ = true; + } void loop() override; - void update() override; void setup() override; void dump_config() override; @@ -53,15 +127,25 @@ class BL0942 : public PollingComponent, public uart::UARTDevice { // Divide by this to turn into Watt float power_reference_ = BL0942_PREF; + bool power_reference_set_ = false; // Divide by this to turn into Volt float voltage_reference_ = BL0942_UREF; + bool voltage_reference_set_ = false; // Divide by this to turn into Ampere float current_reference_ = BL0942_IREF; + bool current_reference_set_ = false; // Divide by this to turn into kWh float energy_reference_ = BL0942_EREF; + bool energy_reference_set_ = false; + uint8_t address_ = 0; + bool reset_ = false; + LineFrequency line_freq_ = LINE_FREQUENCY_50HZ; + uint32_t rx_start_ = 0; + uint32_t prev_cf_cnt_ = 0; - static bool validate_checksum(DataPacket *data); - + bool validate_checksum_(DataPacket *data); + int read_reg_(uint8_t reg); + void write_reg_(uint8_t reg, uint32_t val); void received_package_(DataPacket *data); }; } // namespace bl0942 diff --git a/esphome/components/bl0942/sensor.py b/esphome/components/bl0942/sensor.py index 9612df6d4c..550f534b74 100644 --- a/esphome/components/bl0942/sensor.py +++ b/esphome/components/bl0942/sensor.py @@ -1,32 +1,46 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, uart +import esphome.config_validation as cv from esphome.const import ( + CONF_ADDRESS, CONF_CURRENT, CONF_ENERGY, + CONF_FREQUENCY, CONF_ID, + CONF_LINE_FREQUENCY, CONF_POWER, CONF_VOLTAGE, - CONF_FREQUENCY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_FREQUENCY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - DEVICE_CLASS_FREQUENCY, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, + UNIT_HERTZ, UNIT_KILOWATT_HOURS, UNIT_VOLT, UNIT_WATT, - UNIT_HERTZ, - STATE_CLASS_TOTAL_INCREASING, ) +CONF_CURRENT_REFERENCE = "current_reference" +CONF_ENERGY_REFERENCE = "energy_reference" +CONF_POWER_REFERENCE = "power_reference" +CONF_RESET = "reset" +CONF_VOLTAGE_REFERENCE = "voltage_reference" + DEPENDENCIES = ["uart"] bl0942_ns = cg.esphome_ns.namespace("bl0942") BL0942 = bl0942_ns.class_("BL0942", cg.PollingComponent, uart.UARTDevice) +LineFrequency = bl0942_ns.enum("LineFrequency") +LINE_FREQS = { + 50: LineFrequency.LINE_FREQUENCY_50HZ, + 60: LineFrequency.LINE_FREQUENCY_60HZ, +} + CONFIG_SCHEMA = ( cv.Schema( { @@ -45,22 +59,35 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_WATT, - accuracy_decimals=0, + accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT_HOURS, - accuracy_decimals=0, + accuracy_decimals=3, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( unit_of_measurement=UNIT_HERTZ, - accuracy_decimals=0, + accuracy_decimals=2, device_class=DEVICE_CLASS_FREQUENCY, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_LINE_FREQUENCY, default="50HZ"): cv.All( + cv.frequency, + cv.enum( + LINE_FREQS, + int=True, + ), + ), + cv.Optional(CONF_ADDRESS, default=0): cv.int_range(min=0, max=3), + cv.Optional(CONF_RESET, default=True): cv.boolean, + cv.Optional(CONF_CURRENT_REFERENCE): cv.float_, + cv.Optional(CONF_ENERGY_REFERENCE): cv.float_, + cv.Optional(CONF_POWER_REFERENCE): cv.float_, + cv.Optional(CONF_VOLTAGE_REFERENCE): cv.float_, } ) .extend(cv.polling_component_schema("60s")) @@ -88,3 +115,14 @@ async def to_code(config): if frequency_config := config.get(CONF_FREQUENCY): sens = await sensor.new_sensor(frequency_config) cg.add(var.set_frequency_sensor(sens)) + cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) + cg.add(var.set_address(config[CONF_ADDRESS])) + cg.add(var.set_reset(config[CONF_RESET])) + if (current_reference := config.get(CONF_CURRENT_REFERENCE, None)) is not None: + cg.add(var.set_current_reference(current_reference)) + if (voltage_reference := config.get(CONF_VOLTAGE_REFERENCE, None)) is not None: + cg.add(var.set_voltage_reference(voltage_reference)) + if (power_reference := config.get(CONF_POWER_REFERENCE, None)) is not None: + cg.add(var.set_power_reference(power_reference)) + if (energy_reference := config.get(CONF_ENERGY_REFERENCE, None)) is not None: + cg.add(var.set_energy_reference(energy_reference)) diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 6bf4ff739e..bc7d517695 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -65,9 +65,7 @@ CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification" 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 -# enforce this in yaml checks. -MULTI_CONF = 3 +MULTI_CONF = True CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index d1fdc80289..3a0f1ade98 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, - cv.Optional(CONF_IBEACON_UUID): cv.uuid, + cv.Optional(CONF_IBEACON_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_TIMEOUT, default="5min"): cv.positive_time_period, cv.Optional(CONF_MIN_RSSI): cv.All( cv.decibel, cv.int_range(min=-100, max=-30) @@ -83,7 +83,7 @@ async def to_code(config): cg.add(var.set_service_uuid128(uuid128)) if ibeacon_uuid := config.get(CONF_IBEACON_UUID): - ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(ibeacon_uuid)) + ibeacon_uuid = esp32_ble_tracker.as_reversed_hex_array(ibeacon_uuid) cg.add(var.set_ibeacon_uuid(ibeacon_uuid)) if (ibeacon_major := config.get(CONF_IBEACON_MAJOR)) is not None: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index f188439d0e..bd1c8b7ea4 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -54,6 +54,9 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p } resp.advertisements.push_back(std::move(adv)); + + ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], + result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); } ESP_LOGV(TAG, "Proxying %d packets", count); this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); @@ -87,6 +90,8 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); ESP_LOGCONFIG(TAG, " Active: %s", YESNO(this->active_)); + ESP_LOGCONFIG(TAG, " Connections: %d", this->connections_.size()); + ESP_LOGCONFIG(TAG, " Raw advertisements: %s", YESNO(this->raw_advertisements_)); } int BluetoothProxy::get_bluetooth_connections_free() { diff --git a/esphome/components/bme68x_bsec2/__init__.py b/esphome/components/bme68x_bsec2/__init__.py new file mode 100644 index 0000000000..1930c7c9e3 --- /dev/null +++ b/esphome/components/bme68x_bsec2/__init__.py @@ -0,0 +1,196 @@ +import hashlib +from pathlib import Path + +from esphome import core, external_files +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODEL, + CONF_RAW_DATA_ID, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE_OFFSET, +) + +CODEOWNERS = ["@neffs", "@kbx81"] + +DOMAIN = "bme68x_bsec2" + +BSEC2_LIBRARY_VERSION = "v1.7.2502" + +CONF_ALGORITHM_OUTPUT = "algorithm_output" +CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id" +CONF_IAQ_MODE = "iaq_mode" +CONF_OPERATING_AGE = "operating_age" +CONF_STATE_SAVE_INTERVAL = "state_save_interval" +CONF_SUPPLY_VOLTAGE = "supply_voltage" + +bme68x_bsec2_ns = cg.esphome_ns.namespace("bme68x_bsec2") +BME68xBSEC2Component = bme68x_bsec2_ns.class_("BME68xBSEC2Component", cg.Component) + + +MODEL_OPTIONS = ["bme680", "bme688"] + +AlgorithmOutput = bme68x_bsec2_ns.enum("AlgorithmOutput") +ALGORITHM_OUTPUT_OPTIONS = { + "classification": AlgorithmOutput.ALGORITHM_OUTPUT_CLASSIFICATION, + "regression": AlgorithmOutput.ALGORITHM_OUTPUT_REGRESSION, +} + +OperatingAge = bme68x_bsec2_ns.enum("OperatingAge") +OPERATING_AGE_OPTIONS = { + "4d": OperatingAge.OPERATING_AGE_4D, + "28d": OperatingAge.OPERATING_AGE_28D, +} + +SampleRate = bme68x_bsec2_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "LP": SampleRate.SAMPLE_RATE_LP, + "ULP": SampleRate.SAMPLE_RATE_ULP, +} + +Voltage = bme68x_bsec2_ns.enum("Voltage") +VOLTAGE_OPTIONS = { + "1.8V": Voltage.VOLTAGE_1_8V, + "3.3V": Voltage.VOLTAGE_3_3V, +} + +ALGORITHM_OUTPUT_FILE_NAME = { + "classification": "sel", + "regression": "reg", +} + +SAMPLE_RATE_FILE_NAME = { + "LP": "3s", + "ULP": "300s", +} + +VOLTAGE_FILE_NAME = { + "1.8V": "18v", + "3.3V": "33v", +} + + +def _compute_local_file_path(url: str) -> Path: + h = hashlib.new("sha256") + h.update(url.encode()) + key = h.hexdigest()[:8] + base_dir = external_files.compute_local_file_dir(DOMAIN) + return base_dir / key + + +def _compute_url(config: dict) -> str: + model = config.get(CONF_MODEL) + operating_age = config.get(CONF_OPERATING_AGE) + sample_rate = SAMPLE_RATE_FILE_NAME[config.get(CONF_SAMPLE_RATE)] + volts = VOLTAGE_FILE_NAME[config.get(CONF_SUPPLY_VOLTAGE)] + if model == "bme688": + algo = ALGORITHM_OUTPUT_FILE_NAME[ + config.get(CONF_ALGORITHM_OUTPUT, "classification") + ] + filename = "bsec_selectivity" + else: + algo = "iaq" + filename = "bsec_iaq" + return f"https://raw.githubusercontent.com/boschsensortec/Bosch-BSEC2-Library/{BSEC2_LIBRARY_VERSION}/src/config/{model}/{model}_{algo}_{volts}_{sample_rate}_{operating_age}/{filename}.txt" + + +def download_bme68x_blob(config): + url = _compute_url(config) + path = _compute_local_file_path(url) + external_files.download_content(url, path) + + return config + + +def validate_bme68x(config): + if CONF_ALGORITHM_OUTPUT not in config: + return config + + if config[CONF_MODEL] != "bme688": + raise cv.Invalid(f"{CONF_ALGORITHM_OUTPUT} is only valid for BME688") + + if config[CONF_ALGORITHM_OUTPUT] == "regression" and ( + config[CONF_OPERATING_AGE] != "4d" + or config[CONF_SAMPLE_RATE] != "ULP" + or config[CONF_SUPPLY_VOLTAGE] != "1.8V" + ): + raise cv.Invalid( + f" To use '{CONF_ALGORITHM_OUTPUT}: regression', {CONF_OPERATING_AGE} must be '4d', {CONF_SAMPLE_RATE} must be 'ULP' and {CONF_SUPPLY_VOLTAGE} must be '1.8V'" + ) + return config + + +CONFIG_SCHEMA_BASE = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME68xBSEC2Component), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_MODEL): cv.one_of(*MODEL_OPTIONS, lower=True), + cv.Optional(CONF_ALGORITHM_OUTPUT): cv.enum( + ALGORITHM_OUTPUT_OPTIONS, lower=True + ), + cv.Optional(CONF_OPERATING_AGE, default="28d"): cv.enum( + OPERATING_AGE_OPTIONS, lower=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( + SAMPLE_RATE_OPTIONS, upper=True + ), + cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum( + VOLTAGE_OPTIONS, upper=True + ), + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional( + CONF_STATE_SAVE_INTERVAL, default="6hours" + ): cv.positive_time_period_minutes, + }, + ) + .add_extra(cv.only_with_arduino) + .add_extra(validate_bme68x) + .add_extra(download_bme68x_blob) +) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if algo_output := config.get(CONF_ALGORITHM_OUTPUT): + cg.add(var.set_algorithm_output(algo_output)) + cg.add(var.set_operating_age(config[CONF_OPERATING_AGE])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add(var.set_voltage(config[CONF_SUPPLY_VOLTAGE])) + cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + cg.add( + var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) + ) + + path = _compute_local_file_path(_compute_url(config)) + + try: + with open(path, encoding="utf-8") as f: + bsec2_iaq_config = f.read() + except Exception as e: + raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}") + + # Convert retrieved BSEC2 config to an array of ints + rhs = [int(x) for x in bsec2_iaq_config.split(",")] + # Create an array which will reside in program memory and configure the sensor instance to use it + bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) + + # Although this component does not use SPI, the BSEC2 library requires the SPI library + cg.add_library("SPI", None) + cg.add_library( + "BME68x Sensor library", + "1.1.40407", + ) + cg.add_library( + "BSEC2 Software Library", + None, + f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}", + ) + + cg.add_define("USE_BSEC2") + + return var diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp new file mode 100644 index 0000000000..5425bbd5b7 --- /dev/null +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -0,0 +1,523 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_BSEC2 +#include "bme68x_bsec2.h" + +#include + +namespace esphome { +namespace bme68x_bsec2 { + +#define BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(a) (a == ALGORITHM_OUTPUT_CLASSIFICATION ? "Classification" : "Regression") +#define BME68X_BSEC2_OPERATING_AGE_LOG(o) (o == OPERATING_AGE_4D ? "4 days" : "28 days") +#define BME68X_BSEC2_SAMPLE_RATE_LOG(r) (r == SAMPLE_RATE_DEFAULT ? "Default" : (r == SAMPLE_RATE_ULP ? "ULP" : "LP")) +#define BME68X_BSEC2_VOLTAGE_LOG(v) (v == VOLTAGE_3_3V ? "3.3V" : "1.8V") + +static const char *const TAG = "bme68x_bsec2.sensor"; + +static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +void BME68xBSEC2Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up BME68X via BSEC2..."); + + this->bsec_status_ = bsec_init_m(&this->bsec_instance_); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_init_m failed: status %d", this->bsec_status_); + return; + } + + bsec_get_version_m(&this->bsec_instance_, &this->version_); + + this->bme68x_status_ = bme68x_init(&this->bme68x_); + if (this->bme68x_status_ != BME68X_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bme68x_init failed: status %d", this->bme68x_status_); + return; + } + if (this->bsec2_configuration_ != nullptr && this->bsec2_configuration_length_) { + this->set_config_(this->bsec2_configuration_, this->bsec2_configuration_length_); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_set_configuration_m failed: status %d", this->bsec_status_); + return; + } + } + + this->update_subscription_(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + ESP_LOGE(TAG, "bsec_update_subscription_m failed: status %d", this->bsec_status_); + return; + } + + this->load_state_(); +} + +void BME68xBSEC2Component::dump_config() { + ESP_LOGCONFIG(TAG, "BME68X via BSEC2:"); + + ESP_LOGCONFIG(TAG, " BSEC2 version: %d.%d.%d.%d", this->version_.major, this->version_.minor, + this->version_.major_bugfix, this->version_.minor_bugfix); + + ESP_LOGCONFIG(TAG, " BSEC2 configuration blob:"); + ESP_LOGCONFIG(TAG, " Configured: %s", YESNO(this->bsec2_blob_configured_)); + if (this->bsec2_configuration_ != nullptr && this->bsec2_configuration_length_) { + ESP_LOGCONFIG(TAG, " Size: %" PRIu32, this->bsec2_configuration_length_); + } + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_, + this->bme68x_status_); + } + + if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) { + ESP_LOGCONFIG(TAG, " Algorithm output: %s", BME68X_BSEC2_ALGORITHM_OUTPUT_LOG(this->algorithm_output_)); + } + ESP_LOGCONFIG(TAG, " Operating age: %s", BME68X_BSEC2_OPERATING_AGE_LOG(this->operating_age_)); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->sample_rate_)); + ESP_LOGCONFIG(TAG, " Voltage: %s", BME68X_BSEC2_VOLTAGE_LOG(this->voltage_)); + ESP_LOGCONFIG(TAG, " State save interval: %ims", this->state_save_interval_ms_); + ESP_LOGCONFIG(TAG, " Temperature offset: %.2f", this->temperature_offset_); + +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->temperature_sample_rate_)); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->pressure_sample_rate_)); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + ESP_LOGCONFIG(TAG, " Sample rate: %s", BME68X_BSEC2_SAMPLE_RATE_LOG(this->humidity_sample_rate_)); + LOG_SENSOR(" ", "Gas resistance", this->gas_resistance_sensor_); + LOG_SENSOR(" ", "CO2 equivalent", this->co2_equivalent_sensor_); + LOG_SENSOR(" ", "Breath VOC equivalent", this->breath_voc_equivalent_sensor_); + LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); + LOG_SENSOR(" ", "IAQ static", this->iaq_static_sensor_); + LOG_SENSOR(" ", "Numeric IAQ accuracy", this->iaq_accuracy_sensor_); +#endif +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "IAQ accuracy", this->iaq_accuracy_text_sensor_); +#endif +} + +float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; } + +void BME68xBSEC2Component::loop() { + this->run_(); + + if (this->bsec_status_ < BSEC_OK || this->bme68x_status_ < BME68X_OK) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + if (this->bsec_status_ > BSEC_OK || this->bme68x_status_ > BME68X_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + // Process a single action from the queue. These are primarily sensor state publishes + // that in totality take too long to send in a single call. + if (this->queue_.size()) { + auto action = std::move(this->queue_.front()); + this->queue_.pop(); + action(); + } +} + +void BME68xBSEC2Component::set_config_(const uint8_t *config, uint32_t len) { + if (len > BSEC_MAX_PROPERTY_BLOB_SIZE) { + ESP_LOGE(TAG, "Configuration is larger than BSEC_MAX_PROPERTY_BLOB_SIZE"); + this->mark_failed(); + return; + } + uint8_t work_buffer[BSEC_MAX_PROPERTY_BLOB_SIZE]; + this->bsec_status_ = bsec_set_configuration_m(&this->bsec_instance_, config, len, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ == BSEC_OK) { + this->bsec2_blob_configured_ = true; + } +} + +float BME68xBSEC2Component::calc_sensor_sample_rate_(SampleRate sample_rate) { + if (sample_rate == SAMPLE_RATE_DEFAULT) { + sample_rate = this->sample_rate_; + } + return sample_rate == SAMPLE_RATE_ULP ? BSEC_SAMPLE_RATE_ULP : BSEC_SAMPLE_RATE_LP; +} + +void BME68xBSEC2Component::update_subscription_() { + bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; + uint8_t num_virtual_sensors = 0; +#ifdef USE_SENSOR + if (this->iaq_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->iaq_static_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_STATIC_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->co2_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->breath_voc_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->pressure_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->pressure_sample_rate_); + num_virtual_sensors++; + } + + if (this->gas_resistance_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(SAMPLE_RATE_DEFAULT); + num_virtual_sensors++; + } + + if (this->temperature_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->temperature_sample_rate_); + num_virtual_sensors++; + } + + if (this->humidity_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY; + virtual_sensors[num_virtual_sensors].sample_rate = this->calc_sensor_sample_rate_(this->humidity_sample_rate_); + num_virtual_sensors++; + } +#endif + bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR]; + uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; + this->bsec_status_ = bsec_update_subscription_m(&this->bsec_instance_, virtual_sensors, num_virtual_sensors, + sensor_settings, &num_sensor_settings); +} + +void BME68xBSEC2Component::run_() { + int64_t curr_time_ns = this->get_time_ns_(); + if (curr_time_ns < this->next_call_ns_) { + return; + } + this->op_mode_ = this->bsec_settings_.op_mode; + uint8_t status; + + ESP_LOGV(TAG, "Performing sensor run"); + + struct bme68x_conf bme68x_conf; + this->bsec_status_ = bsec_sensor_control_m(&this->bsec_instance_, curr_time_ns, &this->bsec_settings_); + if (this->bsec_status_ < BSEC_OK) { + ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_); + return; + } + this->next_call_ns_ = this->bsec_settings_.next_call; + + if (this->bsec_settings_.trigger_measurement) { + bme68x_get_conf(&bme68x_conf, &this->bme68x_); + + bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling; + bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling; + bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling; + bme68x_set_conf(&bme68x_conf, &this->bme68x_); + + switch (this->bsec_settings_.op_mode) { + case BME68X_FORCED_MODE: + this->bme68x_heatr_conf_.enable = BME68X_ENABLE; + this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature; + this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration; + + status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); + status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); + this->op_mode_ = BME68X_FORCED_MODE; + this->sleep_mode_ = false; + ESP_LOGV(TAG, "Using forced mode"); + + break; + case BME68X_PARALLEL_MODE: + if (this->op_mode_ != this->bsec_settings_.op_mode) { + this->bme68x_heatr_conf_.enable = BME68X_ENABLE; + this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile; + this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile; + this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len; + this->bme68x_heatr_conf_.shared_heatr_dur = + BSEC_TOTAL_HEAT_DUR - + (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000)); + + status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + + status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); + this->op_mode_ = BME68X_PARALLEL_MODE; + this->sleep_mode_ = false; + ESP_LOGV(TAG, "Using parallel mode"); + } + break; + case BME68X_SLEEP_MODE: + if (!this->sleep_mode_) { + bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_); + this->sleep_mode_ = true; + ESP_LOGV(TAG, "Using sleep mode"); + } + break; + } + + uint32_t meas_dur = 0; + meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); + ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); + this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); }); + } else { + ESP_LOGV(TAG, "Measurement not required"); + this->read_(curr_time_ns); + } +} + +void BME68xBSEC2Component::read_(int64_t trigger_time_ns) { + ESP_LOGV(TAG, "Reading data"); + + if (this->bsec_settings_.trigger_measurement) { + uint8_t current_op_mode; + this->bme68x_status_ = bme68x_get_op_mode(¤t_op_mode, &this->bme68x_); + + if (current_op_mode == BME68X_SLEEP_MODE) { + ESP_LOGV(TAG, "Still in sleep mode, doing nothing"); + return; + } + } + + if (!this->bsec_settings_.process_data) { + ESP_LOGV(TAG, "Data processing not required"); + return; + } + + struct bme68x_data data[3]; + uint8_t nFields = 0; + this->bme68x_status_ = bme68x_get_data(this->op_mode_, &data[0], &nFields, &this->bme68x_); + + if (this->bme68x_status_ != BME68X_OK) { + ESP_LOGW(TAG, "Failed to get sensor data (BME68X error code %d)", this->bme68x_status_); + return; + } + if (nFields < 1) { + ESP_LOGD(TAG, "BME68X did not provide new data"); + return; + } + + for (uint8_t i = 0; i < nFields; i++) { + bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance + uint8_t num_inputs = 0; + + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_TEMPERATURE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; + inputs[num_inputs].signal = data[i].temperature; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_HEATSOURCE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; + inputs[num_inputs].signal = this->temperature_offset_; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_HUMIDITY)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; + inputs[num_inputs].signal = data[i].humidity; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_PRESSURE)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; + inputs[num_inputs].signal = data[i].pressure; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_GASRESISTOR)) { + if (data[i].status & BME68X_GASM_VALID_MSK) { + inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; + inputs[num_inputs].signal = data[i].gas_resistance; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } else { + ESP_LOGD(TAG, "BME68X did not report gas data"); + } + } + if (BSEC_CHECK_INPUT(this->bsec_settings_.process_data, BSEC_INPUT_PROFILE_PART) && + (data[i].status & BME68X_GASM_VALID_MSK)) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PROFILE_PART; + inputs[num_inputs].signal = (this->op_mode_ == BME68X_FORCED_MODE) ? 0 : data[i].gas_index; + inputs[num_inputs].time_stamp = trigger_time_ns; + num_inputs++; + } + + if (num_inputs < 1) { + ESP_LOGD(TAG, "No signal inputs available for BSEC2"); + return; + } + + bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; + uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; + this->bsec_status_ = bsec_do_steps_m(&this->bsec_instance_, inputs, num_inputs, outputs, &num_outputs); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "BSEC2 failed to process signals (BSEC2 error code %d)", this->bsec_status_); + return; + } + if (num_outputs < 1) { + ESP_LOGD(TAG, "No signal outputs provided by BSEC2"); + return; + } + + this->publish_(outputs, num_outputs); + } +} + +void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { + ESP_LOGV(TAG, "Publishing sensor states"); + bool update_accuracy = false; + uint8_t max_accuracy = 0; + for (uint8_t i = 0; i < num_outputs; i++) { + float signal = outputs[i].signal; + switch (outputs[i].sensor_id) { + case BSEC_OUTPUT_IAQ: + max_accuracy = std::max(outputs[i].accuracy, max_accuracy); + update_accuracy = true; +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_STATIC_IAQ: + max_accuracy = std::max(outputs[i].accuracy, max_accuracy); + update_accuracy = true; +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_static_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_CO2_EQUIVALENT: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_RAW_PRESSURE: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); }); +#endif + break; + case BSEC_OUTPUT_RAW_GAS: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); }); +#endif + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: +#ifdef USE_SENSOR + this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); }); +#endif + break; + } + } + if (update_accuracy) { +#ifdef USE_SENSOR + this->queue_push_( + [this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); }); +#endif +#ifdef USE_TEXT_SENSOR + this->queue_push_([this, max_accuracy]() { + this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[max_accuracy]); + }); +#endif + // Queue up an opportunity to save state + this->queue_push_([this, max_accuracy]() { this->save_state_(max_accuracy); }); + } +} + +int64_t BME68xBSEC2Component::get_time_ns_() { + int64_t time_ms = millis(); + if (this->last_time_ms_ > time_ms) { + this->millis_overflow_counter_++; + } + this->last_time_ms_ = time_ms; + + return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); +} + +#ifdef USE_SENSOR +void BME68xBSEC2Component::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) { + if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} +#endif + +#ifdef USE_TEXT_SENSOR +void BME68xBSEC2Component::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { + if (!sensor || (sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} +#endif + +void BME68xBSEC2Component::load_state_() { + uint32_t hash = this->get_hash(); + this->bsec_state_ = global_preferences->make_preference(hash, true); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + if (this->bsec_state_.load(&state)) { + ESP_LOGV(TAG, "Loading state"); + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = + bsec_set_state_m(&this->bsec_instance_, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed to load state (BSEC2 error code %d)", this->bsec_status_); + } + ESP_LOGI(TAG, "Loaded state"); + } +} + +void BME68xBSEC2Component::save_state_(uint8_t accuracy) { + if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { + return; + } + + ESP_LOGV(TAG, "Saving state"); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; + uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; + + this->bsec_status_ = bsec_get_state_m(&this->bsec_instance_, 0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, + BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed fetch state for save (BSEC2 error code %d)", this->bsec_status_); + return; + } + + if (!this->bsec_state_.save(&state)) { + ESP_LOGW(TAG, "Failed to save state"); + return; + } + this->last_state_save_ms_ = millis(); + + ESP_LOGI(TAG, "Saved state"); +} + +} // namespace bme68x_bsec2 +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h new file mode 100644 index 0000000000..7b9db2b7bf --- /dev/null +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -0,0 +1,163 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" + +#ifdef USE_BSEC2 + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif + +#include +#include + +#include + +namespace esphome { +namespace bme68x_bsec2 { + +enum AlgorithmOutput { + ALGORITHM_OUTPUT_IAQ, + ALGORITHM_OUTPUT_CLASSIFICATION, + ALGORITHM_OUTPUT_REGRESSION, +}; + +enum OperatingAge { + OPERATING_AGE_4D, + OPERATING_AGE_28D, +}; + +enum SampleRate { + SAMPLE_RATE_LP = 0, + SAMPLE_RATE_ULP = 1, + SAMPLE_RATE_DEFAULT = 2, +}; + +enum Voltage { + VOLTAGE_1_8V, + VOLTAGE_3_3V, +}; + +class BME68xBSEC2Component : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; } + void set_operating_age(OperatingAge operating_age) { this->operating_age_ = operating_age; } + void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } + void set_voltage(Voltage voltage) { this->voltage_ = voltage; } + + void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } + void set_temperature_sample_rate(SampleRate sample_rate) { this->temperature_sample_rate_ = sample_rate; } + void set_pressure_sample_rate(SampleRate sample_rate) { this->pressure_sample_rate_ = sample_rate; } + void set_humidity_sample_rate(SampleRate sample_rate) { this->humidity_sample_rate_ = sample_rate; } + + void set_bsec2_configuration(const uint8_t *data, const uint32_t len) { + this->bsec2_configuration_ = data; + this->bsec2_configuration_length_ = len; + } + + void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + +#ifdef USE_SENSOR + void set_temperature_sensor(sensor::Sensor *sensor) { this->temperature_sensor_ = sensor; } + void set_pressure_sensor(sensor::Sensor *sensor) { this->pressure_sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } + void set_gas_resistance_sensor(sensor::Sensor *sensor) { this->gas_resistance_sensor_ = sensor; } + void set_iaq_sensor(sensor::Sensor *sensor) { this->iaq_sensor_ = sensor; } + void set_iaq_static_sensor(sensor::Sensor *sensor) { this->iaq_static_sensor_ = sensor; } + void set_iaq_accuracy_sensor(sensor::Sensor *sensor) { this->iaq_accuracy_sensor_ = sensor; } + void set_co2_equivalent_sensor(sensor::Sensor *sensor) { this->co2_equivalent_sensor_ = sensor; } + void set_breath_voc_equivalent_sensor(sensor::Sensor *sensor) { this->breath_voc_equivalent_sensor_ = sensor; } +#endif +#ifdef USE_TEXT_SENSOR + void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *sensor) { this->iaq_accuracy_text_sensor_ = sensor; } +#endif + virtual uint32_t get_hash() = 0; + + protected: + void set_config_(const uint8_t *config, u_int32_t len); + float calc_sensor_sample_rate_(SampleRate sample_rate); + void update_subscription_(); + + void run_(); + void read_(int64_t trigger_time_ns); + void publish_(const bsec_output_t *outputs, uint8_t num_outputs); + int64_t get_time_ns_(); + +#ifdef USE_SENSOR + void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); +#endif +#ifdef USE_TEXT_SENSOR + void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); +#endif + + void load_state_(); + void save_state_(uint8_t accuracy); + + void queue_push_(std::function &&f) { this->queue_.push(std::move(f)); } + + struct bme68x_dev bme68x_; + bsec_bme_settings_t bsec_settings_; + bsec_version_t version_; + uint8_t bsec_instance_[BSEC_INSTANCE_SIZE]; + + struct bme68x_heatr_conf bme68x_heatr_conf_; + uint8_t op_mode_; // operating mode of sensor + bool sleep_mode_; + bsec_library_return_t bsec_status_{BSEC_OK}; + int8_t bme68x_status_{BME68X_OK}; + + int64_t last_time_ms_{0}; + uint32_t millis_overflow_counter_{0}; + int64_t next_call_ns_{0}; + + std::queue> queue_; + + uint8_t const *bsec2_configuration_{nullptr}; + uint32_t bsec2_configuration_length_{0}; + bool bsec2_blob_configured_{false}; + + ESPPreferenceObject bsec_state_; + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_ = 0; + + float temperature_offset_{0}; + + AlgorithmOutput algorithm_output_{ALGORITHM_OUTPUT_IAQ}; + OperatingAge operating_age_{OPERATING_AGE_28D}; + Voltage voltage_{VOLTAGE_3_3V}; + + SampleRate sample_rate_{SAMPLE_RATE_LP}; // Core/gas sample rate + SampleRate temperature_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate pressure_sample_rate_{SAMPLE_RATE_DEFAULT}; + SampleRate humidity_sample_rate_{SAMPLE_RATE_DEFAULT}; + +#ifdef USE_SENSOR + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *pressure_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *gas_resistance_sensor_{nullptr}; + sensor::Sensor *iaq_sensor_{nullptr}; + sensor::Sensor *iaq_static_sensor_{nullptr}; + sensor::Sensor *iaq_accuracy_sensor_{nullptr}; + sensor::Sensor *co2_equivalent_sensor_{nullptr}; + sensor::Sensor *breath_voc_equivalent_sensor_{nullptr}; +#endif +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *iaq_accuracy_text_sensor_{nullptr}; +#endif +}; + +} // namespace bme68x_bsec2 +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2/sensor.py b/esphome/components/bme68x_bsec2/sensor.py new file mode 100644 index 0000000000..419f47b248 --- /dev/null +++ b/esphome/components/bme68x_bsec2/sensor.py @@ -0,0 +1,130 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_GAS_RESISTANCE, + CONF_HUMIDITY, + CONF_IAQ_ACCURACY, + CONF_PRESSURE, + CONF_SAMPLE_RATE, + CONF_TEMPERATURE, + DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + ICON_GAS_CYLINDER, + ICON_GAUGE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_OHM, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, +) + +from . import CONF_BME68X_BSEC2_ID, SAMPLE_RATE_OPTIONS, BME68xBSEC2Component + +DEPENDENCIES = ["bme68x_bsec2"] + +CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" +CONF_CO2_EQUIVALENT = "co2_equivalent" +CONF_IAQ = "iaq" +CONF_IAQ_STATIC = "iaq_static" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" +ICON_TEST_TUBE = "mdi:test-tube" +UNIT_IAQ = "IAQ" + +TYPES = [ + CONF_TEMPERATURE, + CONF_PRESSURE, + CONF_HUMIDITY, + CONF_GAS_RESISTANCE, + CONF_IAQ, + CONF_IAQ_STATIC, + CONF_IAQ_ACCURACY, + CONF_CO2_EQUIVALENT, + CONF_BREATH_VOC_EQUIVALENT, +] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} + ), + cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ): sensor.sensor_schema( + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ_STATIC): sensor.sensor_schema( + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( + icon=ICON_ACCURACY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +) + + +async def setup_conf(config, key, hub): + if conf := config.get(key): + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) + if sample_rate := conf.get(CONF_SAMPLE_RATE): + cg.add(getattr(hub, f"set_{key}_sample_rate")(sample_rate)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2/text_sensor.py b/esphome/components/bme68x_bsec2/text_sensor.py new file mode 100644 index 0000000000..fce00afe34 --- /dev/null +++ b/esphome/components/bme68x_bsec2/text_sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_IAQ_ACCURACY + +from . import CONF_BME68X_BSEC2_ID, BME68xBSEC2Component + +DEPENDENCIES = ["bme68x_bsec2"] + +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" + +TYPES = [CONF_IAQ_ACCURACY] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), + cv.Optional(CONF_IAQ_ACCURACY): text_sensor.text_sensor_schema( + icon=ICON_ACCURACY + ), + } +) + + +async def setup_conf(config, key, hub): + if conf := config.get(key): + sens = await text_sensor.new_text_sensor(conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_BME68X_BSEC2_ID]) + for key in TYPES: + await setup_conf(config, key, hub) diff --git a/esphome/components/bme68x_bsec2_i2c/__init__.py b/esphome/components/bme68x_bsec2_i2c/__init__.py new file mode 100644 index 0000000000..d6fb7fa9be --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.bme68x_bsec2 import ( + CONFIG_SCHEMA_BASE, + BME68xBSEC2Component, + to_code_base, +) +import esphome.config_validation as cv + +CODEOWNERS = ["@neffs", "@kbx81"] + +AUTO_LOAD = ["bme68x_bsec2"] +DEPENDENCIES = ["i2c"] + +bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c") +BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_( + "BME68xBSEC2I2CComponent", BME68xBSEC2Component, i2c.I2CDevice +) + + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + cv.Schema({cv.GenerateID(): cv.declare_id(BME68xBSEC2I2CComponent)}) +).extend(i2c.i2c_device_schema(0x76)) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp new file mode 100644 index 0000000000..874c8bf388 --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.cpp @@ -0,0 +1,53 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_BSEC2 +#include "bme68x_bsec2_i2c.h" +#include "esphome/components/i2c/i2c.h" + +#include + +namespace esphome { +namespace bme68x_bsec2_i2c { + +static const char *const TAG = "bme68x_bsec2_i2c.sensor"; + +void BME68xBSEC2I2CComponent::setup() { + // must set up our bme68x_dev instance before calling setup() + this->bme68x_.intf_ptr = (void *) this; + this->bme68x_.intf = BME68X_I2C_INTF; + this->bme68x_.read = BME68xBSEC2I2CComponent::read_bytes_wrapper; + this->bme68x_.write = BME68xBSEC2I2CComponent::write_bytes_wrapper; + this->bme68x_.delay_us = BME68xBSEC2I2CComponent::delay_us; + this->bme68x_.amb_temp = 25; + + BME68xBSEC2Component::setup(); +} + +void BME68xBSEC2I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BME68xBSEC2Component::dump_config(); +} + +uint32_t BME68xBSEC2I2CComponent::get_hash() { return fnv1_hash("bme68x_bsec_state_" + to_string(this->address_)); } + +int8_t BME68xBSEC2I2CComponent::read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr) { + ESP_LOGVV(TAG, "read_bytes_wrapper: reg = %u", a_register); + return static_cast(intfPtr)->read_bytes(a_register, data, len) ? 0 : -1; +} + +int8_t BME68xBSEC2I2CComponent::write_bytes_wrapper(uint8_t a_register, const uint8_t *data, uint32_t len, + void *intfPtr) { + ESP_LOGVV(TAG, "write_bytes_wrapper: reg = %u", a_register); + return static_cast(intfPtr)->write_bytes(a_register, data, len) ? 0 : -1; +} + +void BME68xBSEC2I2CComponent::delay_us(uint32_t period, void *intfPtr) { + ESP_LOGVV(TAG, "Delaying for %" PRIu32 "us", period); + delayMicroseconds(period); +} + +} // namespace bme68x_bsec2_i2c +} // namespace esphome +#endif diff --git a/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h new file mode 100644 index 0000000000..a21a123f7b --- /dev/null +++ b/esphome/components/bme68x_bsec2_i2c/bme68x_bsec2_i2c.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/preferences.h" + +#ifdef USE_BSEC2 + +#include "esphome/components/bme68x_bsec2/bme68x_bsec2.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bme68x_bsec2_i2c { + +class BME68xBSEC2I2CComponent : public bme68x_bsec2::BME68xBSEC2Component, public i2c::I2CDevice { + void setup() override; + void dump_config() override; + + uint32_t get_hash() override; + + static int8_t read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr); + static int8_t write_bytes_wrapper(uint8_t a_register, const uint8_t *data, uint32_t len, void *intfPtr); + static void delay_us(uint32_t period, void *intfPtr); +}; + +} // namespace bme68x_bsec2_i2c +} // namespace esphome +#endif diff --git a/esphome/components/bmp280/sensor.py b/esphome/components/bmp280/sensor.py index a23bc0766a..a624889982 100644 --- a/esphome/components/bmp280/sensor.py +++ b/esphome/components/bmp280/sensor.py @@ -1,96 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor -from esphome.const import ( - CONF_ID, - CONF_PRESSURE, - CONF_TEMPERATURE, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, - CONF_IIR_FILTER, - CONF_OVERSAMPLING, + +CONFIG_SCHEMA = cv.invalid( + "The bmp280 sensor component has been renamed to bmp280_i2c." ) - -DEPENDENCIES = ["i2c"] - -bmp280_ns = cg.esphome_ns.namespace("bmp280") -BMP280Oversampling = bmp280_ns.enum("BMP280Oversampling") -OVERSAMPLING_OPTIONS = { - "NONE": BMP280Oversampling.BMP280_OVERSAMPLING_NONE, - "1X": BMP280Oversampling.BMP280_OVERSAMPLING_1X, - "2X": BMP280Oversampling.BMP280_OVERSAMPLING_2X, - "4X": BMP280Oversampling.BMP280_OVERSAMPLING_4X, - "8X": BMP280Oversampling.BMP280_OVERSAMPLING_8X, - "16X": BMP280Oversampling.BMP280_OVERSAMPLING_16X, -} - -BMP280IIRFilter = bmp280_ns.enum("BMP280IIRFilter") -IIR_FILTER_OPTIONS = { - "OFF": BMP280IIRFilter.BMP280_IIR_FILTER_OFF, - "2X": BMP280IIRFilter.BMP280_IIR_FILTER_2X, - "4X": BMP280IIRFilter.BMP280_IIR_FILTER_4X, - "8X": BMP280IIRFilter.BMP280_IIR_FILTER_8X, - "16X": BMP280IIRFilter.BMP280_IIR_FILTER_16X, -} - -BMP280Component = bmp280_ns.class_( - "BMP280Component", cg.PollingComponent, i2c.I2CDevice -) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(BMP280Component), - 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_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])) - - cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) diff --git a/esphome/components/bmp280_base/__init__.py b/esphome/components/bmp280_base/__init__.py new file mode 100644 index 0000000000..c0f9af9dd7 --- /dev/null +++ b/esphome/components/bmp280_base/__init__.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, +) + +CODEOWNERS = ["@ademuri"] + +bmp280_ns = cg.esphome_ns.namespace("bmp280_base") +BMP280Oversampling = bmp280_ns.enum("BMP280Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": BMP280Oversampling.BMP280_OVERSAMPLING_NONE, + "1X": BMP280Oversampling.BMP280_OVERSAMPLING_1X, + "2X": BMP280Oversampling.BMP280_OVERSAMPLING_2X, + "4X": BMP280Oversampling.BMP280_OVERSAMPLING_4X, + "8X": BMP280Oversampling.BMP280_OVERSAMPLING_8X, + "16X": BMP280Oversampling.BMP280_OVERSAMPLING_16X, +} + +BMP280IIRFilter = bmp280_ns.enum("BMP280IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": BMP280IIRFilter.BMP280_IIR_FILTER_OFF, + "2X": BMP280IIRFilter.BMP280_IIR_FILTER_2X, + "4X": BMP280IIRFilter.BMP280_IIR_FILTER_4X, + "8X": BMP280IIRFilter.BMP280_IIR_FILTER_8X, + "16X": BMP280IIRFilter.BMP280_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_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code_base(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(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])) + + cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) + + return var diff --git a/esphome/components/bmp280/bmp280.cpp b/esphome/components/bmp280_base/bmp280_base.cpp similarity index 95% rename from esphome/components/bmp280/bmp280.cpp rename to esphome/components/bmp280_base/bmp280_base.cpp index c92daa07fb..f94456f6e6 100644 --- a/esphome/components/bmp280/bmp280.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -1,9 +1,9 @@ -#include "bmp280.h" +#include "bmp280_base.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { -namespace bmp280 { +namespace bmp280_base { static const char *const TAG = "bmp280.sensor"; @@ -59,6 +59,14 @@ static const char *iir_filter_to_str(BMP280IIRFilter filter) { void BMP280Component::setup() { ESP_LOGCONFIG(TAG, "Setting up BMP280..."); uint8_t chip_id = 0; + + // Read the chip id twice, to work around a bug where the first read is 0. + // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 + if (!this->read_byte(0xD0, &chip_id)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } if (!this->read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); @@ -122,7 +130,6 @@ void BMP280Component::setup() { } void BMP280Component::dump_config() { ESP_LOGCONFIG(TAG, "BMP280:"); - LOG_I2C_DEVICE(this); switch (this->error_code_) { case COMMUNICATION_FAILED: ESP_LOGE(TAG, "Communication with BMP280 failed!"); @@ -262,5 +269,5 @@ uint16_t BMP280Component::read_u16_le_(uint8_t a_register) { } int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } -} // namespace bmp280 +} // namespace bmp280_base } // namespace esphome diff --git a/esphome/components/bmp280/bmp280.h b/esphome/components/bmp280_base/bmp280_base.h similarity index 88% rename from esphome/components/bmp280/bmp280.h rename to esphome/components/bmp280_base/bmp280_base.h index 96eb470155..4b22e98f13 100644 --- a/esphome/components/bmp280/bmp280.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -2,10 +2,9 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" namespace esphome { -namespace bmp280 { +namespace bmp280_base { /// Internal struct storing the calibration values of an BMP280. struct BMP280CalibrationData { @@ -50,8 +49,8 @@ enum BMP280IIRFilter { BMP280_IIR_FILTER_16X = 0b100, }; -/// This class implements support for the BMP280 Temperature+Pressure i2c sensor. -class BMP280Component : public PollingComponent, public i2c::I2CDevice { +/// This class implements support for the BMP280 Temperature+Pressure sensor. +class BMP280Component : public PollingComponent { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } @@ -68,6 +67,11 @@ class BMP280Component : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override; void update() override; + 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; + protected: /// Read the temperature value and store the calculated ambient temperature in t_fine. float read_temperature_(int32_t *t_fine); @@ -90,5 +94,5 @@ class BMP280Component : public PollingComponent, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace bmp280 +} // namespace bmp280_base } // namespace esphome diff --git a/esphome/components/bmp280_i2c/__init__.py b/esphome/components/bmp280_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.cpp b/esphome/components/bmp280_i2c/bmp280_i2c.cpp new file mode 100644 index 0000000000..04b8bd8b10 --- /dev/null +++ b/esphome/components/bmp280_i2c/bmp280_i2c.cpp @@ -0,0 +1,27 @@ +#include "bmp280_i2c.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bmp280_i2c { + +bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { + return I2CDevice::read_byte(a_register, data); +}; +bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) { + return I2CDevice::write_byte(a_register, data); +}; +bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::read_bytes(a_register, data, len); +}; +bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + return I2CDevice::read_byte_16(a_register, data); +}; + +void BMP280I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BMP280Component::dump_config(); +} + +} // namespace bmp280_i2c +} // namespace esphome diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.h b/esphome/components/bmp280_i2c/bmp280_i2c.h new file mode 100644 index 0000000000..66d78d788b --- /dev/null +++ b/esphome/components/bmp280_i2c/bmp280_i2c.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/bmp280_base/bmp280_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bmp280_i2c { + +static const char *const TAG = "bmp280_i2c.sensor"; + +/// This class implements support for the BMP280 Temperature+Pressure i2c sensor. +class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice { + public: + 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 bmp280_i2c +} // namespace esphome diff --git a/esphome/components/bmp280_i2c/sensor.py b/esphome/components/bmp280_i2c/sensor.py new file mode 100644 index 0000000000..991bb827a3 --- /dev/null +++ b/esphome/components/bmp280_i2c/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from ..bmp280_base import to_code_base, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["bmp280_base"] +CODEOWNERS = ["@ademuri"] +DEPENDENCIES = ["i2c"] + +bmp280_ns = cg.esphome_ns.namespace("bmp280_i2c") +BMP280I2CComponent = bmp280_ns.class_( + "BMP280I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(BMP280I2CComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/bmp280_spi/__init__.py b/esphome/components/bmp280_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/bmp280_spi/bmp280_spi.cpp b/esphome/components/bmp280_spi/bmp280_spi.cpp new file mode 100644 index 0000000000..a35e829432 --- /dev/null +++ b/esphome/components/bmp280_spi/bmp280_spi.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include "bmp280_spi.h" +#include + +namespace esphome { +namespace bmp280_spi { + +uint8_t set_bit(uint8_t num, uint8_t position) { + uint8_t mask = 1 << position; + return num | mask; +} + +uint8_t clear_bit(uint8_t num, uint8_t position) { + uint8_t mask = 1 << position; + return num & ~mask; +} + +void BMP280SPIComponent::setup() { + this->spi_setup(); + BMP280Component::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-bmp280-ds001.pdf + +bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + *data = this->transfer_byte(0); + this->disable(); + return true; +} + +bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->transfer_byte(clear_bit(a_register, 7)); + this->transfer_byte(data); + this->disable(); + return true; +} + +bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + this->read_array(data, len); + this->disable(); + return true; +} + +bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + this->enable(); + this->transfer_byte(set_bit(a_register, 7)); + ((uint8_t *) data)[1] = this->transfer_byte(0); + ((uint8_t *) data)[0] = this->transfer_byte(0); + this->disable(); + return true; +} + +} // namespace bmp280_spi +} // namespace esphome diff --git a/esphome/components/bmp280_spi/bmp280_spi.h b/esphome/components/bmp280_spi/bmp280_spi.h new file mode 100644 index 0000000000..dd226502f6 --- /dev/null +++ b/esphome/components/bmp280_spi/bmp280_spi.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/bmp280_base/bmp280_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace bmp280_spi { + +class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component, + public spi::SPIDevice { + 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 bmp280_spi +} // namespace esphome diff --git a/esphome/components/bmp280_spi/sensor.py b/esphome/components/bmp280_spi/sensor.py new file mode 100644 index 0000000000..511d45b24e --- /dev/null +++ b/esphome/components/bmp280_spi/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from ..bmp280_base import to_code_base, CONFIG_SCHEMA_BASE + +AUTO_LOAD = ["bmp280_base"] +CODEOWNERS = ["@ademuri"] +DEPENDENCIES = ["spi"] + +bmp280_ns = cg.esphome_ns.namespace("bmp280_spi") +BMP280SPIComponent = bmp280_ns.class_( + "BMP280SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + spi.spi_device_schema(default_mode="mode3") +).extend({cv.GenerateID(): cv.declare_id(BMP280SPIComponent)}) + + +async def to_code(config): + var = await to_code_base(config) + await spi.register_spi_device(var, config) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 773ab9d37f..3010d3006a 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -1,16 +1,16 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, + CONF_MQTT_ID, CONF_ON_PRESS, CONF_TRIGGER_ID, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, DEVICE_CLASS_EMPTY, DEVICE_CLASS_IDENTIFY, @@ -18,8 +18,8 @@ from esphome.const import ( DEVICE_CLASS_UPDATE, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 630e00f0b7..d1960e9a93 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -1,4 +1,5 @@ #include "captive_portal.h" +#ifdef USE_CAPTIVE_PORTAL #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/components/wifi/wifi_component.h" @@ -91,3 +92,4 @@ CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avo } // namespace captive_portal } // namespace esphome +#endif diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index df45d40d12..24d1295e6a 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_CAPTIVE_PORTAL #include #ifdef USE_ARDUINO #include @@ -71,3 +72,4 @@ extern CaptivePortal *global_captive_portal; // NOLINT(cppcoreguidelines-avoid- } // namespace captive_portal } // namespace esphome +#endif diff --git a/esphome/components/ch422g/__init__.py b/esphome/components/ch422g/__init__.py new file mode 100644 index 0000000000..cf8b5f65d3 --- /dev/null +++ b/esphome/components/ch422g/__init__.py @@ -0,0 +1,67 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, + CONF_RESTORE_VALUE, +) + +CODEOWNERS = ["@jesterret"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +ch422g_ns = cg.esphome_ns.namespace("ch422g") + +CH422GComponent = ch422g_ns.class_("CH422GComponent", cg.Component, i2c.I2CDevice) +CH422GGPIOPin = ch422g_ns.class_( + "CH422GGPIOPin", cg.GPIOPin, cg.Parented.template(CH422GComponent) +) + +CONF_CH422G = "ch422g" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(CH422GComponent), + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x24)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + +CH422G_PIN_SCHEMA = pins.gpio_base_schema( + CH422GGPIOPin, + cv.int_range(min=0, max=7), + modes=[CONF_INPUT, CONF_OUTPUT], +).extend( + { + cv.Required(CONF_CH422G): cv.use_id(CH422GComponent), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH422G, CH422G_PIN_SCHEMA) +async def ch422g_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_CH422G]) + + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp new file mode 100644 index 0000000000..25038991ed --- /dev/null +++ b/esphome/components/ch422g/ch422g.cpp @@ -0,0 +1,122 @@ +#include "ch422g.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ch422g { + +const uint8_t CH422G_REG_IN = 0x26; +const uint8_t CH422G_REG_OUT = 0x38; +const uint8_t OUT_REG_DEFAULT_VAL = 0xdf; + +static const char *const TAG = "ch422g"; + +void CH422GComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up CH422G..."); + // Test to see if device exists + if (!this->read_inputs_()) { + ESP_LOGE(TAG, "CH422G not detected at 0x%02X", this->address_); + this->mark_failed(); + return; + } + + // restore defaults over whatever got saved on last boot + if (!this->restore_value_) { + this->write_output_(OUT_REG_DEFAULT_VAL); + } + + ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); +} + +void CH422GComponent::loop() { + // Clear all the previously read flags. + this->pin_read_cache_ = 0x00; +} + +void CH422GComponent::dump_config() { + ESP_LOGCONFIG(TAG, "CH422G:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with CH422G failed!"); + } +} + +// ch422g doesn't have any flag support (needs docs?) +void CH422GComponent::pin_mode(uint8_t pin, gpio::Flags flags) {} + +bool CH422GComponent::digital_read(uint8_t pin) { + if (this->pin_read_cache_ == 0 || this->pin_read_cache_ & (1 << pin)) { + // Read values on first access or in case it's being read again in the same loop + this->read_inputs_(); + } + + this->pin_read_cache_ |= (1 << pin); + return this->state_mask_ & (1 << pin); +} + +void CH422GComponent::digital_write(uint8_t pin, bool value) { + if (value) { + this->write_output_(this->state_mask_ | (1 << pin)); + } else { + this->write_output_(this->state_mask_ & ~(1 << pin)); + } +} + +bool CH422GComponent::read_inputs_() { + if (this->is_failed()) { + return false; + } + + uint8_t temp = 0; + if ((this->last_error_ = this->read(&temp, 1)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("read_inputs_(): I2C I/O error: %d", (int) this->last_error_).c_str()); + return false; + } + + uint8_t output = 0; + if ((this->last_error_ = this->bus_->read(CH422G_REG_IN, &output, 1)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("read_inputs_(): I2C I/O error: %d", (int) this->last_error_).c_str()); + return false; + } + + this->state_mask_ = output; + this->status_clear_warning(); + + return true; +} + +bool CH422GComponent::write_output_(uint8_t value) { + const uint8_t temp = 1; + if ((this->last_error_ = this->write(&temp, 1, false)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(str_sprintf("write_output_(): I2C I/O error: %d", (int) this->last_error_).c_str()); + return false; + } + + uint8_t write_mask = value; + if ((this->last_error_ = this->bus_->write(CH422G_REG_OUT, &write_mask, 1)) != esphome::i2c::ERROR_OK) { + this->status_set_warning( + str_sprintf("write_output_(): I2C I/O error: %d for write_mask: %d", (int) this->last_error_, (int) write_mask) + .c_str()); + return false; + } + + this->state_mask_ = value; + this->status_clear_warning(); + return true; +} + +float CH422GComponent::get_setup_priority() const { return setup_priority::IO; } + +// Run our loop() method very early in the loop, so that we cache read values +// before other components call our digital_read() method. +float CH422GComponent::get_loop_priority() const { return 9.0f; } // Just after WIFI + +void CH422GGPIOPin::setup() { pin_mode(flags_); } +void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } + +void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string CH422GGPIOPin::dump_summary() const { return str_sprintf("EXIO%u via CH422G", pin_); } + +} // namespace ch422g +} // namespace esphome diff --git a/esphome/components/ch422g/ch422g.h b/esphome/components/ch422g/ch422g.h new file mode 100644 index 0000000000..781df65437 --- /dev/null +++ b/esphome/components/ch422g/ch422g.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ch422g { + +class CH422GComponent : public Component, public i2c::I2CDevice { + public: + CH422GComponent() = default; + + /// Check i2c availability and setup masks + void setup() override; + /// Poll for input changes periodically + void loop() override; + /// Helper function to read the value of a pin. + bool digital_read(uint8_t pin); + /// Helper function to write the value of a pin. + void digital_write(uint8_t pin, bool value); + /// Helper function to set the pin mode of a pin. + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + + float get_loop_priority() const override; + + void dump_config() override; + + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + bool read_inputs_(); + + bool write_output_(uint8_t value); + + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t state_mask_{0x00}; + /// Flags to check if read previously during this loop + uint8_t pin_read_cache_ = {0x00}; + /// Storage for last I2C error seen + esphome::i2c::ErrorCode last_error_; + /// Whether we want to override stored values on expander + bool restore_value_{false}; +}; + +/// Helper class to expose a CH422G pin as an internal input GPIO pin. +class CH422GGPIOPin : public GPIOPin { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(CH422GComponent *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + protected: + CH422GComponent *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace ch422g +} // namespace esphome diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index ccd7a3da4e..c7e4ce7745 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -1,8 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.cpp_helpers import setup_entity from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ACTION_STATE_TOPIC, CONF_AWAY, @@ -21,6 +20,7 @@ from esphome.const import ( CONF_MODE, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, + CONF_MQTT_ID, CONF_ON_CONTROL, CONF_ON_STATE, CONF_PRESET, @@ -33,20 +33,20 @@ from esphome.const import ( CONF_TARGET_HUMIDITY_STATE_TOPIC, CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, - CONF_TARGET_TEMPERATURE_STATE_TOPIC, CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_HIGH_STATE_TOPIC, CONF_TARGET_TEMPERATURE_LOW, CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC, + CONF_TARGET_TEMPERATURE_STATE_TOPIC, CONF_TEMPERATURE_STEP, CONF_TRIGGER_ID, CONF_VISUAL, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 7c2a0b1ed3..d81702fb0c 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -141,7 +141,7 @@ struct ClimateDeviceRestoreState { float target_temperature_low; float target_temperature_high; }; - }; + } __attribute__((packed)); float target_humidity; /// Convert this struct to a climate call that can be performed. diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 313b2c5928..d25dd91148 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -1,23 +1,23 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.automation import maybe_simple_id, Condition +from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, CONF_DEVICE_CLASS, - CONF_STATE, + CONF_ID, + CONF_MQTT_ID, CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, CONF_POSITION_STATE_TOPIC, + CONF_STATE, + CONF_STOP, CONF_TILT, CONF_TILT_COMMAND_TOPIC, CONF_TILT_STATE_TOPIC, - CONF_STOP, - CONF_MQTT_ID, - CONF_WEB_SERVER_ID, CONF_TRIGGER_ID, + CONF_WEB_SERVER_ID, DEVICE_CLASS_AWNING, DEVICE_CLASS_BLIND, DEVICE_CLASS_CURTAIN, diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp index 69728dc666..d4e43d30f5 100644 --- a/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.cpp @@ -5,13 +5,17 @@ namespace cst226 { void CST226Touchscreen::setup() { esph_log_config(TAG, "Setting up CST226 Touchscreen..."); - this->reset_pin_->setup(); - this->reset_pin_->digital_write(true); - delay(5); - this->reset_pin_->digital_write(false); - delay(5); - this->reset_pin_->digital_write(true); - this->set_timeout(30, [this] { this->continue_setup_(); }); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + this->set_timeout(30, [this] { this->continue_setup_(); }); + } else { + this->continue_setup_(); + } } void CST226Touchscreen::update_touches() { diff --git a/esphome/components/cst226/touchscreen/cst226_touchscreen.h b/esphome/components/cst226/touchscreen/cst226_touchscreen.h index 1b15b952c4..9f518e5068 100644 --- a/esphome/components/cst226/touchscreen/cst226_touchscreen.h +++ b/esphome/components/cst226/touchscreen/cst226_touchscreen.h @@ -35,7 +35,7 @@ class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice void continue_setup_(); InternalGPIOPin *interrupt_pin_{}; - GPIOPin *reset_pin_{NULL_PIN}; + GPIOPin *reset_pin_{}; uint8_t chip_id_{}; bool setup_complete_{}; }; diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index c118216a2d..5429121d56 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -1,32 +1,30 @@ -import esphome.codegen as cg - -import esphome.config_validation as cv from esphome import automation -from esphome.components import mqtt, web_server, time +import esphome.codegen as cg +from esphome.components import mqtt, time, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_DATE, + CONF_DATETIME, + CONF_DAY, + CONF_HOUR, CONF_ID, + CONF_MINUTE, + CONF_MONTH, + CONF_MQTT_ID, CONF_ON_TIME, CONF_ON_VALUE, + CONF_SECOND, + CONF_TIME, CONF_TIME_ID, CONF_TRIGGER_ID, CONF_TYPE, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, - CONF_DATE, - CONF_DATETIME, - CONF_TIME, CONF_YEAR, - CONF_MONTH, - CONF_DAY, - CONF_SECOND, - CONF_HOUR, - CONF_MINUTE, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity - CODEOWNERS = ["@rfdarter", "@jesserockz"] DEPENDENCIES = ["time"] @@ -188,7 +186,7 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): date_config = config[CONF_DATE] if cg.is_template(date_config): - template_ = await cg.templatable(date_config, [], cg.ESPTime) + template_ = await cg.templatable(date_config, args, cg.ESPTime) cg.add(action_var.set_date(template_)) else: date_struct = cg.StructInitializer( @@ -219,7 +217,7 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): time_config = config[CONF_TIME] if cg.is_template(time_config): - template_ = await cg.templatable(time_config, [], cg.ESPTime) + template_ = await cg.templatable(time_config, args, cg.ESPTime) cg.add(action_var.set_time(template_)) else: time_struct = cg.StructInitializer( @@ -250,7 +248,7 @@ async def datetime_datetime_set_to_code(config, action_id, template_arg, args): datetime_config = config[CONF_DATETIME] if cg.is_template(datetime_config): - template_ = await cg.templatable(datetime_config, [], cg.ESPTime) + template_ = await cg.templatable(datetime_config, args, cg.ESPTime) cg.add(action_var.set_datetime(template_)) else: datetime_struct = cg.StructInitializer( diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index cfdfdd2a61..34aea9e26b 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -16,6 +16,8 @@ #include #elif defined(USE_ESP32_VARIANT_ESP32S3) #include +#elif defined(USE_ESP32_VARIANT_ESP32H2) +#include #endif #ifdef USE_ARDUINO #include @@ -61,7 +63,7 @@ std::string DebugComponent::get_reset_reason_() { case RTCWDT_SYS_RESET: reset_reason = "RTC Watch Dog Reset Digital Core"; break; -#if !defined(USE_ESP32_VARIANT_ESP32C6) +#if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) case INTRUSION_RESET: reset_reason = "Intrusion Reset CPU"; break; diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h index b4afa03e11..d965d987de 100644 --- a/esphome/components/demo/demo_sensor.h +++ b/esphome/components/demo/demo_sensor.h @@ -16,10 +16,11 @@ class DemoSensor : public sensor::Sensor, public PollingComponent { float base = std::isnan(this->state) ? 0.0f : this->state; this->publish_state(base + val * 10); } else { - if (val < 0.1) + if (val < 0.1) { this->publish_state(NAN); - else + } else { this->publish_state(val * 100); + } } } }; diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index c4bb12b75d..32a8b3b090 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -1,15 +1,15 @@ +from esphome import automation, core +from esphome.automation import maybe_simple_id import esphome.codegen as cg import esphome.config_validation as cv -from esphome import core, automation -from esphome.automation import maybe_simple_id from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, + CONF_FROM, CONF_ID, CONF_LAMBDA, - CONF_PAGES, CONF_PAGE_ID, + CONF_PAGES, CONF_ROTATION, - CONF_FROM, CONF_TO, CONF_TRIGGER_ID, ) @@ -195,3 +195,4 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(display_ns.using) + cg.add_define("USE_DISPLAY") diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 75205292f7..63c74e09ca 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -675,5 +675,36 @@ void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; } void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; } const display_writer_t &DisplayPage::get_writer() const { return this->writer_; } +const LogString *text_align_to_string(TextAlign textalign) { + switch (textalign) { + case TextAlign::TOP_LEFT: + return LOG_STR("TOP_LEFT"); + case TextAlign::TOP_CENTER: + return LOG_STR("TOP_CENTER"); + case TextAlign::TOP_RIGHT: + return LOG_STR("TOP_RIGHT"); + case TextAlign::CENTER_LEFT: + return LOG_STR("CENTER_LEFT"); + case TextAlign::CENTER: + return LOG_STR("CENTER"); + case TextAlign::CENTER_RIGHT: + return LOG_STR("CENTER_RIGHT"); + case TextAlign::BASELINE_LEFT: + return LOG_STR("BASELINE_LEFT"); + case TextAlign::BASELINE_CENTER: + return LOG_STR("BASELINE_CENTER"); + case TextAlign::BASELINE_RIGHT: + return LOG_STR("BASELINE_RIGHT"); + case TextAlign::BOTTOM_LEFT: + return LOG_STR("BOTTOM_LEFT"); + case TextAlign::BOTTOM_CENTER: + return LOG_STR("BOTTOM_CENTER"); + case TextAlign::BOTTOM_RIGHT: + return LOG_STR("BOTTOM_RIGHT"); + default: + return LOG_STR("UNKNOWN"); + } +} + } // namespace display } // namespace esphome diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 4ee7ef93cb..34feafea6e 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -8,6 +8,7 @@ #include "esphome/core/color.h" #include "esphome/core/automation.h" #include "esphome/core/time.h" +#include "esphome/core/log.h" #include "display_color_utils.h" #ifdef USE_GRAPH @@ -737,5 +738,7 @@ class DisplayOnPageChangeTrigger : public Trigger DisplayPage *to_{nullptr}; }; +const LogString *text_align_to_string(TextAlign textalign); + } // namespace display } // namespace esphome diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py index 0c738ba838..8ae9cbc2a4 100644 --- a/esphome/components/display_menu_base/__init__.py +++ b/esphome/components/display_menu_base/__init__.py @@ -1,23 +1,26 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv + from esphome import automation, core +from esphome.automation import maybe_simple_id +import esphome.codegen as cg +from esphome.components.number import Number +from esphome.components.select import Select +from esphome.components.switch import Switch +import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_TYPE, - CONF_TRIGGER_ID, - CONF_ON_VALUE, + CONF_ACTIVE, CONF_COMMAND, CONF_CUSTOM, - CONF_NUMBER, CONF_FORMAT, + CONF_ID, + CONF_ITEMS, CONF_MODE, - CONF_ACTIVE, + CONF_NUMBER, + CONF_ON_VALUE, + CONF_TEXT, + CONF_TRIGGER_ID, + CONF_TYPE, ) -from esphome.automation import maybe_simple_id -from esphome.components.select import Select -from esphome.components.number import Number -from esphome.components.switch import Switch CODEOWNERS = ["@numo68"] @@ -29,10 +32,8 @@ CONF_JOYSTICK = "joystick" CONF_LABEL = "label" CONF_MENU = "menu" CONF_BACK = "back" -CONF_TEXT = "text" CONF_SELECT = "select" CONF_SWITCH = "switch" -CONF_ITEMS = "items" CONF_ON_TEXT = "on_text" CONF_OFF_TEXT = "off_text" CONF_VALUE_LAMBDA = "value_lambda" diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index f382730912..193ea1d4e5 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -256,6 +256,7 @@ bool Dsmr::parse_telegram() { MyData data; ESP_LOGV(TAG, "Trying to parse telegram"); this->stop_requesting_data_(); + ::dsmr::ParseResult res = ::dsmr::P1Parser::parse(&data, this->telegram_, this->bytes_read_, false, this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. @@ -267,6 +268,11 @@ bool Dsmr::parse_telegram() { } else { this->status_clear_warning(); this->publish_sensors(data); + + // publish the telegram, after publishing the sensors so it can also trigger action based on latest values + if (this->s_telegram_ != nullptr) { + this->s_telegram_->publish_state(std::string(this->telegram_, this->bytes_read_)); + } return true; } } diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 6621d02cae..7304737b50 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -85,6 +85,9 @@ class Dsmr : public Component, public uart::UARTDevice { void set_##s(text_sensor::TextSensor *sensor) { s_##s##_ = sensor; } DSMR_TEXT_SENSOR_LIST(DSMR_SET_TEXT_SENSOR, ) + // handled outside dsmr + void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; } + protected: void receive_telegram_(); void receive_encrypted_telegram_(); @@ -124,6 +127,9 @@ class Dsmr : public Component, public uart::UARTDevice { bool header_found_{false}; bool footer_found_{false}; + // handled outside dsmr + text_sensor::TextSensor *s_telegram_{nullptr}; + // Sensor member pointers #define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr}; DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py index 202cc07020..7c13fe7d58 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor - +from esphome.const import CONF_INTERNAL from . import Dsmr, CONF_DSMR_ID AUTO_LOAD = ["dsmr"] @@ -22,6 +22,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("water_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), + cv.Optional("telegram"): text_sensor.text_sensor_schema().extend( + {cv.Optional(CONF_INTERNAL, default=True): cv.boolean} + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -37,7 +40,9 @@ async def to_code(config): if id and id.type == text_sensor.TextSensor: var = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}")(var)) - text_sensors.append(f"F({key})") + if key != "telegram": + # telegram is not handled by dsmr + text_sensors.append(f"F({key})") if text_sensors: cg.add_define( diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index c3ff21c1a0..a74fc9be4a 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -1,4 +1,5 @@ #include "e131.h" +#ifdef USE_NETWORK #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" @@ -118,3 +119,4 @@ bool E131Component::process_(int universe, const E131Packet &packet) { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 91b67f62eb..d0e38fa98c 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include "esphome/components/socket/socket.h" #include "esphome/core/component.h" @@ -53,3 +54,4 @@ class E131Component : public esphome::Component { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index be3144f590..4d1f98ab6c 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -1,5 +1,6 @@ #include "e131_addressable_light_effect.h" #include "e131.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" namespace esphome { @@ -90,3 +91,4 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index 56df9cd80f..17d7bd2829 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/light/addressable_light_effect.h" - +#ifdef USE_NETWORK namespace esphome { namespace e131 { @@ -42,3 +42,4 @@ class E131AddressableLightEffect : public light::AddressableLightEffect { } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index e1ae41cbaf..b8fa73b707 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,5 +1,6 @@ #include #include "e131.h" +#ifdef USE_NETWORK #include "esphome/components/network/ip_address.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -137,3 +138,4 @@ bool E131Component::packet_(const std::vector &data, int &universe, E13 } // namespace e131 } // namespace esphome +#endif diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0a5dd46478..9cb9ac257a 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -172,6 +172,19 @@ def add_idf_component( KEY_COMPONENTS: components, KEY_SUBMODULES: submodules, } + else: + component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] + if components is not None: + component_config[KEY_COMPONENTS] = list( + set(component_config[KEY_COMPONENTS] + components) + ) + if submodules is not None: + if component_config[KEY_SUBMODULES] is None: + component_config[KEY_SUBMODULES] = submodules + else: + component_config[KEY_SUBMODULES] = list( + set(component_config[KEY_SUBMODULES] + submodules) + ) def add_extra_script(stage: str, filename: str, path: str): @@ -226,7 +239,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 7) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 8) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index 57c2f9df94..07ac719434 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -31,6 +31,13 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128); return ret; } +ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) { + ESPBTUUID ret; + ret.uuid_.len = ESP_UUID_LEN_128; + for (int i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[ESP_UUID_LEN_128 - 1 - i] = data[i]; + return ret; +} ESPBTUUID ESPBTUUID::from_raw(const std::string &data) { ESPBTUUID ret; if (data.length() == 4) { diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index 790a57c59d..d90db3a599 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -20,6 +20,7 @@ class ESPBTUUID { static ESPBTUUID from_uint32(uint32_t uuid); static ESPBTUUID from_raw(const uint8_t *data); + static ESPBTUUID from_raw_reversed(const uint8_t *data); static ESPBTUUID from_raw(const std::string &data); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index d154d4e519..74b4b9aa89 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -462,14 +462,16 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } for (auto &data : this->manufacturer_datas_) { - ESP_LOGVV(TAG, " Manufacturer data: %s", format_hex_pretty(data.data).c_str()); - if (this->get_ibeacon().has_value()) { - auto ibeacon = this->get_ibeacon().value(); - ESP_LOGVV(TAG, " iBeacon data:"); - ESP_LOGVV(TAG, " UUID: %s", ibeacon.get_uuid().to_string().c_str()); - ESP_LOGVV(TAG, " Major: %u", ibeacon.get_major()); - ESP_LOGVV(TAG, " Minor: %u", ibeacon.get_minor()); - ESP_LOGVV(TAG, " TXPower: %d", ibeacon.get_signal_power()); + auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data); + if (ibeacon.has_value()) { + ESP_LOGVV(TAG, " Manufacturer iBeacon:"); + ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str()); + ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major()); + ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor()); + ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power()); + } else { + ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", data.uuid.to_string().c_str(), + format_hex_pretty(data.data).c_str()); } } for (auto &data : this->service_datas_) { @@ -478,7 +480,7 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); } - ESP_LOGVV(TAG, "Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); + ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 3db7a54f6e..d2bb6a6e6d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -44,10 +44,10 @@ class ESPBLEiBeacon { ESPBLEiBeacon(const uint8_t *data); static optional from_manufacturer_data(const ServiceData &data); - uint16_t get_major() { return ((this->beacon_data_.major & 0xFF) << 8) | (this->beacon_data_.major >> 8); } - uint16_t get_minor() { return ((this->beacon_data_.minor & 0xFF) << 8) | (this->beacon_data_.minor >> 8); } + uint16_t get_major() { return byteswap(this->beacon_data_.major); } + uint16_t get_minor() { return byteswap(this->beacon_data_.minor); } int8_t get_signal_power() { return this->beacon_data_.signal_power; } - ESPBTUUID get_uuid() { return ESPBTUUID::from_raw(this->beacon_data_.proximity_uuid); } + ESPBTUUID get_uuid() { return ESPBTUUID::from_raw_reversed(this->beacon_data_.proximity_uuid); } protected: struct { diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 4187429412..2f1f9b90bb 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -140,6 +140,8 @@ CONF_TEST_PATTERN = "test_pattern" # framerates CONF_MAX_FRAMERATE = "max_framerate" CONF_IDLE_FRAMERATE = "idle_framerate" +# frame buffer +CONF_FRAME_BUFFER_COUNT = "frame_buffer_count" # stream trigger CONF_ON_STREAM_START = "on_stream_start" @@ -213,6 +215,7 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( cv.framerate, cv.Range(min=0, max=1) ), + cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -285,6 +288,7 @@ async def to_code(config): cg.add(var.set_idle_update_interval(0)) else: cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE])) + cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT])) cg.add(var.set_frame_size(config[CONF_RESOLUTION])) cg.add_define("USE_ESP32_CAMERA") diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 555f6ca5f1..e9e9d3cffb 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -127,7 +127,7 @@ void ESP32Camera::dump_config() { sensor_t *s = esp_camera_sensor_get(); auto st = s->status; ESP_LOGCONFIG(TAG, " JPEG Quality: %u", st.quality); - // ESP_LOGCONFIG(TAG, " Framebuffer Count: %u", conf.fb_count); + ESP_LOGCONFIG(TAG, " Framebuffer Count: %u", conf.fb_count); ESP_LOGCONFIG(TAG, " Contrast: %d", st.contrast); ESP_LOGCONFIG(TAG, " Brightness: %d", st.brightness); ESP_LOGCONFIG(TAG, " Saturation: %d", st.saturation); @@ -212,6 +212,8 @@ ESP32Camera::ESP32Camera() { this->config_.frame_size = FRAMESIZE_VGA; // 640x480 this->config_.jpeg_quality = 10; this->config_.fb_count = 1; + this->config_.grab_mode = CAMERA_GRAB_WHEN_EMPTY; + this->config_.fb_location = CAMERA_FB_IN_PSRAM; global_esp32_camera = this; } @@ -333,6 +335,12 @@ void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) { void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { this->idle_update_interval_ = idle_update_interval; } +/* set frame buffer parameters */ +void ESP32Camera::set_frame_buffer_mode(camera_grab_mode_t mode) { this->config_.grab_mode = mode; } +void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) { + this->config_.fb_count = fb_count; + this->set_frame_buffer_mode(fb_count > 1 ? CAMERA_GRAB_LATEST : CAMERA_GRAB_WHEN_EMPTY); +} /* ---------------- public API (specific) ---------------- */ void ESP32Camera::add_image_callback(std::function)> &&callback) { diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 0c25381039..71f47d3c06 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -145,6 +145,9 @@ class ESP32Camera : public Component, public EntityBase { /* -- framerates */ void set_max_update_interval(uint32_t max_update_interval); void set_idle_update_interval(uint32_t idle_update_interval); + /* -- frame buffer */ + void set_frame_buffer_mode(camera_grab_mode_t mode); + void set_frame_buffer_count(uint8_t fb_count); /* public API (derivated) */ void setup() override; diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index f4ba032009..37bdfa3962 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -1,18 +1,23 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import canbus -from esphome.const import CONF_ID, CONF_RX_PIN, CONF_TX_PIN -from esphome.components.canbus import CanbusComponent, CanSpeed, CONF_BIT_RATE - +from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32S3, VARIANT_ESP32C3, VARIANT_ESP32C6, VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_RX_PIN, + CONF_RX_QUEUE_LEN, + CONF_TX_PIN, + CONF_TX_QUEUE_LEN, ) CODEOWNERS = ["@Sympatron"] @@ -77,6 +82,8 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend( cv.Optional(CONF_BIT_RATE, default="125KBPS"): validate_bit_rate, cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_RX_QUEUE_LEN): cv.uint32_t, + cv.Optional(CONF_TX_QUEUE_LEN): cv.uint32_t, } ) @@ -87,3 +94,7 @@ async def to_code(config): cg.add(var.set_rx(config[CONF_RX_PIN])) cg.add(var.set_tx(config[CONF_TX_PIN])) + if (rx_queue_len := config.get(CONF_RX_QUEUE_LEN)) is not None: + cg.add(var.set_rx_queue_len(rx_queue_len)) + if (tx_queue_len := config.get(CONF_TX_QUEUE_LEN)) is not None: + cg.add(var.set_tx_queue_len(tx_queue_len)) diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index 79e4b70f97..5a45859b1f 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -69,6 +69,13 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config bool ESP32Can::setup_internal() { twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); + if (this->tx_queue_len_.has_value()) { + g_config.tx_queue_len = this->tx_queue_len_.value(); + } + if (this->rx_queue_len_.has_value()) { + g_config.rx_queue_len = this->rx_queue_len_.value(); + } + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); twai_timing_config_t t_config; @@ -111,6 +118,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { .flags = flags, .identifier = frame->can_id, .data_length_code = frame->can_data_length_code, + .data = {}, // to suppress warning, data is initialized properly below }; if (!frame->remote_transmission_request) { memcpy(message.data, frame->data, frame->can_data_length_code); diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h index a428834f65..b3086f9a48 100644 --- a/esphome/components/esp32_can/esp32_can.h +++ b/esphome/components/esp32_can/esp32_can.h @@ -12,6 +12,8 @@ class ESP32Can : public canbus::Canbus { public: void set_rx(int rx) { rx_ = rx; } void set_tx(int tx) { tx_ = tx; } + void set_tx_queue_len(uint32_t tx_queue_len) { this->tx_queue_len_ = tx_queue_len; } + void set_rx_queue_len(uint32_t rx_queue_len) { this->rx_queue_len_ = rx_queue_len; } ESP32Can(){}; protected: @@ -21,6 +23,8 @@ class ESP32Can : public canbus::Canbus { int rx_{-1}; int tx_{-1}; + optional tx_queue_len_{}; + optional rx_queue_len_{}; }; } // namespace esp32_can diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index 62d9cd376c..705dff0f1b 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -1,9 +1,8 @@ import esphome.codegen as cg +from esphome.components import binary_sensor, esp32_ble_server, output import esphome.config_validation as cv -from esphome.components import binary_sensor, output, esp32_ble_server from esphome.const import CONF_ID - AUTO_LOAD = ["esp32_ble_server"] CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["wifi", "esp32"] @@ -50,7 +49,7 @@ async def to_code(config): cg.add(ble_server.register_service_component(var)) cg.add_define("USE_IMPROV") - cg.add_library("esphome/Improv", "1.2.3") + cg.add_library("improv/Improv", "1.2.4") cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 7727b64f29..71ab099de5 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -38,7 +38,8 @@ void ESP32RMTLEDStripLightOutput::setup() { } ExternalRAMAllocator rmt_allocator(ExternalRAMAllocator::ALLOW_FAILURE); - this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8); // 8 bits per byte, 1 rmt_item32_t per bit + this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + + 1); // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset rmt_config_t config; memset(&config, 0, sizeof(config)); @@ -66,7 +67,7 @@ void ESP32RMTLEDStripLightOutput::setup() { } 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, uint32_t reset_time_high, uint32_t reset_time_low) { float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; // 0-bit @@ -79,6 +80,11 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi this->bit1_.level0 = 1; this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); this->bit1_.level1 = 0; + // reset + this->reset_.duration0 = (uint32_t) (ratio * reset_time_high); + this->reset_.level0 = 1; + this->reset_.duration1 = (uint32_t) (ratio * reset_time_low); + this->reset_.level1 = 0; } void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { @@ -118,6 +124,12 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { psrc++; } + if (this->reset_.duration0 > 0 || this->reset_.duration1 > 0) { + pdest->val = this->reset_.val; + pdest++; + len++; + } + if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) { ESP_LOGE(TAG, "RMT TX error"); this->status_set_warning(); diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h index e9b19c9399..43215cf12b 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.h +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -49,7 +49,8 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { /// 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_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low); + void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low, + uint32_t reset_time_high, uint32_t reset_time_low); void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } @@ -75,7 +76,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { bool is_rgbw_; bool is_wrgb_; - rmt_item32_t bit0_, bit1_; + rmt_item32_t bit0_, bit1_, reset_; RGBOrder rgb_order_; rmt_channel_t channel_; diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 4c8472b8d2..1e3c2d4f72 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -43,13 +43,16 @@ class LEDStripTimings: bit0_low: int bit1_high: int bit1_low: int + reset_high: int + reset_low: int CHIPSETS = { - "WS2812": LEDStripTimings(400, 1000, 1000, 400), - "SK6812": LEDStripTimings(300, 900, 600, 600), - "APA106": LEDStripTimings(350, 1360, 1360, 350), - "SM16703": LEDStripTimings(300, 900, 900, 300), + "WS2811": LEDStripTimings(300, 1090, 1090, 320, 0, 300000), + "WS2812": LEDStripTimings(400, 1000, 1000, 400, 0, 0), + "SK6812": LEDStripTimings(300, 900, 600, 600, 0, 0), + "APA106": LEDStripTimings(350, 1360, 1360, 350, 0, 0), + "SM16703": LEDStripTimings(300, 900, 900, 300, 0, 0), } @@ -58,6 +61,8 @@ CONF_BIT0_HIGH = "bit0_high" CONF_BIT0_LOW = "bit0_low" CONF_BIT1_HIGH = "bit1_high" CONF_BIT1_LOW = "bit1_low" +CONF_RESET_HIGH = "reset_high" +CONF_RESET_LOW = "reset_low" CONFIG_SCHEMA = cv.All( @@ -88,6 +93,14 @@ CONFIG_SCHEMA = cv.All( CONF_BIT1_LOW, "custom", ): cv.positive_time_period_nanoseconds, + cv.Optional( + CONF_RESET_HIGH, + default="0 us", + ): cv.positive_time_period_nanoseconds, + cv.Optional( + CONF_RESET_LOW, + default="0 us", + ): cv.positive_time_period_nanoseconds, } ), cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), @@ -113,6 +126,8 @@ async def to_code(config): chipset.bit0_low, chipset.bit1_high, chipset.bit1_low, + chipset.reset_high, + chipset.reset_low, ) ) else: @@ -122,6 +137,8 @@ async def to_code(config): config[CONF_BIT0_LOW], config[CONF_BIT1_HIGH], config[CONF_BIT1_LOW], + config[CONF_RESET_HIGH], + config[CONF_RESET_LOW], ) ) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 9d5044aaeb..7e2ef42a97 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -1,5 +1,5 @@ #include "ota_esphome.h" - +#ifdef USE_OTA #include "esphome/components/md5/md5.h" #include "esphome/components/network/util.h" #include "esphome/components/ota/ota_backend.h" @@ -410,3 +410,4 @@ float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::A uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } } // namespace esphome +#endif diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 42629b4346..e0d09ff37e 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/defines.h" +#ifdef USE_OTA #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #include "esphome/components/ota/ota_backend.h" @@ -41,3 +42,4 @@ class ESPHomeOTAComponent : public ota::OTAComponent { }; } // namespace esphome +#endif diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 962a864a29..fdb6eb2da0 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -472,13 +472,13 @@ void EthernetComponent::start_connect_() { if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); } -#if USE_NETWORK_IPV6 - err = esp_netif_create_ip6_linklocal(this->eth_netif_); - if (err != ESP_OK) { - ESPHL_ERROR_CHECK(err, "Enable IPv6 link local failed"); - } -#endif /* USE_NETWORK_IPV6 */ } +#if USE_NETWORK_IPV6 + err = esp_netif_create_ip6_linklocal(this->eth_netif_); + if (err != ESP_OK) { + ESPHL_ERROR_CHECK(err, "Enable IPv6 link local failed"); + } +#endif /* USE_NETWORK_IPV6 */ this->connect_begin_ = millis(); this->status_set_warning(); diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 241e884386..031a4c0de8 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -1,24 +1,24 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, + CONF_EVENT_TYPE, CONF_ICON, CONF_ID, + CONF_MQTT_ID, CONF_ON_EVENT, CONF_TRIGGER_ID, - CONF_MQTT_ID, - CONF_EVENT_TYPE, DEVICE_CLASS_BUTTON, DEVICE_CLASS_DOORBELL, DEVICE_CLASS_EMPTY, DEVICE_CLASS_MOTION, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@nohat"] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 847a59baa1..62624ec6e3 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -1,31 +1,31 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_DIRECTION, CONF_ID, CONF_MQTT_ID, - CONF_WEB_SERVER_ID, - CONF_OSCILLATING, - CONF_OSCILLATION_COMMAND_TOPIC, - CONF_OSCILLATION_STATE_TOPIC, - CONF_SPEED, - CONF_SPEED_LEVEL_COMMAND_TOPIC, - CONF_SPEED_LEVEL_STATE_TOPIC, - CONF_SPEED_COMMAND_TOPIC, - CONF_SPEED_STATE_TOPIC, CONF_OFF_SPEED_CYCLE, CONF_ON_DIRECTION_SET, CONF_ON_OSCILLATING_SET, + CONF_ON_PRESET_SET, CONF_ON_SPEED_SET, CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_ON_PRESET_SET, - CONF_TRIGGER_ID, - CONF_DIRECTION, + CONF_OSCILLATING, + CONF_OSCILLATION_COMMAND_TOPIC, + CONF_OSCILLATION_STATE_TOPIC, CONF_RESTORE_MODE, + CONF_SPEED, + CONF_SPEED_COMMAND_TOPIC, + CONF_SPEED_LEVEL_COMMAND_TOPIC, + CONF_SPEED_LEVEL_STATE_TOPIC, + CONF_SPEED_STATE_TOPIC, + CONF_TRIGGER_ID, + CONF_WEB_SERVER_ID, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index c2cab368c9..0dfea49b8b 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -307,7 +307,7 @@ void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { void FingerprintGrowComponent::delete_all_fingerprints() { ESP_LOGI(TAG, "Deleting all stored fingerprints"); - this->data_ = {EMPTY}; + this->data_ = {DELETE_ALL}; switch (this->send_command_()) { case OK: ESP_LOGI(TAG, "Deleted all fingerprints"); diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 20ff60997b..1c3098ef14 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -36,7 +36,7 @@ enum GrowCommand { LOAD = 0x07, UPLOAD = 0x08, DELETE = 0x0C, - EMPTY = 0x0D, + DELETE_ALL = 0x0D, // aka EMPTY READ_SYS_PARAM = 0x0F, SET_PASSWORD = 0x12, VERIFY_PASSWORD = 0x13, diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 7e4674ffda..b5ed02e89a 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,43 +1,35 @@ +import functools import hashlib import logging - -import functools -from pathlib import Path import os +from pathlib import Path import re + from packaging import version import requests -from esphome import core -from esphome import external_files -import esphome.config_validation as cv +from esphome import core, external_files import esphome.codegen as cg -from esphome.helpers import ( - copy_file_if_changed, - cpp_string_escape, -) +import esphome.config_validation as cv from esphome.const import ( CONF_FAMILY, CONF_FILE, CONF_GLYPHS, CONF_ID, + CONF_PATH, CONF_RAW_DATA_ID, - CONF_TYPE, CONF_REFRESH, CONF_SIZE, - CONF_PATH, - CONF_WEIGHT, + CONF_TYPE, CONF_URL, + CONF_WEIGHT, ) -from esphome.core import ( - CORE, - HexInt, -) +from esphome.core import CORE, HexInt +from esphome.helpers import copy_file_if_changed, cpp_string_escape _LOGGER = logging.getLogger(__name__) DOMAIN = "font" -DEPENDENCIES = ["display"] MULTI_CONF = True CODEOWNERS = ["@esphome/core", "@clydebarrow"] @@ -400,10 +392,7 @@ class EFont: def convert_bitmap_to_pillow_font(filepath): - from PIL import ( - PcfFontFile, - BdfFontFile, - ) + from PIL import BdfFontFile, PcfFontFile local_bitmap_font_file = external_files.compute_local_file_dir( DOMAIN, diff --git a/esphome/components/font/font.cpp b/esphome/components/font/font.cpp index 3b62b8ca66..aeca0f5cc0 100644 --- a/esphome/components/font/font.cpp +++ b/esphome/components/font/font.cpp @@ -1,9 +1,8 @@ #include "font.h" +#include "esphome/core/color.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/color.h" -#include "esphome/components/display/display_buffer.h" namespace esphome { namespace font { @@ -68,6 +67,7 @@ int Font::match_next_glyph(const uint8_t *str, int *match_length) { return -1; return lo; } +#ifdef USE_DISPLAY void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { *baseline = this->baseline_; *height = this->height_; @@ -164,6 +164,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo i += match_length; } } +#endif } // namespace font } // namespace esphome diff --git a/esphome/components/font/font.h b/esphome/components/font/font.h index 57002cf510..5cde694d91 100644 --- a/esphome/components/font/font.h +++ b/esphome/components/font/font.h @@ -1,8 +1,11 @@ #pragma once -#include "esphome/core/datatypes.h" #include "esphome/core/color.h" -#include "esphome/components/display/display_buffer.h" +#include "esphome/core/datatypes.h" +#include "esphome/core/defines.h" +#ifdef USE_DISPLAY +#include "esphome/components/display/display.h" +#endif namespace esphome { namespace font { @@ -38,7 +41,11 @@ class Glyph { const GlyphData *glyph_data_; }; -class Font : public display::BaseFont { +class Font +#ifdef USE_DISPLAY + : public display::BaseFont +#endif +{ public: /** Construct the font with the given glyphs. * @@ -50,9 +57,11 @@ class Font : public display::BaseFont { int match_next_glyph(const uint8_t *str, int *match_length); +#ifdef USE_DISPLAY void print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) override; void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override; +#endif inline int get_baseline() { return this->baseline_; } inline int get_height() { return this->height_; } inline int get_bpp() { return this->bpp_; } diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py index 1b3ed7f8cd..f4d59b22b8 100644 --- a/esphome/components/graphical_display_menu/__init__.py +++ b/esphome/components/graphical_display_menu/__init__.py @@ -1,19 +1,22 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import display, font, color -from esphome.const import CONF_DISPLAY, CONF_ID, CONF_TRIGGER_ID from esphome import automation, core - +import esphome.codegen as cg +from esphome.components import color, display, font from esphome.components.display_menu_base import ( DISPLAY_MENU_BASE_SCHEMA, DisplayMenuComponent, display_menu_to_code, ) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BACKGROUND_COLOR, + CONF_DISPLAY, + CONF_FONT, + CONF_FOREGROUND_COLOR, + CONF_ID, + CONF_TRIGGER_ID, +) -CONF_FONT = "font" CONF_MENU_ITEM_VALUE = "menu_item_value" -CONF_FOREGROUND_COLOR = "foreground_color" -CONF_BACKGROUND_COLOR = "background_color" CONF_ON_REDRAW = "on_redraw" graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu") diff --git a/esphome/components/gree/climate.py b/esphome/components/gree/climate.py index c88a428391..75436f2cf5 100644 --- a/esphome/components/gree/climate.py +++ b/esphome/components/gree/climate.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import climate_ir +import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODEL CODEOWNERS = ["@orestismers"] @@ -17,6 +17,7 @@ MODELS = { "yaa": Model.GREE_YAA, "yac": Model.GREE_YAC, "yac1fb9": Model.GREE_YAC1FB9, + "yx1ff": Model.GREE_YX1FF, } CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( diff --git a/esphome/components/gree/gree.cpp b/esphome/components/gree/gree.cpp index cce2a8ffee..6d179a947b 100644 --- a/esphome/components/gree/gree.cpp +++ b/esphome/components/gree/gree.cpp @@ -6,7 +6,15 @@ namespace gree { static const char *const TAG = "gree.climate"; -void GreeClimate::set_model(Model model) { this->model_ = model; } +void GreeClimate::set_model(Model model) { + if (model == GREE_YX1FF) { + this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed + this->presets_.insert(climate::CLIMATE_PRESET_NONE); // YX1FF sleep mode + this->presets_.insert(climate::CLIMATE_PRESET_SLEEP); // YX1FF sleep mode + } + + this->model_ = model; +} void GreeClimate::transmit_state() { uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00}; @@ -14,7 +22,7 @@ void GreeClimate::transmit_state() { remote_state[0] = this->fan_speed_() | this->operation_mode_(); remote_state[1] = this->temperature_(); - if (this->model_ == GREE_YAN) { + if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { remote_state[2] = 0x60; remote_state[3] = 0x50; remote_state[4] = this->vertical_swing_(); @@ -36,8 +44,18 @@ void GreeClimate::transmit_state() { } } + if (this->model_ == GREE_YX1FF) { + if (this->fan_speed_() == GREE_FAN_TURBO) { + remote_state[2] |= GREE_FAN_TURBO_BIT; + } + + if (this->preset_() == GREE_PRESET_SLEEP) { + remote_state[0] |= GREE_PRESET_SLEEP_BIT; + } + } + // Calculate the checksum - if (this->model_ == GREE_YAN) { + if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) { remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0); } else { remote_state[7] = @@ -124,6 +142,23 @@ uint8_t GreeClimate::operation_mode_() { } uint8_t GreeClimate::fan_speed_() { + // YX1FF has 4 fan speeds -- we treat low as quiet and turbo as high + if (this->model_ == GREE_YX1FF) { + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_QUIET: + return GREE_FAN_1; + case climate::CLIMATE_FAN_LOW: + return GREE_FAN_2; + case climate::CLIMATE_FAN_MEDIUM: + return GREE_FAN_3; + case climate::CLIMATE_FAN_HIGH: + return GREE_FAN_TURBO; + case climate::CLIMATE_FAN_AUTO: + default: + return GREE_FAN_AUTO; + } + } + switch (this->fan_mode.value()) { case climate::CLIMATE_FAN_LOW: return GREE_FAN_1; @@ -161,5 +196,21 @@ uint8_t GreeClimate::temperature_() { return (uint8_t) roundf(clamp(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX)); } +uint8_t GreeClimate::preset_() { + // YX1FF has sleep preset + if (this->model_ == GREE_YX1FF) { + switch (this->preset.value()) { + case climate::CLIMATE_PRESET_NONE: + return GREE_PRESET_NONE; + case climate::CLIMATE_PRESET_SLEEP: + return GREE_PRESET_SLEEP; + default: + return GREE_PRESET_NONE; + } + } + + return GREE_PRESET_NONE; +} + } // namespace gree } // namespace esphome diff --git a/esphome/components/gree/gree.h b/esphome/components/gree/gree.h index 524a95aebd..6762b41eb0 100644 --- a/esphome/components/gree/gree.h +++ b/esphome/components/gree/gree.h @@ -25,7 +25,6 @@ const uint8_t GREE_FAN_AUTO = 0x00; const uint8_t GREE_FAN_1 = 0x10; const uint8_t GREE_FAN_2 = 0x20; const uint8_t GREE_FAN_3 = 0x30; -const uint8_t GREE_FAN_TURBO = 0x80; // IR Transmission const uint32_t GREE_IR_FREQUENCY = 38000; @@ -70,8 +69,16 @@ const uint8_t GREE_HDIR_MIDDLE = 0x04; const uint8_t GREE_HDIR_MRIGHT = 0x05; const uint8_t GREE_HDIR_RIGHT = 0x06; +// Only available on YX1FF +// Turbo (high) fan mode + sleep preset mode +const uint8_t GREE_FAN_TURBO = 0x80; +const uint8_t GREE_FAN_TURBO_BIT = 0x10; +const uint8_t GREE_PRESET_NONE = 0x00; +const uint8_t GREE_PRESET_SLEEP = 0x01; +const uint8_t GREE_PRESET_SLEEP_BIT = 0x80; + // Model codes -enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9 }; +enum Model { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF }; class GreeClimate : public climate_ir::ClimateIR { public: @@ -93,6 +100,7 @@ class GreeClimate : public climate_ir::ClimateIR { uint8_t horizontal_swing_(); uint8_t vertical_swing_(); uint8_t temperature_(); + uint8_t preset_(); Model model_{}; }; diff --git a/esphome/components/gt911/touchscreen/__init__.py b/esphome/components/gt911/touchscreen/__init__.py index 9a0d5cc169..6c80ff280f 100644 --- a/esphome/components/gt911/touchscreen/__init__.py +++ b/esphome/components/gt911/touchscreen/__init__.py @@ -1,11 +1,10 @@ -import esphome.codegen as cg -import esphome.config_validation as cv - from esphome import pins +import esphome.codegen as cg from esphome.components import i2c, touchscreen -from esphome.const import CONF_INTERRUPT_PIN, CONF_ID -from .. import gt911_ns +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN +from .. import gt911_ns GT911ButtonListener = gt911_ns.class_("GT911ButtonListener") GT911Touchscreen = gt911_ns.class_( @@ -18,6 +17,7 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(GT911Touchscreen), cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, } ).extend(i2c.i2c_device_schema(0x5D)) @@ -29,3 +29,5 @@ async def to_code(config): if interrupt_pin := config.get(CONF_INTERRUPT_PIN): cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) + if reset_pin := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin))) diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 99dba66c22..84811b818f 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -26,6 +26,23 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned void GT911Touchscreen::setup() { i2c::ErrorCode err; ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen..."); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + if (this->interrupt_pin_ != nullptr) { + // The interrupt pin is used as an input during reset to select the I2C address. + this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->interrupt_pin_->setup(); + this->interrupt_pin_->digital_write(false); + } + delay(2); + this->reset_pin_->digital_write(true); + delay(50); // NOLINT + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); + this->interrupt_pin_->setup(); + } + } // check the configuration of the int line. uint8_t data[4]; diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.h b/esphome/components/gt911/touchscreen/gt911_touchscreen.h index a9e1279ed3..17636a2ada 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.h +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.h @@ -19,12 +19,14 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice void dump_config() override; void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } protected: void update_touches() override; InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{}; std::vector button_listeners_; uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update. }; diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index c0bf878519..7d92a6611c 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -80,8 +80,8 @@ class HaierClimateBase : public esphome::Component, const char *phase_to_string_(ProtocolPhases phase); virtual void set_handlers() = 0; virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; - virtual haier_protocol::HaierMessage get_control_message() = 0; - virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; + virtual haier_protocol::HaierMessage get_control_message() = 0; // NOLINT(readability-identifier-naming) + virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; // NOLINT(readability-identifier-naming) virtual void initialization(){}; virtual bool prepare_pending_action(); virtual void process_protocol_reset(); diff --git a/esphome/components/hmac_md5/__init__.py b/esphome/components/hmac_md5/__init__.py new file mode 100644 index 0000000000..fe245c0cfd --- /dev/null +++ b/esphome/components/hmac_md5/__init__.py @@ -0,0 +1,2 @@ +AUTO_LOAD = ["md5"] +CODEOWNERS = ["@dwmw2"] diff --git a/esphome/components/hmac_md5/hmac_md5.cpp b/esphome/components/hmac_md5/hmac_md5.cpp new file mode 100644 index 0000000000..90bf91882f --- /dev/null +++ b/esphome/components/hmac_md5/hmac_md5.cpp @@ -0,0 +1,56 @@ +#include +#include +#include "hmac_md5.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace hmac_md5 { +void HmacMD5::init(const uint8_t *key, size_t len) { + uint8_t ipad[64], opad[64]; + + memset(ipad, 0, sizeof(ipad)); + if (len > 64) { + md5::MD5Digest keymd5; + keymd5.init(); + keymd5.add(key, len); + keymd5.calculate(); + keymd5.get_bytes(ipad); + } else { + memcpy(ipad, key, len); + } + memcpy(opad, ipad, sizeof(opad)); + + for (int i = 0; i < 64; i++) { + ipad[i] ^= 0x36; + opad[i] ^= 0x5c; + } + + this->ihash_.init(); + this->ihash_.add(ipad, sizeof(ipad)); + + this->ohash_.init(); + this->ohash_.add(opad, sizeof(opad)); +} + +void HmacMD5::add(const uint8_t *data, size_t len) { this->ihash_.add(data, len); } + +void HmacMD5::calculate() { + uint8_t ibytes[16]; + + this->ihash_.calculate(); + this->ihash_.get_bytes(ibytes); + + this->ohash_.add(ibytes, sizeof(ibytes)); + this->ohash_.calculate(); +} + +void HmacMD5::get_bytes(uint8_t *output) { this->ohash_.get_bytes(output); } + +void HmacMD5::get_hex(char *output) { this->ohash_.get_hex(output); } + +bool HmacMD5::equals_bytes(const uint8_t *expected) { return this->ohash_.equals_bytes(expected); } + +bool HmacMD5::equals_hex(const char *expected) { return this->ohash_.equals_hex(expected); } + +} // namespace hmac_md5 +} // namespace esphome diff --git a/esphome/components/hmac_md5/hmac_md5.h b/esphome/components/hmac_md5/hmac_md5.h new file mode 100644 index 0000000000..e6a97ad2e3 --- /dev/null +++ b/esphome/components/hmac_md5/hmac_md5.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/components/md5/md5.h" + +#include + +namespace esphome { +namespace hmac_md5 { + +class HmacMD5 { + public: + HmacMD5() = default; + ~HmacMD5() = default; + + /// Initialize a new MD5 digest computation. + void init(const uint8_t *key, size_t len); + void init(const char *key, size_t len) { this->init((const uint8_t *) key, len); } + void init(const std::string &key) { this->init(key.c_str(), key.length()); } + + /// Add bytes of data for the digest. + void add(const uint8_t *data, size_t len); + void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + + /// Compute the digest, based on the provided data. + void calculate(); + + /// Retrieve the HMAC-MD5 digest as bytes. + /// The output must be able to hold 16 bytes or more. + void get_bytes(uint8_t *output); + + /// Retrieve the HMAC-MD5 digest as hex characters. + /// The output must be able to hold 32 bytes or more. + void get_hex(char *output); + + /// Compare the digest against a provided byte-encoded digest (16 bytes). + bool equals_bytes(const uint8_t *expected); + + /// Compare the digest against a provided hex-encoded digest (32 bytes). + bool equals_hex(const char *expected); + + protected: + md5::MD5Digest ihash_; + md5::MD5Digest ohash_; +}; + +} // namespace hmac_md5 +} // namespace esphome diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 776aa7fd7b..223d6c18c3 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -2,9 +2,22 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@OttoWinter", "@esphome/core"] homeassistant_ns = cg.esphome_ns.namespace("homeassistant") + +def validate_entity_domain(platform, supported_domains): + def validator(config): + domain = config[CONF_ENTITY_ID].split(".", 1)[0] + if domain not in supported_domains: + raise cv.Invalid( + f"Entity ID {config[CONF_ENTITY_ID]} is not supported by the {platform} platform." + ) + return config + + return validator + + HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( { cv.Required(CONF_ENTITY_ID): cv.entity_id, @@ -13,6 +26,13 @@ HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( } ) +HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema( + { + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_INTERNAL, default=True): cv.boolean, + } +) + def setup_home_assistant_entity(var, config): cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) diff --git a/esphome/components/homeassistant/number/__init__.py b/esphome/components/homeassistant/number/__init__.py new file mode 100644 index 0000000000..a6cc615a64 --- /dev/null +++ b/esphome/components/homeassistant/number/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv + +from .. import ( + HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, +) + +CODEOWNERS = ["@landonr"] +DEPENDENCIES = ["api"] + +HomeassistantNumber = homeassistant_ns.class_( + "HomeassistantNumber", number.Number, cg.Component +) + +CONFIG_SCHEMA = ( + number.number_schema(HomeassistantNumber) + .extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await number.new_number( + config, + min_value=0, + max_value=0, + step=0, + ) + await cg.register_component(var, config) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp new file mode 100644 index 0000000000..d3e285f4ac --- /dev/null +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -0,0 +1,100 @@ +#include "homeassistant_number.h" + +#include "esphome/components/api/api_pb2.h" +#include "esphome/components/api/api_server.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace homeassistant { + +static const char *const TAG = "homeassistant.number"; + +void HomeassistantNumber::state_changed_(const std::string &state) { + auto number_value = parse_number(state); + if (!number_value.has_value()) { + ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str()); + this->publish_state(NAN); + return; + } + if (this->state == number_value.value()) { + return; + } + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), state.c_str()); + this->publish_state(number_value.value()); +} + +void HomeassistantNumber::min_retrieved_(const std::string &min) { + auto min_value = parse_number(min); + if (!min_value.has_value()) { + ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_.c_str(), min.c_str()); + } + ESP_LOGD(TAG, "'%s': Min retrieved: %s", get_name().c_str(), min.c_str()); + this->traits.set_min_value(min_value.value()); +} + +void HomeassistantNumber::max_retrieved_(const std::string &max) { + auto max_value = parse_number(max); + if (!max_value.has_value()) { + ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_.c_str(), max.c_str()); + } + ESP_LOGD(TAG, "'%s': Max retrieved: %s", get_name().c_str(), max.c_str()); + this->traits.set_max_value(max_value.value()); +} + +void HomeassistantNumber::step_retrieved_(const std::string &step) { + auto step_value = parse_number(step); + if (!step_value.has_value()) { + ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_.c_str(), step.c_str()); + } + ESP_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str()); + this->traits.set_step(step_value.value()); +} + +void HomeassistantNumber::setup() { + api::global_api_server->subscribe_home_assistant_state( + this->entity_id_, nullopt, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1)); + + api::global_api_server->get_home_assistant_state( + this->entity_id_, optional("min"), + std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1)); + api::global_api_server->get_home_assistant_state( + this->entity_id_, optional("max"), + std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1)); + api::global_api_server->get_home_assistant_state( + this->entity_id_, optional("step"), + std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1)); +} + +void HomeassistantNumber::dump_config() { + LOG_NUMBER("", "Homeassistant Number", this); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); +} + +float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +void HomeassistantNumber::control(float value) { + if (!api::global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients connected to API server"); + return; + } + + this->publish_state(value); + + api::HomeassistantServiceResponse resp; + resp.service = "number.set_value"; + + api::HomeassistantServiceMap entity_id; + entity_id.key = "entity_id"; + entity_id.value = this->entity_id_; + resp.data.push_back(entity_id); + + api::HomeassistantServiceMap entity_value; + entity_value.key = "value"; + entity_value.value = to_string(value); + resp.data.push_back(entity_value); + + api::global_api_server->send_homeassistant_service_call(resp); +} + +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/homeassistant/number/homeassistant_number.h b/esphome/components/homeassistant/number/homeassistant_number.h new file mode 100644 index 0000000000..0860b4e91c --- /dev/null +++ b/esphome/components/homeassistant/number/homeassistant_number.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "esphome/components/number/number.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace homeassistant { + +class HomeassistantNumber : public number::Number, public Component { + public: + void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + void state_changed_(const std::string &state); + void min_retrieved_(const std::string &min); + void max_retrieved_(const std::string &max); + void step_retrieved_(const std::string &step); + + void control(float value) override; + + std::string entity_id_; +}; +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/homeassistant/switch/__init__.py b/esphome/components/homeassistant/switch/__init__.py new file mode 100644 index 0000000000..384f82bbad --- /dev/null +++ b/esphome/components/homeassistant/switch/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import CONF_ID + +from .. import ( + HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, + validate_entity_domain, +) + +CODEOWNERS = ["@Links2004"] +DEPENDENCIES = ["api"] + +SUPPORTED_DOMAINS = [ + "automation", + "fan", + "humidifier", + "input_boolean", + "light", + "remote", + "siren", + "switch", +] + +HomeassistantSwitch = homeassistant_ns.class_( + "HomeassistantSwitch", switch.Switch, cg.Component +) + +CONFIG_SCHEMA = cv.All( + switch.switch_schema(HomeassistantSwitch) + .extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + validate_entity_domain("switch", SUPPORTED_DOMAINS), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await switch.register_switch(var, config) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.cpp b/esphome/components/homeassistant/switch/homeassistant_switch.cpp new file mode 100644 index 0000000000..0451c95069 --- /dev/null +++ b/esphome/components/homeassistant/switch/homeassistant_switch.cpp @@ -0,0 +1,59 @@ +#include "homeassistant_switch.h" +#include "esphome/components/api/api_server.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace homeassistant { + +static const char *const TAG = "homeassistant.switch"; + +using namespace esphome::switch_; + +void HomeassistantSwitch::setup() { + api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullopt, [this](const std::string &state) { + auto val = parse_on_off(state.c_str()); + switch (val) { + case PARSE_NONE: + case PARSE_TOGGLE: + ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str()); + break; + case PARSE_ON: + case PARSE_OFF: + bool new_state = val == PARSE_ON; + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + this->publish_state(new_state); + break; + } + }); +} + +void HomeassistantSwitch::dump_config() { + LOG_SWITCH("", "Homeassistant Switch", this); + ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str()); +} + +float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } + +void HomeassistantSwitch::write_state(bool state) { + if (!api::global_api_server->is_connected()) { + ESP_LOGE(TAG, "No clients connected to API server"); + return; + } + + api::HomeassistantServiceResponse resp; + if (state) { + resp.service = "homeassistant.turn_on"; + } else { + resp.service = "homeassistant.turn_off"; + } + + api::HomeassistantServiceMap entity_id_kv; + entity_id_kv.key = "entity_id"; + entity_id_kv.value = this->entity_id_; + resp.data.push_back(entity_id_kv); + + api::global_api_server->send_homeassistant_service_call(resp); +} + +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/homeassistant/switch/homeassistant_switch.h b/esphome/components/homeassistant/switch/homeassistant_switch.h new file mode 100644 index 0000000000..a4da257960 --- /dev/null +++ b/esphome/components/homeassistant/switch/homeassistant_switch.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/switch/switch.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace homeassistant { + +class HomeassistantSwitch : public switch_::Switch, public Component { + public: + void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + void write_state(bool state) override; + std::string entity_id_; +}; + +} // namespace homeassistant +} // namespace esphome diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 39e418c9ea..e83bf2dba8 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -1,15 +1,14 @@ +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( + CONF_MAC_ADDRESS, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_HOST, - CONF_MAC_ADDRESS, ) from esphome.core import CORE -from esphome.helpers import IS_MACOS -import esphome.config_validation as cv -import esphome.codegen as cg from .const import KEY_HOST @@ -42,8 +41,5 @@ async def to_code(config): cg.add_build_flag("-DUSE_HOST") cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) cg.add_build_flag("-std=c++17") - cg.add_build_flag("-lsodium") - if IS_MACOS: - cg.add_build_flag("-L/opt/homebrew/lib") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 0a14dfd933..059148e7e5 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -138,8 +138,8 @@ void HttpRequestUpdate::update() { this->publish_state(); } -void HttpRequestUpdate::perform() { - if (this->state_ != update::UPDATE_STATE_AVAILABLE) { +void HttpRequestUpdate::perform(bool force) { + if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) { return; } diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index a6bc97392b..45c7e6a447 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -15,7 +15,8 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { void setup() override; void update() override; - void perform() override; + void perform(bool force) override; + void check() override { this->update(); } void set_source_url(const std::string &source_url) { this->source_url_ = source_url; } diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index dbbf4c91f4..1a7169eed7 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -39,8 +39,8 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { return false; } - this->status_clear_warning(); uint32_t data = 0; + bool final_dout; { InterruptLock lock; @@ -59,8 +59,17 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { this->sck_pin_->digital_write(false); delayMicroseconds(1); } + final_dout = this->dout_pin_->digital_read(); } + if (!final_dout) { + ESP_LOGW(TAG, "HX711 DOUT pin not high after reading (data 0x%" PRIx32 ")!", data); + this->status_set_warning(); + return false; + } + + this->status_clear_warning(); + if (data & 0x800000ULL) { data |= 0xFF000000ULL; } diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 95702fe9e8..92d7774193 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -236,7 +236,7 @@ void HydreonRGxxComponent::process_line_() { } bool is_data_line = false; for (int i = 0; i < NUM_SENSORS; i++) { - if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) { + if (this->sensors_[i] != nullptr && this->buffer_.find(PROTOCOL_NAMES[i]) != std::string::npos) { is_data_line = true; break; } diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 05e44696d8..d376907925 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,16 +1,16 @@ -import esphome.config_validation as cv -import esphome.final_validate as fv -import esphome.codegen as cg - from esphome import pins -from esphome.const import CONF_ID +import esphome.codegen as cg from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, + VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32C3, ) +import esphome.config_validation as cv +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE +from esphome.cpp_generator import MockObjClass +import esphome.final_validate as fv CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] @@ -25,16 +25,26 @@ CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" CONF_I2S_AUDIO = "i2s_audio" CONF_I2S_AUDIO_ID = "i2s_audio_id" +CONF_BITS_PER_SAMPLE = "bits_per_sample" CONF_I2S_MODE = "i2s_mode" CONF_PRIMARY = "primary" CONF_SECONDARY = "secondary" +CONF_USE_APLL = "use_apll" +CONF_BITS_PER_SAMPLE = "bits_per_sample" +CONF_BITS_PER_CHANNEL = "bits_per_channel" +CONF_MONO = "mono" +CONF_LEFT = "left" +CONF_RIGHT = "right" +CONF_STEREO = "stereo" + i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio") I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component) -I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", cg.Parented.template(I2SAudioComponent)) -I2SAudioOut = i2s_audio_ns.class_( - "I2SAudioOut", cg.Parented.template(I2SAudioComponent) +I2SAudioBase = i2s_audio_ns.class_( + "I2SAudioBase", cg.Parented.template(I2SAudioComponent) ) +I2SAudioIn = i2s_audio_ns.class_("I2SAudioIn", I2SAudioBase) +I2SAudioOut = i2s_audio_ns.class_("I2SAudioOut", I2SAudioBase) i2s_mode_t = cg.global_ns.enum("i2s_mode_t") I2S_MODE_OPTIONS = { @@ -50,6 +60,75 @@ I2S_PORTS = { VARIANT_ESP32C3: 1, } +i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") +I2S_CHANNELS = { + CONF_MONO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ALL_LEFT, + CONF_LEFT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, + CONF_RIGHT: i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, + CONF_STEREO: i2s_channel_fmt_t.I2S_CHANNEL_FMT_RIGHT_LEFT, +} + +i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") +I2S_BITS_PER_SAMPLE = { + 8: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_8BIT, + 16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT, + 24: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_24BIT, + 32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT, +} + +i2s_bits_per_chan_t = cg.global_ns.enum("i2s_bits_per_chan_t") +I2S_BITS_PER_CHANNEL = { + "default": i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_DEFAULT, + 8: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_8BIT, + 16: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_16BIT, + 24: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_24BIT, + 32: i2s_bits_per_chan_t.I2S_BITS_PER_CHAN_32BIT, +} + +_validate_bits = cv.float_with_unit("bits", "bit") + + +def i2s_audio_component_schema( + class_: MockObjClass, + *, + default_sample_rate: int, + default_channel: str, + default_bits_per_sample: str, +): + return cv.Schema( + { + cv.GenerateID(): cv.declare_id(class_), + cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), + cv.Optional(CONF_CHANNEL, default=default_channel): cv.enum(I2S_CHANNELS), + cv.Optional(CONF_SAMPLE_RATE, default=default_sample_rate): cv.int_range( + min=1 + ), + cv.Optional(CONF_BITS_PER_SAMPLE, default=default_bits_per_sample): cv.All( + _validate_bits, cv.enum(I2S_BITS_PER_SAMPLE) + ), + cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum( + I2S_MODE_OPTIONS, lower=True + ), + cv.Optional(CONF_USE_APLL, default=False): cv.boolean, + cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All( + cv.Any(cv.float_with_unit("bits", "bit"), "default"), + cv.enum(I2S_BITS_PER_CHANNEL), + ), + } + ) + + +async def register_i2s_audio_component(var, config): + await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + + cg.add(var.set_i2s_mode(config[CONF_I2S_MODE])) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) + cg.add(var.set_bits_per_channel(config[CONF_BITS_PER_CHANNEL])) + cg.add(var.set_use_apll(config[CONF_USE_APLL])) + + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(I2SAudioComponent), diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index d8d4a23dde..7e2798c33d 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -11,9 +11,27 @@ namespace i2s_audio { class I2SAudioComponent; -class I2SAudioIn : public Parented {}; +class I2SAudioBase : public Parented { + public: + void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; } + void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } + void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } + void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } + void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; } + void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } -class I2SAudioOut : public Parented {}; + protected: + i2s_mode_t i2s_mode_{}; + i2s_channel_fmt_t channel_; + uint32_t sample_rate_; + i2s_bits_per_sample_t bits_per_sample_; + i2s_bits_per_chan_t bits_per_channel_; + bool use_apll_; +}; + +class I2SAudioIn : public I2SAudioBase {}; + +class I2SAudioOut : public I2SAudioBase {}; class I2SAudioComponent : public Component { public: diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 600a308e6c..dfa69ecadd 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -12,6 +12,10 @@ from .. import ( I2SAudioOut, CONF_I2S_AUDIO_ID, CONF_I2S_DOUT_PIN, + CONF_LEFT, + CONF_RIGHT, + CONF_MONO, + CONF_STEREO, ) CODEOWNERS = ["@jesserockz"] @@ -30,12 +34,12 @@ CONF_DAC_TYPE = "dac_type" CONF_I2S_COMM_FMT = "i2s_comm_fmt" INTERNAL_DAC_OPTIONS = { - "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, - "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, - "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, + CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, + CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, + CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, } -EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] +EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO] NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h index 5afe778122..4672f94d7e 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.h @@ -23,7 +23,7 @@ enum I2SState : uint8_t { I2S_STATE_STOPPING, }; -class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, public I2SAudioOut { +class I2SAudioMediaPlayer : public Component, public Parented, public media_player::MediaPlayer { public: void setup() override; float get_setup_priority() const override { return esphome::setup_priority::LATE; } diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 844f176bea..161046e962 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -1,20 +1,17 @@ -import esphome.config_validation as cv -import esphome.codegen as cg - from esphome import pins -from esphome.const import CONF_CHANNEL, CONF_ID, CONF_NUMBER, CONF_SAMPLE_RATE -from esphome.components import microphone, esp32 +import esphome.codegen as cg +from esphome.components import esp32, microphone from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_NUMBER from .. import ( - CONF_I2S_MODE, - CONF_PRIMARY, - I2S_MODE_OPTIONS, - i2s_audio_ns, - I2SAudioComponent, - I2SAudioIn, - CONF_I2S_AUDIO_ID, CONF_I2S_DIN_PIN, + CONF_RIGHT, + I2SAudioIn, + i2s_audio_component_schema, + i2s_audio_ns, + register_i2s_audio_component, ) CODEOWNERS = ["@jesserockz"] @@ -23,29 +20,14 @@ DEPENDENCIES = ["i2s_audio"] CONF_ADC_PIN = "adc_pin" CONF_ADC_TYPE = "adc_type" CONF_PDM = "pdm" -CONF_BITS_PER_SAMPLE = "bits_per_sample" -CONF_USE_APLL = "use_apll" I2SAudioMicrophone = i2s_audio_ns.class_( "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component ) -i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") -CHANNELS = { - "left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, - "right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, -} -i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") -BITS_PER_SAMPLE = { - 16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT, - 32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT, -} - INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] -_validate_bits = cv.float_with_unit("bits", "bit") - def validate_esp32_variant(config): variant = esp32.get_esp32_variant() @@ -62,21 +44,15 @@ def validate_esp32_variant(config): BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), - cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS), - cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), - cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All( - _validate_bits, cv.enum(BITS_PER_SAMPLE) - ), - cv.Optional(CONF_USE_APLL, default=False): cv.boolean, - cv.Optional(CONF_I2S_MODE, default=CONF_PRIMARY): cv.enum( - I2S_MODE_OPTIONS, lower=True - ), - } + i2s_audio_component_schema( + I2SAudioMicrophone, + default_sample_rate=16000, + default_channel=CONF_RIGHT, + default_bits_per_sample="32bit", + ) ).extend(cv.COMPONENT_SCHEMA) + CONFIG_SCHEMA = cv.All( cv.typed_schema( { @@ -88,7 +64,7 @@ CONFIG_SCHEMA = cv.All( "external": BASE_SCHEMA.extend( { cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, - cv.Required(CONF_PDM): cv.boolean, + cv.Optional(CONF_PDM, default=False): cv.boolean, } ), }, @@ -101,8 +77,8 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - - await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) + await register_i2s_audio_component(var, config) + await microphone.register_microphone(var, config) if config[CONF_ADC_TYPE] == "internal": variant = esp32.get_esp32_variant() @@ -112,11 +88,3 @@ async def to_code(config): else: cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) cg.add(var.set_pdm(config[CONF_PDM])) - - cg.add(var.set_i2s_mode(config[CONF_I2S_MODE])) - cg.add(var.set_channel(config[CONF_CHANNEL])) - cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) - cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) - cg.add(var.set_use_apll(config[CONF_USE_APLL])) - - await microphone.register_microphone(var, config) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index cb49a744fc..23689afb91 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -58,7 +58,7 @@ void I2SAudioMicrophone::start_() { .tx_desc_auto_clear = false, .fixed_mclk = 0, .mclk_multiple = I2S_MCLK_MULTIPLE_256, - .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, + .bits_per_chan = this->bits_per_channel_, }; esp_err_t err; @@ -167,21 +167,24 @@ size_t I2SAudioMicrophone::read(int16_t *buf, size_t len) { return 0; } this->status_clear_warning(); - if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { - return bytes_read; - } else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) { - std::vector samples; - size_t samples_read = bytes_read / sizeof(int32_t); - samples.resize(samples_read); - for (size_t i = 0; i < samples_read; i++) { - int32_t temp = reinterpret_cast(buf)[i] >> 14; - samples[i] = clamp(temp, INT16_MIN, INT16_MAX); + // ESP-IDF I2S implementation right-extends 8-bit data to 16 bits, + // and 24-bit data to 32 bits. + switch (this->bits_per_sample_) { + case I2S_BITS_PER_SAMPLE_8BIT: + case I2S_BITS_PER_SAMPLE_16BIT: + return bytes_read; + case I2S_BITS_PER_SAMPLE_24BIT: + case I2S_BITS_PER_SAMPLE_32BIT: { + size_t samples_read = bytes_read / sizeof(int32_t); + for (size_t i = 0; i < samples_read; i++) { + int32_t temp = reinterpret_cast(buf)[i] >> 14; + buf[i] = clamp(temp, INT16_MIN, INT16_MAX); + } + return samples_read * sizeof(int16_t); } - memcpy(buf, samples.data(), samples_read * sizeof(int16_t)); - return samples_read * sizeof(int16_t); - } else { - ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_); - return 0; + default: + ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_); + return 0; } } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 07ca0528aa..ea3f357624 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -30,13 +30,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub } #endif - void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; } - - void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } - void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } - void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } - void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } - protected: void start_(); void stop_(); @@ -48,11 +41,6 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool adc_{false}; #endif bool pdm_{false}; - i2s_mode_t i2s_mode_{}; - i2s_channel_fmt_t channel_; - uint32_t sample_rate_; - i2s_bits_per_sample_t bits_per_sample_; - bool use_apll_; HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 72455af1b7..22a5af259d 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -1,15 +1,19 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ID, CONF_MODE +import esphome.codegen as cg from esphome.components import esp32, speaker +import esphome.config_validation as cv +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_MODE, CONF_TIMEOUT from .. import ( - CONF_I2S_AUDIO_ID, CONF_I2S_DOUT_PIN, - I2SAudioComponent, + CONF_LEFT, + CONF_MONO, + CONF_RIGHT, + CONF_STEREO, I2SAudioOut, + i2s_audio_component_schema, i2s_audio_ns, + register_i2s_audio_component, ) CODEOWNERS = ["@jesserockz"] @@ -19,19 +23,16 @@ I2SAudioSpeaker = i2s_audio_ns.class_( "I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut ) -i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") -CONF_MUTE_PIN = "mute_pin" CONF_DAC_TYPE = "dac_type" +i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") INTERNAL_DAC_OPTIONS = { - "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, - "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, - "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, + CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, + CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, + CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, } -EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] - NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] @@ -44,28 +45,40 @@ def validate_esp32_variant(config): return config +BASE_SCHEMA = ( + speaker.SPEAKER_SCHEMA.extend( + i2s_audio_component_schema( + I2SAudioSpeaker, + default_sample_rate=16000, + default_channel=CONF_MONO, + default_bits_per_sample="16bit", + ) + ) + .extend( + { + cv.Optional( + CONF_TIMEOUT, default="100ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + CONFIG_SCHEMA = cv.All( cv.typed_schema( { - "internal": speaker.SPEAKER_SCHEMA.extend( + "internal": BASE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(I2SAudioSpeaker), - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), } - ).extend(cv.COMPONENT_SCHEMA), - "external": speaker.SPEAKER_SCHEMA.extend( + ), + "external": BASE_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(I2SAudioSpeaker), - cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Required( CONF_I2S_DOUT_PIN ): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_MODE, default="mono"): cv.one_of( - *EXTERNAL_DAC_OPTIONS, lower=True - ), } - ).extend(cv.COMPONENT_SCHEMA), + ), }, key=CONF_DAC_TYPE, ), @@ -76,12 +89,11 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + await register_i2s_audio_component(var, config) await speaker.register_speaker(var, config) - await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) - if config[CONF_DAC_TYPE] == "internal": - cg.add(var.set_internal_dac_mode(config[CONF_MODE])) + cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) else: cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) - cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) + cg.add(var.set_timeout(config[CONF_TIMEOUT])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 6b07ecb1b6..4b427898a2 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -56,6 +56,21 @@ void I2SAudioSpeaker::start_() { this->task_created_ = true; } +template const uint8_t *convert_data_format(const a *from, b *to, size_t &bytes, bool repeat) { + if (sizeof(a) == sizeof(b) && !repeat) { + return reinterpret_cast(from); + } + const b *result = to; + for (size_t i = 0; i < bytes; i += sizeof(a)) { + b value = static_cast(*from++) << (sizeof(b) - sizeof(a)) * 8; + *to++ = value; + if (repeat) + *to++ = value; + } + bytes *= (sizeof(b) / sizeof(a)) * (repeat ? 2 : 1); // NOLINT + return reinterpret_cast(result); +} + void I2SAudioSpeaker::player_task(void *params) { I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; @@ -64,19 +79,19 @@ void I2SAudioSpeaker::player_task(void *params) { xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); i2s_driver_config_t config = { - .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX), - .sample_rate = 16000, - .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, - .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, + .mode = (i2s_mode_t) (this_speaker->i2s_mode_ | I2S_MODE_TX), + .sample_rate = this_speaker->sample_rate_, + .bits_per_sample = this_speaker->bits_per_sample_, + .channel_format = this_speaker->channel_, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, - .dma_buf_len = 128, - .use_apll = false, + .dma_buf_len = 256, + .use_apll = this_speaker->use_apll_, .tx_desc_auto_clear = true, - .fixed_mclk = I2S_PIN_NO_CHANGE, + .fixed_mclk = 0, .mclk_multiple = I2S_MCLK_MULTIPLE_256, - .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, + .bits_per_chan = this_speaker->bits_per_channel_, }; #if SOC_I2S_SUPPORTS_DAC if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { @@ -114,10 +129,11 @@ void I2SAudioSpeaker::player_task(void *params) { event.type = TaskEventType::STARTED; xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); - int16_t buffer[BUFFER_SIZE / 2]; + int32_t buffer[BUFFER_SIZE]; while (true) { - if (xQueueReceive(this_speaker->buffer_queue_, &data_event, 100 / portTICK_PERIOD_MS) != pdTRUE) { + if (xQueueReceive(this_speaker->buffer_queue_, &data_event, this_speaker->timeout_ / portTICK_PERIOD_MS) != + pdTRUE) { break; // End of audio from main thread } if (data_event.stop) { @@ -125,17 +141,28 @@ void I2SAudioSpeaker::player_task(void *params) { xQueueReset(this_speaker->buffer_queue_); // Flush queue break; } - size_t bytes_written; - memmove(buffer, data_event.data, data_event.len); - size_t remaining = data_event.len / 2; - size_t current = 0; + const uint8_t *data = data_event.data; + size_t remaining = data_event.len; + switch (this_speaker->bits_per_sample_) { + case I2S_BITS_PER_SAMPLE_8BIT: + case I2S_BITS_PER_SAMPLE_16BIT: { + data = convert_data_format(reinterpret_cast(data), reinterpret_cast(buffer), + remaining, this_speaker->channel_ == I2S_CHANNEL_FMT_ALL_LEFT); + break; + } + case I2S_BITS_PER_SAMPLE_24BIT: + case I2S_BITS_PER_SAMPLE_32BIT: { + data = convert_data_format(reinterpret_cast(data), reinterpret_cast(buffer), + remaining, this_speaker->channel_ == I2S_CHANNEL_FMT_ALL_LEFT); + break; + } + } - while (remaining > 0) { - uint32_t sample = (buffer[current] << 16) | (buffer[current] & 0xFFFF); - - esp_err_t err = i2s_write(this_speaker->parent_->get_port(), &sample, sizeof(sample), &bytes_written, - (10 / portTICK_PERIOD_MS)); + while (remaining != 0) { + size_t bytes_written; + esp_err_t err = + i2s_write(this_speaker->parent_->get_port(), data, remaining, &bytes_written, (32 / portTICK_PERIOD_MS)); if (err != ESP_OK) { event = {.type = TaskEventType::WARNING, .err = err}; if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) { @@ -143,21 +170,8 @@ void I2SAudioSpeaker::player_task(void *params) { } continue; } - if (bytes_written != sizeof(sample)) { - event = {.type = TaskEventType::WARNING, .err = ESP_FAIL}; - if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) { - ESP_LOGW(TAG, "Failed to send WARNING event"); - } - continue; - } - remaining--; - current++; - } - - event.type = TaskEventType::PLAYING; - event.err = current; - if (xQueueSend(this_speaker->event_queue_, &event, 10 / portTICK_PERIOD_MS) != pdTRUE) { - ESP_LOGW(TAG, "Failed to send PLAYING event"); + data += bytes_written; + remaining -= bytes_written; } } @@ -180,7 +194,11 @@ void I2SAudioSpeaker::player_task(void *params) { } } -void I2SAudioSpeaker::stop() { +void I2SAudioSpeaker::stop() { this->stop_(false); } + +void I2SAudioSpeaker::finish() { this->stop_(true); } + +void I2SAudioSpeaker::stop_(bool wait_on_empty) { if (this->is_failed()) return; if (this->state_ == speaker::STATE_STOPPED) @@ -192,7 +210,11 @@ void I2SAudioSpeaker::stop() { this->state_ = speaker::STATE_STOPPING; DataEvent data; data.stop = true; - xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY); + if (wait_on_empty) { + xQueueSend(this->buffer_queue_, &data, portMAX_DELAY); + } else { + xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY); + } } void I2SAudioSpeaker::watch_() { @@ -205,13 +227,11 @@ void I2SAudioSpeaker::watch_() { case TaskEventType::STARTED: ESP_LOGD(TAG, "Started I2S Audio Speaker"); this->state_ = speaker::STATE_RUNNING; + this->status_clear_warning(); break; case TaskEventType::STOPPING: ESP_LOGD(TAG, "Stopping I2S Audio Speaker"); break; - case TaskEventType::PLAYING: - this->status_clear_warning(); - break; case TaskEventType::STOPPED: this->state_ = speaker::STATE_STOPPED; vTaskDelete(this->player_task_handle_); @@ -233,6 +253,7 @@ void I2SAudioSpeaker::loop() { switch (this->state_) { case speaker::STATE_STARTING: this->start_(); + [[fallthrough]]; case speaker::STATE_RUNNING: case speaker::STATE_STOPPING: this->watch_(); diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 1800feaeec..7adc4e8a24 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -21,7 +21,6 @@ static const size_t BUFFER_SIZE = 1024; enum class TaskEventType : uint8_t { STARTING = 0, STARTED, - PLAYING, STOPPING, STOPPED, WARNING = 255, @@ -38,21 +37,22 @@ struct DataEvent { uint8_t data[BUFFER_SIZE]; }; -class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAudioOut { +class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Component { public: float get_setup_priority() const override { return esphome::setup_priority::LATE; } void setup() override; void loop() override; + void set_timeout(uint32_t ms) { this->timeout_ = ms; } void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } #if SOC_I2S_SUPPORTS_DAC void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } #endif - void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } void start() override; void stop() override; + void finish() override; size_t play(const uint8_t *data, size_t length) override; @@ -60,6 +60,7 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud protected: void start_(); + void stop_(bool wait_on_empty); void watch_(); static void player_task(void *params); @@ -68,13 +69,13 @@ class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAud QueueHandle_t buffer_queue_; QueueHandle_t event_queue_; + uint32_t timeout_{0}; uint8_t dout_pin_{0}; bool task_created_{false}; #if SOC_I2S_SUPPORTS_DAC i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; #endif - uint8_t external_dac_channels_; }; } // namespace i2s_audio diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 483f2b886c..2182ca9a6d 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -1,31 +1,31 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import core, pins -from esphome.components import display, spi, font +import esphome.codegen as cg +from esphome.components import display, font, spi from esphome.components.display import validate_rotation -from esphome.core import CORE, HexInt +import esphome.config_validation as cv from esphome.const import ( + CONF_COLOR_ORDER, CONF_COLOR_PALETTE, CONF_DC_PIN, - CONF_ID, - CONF_LAMBDA, - CONF_MODEL, - CONF_RAW_DATA_ID, - CONF_PAGES, - CONF_RESET_PIN, CONF_DIMENSIONS, - CONF_WIDTH, CONF_HEIGHT, - CONF_ROTATION, + CONF_ID, + CONF_INVERT_COLORS, + CONF_LAMBDA, CONF_MIRROR_X, CONF_MIRROR_Y, - CONF_SWAP_XY, - CONF_COLOR_ORDER, + CONF_MODEL, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, + CONF_PAGES, + CONF_RAW_DATA_ID, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SWAP_XY, CONF_TRANSFORM, - CONF_INVERT_COLORS, + CONF_WIDTH, ) +from esphome.core import CORE, HexInt DEPENDENCIES = ["spi"] @@ -177,7 +177,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INVERT_DISPLAY): cv.invalid( "'invert_display' has been replaced by 'invert_colors'" ), - cv.Optional(CONF_INVERT_COLORS): cv.boolean, + cv.Required(CONF_INVERT_COLORS): cv.boolean, cv.Optional(CONF_COLOR_ORDER): cv.one_of(*COLOR_ORDERS.keys(), upper=True), cv.Exclusive(CONF_ROTATION, CONF_ROTATION): validate_rotation, cv.Exclusive(CONF_TRANSFORM, CONF_ROTATION): cv.Schema( @@ -287,5 +287,4 @@ async def to_code(config): prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.add(var.set_palette(prog_arr)) - if CONF_INVERT_COLORS in config: - cg.add(var.invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.invert_colors(config[CONF_INVERT_COLORS])) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 4f035edbb0..81976dd2c9 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -118,6 +118,7 @@ void ILI9XXXDisplay::dump_config() { ESP_LOGCONFIG(TAG, " Swap_xy: %s", YESNO(this->swap_xy_)); ESP_LOGCONFIG(TAG, " Mirror_x: %s", YESNO(this->mirror_x_)); ESP_LOGCONFIG(TAG, " Mirror_y: %s", YESNO(this->mirror_y_)); + ESP_LOGCONFIG(TAG, " Invert colors: %s", YESNO(this->pre_invertcolors_)); if (this->is_failed()) { ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!"); @@ -154,7 +155,6 @@ void ILI9XXXDisplay::fill(Color color) { } } return; - break; default: new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); break; diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index 6121488d15..5033f702de 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -28,8 +28,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer, spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_40MHZ> { public: ILI9XXXDisplay() = default; - ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height, bool invert_colors) - : init_sequence_{init_sequence}, width_{width}, height_{height}, pre_invertcolors_{invert_colors} { + ILI9XXXDisplay(uint8_t const *init_sequence, int16_t width, int16_t height) + : init_sequence_{init_sequence}, width_{width}, height_{height} { uint8_t cmd, num_args, bits; const uint8_t *addr = init_sequence; while ((cmd = *addr++) != 0) { @@ -144,7 +144,7 @@ class ILI9XXXDisplay : public display::DisplayBuffer, bool need_update_ = false; bool is_18bitdisplay_ = false; PixelMode pixel_mode_{}; - bool pre_invertcolors_ = false; + bool pre_invertcolors_{}; display::ColorOrder color_order_{display::COLOR_ORDER_BGR}; bool swap_xy_{}; bool mirror_x_{}; @@ -154,54 +154,54 @@ class ILI9XXXDisplay : public display::DisplayBuffer, //----------- M5Stack display -------------- class ILI9XXXM5Stack : public ILI9XXXDisplay { public: - ILI9XXXM5Stack() : ILI9XXXDisplay(INITCMD_M5STACK, 320, 240, true) {} + ILI9XXXM5Stack() : ILI9XXXDisplay(INITCMD_M5STACK, 320, 240) {} }; //----------- M5Stack display -------------- class ILI9XXXM5CORE : public ILI9XXXDisplay { public: - ILI9XXXM5CORE() : ILI9XXXDisplay(INITCMD_M5CORE, 320, 240, true) {} + ILI9XXXM5CORE() : ILI9XXXDisplay(INITCMD_M5CORE, 320, 240) {} }; //----------- ST7789V display -------------- class ILI9XXXST7789V : public ILI9XXXDisplay { public: - ILI9XXXST7789V() : ILI9XXXDisplay(INITCMD_ST7789V, 240, 320, false) {} + ILI9XXXST7789V() : ILI9XXXDisplay(INITCMD_ST7789V, 240, 320) {} }; //----------- ILI9XXX_24_TFT display -------------- class ILI9XXXILI9341 : public ILI9XXXDisplay { public: - ILI9XXXILI9341() : ILI9XXXDisplay(INITCMD_ILI9341, 240, 320, false) {} + ILI9XXXILI9341() : ILI9XXXDisplay(INITCMD_ILI9341, 240, 320) {} }; //----------- ILI9XXX_24_TFT rotated display -------------- class ILI9XXXILI9342 : public ILI9XXXDisplay { public: - ILI9XXXILI9342() : ILI9XXXDisplay(INITCMD_ILI9341, 320, 240, false) {} + ILI9XXXILI9342() : ILI9XXXDisplay(INITCMD_ILI9341, 320, 240) {} }; //----------- ILI9XXX_??_TFT rotated display -------------- class ILI9XXXILI9481 : public ILI9XXXDisplay { public: - ILI9XXXILI9481() : ILI9XXXDisplay(INITCMD_ILI9481, 480, 320, false) {} + ILI9XXXILI9481() : ILI9XXXDisplay(INITCMD_ILI9481, 480, 320) {} }; //----------- ILI9481 in 18 bit mode -------------- class ILI9XXXILI948118 : public ILI9XXXDisplay { public: - ILI9XXXILI948118() : ILI9XXXDisplay(INITCMD_ILI9481_18, 320, 480, true) {} + ILI9XXXILI948118() : ILI9XXXDisplay(INITCMD_ILI9481_18, 320, 480) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXILI9486 : public ILI9XXXDisplay { public: - ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320, false) {} + ILI9XXXILI9486() : ILI9XXXDisplay(INITCMD_ILI9486, 480, 320) {} }; class ILI9XXXILI9488 : public ILI9XXXDisplay { public: - ILI9XXXILI9488(const uint8_t *seq = INITCMD_ILI9488) : ILI9XXXDisplay(seq, 480, 320, true) {} + ILI9XXXILI9488(const uint8_t *seq = INITCMD_ILI9488) : ILI9XXXDisplay(seq, 480, 320) {} protected: void set_madctl() override { @@ -246,34 +246,34 @@ class WAVESHARERES35 : public ILI9XXXILI9488 { //----------- ILI9XXX_35_TFT origin colors rotated display -------------- class ILI9XXXILI9488A : public ILI9XXXDisplay { public: - ILI9XXXILI9488A() : ILI9XXXDisplay(INITCMD_ILI9488_A, 480, 320, true) {} + ILI9XXXILI9488A() : ILI9XXXDisplay(INITCMD_ILI9488_A, 480, 320) {} }; //----------- ILI9XXX_35_TFT rotated display -------------- class ILI9XXXST7796 : public ILI9XXXDisplay { public: - ILI9XXXST7796() : ILI9XXXDisplay(INITCMD_ST7796, 320, 480, false) {} + ILI9XXXST7796() : ILI9XXXDisplay(INITCMD_ST7796, 320, 480) {} }; class ILI9XXXS3Box : public ILI9XXXDisplay { public: - ILI9XXXS3Box() : ILI9XXXDisplay(INITCMD_S3BOX, 320, 240, false) {} + ILI9XXXS3Box() : ILI9XXXDisplay(INITCMD_S3BOX, 320, 240) {} }; class ILI9XXXS3BoxLite : public ILI9XXXDisplay { public: - ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240, true) {} + ILI9XXXS3BoxLite() : ILI9XXXDisplay(INITCMD_S3BOXLITE, 320, 240) {} }; class ILI9XXXGC9A01A : public ILI9XXXDisplay { public: - ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240, true) {} + ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240) {} }; //----------- ILI9XXX_24_TFT display -------------- class ILI9XXXST7735 : public ILI9XXXDisplay { public: - ILI9XXXST7735() : ILI9XXXDisplay(INITCMD_ST7735, 128, 160, false) {} + ILI9XXXST7735() : ILI9XXXDisplay(INITCMD_ST7735, 128, 160) {} }; } // namespace ili9xxx diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index 5a67812bc1..b176680f43 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -101,7 +101,6 @@ static const uint8_t PROGMEM INITCMD_ILI9481[] = { ILI9XXX_MADCTL , 1, MADCTL_MV | MADCTL_BGR, // Memory Access Control ILI9XXX_CSCON , 1, 0x01, ILI9XXX_PIXFMT, 1, 0x55, // 16 bit mode - ILI9XXX_INVON, 0, ILI9XXX_DISPON, 0x80, // Set display on 0x00 // end }; @@ -121,7 +120,6 @@ static const uint8_t PROGMEM INITCMD_ILI9481_18[] = { ILI9XXX_MADCTL , 1, MADCTL_MX| MADCTL_BGR, // Memory Access Control ILI9XXX_CSCON , 1, 0x01, ILI9XXX_PIXFMT, 1, 0x66, // 18 bit mode - ILI9XXX_INVON, 0, ILI9XXX_DISPON, 0x80, // Set display on 0x00 // end }; @@ -204,7 +202,6 @@ static const uint8_t PROGMEM INITCMD_ILI9488_A[] = { ILI9XXX_SLPOUT, 0x80, // Exit sleep mode - //ILI9XXX_INVON , 0, ILI9XXX_DISPON, 0x80, // Set display on 0x00 // end }; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index e5a205f1e0..e80ba4498f 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,18 +1,17 @@ from __future__ import annotations -import logging - import hashlib import io +import logging from pathlib import Path import re + from magic import Magic -from esphome import core -from esphome.components import font -from esphome import external_files -import esphome.config_validation as cv +from esphome import core, external_files import esphome.codegen as cg +from esphome.components import font +import esphome.config_validation as cv from esphome.const import ( CONF_DITHER, CONF_FILE, @@ -239,12 +238,11 @@ CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) def load_svg_image(file: bytes, resize: tuple[int, int]): # Local import only to allow "validate_pillow_installed" to run *before* importing it - from PIL import Image - # This import is only needed in case of SVG images; adding it # to the top would force configurations not using SVG to also have it # installed for no reason. from cairosvg import svg2png + from PIL import Image if resize: req_width, req_height = resize @@ -274,6 +272,9 @@ async def to_code(config): elif conf_file[CONF_SOURCE] == SOURCE_WEB: path = compute_local_image_path(conf_file).as_posix() + else: + raise core.EsphomeError(f"Unknown image source: {conf_file[CONF_SOURCE]}") + try: with open(path, "rb") as f: file_contents = f.read() diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py index 5c2853a5c6..aa75f4d89c 100644 --- a/esphome/components/improv_base/__init__.py +++ b/esphome/components/improv_base/__init__.py @@ -1,8 +1,7 @@ import re -import esphome.config_validation as cv import esphome.codegen as cg - +import esphome.config_validation as cv from esphome.const import __version__ CODEOWNERS = ["@esphome/core"] @@ -39,4 +38,4 @@ def _process_next_url(url: str): async def setup_improv_core(var, config): if CONF_NEXT_URL in config: cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL]))) - cg.add_library("esphome/Improv", "1.2.3") + cg.add_library("improv/Improv", "1.2.4") diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 2b377d77b8..544af212e0 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,12 +1,10 @@ +import esphome.codegen as cg from esphome.components import improv_base from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import ( - VARIANT_ESP32S3, -) +from esphome.components.esp32.const import VARIANT_ESP32S3 from esphome.components.logger import USB_CDC -from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER -import esphome.codegen as cg import esphome.config_validation as cv +from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER from esphome.core import CORE import esphome.final_validate as fv @@ -19,11 +17,7 @@ improv_serial_ns = cg.esphome_ns.namespace("improv_serial") ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component) CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(ImprovSerialComponent), - } - ) + cv.Schema({cv.GenerateID(): cv.declare_id(ImprovSerialComponent)}) .extend(improv_base.IMPROV_SCHEMA) .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 12809e38cb..c3a0f2eacc 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -1,5 +1,5 @@ #include "improv_serial_component.h" - +#ifdef USE_WIFI #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -170,7 +170,11 @@ std::vector ImprovSerialComponent::build_rpc_settings_response_(improv: } std::vector ImprovSerialComponent::build_version_info_() { +#ifdef ESPHOME_PROJECT_NAME + std::vector infos = {ESPHOME_PROJECT_NAME, ESPHOME_PROJECT_VERSION, ESPHOME_VARIANT, App.get_name()}; +#else std::vector infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()}; +#endif std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); return data; }; @@ -309,3 +313,4 @@ ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidel } // namespace improv_serial } // namespace esphome +#endif diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index f737f93d86..5d2534c2fc 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -5,7 +5,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" - +#ifdef USE_WIFI #include #include @@ -78,3 +78,4 @@ extern ImprovSerialComponent } // namespace improv_serial } // namespace esphome +#endif diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 90e11fe4ad..4ced4b8f76 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -8,6 +8,8 @@ #endif #include +#include + #define CLOCK_FREQUENCY 80e6f #ifdef USE_ARDUINO @@ -115,20 +117,22 @@ void LEDCOutput::write_state(float state) { const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast(duty_rounded); + ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); #ifdef USE_ARDUINO - ESP_LOGV(TAG, "Setting duty: %u on channel %u", duty, this->channel_); ledcWrite(this->channel_, duty); #endif #ifdef USE_ESP_IDF - // ensure that 100% on is not 99.975% on - if ((duty == max_duty) && (max_duty != 1)) { - duty = max_duty + 1; - } auto speed_mode = get_speed_mode(channel_); auto chan_num = static_cast(channel_ % 8); int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); - ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); - ledc_update_duty(speed_mode, chan_num); + if (duty == max_duty) { + ledc_stop(speed_mode, chan_num, 1); + } else if (duty == 0) { + ledc_stop(speed_mode, chan_num, 0); + } else { + ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); + ledc_update_duty(speed_mode, chan_num); + } #endif } diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index a8034f8fab..cc7fae7e70 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -1,10 +1,6 @@ import json import logging -from os.path import ( - dirname, - isfile, - join, -) +from os.path import dirname, isfile, join import esphome.codegen as cg import esphome.config_validation as cv @@ -174,12 +170,11 @@ def _notify_old_style(config): return config -# NOTE: Keep this in mind when updating the recommended version: -# * For all constants below, update platformio.ini (in this repo) +# The dev and latest branches will be at *least* this version, which is what matters. ARDUINO_VERSIONS = { - "dev": (cv.Version(0, 0, 0), "https://github.com/libretiny-eu/libretiny.git"), - "latest": (cv.Version(0, 0, 0), None), - "recommended": (cv.Version(1, 5, 1), None), + "dev": (cv.Version(1, 7, 0), "https://github.com/libretiny-eu/libretiny.git"), + "latest": (cv.Version(1, 7, 0), "libretiny"), + "recommended": (cv.Version(1, 7, 0), None), } @@ -282,10 +277,10 @@ async def component_to_code(config): # if platform version is a valid version constraint, prefix the default package framework = config[CONF_FRAMEWORK] cv.platformio_version_constraint(framework[CONF_VERSION]) - if str(framework[CONF_VERSION]) != "0.0.0": - cg.add_platformio_option("platform", f"libretiny @ {framework[CONF_VERSION]}") - elif framework[CONF_SOURCE]: + if framework[CONF_SOURCE]: cg.add_platformio_option("platform", framework[CONF_SOURCE]) + elif str(framework[CONF_VERSION]) != "0.0.0": + cg.add_platformio_option("platform", f"libretiny @ {framework[CONF_VERSION]}") else: cg.add_platformio_option("platform", "libretiny") diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 161b4d8cd9..d9f139d2f4 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,8 +1,9 @@ -import esphome.codegen as cg -import esphome.config_validation as cv import esphome.automation as auto +import esphome.codegen as cg from esphome.components import mqtt, power_supply, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_COLOR_CORRECT, CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, @@ -10,36 +11,36 @@ from esphome.const import ( CONF_GAMMA_CORRECT, CONF_ID, CONF_MQTT_ID, - CONF_WEB_SERVER_ID, - CONF_POWER_SUPPLY, - CONF_RESTORE_MODE, + CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, - CONF_ON_STATE, + CONF_POWER_SUPPLY, + CONF_RESTORE_MODE, CONF_TRIGGER_ID, - CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE, + CONF_WEB_SERVER_ID, ) from esphome.core import coroutine_with_priority from esphome.cpp_helpers import setup_entity + from .automation import light_control_to_code # noqa from .effects import ( - validate_effects, + ADDRESSABLE_EFFECTS, BINARY_EFFECTS, + EFFECTS_REGISTRY, MONOCHROMATIC_EFFECTS, RGB_EFFECTS, - ADDRESSABLE_EFFECTS, - EFFECTS_REGISTRY, + validate_effects, ) from .types import ( # noqa - LightState, - AddressableLightState, - light_ns, - LightOutput, AddressableLight, - LightTurnOnTrigger, - LightTurnOffTrigger, + AddressableLightState, + LightOutput, + LightState, LightStateTrigger, + LightTurnOffTrigger, + LightTurnOnTrigger, + light_ns, ) CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 73083a58b7..d622ec0375 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -114,10 +114,11 @@ class AddressableColorWipeEffect : public AddressableLightEffect { if (now - this->last_add_ < this->add_led_interval_) return; this->last_add_ = now; - if (this->reverse_) + if (this->reverse_) { it.shift_left(1); - else + } else { it.shift_right(1); + } const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_]; Color esp_color = Color(color.r, color.g, color.b, color.w); if (color.gradient) { @@ -127,10 +128,11 @@ class AddressableColorWipeEffect : public AddressableLightEffect { uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds); esp_color = esp_color.gradient(next_esp_color, gradient); } - if (this->reverse_) + if (this->reverse_) { it[-1] = esp_color; - else + } else { it[0] = esp_color; + } if (++this->leds_added_ >= color.num_leds) { this->leds_added_ = 0; this->at_color_ = (this->at_color_ + 1) % this->colors_.size(); @@ -207,10 +209,11 @@ class AddressableTwinkleEffect : public AddressableLightEffect { const uint8_t sine = half_sin8(view.get_effect_data()); view = current_color * sine; const uint8_t new_pos = view.get_effect_data() + pos_add; - if (new_pos < view.get_effect_data()) + if (new_pos < view.get_effect_data()) { view.set_effect_data(0); - else + } else { view.set_effect_data(new_pos); + } } else { view = Color::BLACK; } @@ -254,10 +257,11 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { view = Color(((color >> 2) & 1) * sine, ((color >> 1) & 1) * sine, ((color >> 0) & 1) * sine); } const uint8_t new_x = x + pos_add; - if (new_x > 0b11111) + if (new_x > 0b11111) { view.set_effect_data(0); - else + } else { view.set_effect_data((new_x << 3) | color); + } } else { view = Color(0, 0, 0, 0); } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index b63fc93dc5..6e055741da 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -7,6 +7,8 @@ namespace esphome { namespace light { +enum class LimitMode { CLAMP, DO_NOTHING }; + template class ToggleAction : public Action { public: explicit ToggleAction(LightState *state) : state_(state) {} @@ -77,7 +79,10 @@ template class DimRelativeAction : public Action { float rel = this->relative_brightness_.value(x...); float cur; this->parent_->remote_values.as_brightness(&cur); - float new_brightness = clamp(cur + rel, 0.0f, 1.0f); + if ((limit_mode_ == LimitMode::DO_NOTHING) && ((cur < min_brightness_) || (cur > max_brightness_))) { + return; + } + float new_brightness = clamp(cur + rel, min_brightness_, max_brightness_); call.set_state(new_brightness != 0.0f); call.set_brightness(new_brightness); @@ -85,8 +90,18 @@ template class DimRelativeAction : public Action { call.perform(); } + void set_min_max_brightness(float min, float max) { + this->min_brightness_ = min; + this->max_brightness_ = max; + } + + void set_limit_mode(LimitMode limit_mode) { this->limit_mode_ = limit_mode; } + protected: LightState *parent_; + float min_brightness_{0.0}; + float max_brightness_{1.0}; + LimitMode limit_mode_{LimitMode::CLAMP}; }; template class LightIsOnCondition : public Condition { diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index cfba273565..ec0375f54a 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -19,10 +19,15 @@ from esphome.const import ( CONF_WARM_WHITE, CONF_RANGE_FROM, CONF_RANGE_TO, + CONF_BRIGHTNESS_LIMITS, + CONF_LIMIT_MODE, + CONF_MIN_BRIGHTNESS, + CONF_MAX_BRIGHTNESS, ) from .types import ( ColorMode, COLOR_MODES, + LIMIT_MODES, DimRelativeAction, ToggleAction, LightState, @@ -167,6 +172,15 @@ LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable( cv.positive_time_period_milliseconds ), + cv.Optional(CONF_BRIGHTNESS_LIMITS): cv.Schema( + { + cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage, + cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage, + cv.Optional(CONF_LIMIT_MODE, default="CLAMP"): cv.enum( + LIMIT_MODES, upper=True, space="_" + ), + } + ), } ) @@ -182,6 +196,13 @@ async def light_dim_relative_to_code(config, action_id, template_arg, args): if CONF_TRANSITION_LENGTH in config: templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) cg.add(var.set_transition_length(templ)) + if conf := config.get(CONF_BRIGHTNESS_LIMITS): + cg.add( + var.set_min_max_brightness( + conf[CONF_MIN_BRIGHTNESS], conf[CONF_MAX_BRIGHTNESS] + ) + ) + cg.add(var.set_limit_mode(conf[CONF_LIMIT_MODE])) return var diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index f7829a3f44..9e02e889c9 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -25,7 +25,7 @@ class PulseLightEffect : public LightEffect { return; } auto call = this->state_->turn_on(); - float out = this->on_ ? this->max_brightness : this->min_brightness; + float out = this->on_ ? this->max_brightness_ : this->min_brightness_; call.set_brightness_if_supported(out); call.set_transition_length_if_supported(this->on_ ? this->transition_on_length_ : this->transition_off_length_); this->on_ = !this->on_; @@ -43,8 +43,8 @@ class PulseLightEffect : public LightEffect { void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } void set_min_max_brightness(float min, float max) { - this->min_brightness = min; - this->max_brightness = max; + this->min_brightness_ = min; + this->max_brightness_ = max; } protected: @@ -53,8 +53,8 @@ class PulseLightEffect : public LightEffect { uint32_t transition_on_length_{}; uint32_t transition_off_length_{}; uint32_t update_interval_{}; - float min_brightness{0.0}; - float max_brightness{1.0}; + float min_brightness_{0.0}; + float max_brightness_{1.0}; }; /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index eedd71ab27..979a1acb07 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -41,29 +41,29 @@ class ESPColorCorrection { if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; - return res; + uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; + return (uint8_t) std::min(res, uint16_t(255)); } protected: diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index a453debd94..64483bcc9c 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -26,6 +26,13 @@ COLOR_MODES = { "RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE, } +# Limit modes +LimitMode = light_ns.enum("LimitMode", is_class=True) +LIMIT_MODES = { + "CLAMP": LimitMode.CLAMP, + "DO_NOTHING": LimitMode.DO_NOTHING, +} + # Actions ToggleAction = light_ns.class_("ToggleAction", automation.Action) LightControlAction = light_ns.class_("LightControlAction", automation.Action) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index c2d6054ed9..6b92bc264b 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -1,14 +1,14 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MQTT_ID, CONF_ON_LOCK, CONF_ON_UNLOCK, CONF_TRIGGER_ID, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, ) from esphome.core import CORE, coroutine_with_priority diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 99aa39c4ba..f30bc23e38 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -1,9 +1,21 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import LambdaAction +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +from esphome.components.libretiny import get_libretiny_component, get_libretiny_family +from esphome.components.libretiny.const import COMPONENT_BK72XX, COMPONENT_RTL87XX +import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, CONF_BAUD_RATE, @@ -18,27 +30,12 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE, PLATFORM_BK72XX, - PLATFORM_RTL87XX, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + PLATFORM_RTL87XX, ) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant -from esphome.components.esp32.const import ( - VARIANT_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32C3, - VARIANT_ESP32S3, - VARIANT_ESP32C2, - VARIANT_ESP32C6, - VARIANT_ESP32H2, -) -from esphome.components.libretiny import get_libretiny_component, get_libretiny_family -from esphome.components.libretiny.const import ( - COMPONENT_BK72XX, - COMPONENT_RTL87XX, -) CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") diff --git a/esphome/components/ltr501/__init__.py b/esphome/components/ltr501/__init__.py new file mode 100644 index 0000000000..dd06cfffea --- /dev/null +++ b/esphome/components/ltr501/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@latonita"] diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp new file mode 100644 index 0000000000..4f4e26f44f --- /dev/null +++ b/esphome/components/ltr501/ltr501.cpp @@ -0,0 +1,542 @@ +#include "ltr501.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +using esphome::i2c::ErrorCode; + +namespace esphome { +namespace ltr501 { + +static const char *const TAG = "ltr501"; + +static const uint8_t MAX_TRIES = 5; +static const uint8_t MAX_SENSITIVITY_ADJUSTMENTS = 10; + +struct GainTimePair { + AlsGain501 gain; + IntegrationTime501 time; +}; + +bool operator==(const GainTimePair &lhs, const GainTimePair &rhs) { + return lhs.gain == rhs.gain && lhs.time == rhs.time; +} + +bool operator!=(const GainTimePair &lhs, const GainTimePair &rhs) { + return !(lhs.gain == rhs.gain && lhs.time == rhs.time); +} + +template T get_next(const T (&array)[size], const T val) { + size_t i = 0; + size_t idx = -1; + while (idx == -1 && i < size) { + if (array[i] == val) { + idx = i; + break; + } + i++; + } + if (idx == -1 || i + 1 >= size) + return val; + return array[i + 1]; +} + +template T get_prev(const T (&array)[size], const T val) { + size_t i = size - 1; + size_t idx = -1; + while (idx == -1 && i > 0) { + if (array[i] == val) { + idx = i; + break; + } + i--; + } + if (idx == -1 || i == 0) + return val; + return array[i - 1]; +} + +static uint16_t get_itime_ms(IntegrationTime501 time) { + static const uint16_t ALS_INT_TIME[4] = {100, 50, 200, 400}; + return ALS_INT_TIME[time & 0b11]; +} + +static uint16_t get_meas_time_ms(MeasurementRepeatRate rate) { + static const uint16_t ALS_MEAS_RATE[8] = {50, 100, 200, 500, 1000, 2000, 2000, 2000}; + return ALS_MEAS_RATE[rate & 0b111]; +} + +static float get_gain_coeff(AlsGain501 gain) { return gain == AlsGain501::GAIN_1 ? 1.0f : 150.0f; } + +static float get_ps_gain_coeff(PsGain501 gain) { + static const float PS_GAIN[4] = {1, 4, 8, 16}; + return PS_GAIN[gain & 0b11]; +} + +void LTRAlsPs501Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up LTR-501/301/558"); + // As per datasheet we need to wait at least 100ms after power on to get ALS chip responsive + this->set_timeout(100, [this]() { this->state_ = State::DELAYED_SETUP; }); +} + +void LTRAlsPs501Component::dump_config() { + auto get_device_type = [](LtrType typ) { + switch (typ) { + case LtrType::LTR_TYPE_ALS_ONLY: + return "ALS only"; + case LtrType::LTR_TYPE_PS_ONLY: + return "PS only"; + case LtrType::LTR_TYPE_ALS_AND_PS: + return "Als + PS"; + default: + return "Unknown"; + } + }; + + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Device type: %s", get_device_type(this->ltr_type_)); + ESP_LOGCONFIG(TAG, " Automatic mode: %s", ONOFF(this->automatic_mode_enabled_)); + ESP_LOGCONFIG(TAG, " Gain: %.0fx", get_gain_coeff(this->gain_)); + ESP_LOGCONFIG(TAG, " Integration time: %d ms", get_itime_ms(this->integration_time_)); + ESP_LOGCONFIG(TAG, " Measurement repeat rate: %d ms", get_meas_time_ms(this->repeat_rate_)); + ESP_LOGCONFIG(TAG, " Glass attenuation factor: %f", this->glass_attenuation_factor_); + ESP_LOGCONFIG(TAG, " Proximity gain: %.0fx", get_ps_gain_coeff(this->ps_gain_)); + ESP_LOGCONFIG(TAG, " Proximity cooldown time: %d s", this->ps_cooldown_time_s_); + ESP_LOGCONFIG(TAG, " Proximity high threshold: %d", this->ps_threshold_high_); + ESP_LOGCONFIG(TAG, " Proximity low threshold: %d", this->ps_threshold_low_); + + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "ALS calculated lux", this->ambient_light_sensor_); + LOG_SENSOR(" ", "CH1 Infrared counts", this->infrared_counts_sensor_); + LOG_SENSOR(" ", "CH0 Visible+IR counts", this->full_spectrum_counts_sensor_); + LOG_SENSOR(" ", "Actual gain", this->actual_gain_sensor_); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with I2C LTR-501/301/558 failed!"); + } +} + +void LTRAlsPs501Component::update() { + if (!this->is_als_()) { + ESP_LOGW(TAG, "Update. ALS data not available. Change configuration to ALS or ALS_PS."); + return; + } + if (this->is_ready() && this->is_als_() && this->state_ == State::IDLE) { + ESP_LOGV(TAG, "Update. Initiating new ALS data collection."); + + this->state_ = this->automatic_mode_enabled_ ? State::COLLECTING_DATA_AUTO : State::WAITING_FOR_DATA; + + this->als_readings_.ch0 = 0; + this->als_readings_.ch1 = 0; + this->als_readings_.gain = this->gain_; + this->als_readings_.integration_time = this->integration_time_; + this->als_readings_.lux = 0; + this->als_readings_.number_of_adjustments = 0; + + } else { + ESP_LOGV(TAG, "Update. Component not ready yet."); + } +} + +void LTRAlsPs501Component::loop() { + ErrorCode err = i2c::ERROR_OK; + static uint8_t tries{0}; + + switch (this->state_) { + case State::DELAYED_SETUP: + err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + ESP_LOGW(TAG, "i2c connection failed"); + this->mark_failed(); + } + this->configure_reset_(); + if (this->is_als_()) { + this->configure_als_(); + this->configure_integration_time_(this->integration_time_); + } + if (this->is_ps_()) { + this->configure_ps_(); + } + + this->state_ = State::IDLE; + break; + + case State::IDLE: + if (this->is_ps_()) { + this->check_and_trigger_ps_(); + } + break; + + case State::WAITING_FOR_DATA: + if (this->is_als_data_ready_(this->als_readings_) == DataAvail::DATA_OK) { + tries = 0; + ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", + get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); + this->read_sensor_data_(this->als_readings_); + this->apply_lux_calculation_(this->als_readings_); + this->state_ = State::DATA_COLLECTED; + } else if (tries >= MAX_TRIES) { + ESP_LOGW(TAG, "Can't get data after several tries. Aborting."); + tries = 0; + this->status_set_warning(); + this->state_ = State::IDLE; + return; + } else { + tries++; + } + break; + + case State::COLLECTING_DATA_AUTO: + case State::DATA_COLLECTED: + // first measurement in auto mode (COLLECTING_DATA_AUTO state) require device reconfiguration + if (this->state_ == State::COLLECTING_DATA_AUTO || this->are_adjustments_required_(this->als_readings_)) { + this->state_ = State::ADJUSTMENT_IN_PROGRESS; + ESP_LOGD(TAG, "Reconfiguring sensitivity: gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), + get_itime_ms(this->als_readings_.integration_time)); + this->configure_integration_time_(this->als_readings_.integration_time); + this->configure_gain_(this->als_readings_.gain); + // if sensitivity adjustment needed - need to wait for first data samples after setting new parameters + this->set_timeout(2 * get_meas_time_ms(this->repeat_rate_), + [this]() { this->state_ = State::WAITING_FOR_DATA; }); + } else { + this->state_ = State::READY_TO_PUBLISH; + } + break; + + case State::ADJUSTMENT_IN_PROGRESS: + // nothing to be done, just waiting for the timeout + break; + + case State::READY_TO_PUBLISH: + this->publish_data_part_1_(this->als_readings_); + this->state_ = State::KEEP_PUBLISHING; + break; + + case State::KEEP_PUBLISHING: + this->publish_data_part_2_(this->als_readings_); + this->status_clear_warning(); + this->state_ = State::IDLE; + break; + + default: + break; + } +} + +void LTRAlsPs501Component::check_and_trigger_ps_() { + static uint32_t last_high_trigger_time{0}; + static uint32_t last_low_trigger_time{0}; + uint16_t ps_data = this->read_ps_data_(); + uint32_t now = millis(); + + if (ps_data != this->ps_readings_) { + this->ps_readings_ = ps_data; + // Higher values - object is closer to sensor + if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { + last_high_trigger_time = now; + ESP_LOGD(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, + this->ps_threshold_high_); + this->on_ps_high_trigger_callback_.call(); + } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { + last_low_trigger_time = now; + ESP_LOGD(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, + this->ps_threshold_low_); + this->on_ps_low_trigger_callback_.call(); + } + } +} + +bool LTRAlsPs501Component::check_part_number_() { + uint8_t manuf_id = this->reg((uint8_t) CommandRegisters::MANUFAC_ID).get(); + if (manuf_id != 0x05) { // 0x05 is Lite-On Semiconductor Corp. ID + ESP_LOGW(TAG, "Unknown manufacturer ID: 0x%02X", manuf_id); + this->mark_failed(); + return false; + } + + // Things getting not really funny here, we can't identify device type by part number ID + // ======================== ========= ===== ================= + // Device Part ID Rev Capabilities + // ======================== ========= ===== ================= + // ltr-558als 0x08 0 als + ps + // ltr-501als 0x08 0 als + ps + // ltr-301als - 0x08 0 als only + + PartIdRegister part_id{0}; + part_id.raw = this->reg((uint8_t) CommandRegisters::PART_ID).get(); + if (part_id.part_number_id != 0x08) { + ESP_LOGW(TAG, "Unknown part number ID: 0x%02X. LTR-501/301 shall have 0x08. It might not work properly.", + part_id.part_number_id); + this->status_set_warning(); + return true; + } + return true; +} + +void LTRAlsPs501Component::configure_reset_() { + ESP_LOGV(TAG, "Resetting"); + + AlsControlRegister501 als_ctrl{0}; + als_ctrl.sw_reset = true; + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + + uint8_t tries = MAX_TRIES; + do { + ESP_LOGV(TAG, "Waiting chip to reset"); + delay(2); + als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + } while (als_ctrl.sw_reset && tries--); // while sw reset bit is on - keep waiting + + if (als_ctrl.sw_reset) { + ESP_LOGW(TAG, "Reset failed"); + } +} + +void LTRAlsPs501Component::configure_als_() { + AlsControlRegister501 als_ctrl{0}; + als_ctrl.sw_reset = false; + als_ctrl.als_mode_active = true; + als_ctrl.gain = this->gain_; + + ESP_LOGV(TAG, "Setting active mode and gain reg 0x%02X", als_ctrl.raw); + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(5); + + uint8_t tries = MAX_TRIES; + do { + ESP_LOGV(TAG, "Waiting for ALS device to become active..."); + delay(2); + als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + } while (!als_ctrl.als_mode_active && tries--); // while active mode is not set - keep waiting + + if (!als_ctrl.als_mode_active) { + ESP_LOGW(TAG, "Failed to activate ALS device"); + } +} + +void LTRAlsPs501Component::configure_ps_() { + PsMeasurementRateRegister ps_meas{0}; + ps_meas.ps_measurement_rate = PsMeasurementRate::PS_MEAS_RATE_50MS; + this->reg((uint8_t) CommandRegisters::PS_MEAS_RATE) = ps_meas.raw; + + PsControlRegister501 ps_ctrl{0}; + ps_ctrl.ps_mode_active = true; + ps_ctrl.ps_mode_xxx = true; + this->reg((uint8_t) CommandRegisters::PS_CONTR) = ps_ctrl.raw; +} + +uint16_t LTRAlsPs501Component::read_ps_data_() { + AlsPsStatusRegister als_status{0}; + als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); + if (!als_status.ps_new_data) { + return this->ps_readings_; + } + + uint8_t ps_low = this->reg((uint8_t) CommandRegisters::PS_DATA_0).get(); + PsData1Register ps_high; + ps_high.raw = this->reg((uint8_t) CommandRegisters::PS_DATA_1).get(); + + uint16_t val = encode_uint16(ps_high.ps_data_high, ps_low); + return val; +} + +void LTRAlsPs501Component::configure_gain_(AlsGain501 gain) { + AlsControlRegister501 als_ctrl{0}; + als_ctrl.als_mode_active = true; + als_ctrl.gain = gain; + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + + AlsControlRegister501 read_als_ctrl{0}; + read_als_ctrl.raw = this->reg((uint8_t) CommandRegisters::ALS_CONTR).get(); + if (read_als_ctrl.gain != gain) { + ESP_LOGW(TAG, "Failed to set gain. We will try one more time."); + this->reg((uint8_t) CommandRegisters::ALS_CONTR) = als_ctrl.raw; + delay(2); + } +} + +void LTRAlsPs501Component::configure_integration_time_(IntegrationTime501 time) { + MeasurementRateRegister501 meas{0}; + meas.measurement_repeat_rate = this->repeat_rate_; + meas.integration_time = time; + this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw; + delay(2); + + MeasurementRateRegister501 read_meas{0}; + read_meas.raw = this->reg((uint8_t) CommandRegisters::MEAS_RATE).get(); + if (read_meas.integration_time != time) { + ESP_LOGW(TAG, "Failed to set integration time. We will try one more time."); + this->reg((uint8_t) CommandRegisters::MEAS_RATE) = meas.raw; + delay(2); + } +} + +DataAvail LTRAlsPs501Component::is_als_data_ready_(AlsReadings &data) { + AlsPsStatusRegister als_status{0}; + als_status.raw = this->reg((uint8_t) CommandRegisters::ALS_PS_STATUS).get(); + if (!als_status.als_new_data) + return DataAvail::NO_DATA; + ESP_LOGV(TAG, "Data ready, reported gain is %.0fx", get_gain_coeff(als_status.gain)); + if (data.gain != als_status.gain) { + ESP_LOGW(TAG, "Actual gain differs from requested (%.0f)", get_gain_coeff(data.gain)); + return DataAvail::BAD_DATA; + } + data.gain = als_status.gain; + return DataAvail::DATA_OK; +} + +void LTRAlsPs501Component::read_sensor_data_(AlsReadings &data) { + data.ch1 = 0; + data.ch0 = 0; + uint8_t ch1_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_0).get(); + uint8_t ch1_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH1_1).get(); + uint8_t ch0_0 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_0).get(); + uint8_t ch0_1 = this->reg((uint8_t) CommandRegisters::ALS_DATA_CH0_1).get(); + data.ch1 = encode_uint16(ch1_1, ch1_0); + data.ch0 = encode_uint16(ch0_1, ch0_0); + + ESP_LOGD(TAG, "Got sensor data: CH1 = %d, CH0 = %d", data.ch1, data.ch0); +} + +bool LTRAlsPs501Component::are_adjustments_required_(AlsReadings &data) { + if (!this->automatic_mode_enabled_) + return false; + + // sometimes sensors fail to change sensitivity. this prevents us from infinite loop + if (data.number_of_adjustments++ > MAX_SENSITIVITY_ADJUSTMENTS) { + ESP_LOGW(TAG, "Too many sensitivity adjustments done. Something wrong with the sensor. Stopping."); + return false; + } + + ESP_LOGV(TAG, "Adjusting sensitivity, run #%d", data.number_of_adjustments); + + // available combinations of gain and integration times: + static const GainTimePair GAIN_TIME_PAIRS[] = { + {AlsGain501::GAIN_1, INTEGRATION_TIME_50MS}, {AlsGain501::GAIN_1, INTEGRATION_TIME_100MS}, + {AlsGain501::GAIN_150, INTEGRATION_TIME_100MS}, {AlsGain501::GAIN_150, INTEGRATION_TIME_200MS}, + {AlsGain501::GAIN_150, INTEGRATION_TIME_400MS}, + }; + + GainTimePair current_pair = {data.gain, data.integration_time}; + + // Here comes funky business with this sensor. it has no internal error checking mechanism + // as in later versions (LTR-303/329/559/..) and sensor gets overwhelmed when saturated + // and readings are strange. We only check high sensitivity mode for now. + // Nothing is documented and it is a result of real-world testing. + if (data.gain == AlsGain501::GAIN_150) { + // when sensor is saturated it returns various crazy numbers + // CH1 = 1, CH0 = 0 + if (data.ch1 == 1 && data.ch0 == 0) { + ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = 1, CH0 = 0, Gain 150x"); + // fake saturation + data.ch0 = 0xffff; + data.ch1 = 0xffff; + } else if (data.ch1 == 65535 && data.ch0 == 0) { + ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = 65535, CH0 = 0, Gain 150x"); + data.ch0 = 0xffff; + } else if (data.ch1 > 1000 && data.ch0 == 0) { + ESP_LOGV(TAG, "Looks like sensor got saturated (?) CH1 = %d, CH0 = 0, Gain 150x", data.ch1); + data.ch0 = 0xffff; + } + } + + static const uint16_t LOW_INTENSITY_THRESHOLD_1 = 100; + static const uint16_t LOW_INTENSITY_THRESHOLD_200 = 2000; + static const uint16_t HIGH_INTENSITY_THRESHOLD = 25000; + + if (data.ch0 <= (data.gain == AlsGain501::GAIN_1 ? LOW_INTENSITY_THRESHOLD_1 : LOW_INTENSITY_THRESHOLD_200) || + (data.gain == AlsGain501::GAIN_1 && data.lux < 320)) { + GainTimePair next_pair = get_next(GAIN_TIME_PAIRS, current_pair); + if (next_pair != current_pair) { + data.gain = next_pair.gain; + data.integration_time = next_pair.time; + ESP_LOGV(TAG, "Low illuminance. Increasing sensitivity."); + return true; + } + + } else if (data.ch0 >= HIGH_INTENSITY_THRESHOLD || data.ch1 >= HIGH_INTENSITY_THRESHOLD) { + GainTimePair prev_pair = get_prev(GAIN_TIME_PAIRS, current_pair); + if (prev_pair != current_pair) { + data.gain = prev_pair.gain; + data.integration_time = prev_pair.time; + ESP_LOGV(TAG, "High illuminance. Decreasing sensitivity."); + return true; + } + } else { + ESP_LOGD(TAG, "Illuminance is good enough."); + return false; + } + ESP_LOGD(TAG, "Can't adjust sensitivity anymore."); + return false; +} + +void LTRAlsPs501Component::apply_lux_calculation_(AlsReadings &data) { + if ((data.ch0 == 0xFFFF) || (data.ch1 == 0xFFFF)) { + ESP_LOGW(TAG, "Sensors got saturated"); + data.lux = 0.0f; + return; + } + + if ((data.ch0 == 0x0000) && (data.ch1 == 0x0000)) { + ESP_LOGW(TAG, "Sensors blacked out"); + data.lux = 0.0f; + return; + } + + float ch0 = data.ch0; + float ch1 = data.ch1; + float ratio = ch1 / (ch0 + ch1); + float als_gain = get_gain_coeff(data.gain); + float als_time = ((float) get_itime_ms(data.integration_time)) / 100.0f; + float inv_pfactor = this->glass_attenuation_factor_; + float lux = 0.0f; + + // method from + // https://github.com/fards/Ainol_fire_kernel/blob/83832cf8a3082fd8e963230f4b1984479d1f1a84/customer/drivers/lightsensor/ltr501als.c#L295 + + if (ratio < 0.45) { + lux = 1.7743 * ch0 + 1.1059 * ch1; + } else if (ratio < 0.64) { + lux = 3.7725 * ch0 - 1.3363 * ch1; + } else if (ratio < 0.85) { + lux = 1.6903 * ch0 - 0.1693 * ch1; + } else { + ESP_LOGW(TAG, "Impossible ch1/(ch0 + ch1) ratio"); + lux = 0.0f; + } + + lux = inv_pfactor * lux / als_gain / als_time; + data.lux = lux; + + ESP_LOGD(TAG, "Lux calculation: ratio %.3f, gain %.0fx, int time %.1f, inv_pfactor %.3f, lux %.3f", ratio, als_gain, + als_time, inv_pfactor, lux); +} + +void LTRAlsPs501Component::publish_data_part_1_(AlsReadings &data) { + if (this->proximity_counts_sensor_ != nullptr) { + this->proximity_counts_sensor_->publish_state(this->ps_readings_); + } + if (this->ambient_light_sensor_ != nullptr) { + this->ambient_light_sensor_->publish_state(data.lux); + } + if (this->infrared_counts_sensor_ != nullptr) { + this->infrared_counts_sensor_->publish_state(data.ch1); + } + if (this->full_spectrum_counts_sensor_ != nullptr) { + this->full_spectrum_counts_sensor_->publish_state(data.ch0); + } +} + +void LTRAlsPs501Component::publish_data_part_2_(AlsReadings &data) { + if (this->actual_gain_sensor_ != nullptr) { + this->actual_gain_sensor_->publish_state(get_gain_coeff(data.gain)); + } + if (this->actual_integration_time_sensor_ != nullptr) { + this->actual_integration_time_sensor_->publish_state(get_itime_ms(data.integration_time)); + } +} +} // namespace ltr501 +} // namespace esphome diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h new file mode 100644 index 0000000000..07b69fa0d0 --- /dev/null +++ b/esphome/components/ltr501/ltr501.h @@ -0,0 +1,184 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "esphome/core/automation.h" + +#include "ltr_definitions_501.h" + +namespace esphome { +namespace ltr501 { + +enum DataAvail : uint8_t { NO_DATA, BAD_DATA, DATA_OK }; + +enum LtrType : uint8_t { + LTR_TYPE_UNKNOWN = 0, + LTR_TYPE_ALS_ONLY = 1, + LTR_TYPE_PS_ONLY = 2, + LTR_TYPE_ALS_AND_PS = 3, +}; + +class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { + public: + // + // EspHome framework functions + // + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + void loop() override; + + // Configuration setters : General + // + void set_ltr_type(LtrType type) { this->ltr_type_ = type; } + + // Configuration setters : ALS + // + void set_als_auto_mode(bool enable) { this->automatic_mode_enabled_ = enable; } + void set_als_gain(AlsGain501 gain) { this->gain_ = gain; } + void set_als_integration_time(IntegrationTime501 time) { this->integration_time_ = time; } + void set_als_meas_repeat_rate(MeasurementRepeatRate rate) { this->repeat_rate_ = rate; } + void set_als_glass_attenuation_factor(float factor) { this->glass_attenuation_factor_ = factor; } + + // Configuration setters : PS + // + void set_ps_high_threshold(uint16_t threshold) { this->ps_threshold_high_ = threshold; } + void set_ps_low_threshold(uint16_t threshold) { this->ps_threshold_low_ = threshold; } + void set_ps_cooldown_time_s(uint16_t time) { this->ps_cooldown_time_s_ = time; } + void set_ps_gain(PsGain501 gain) { this->ps_gain_ = gain; } + + // Sensors setters + // + void set_ambient_light_sensor(sensor::Sensor *sensor) { this->ambient_light_sensor_ = sensor; } + void set_full_spectrum_counts_sensor(sensor::Sensor *sensor) { this->full_spectrum_counts_sensor_ = sensor; } + void set_infrared_counts_sensor(sensor::Sensor *sensor) { this->infrared_counts_sensor_ = sensor; } + void set_actual_gain_sensor(sensor::Sensor *sensor) { this->actual_gain_sensor_ = sensor; } + void set_actual_integration_time_sensor(sensor::Sensor *sensor) { this->actual_integration_time_sensor_ = sensor; } + void set_proximity_counts_sensor(sensor::Sensor *sensor) { this->proximity_counts_sensor_ = sensor; } + + protected: + // + // Internal state machine, used to split all the actions into + // small steps in loop() to make sure we are not blocking execution + // + enum class State : uint8_t { + NOT_INITIALIZED, + DELAYED_SETUP, + IDLE, + WAITING_FOR_DATA, + COLLECTING_DATA_AUTO, + DATA_COLLECTED, + ADJUSTMENT_IN_PROGRESS, + READY_TO_PUBLISH, + KEEP_PUBLISHING + } state_{State::NOT_INITIALIZED}; + + LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY}; + + // + // Current measurements data + // + struct AlsReadings { + uint16_t ch0{0}; + uint16_t ch1{0}; + AlsGain501 gain{AlsGain501::GAIN_1}; + IntegrationTime501 integration_time{IntegrationTime501::INTEGRATION_TIME_100MS}; + float lux{0.0f}; + uint8_t number_of_adjustments{0}; + } als_readings_; + uint16_t ps_readings_{0xfffe}; + + inline bool is_als_() const { + return this->ltr_type_ == LtrType::LTR_TYPE_ALS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS; + } + inline bool is_ps_() const { + return this->ltr_type_ == LtrType::LTR_TYPE_PS_ONLY || this->ltr_type_ == LtrType::LTR_TYPE_ALS_AND_PS; + } + + // + // Device interaction and data manipulation + // + bool check_part_number_(); + + void configure_reset_(); + void configure_als_(); + void configure_integration_time_(IntegrationTime501 time); + void configure_gain_(AlsGain501 gain); + DataAvail is_als_data_ready_(AlsReadings &data); + void read_sensor_data_(AlsReadings &data); + bool are_adjustments_required_(AlsReadings &data); + void apply_lux_calculation_(AlsReadings &data); + void publish_data_part_1_(AlsReadings &data); + void publish_data_part_2_(AlsReadings &data); + + void configure_ps_(); + uint16_t read_ps_data_(); + void check_and_trigger_ps_(); + + // + // Component configuration + // + bool automatic_mode_enabled_{false}; + AlsGain501 gain_{AlsGain501::GAIN_1}; + IntegrationTime501 integration_time_{IntegrationTime501::INTEGRATION_TIME_100MS}; + MeasurementRepeatRate repeat_rate_{MeasurementRepeatRate::REPEAT_RATE_500MS}; + float glass_attenuation_factor_{1.0}; + + uint16_t ps_cooldown_time_s_{5}; + PsGain501 ps_gain_{PsGain501::PS_GAIN_1}; + uint16_t ps_threshold_high_{0xffff}; + uint16_t ps_threshold_low_{0x0000}; + + // + // Sensors for publishing data + // + sensor::Sensor *infrared_counts_sensor_{nullptr}; // direct reading CH1, infrared only + sensor::Sensor *full_spectrum_counts_sensor_{nullptr}; // direct reading CH0, infrared + visible light + sensor::Sensor *ambient_light_sensor_{nullptr}; // calculated lux + sensor::Sensor *actual_gain_sensor_{nullptr}; // actual gain of reading + sensor::Sensor *actual_integration_time_sensor_{nullptr}; // actual integration time + sensor::Sensor *proximity_counts_sensor_{nullptr}; // proximity sensor + + bool is_any_als_sensor_enabled_() const { + return this->ambient_light_sensor_ != nullptr || this->full_spectrum_counts_sensor_ != nullptr || + this->infrared_counts_sensor_ != nullptr || this->actual_gain_sensor_ != nullptr || + this->actual_integration_time_sensor_ != nullptr; + } + bool is_any_ps_sensor_enabled_() const { return this->proximity_counts_sensor_ != nullptr; } + + // + // Trigger section for the automations + // + friend class LTRPsHighTrigger; + friend class LTRPsLowTrigger; + + CallbackManager on_ps_high_trigger_callback_; + CallbackManager on_ps_low_trigger_callback_; + + void add_on_ps_high_trigger_callback_(std::function callback) { + this->on_ps_high_trigger_callback_.add(std::move(callback)); + } + + void add_on_ps_low_trigger_callback_(std::function callback) { + this->on_ps_low_trigger_callback_.add(std::move(callback)); + } +}; + +class LTRPsHighTrigger : public Trigger<> { + public: + explicit LTRPsHighTrigger(LTRAlsPs501Component *parent) { + parent->add_on_ps_high_trigger_callback_([this]() { this->trigger(); }); + } +}; + +class LTRPsLowTrigger : public Trigger<> { + public: + explicit LTRPsLowTrigger(LTRAlsPs501Component *parent) { + parent->add_on_ps_low_trigger_callback_([this]() { this->trigger(); }); + } +}; +} // namespace ltr501 +} // namespace esphome diff --git a/esphome/components/ltr501/ltr_definitions_501.h b/esphome/components/ltr501/ltr_definitions_501.h new file mode 100644 index 0000000000..604bd92b68 --- /dev/null +++ b/esphome/components/ltr501/ltr_definitions_501.h @@ -0,0 +1,260 @@ +#pragma once + +#include + +namespace esphome { +namespace ltr501 { + +enum class CommandRegisters : uint8_t { + ALS_CONTR = 0x80, // ALS operation mode control and SW reset + PS_CONTR = 0x81, // PS operation mode control + PS_LED = 0x82, // PS LED pulse frequency control + PS_N_PULSES = 0x83, // PS number of pulses control + PS_MEAS_RATE = 0x84, // PS measurement rate in active mode + MEAS_RATE = 0x85, // ALS measurement rate in active mode + PART_ID = 0x86, // Part Number ID and Revision ID + MANUFAC_ID = 0x87, // Manufacturer ID + ALS_DATA_CH1_0 = 0x88, // ALS measurement CH1 data, lower byte - infrared only + ALS_DATA_CH1_1 = 0x89, // ALS measurement CH1 data, upper byte - infrared only + ALS_DATA_CH0_0 = 0x8A, // ALS measurement CH0 data, lower byte - visible + infrared + ALS_DATA_CH0_1 = 0x8B, // ALS measurement CH0 data, upper byte - visible + infrared + ALS_PS_STATUS = 0x8C, // ALS PS new data status + PS_DATA_0 = 0x8D, // PS measurement data, lower byte + PS_DATA_1 = 0x8E, // PS measurement data, upper byte + ALS_PS_INTERRUPT = 0x8F, // Interrupt status + PS_THRES_UP_0 = 0x90, // PS interrupt upper threshold, lower byte + PS_THRES_UP_1 = 0x91, // PS interrupt upper threshold, upper byte + PS_THRES_LOW_0 = 0x92, // PS interrupt lower threshold, lower byte + PS_THRES_LOW_1 = 0x93, // PS interrupt lower threshold, upper byte + PS_OFFSET_1 = 0x94, // PS offset, upper byte + PS_OFFSET_0 = 0x95, // PS offset, lower byte + // 0x96 - reserved + ALS_THRES_UP_0 = 0x97, // ALS interrupt upper threshold, lower byte + ALS_THRES_UP_1 = 0x98, // ALS interrupt upper threshold, upper byte + ALS_THRES_LOW_0 = 0x99, // ALS interrupt lower threshold, lower byte + ALS_THRES_LOW_1 = 0x9A, // ALS interrupt lower threshold, upper byte + // 0x9B - reserved + // 0x9C - reserved + // 0x9D - reserved + INTERRUPT_PERSIST = 0x9E // Interrupt persistence filter +}; + +// ALS Sensor gain levels +enum AlsGain501 : uint8_t { + GAIN_1 = 0, // GAIN_RANGE_2 // default + GAIN_150 = 1, // GAIN_RANGE_1 +}; +static const uint8_t GAINS_COUNT = 2; + +// ALS Sensor integration times +enum IntegrationTime501 : uint8_t { + INTEGRATION_TIME_100MS = 0, // default + INTEGRATION_TIME_50MS = 1, // only in Dynamic GAIN_RANGE_2 + INTEGRATION_TIME_200MS = 2, // only in Dynamic GAIN_RANGE_1 + INTEGRATION_TIME_400MS = 3, // only in Dynamic GAIN_RANGE_1 +}; +static const uint8_t TIMES_COUNT = 4; + +// ALS Sensor measurement repeat rate +enum MeasurementRepeatRate { + REPEAT_RATE_50MS = 0, + REPEAT_RATE_100MS = 1, + REPEAT_RATE_200MS = 2, + REPEAT_RATE_500MS = 3, // default + REPEAT_RATE_1000MS = 4, + REPEAT_RATE_2000MS = 5 +}; + +// PS Sensor gain levels +enum PsGain501 : uint8_t { + PS_GAIN_1 = 0, // default + PS_GAIN_4 = 1, + PS_GAIN_8 = 2, + PS_GAIN_16 = 3, +}; + +// LED Pulse Modulation Frequency +enum PsLedFreq : uint8_t { + PS_LED_FREQ_30KHZ = 0, + PS_LED_FREQ_40KHZ = 1, + PS_LED_FREQ_50KHZ = 2, + PS_LED_FREQ_60KHZ = 3, // default + PS_LED_FREQ_70KHZ = 4, + PS_LED_FREQ_80KHZ = 5, + PS_LED_FREQ_90KHZ = 6, + PS_LED_FREQ_100KHZ = 7, +}; + +// LED current duty +enum PsLedDuty : uint8_t { + PS_LED_DUTY_25 = 0, + PS_LED_DUTY_50 = 1, // default + PS_LED_DUTY_75 = 2, + PS_LED_DUTY_100 = 3, +}; + +// LED pulsed current level +enum PsLedCurrent : uint8_t { + PS_LED_CURRENT_5MA = 0, + PS_LED_CURRENT_10MA = 1, + PS_LED_CURRENT_20MA = 2, + PS_LED_CURRENT_50MA = 3, // default + PS_LED_CURRENT_100MA = 4, + PS_LED_CURRENT_100MA1 = 5, + PS_LED_CURRENT_100MA2 = 6, + PS_LED_CURRENT_100MA3 = 7, +}; + +// PS measurement rate +enum PsMeasurementRate : uint8_t { + PS_MEAS_RATE_50MS = 0, + PS_MEAS_RATE_70MS = 1, + PS_MEAS_RATE_100MS = 2, // default + PS_MEAS_RATE_200MS = 3, + PS_MEAS_RATE_500MS = 4, + PS_MEAS_RATE_1000MS = 5, + PS_MEAS_RATE_2000MS = 6, + PS_MEAS_RATE_2000MS1 = 7, +}; + +// +// ALS_CONTR Register (0x80) +// +union AlsControlRegister501 { + uint8_t raw; + struct { + bool asl_mode_xxx : 1; + bool als_mode_active : 1; + bool sw_reset : 1; + AlsGain501 gain : 1; + uint8_t reserved : 4; + } __attribute__((packed)); +}; + +// +// PS_CONTR Register (0x81) +// +union PsControlRegister501 { + uint8_t raw; + struct { + bool ps_mode_xxx : 1; + bool ps_mode_active : 1; + PsGain501 ps_gain : 2; + bool reserved_4 : 1; + bool reserved_5 : 1; + bool reserved_6 : 1; + bool reserved_7 : 1; + } __attribute__((packed)); +}; + +// +// PS_LED Register (0x82) +// +union PsLedRegister { + uint8_t raw; + struct { + PsLedCurrent ps_led_current : 3; + PsLedDuty ps_led_duty : 2; + PsLedFreq ps_led_freq : 3; + } __attribute__((packed)); +}; + +// +// PS_N_PULSES Register (0x83) +// +union PsNPulsesRegister501 { + uint8_t raw; + uint8_t number_of_pulses; +}; + +// +// PS_MEAS_RATE Register (0x84) +// +union PsMeasurementRateRegister { + uint8_t raw; + struct { + PsMeasurementRate ps_measurement_rate : 4; + uint8_t reserved : 4; + } __attribute__((packed)); +}; + +// +// ALS_MEAS_RATE Register (0x85) +// +union MeasurementRateRegister501 { + uint8_t raw; + struct { + MeasurementRepeatRate measurement_repeat_rate : 3; + IntegrationTime501 integration_time : 2; + bool reserved_5 : 1; + bool reserved_6 : 1; + bool reserved_7 : 1; + } __attribute__((packed)); +}; + +// +// PART_ID Register (0x86) (Read Only) +// +union PartIdRegister { + uint8_t raw; + struct { + uint8_t part_number_id : 4; + uint8_t revision_id : 4; + } __attribute__((packed)); +}; + +// +// ALS_PS_STATUS Register (0x8C) (Read Only) +// +union AlsPsStatusRegister { + uint8_t raw; + struct { + bool ps_new_data : 1; // 0 - old data, 1 - new data + bool ps_interrupt : 1; // 0 - interrupt signal not active, 1 - interrupt signal active + bool als_new_data : 1; // 0 - old data, 1 - new data + bool als_interrupt : 1; // 0 - interrupt signal not active, 1 - interrupt signal active + AlsGain501 gain : 1; // current ALS gain + bool reserved_5 : 1; + bool reserved_6 : 1; + bool reserved_7 : 1; + } __attribute__((packed)); +}; + +// +// PS_DATA_1 Register (0x8E) (Read Only) +// +union PsData1Register { + uint8_t raw; + struct { + uint8_t ps_data_high : 3; + uint8_t reserved : 4; + bool ps_saturation_flag : 1; + } __attribute__((packed)); +}; + +// +// INTERRUPT Register (0x8F) (Read Only) +// +union InterruptRegister { + uint8_t raw; + struct { + bool ps_interrupt : 1; + bool als_interrupt : 1; + bool interrupt_polarity : 1; // 0 - active low (default), 1 - active high + uint8_t reserved : 5; + } __attribute__((packed)); +}; + +// +// INTERRUPT_PERSIST Register (0x9E) +// +union InterruptPersistRegister { + uint8_t raw; + struct { + uint8_t als_persist : 4; // 0 - every ALS cycle, 1 - every 2 ALS cycles, ... 15 - every 16 ALS cycles + uint8_t ps_persist : 4; // 0 - every PS cycle, 1 - every 2 PS cycles, ... 15 - every 16 PS cycles + } __attribute__((packed)); +}; + +} // namespace ltr501 +} // namespace esphome diff --git a/esphome/components/ltr501/sensor.py b/esphome/components/ltr501/sensor.py new file mode 100644 index 0000000000..153d1b3ad1 --- /dev/null +++ b/esphome/components/ltr501/sensor.py @@ -0,0 +1,274 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ACTUAL_GAIN, + CONF_ACTUAL_INTEGRATION_TIME, + CONF_AMBIENT_LIGHT, + CONF_AUTO_MODE, + CONF_FULL_SPECTRUM_COUNTS, + CONF_GAIN, + CONF_GLASS_ATTENUATION_FACTOR, + CONF_ID, + CONF_INTEGRATION_TIME, + CONF_NAME, + CONF_REPEAT, + CONF_TRIGGER_ID, + CONF_TYPE, + DEVICE_CLASS_DISTANCE, + DEVICE_CLASS_ILLUMINANCE, + ICON_BRIGHTNESS_5, + ICON_BRIGHTNESS_6, + ICON_TIMER, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, + UNIT_MILLISECOND, +) + +CODEOWNERS = ["@latonita"] +DEPENDENCIES = ["i2c"] + +CONF_INFRARED_COUNTS = "infrared_counts" +CONF_ON_PS_HIGH_THRESHOLD = "on_ps_high_threshold" +CONF_ON_PS_LOW_THRESHOLD = "on_ps_low_threshold" +CONF_PS_COOLDOWN = "ps_cooldown" +CONF_PS_COUNTS = "ps_counts" +CONF_PS_GAIN = "ps_gain" +CONF_PS_HIGH_THRESHOLD = "ps_high_threshold" +CONF_PS_LOW_THRESHOLD = "ps_low_threshold" +ICON_BRIGHTNESS_7 = "mdi:brightness-7" +ICON_GAIN = "mdi:multiplication" +ICON_PROXIMITY = "mdi:hand-wave-outline" +UNIT_COUNTS = "#" + +ltr501_ns = cg.esphome_ns.namespace("ltr501") + +LTRAlsPsComponent = ltr501_ns.class_( + "LTRAlsPs501Component", cg.PollingComponent, i2c.I2CDevice +) + +LtrType = ltr501_ns.enum("LtrType") +LTR_TYPES = { + "ALS": LtrType.LTR_TYPE_ALS_ONLY, + "PS": LtrType.LTR_TYPE_PS_ONLY, + "ALS_PS": LtrType.LTR_TYPE_ALS_AND_PS, +} + +AlsGain = ltr501_ns.enum("AlsGain501") +ALS_GAINS = { + "1X": AlsGain.GAIN_1, + "150X": AlsGain.GAIN_150, +} + +IntegrationTime = ltr501_ns.enum("IntegrationTime501") +INTEGRATION_TIMES = { + 50: IntegrationTime.INTEGRATION_TIME_50MS, + 100: IntegrationTime.INTEGRATION_TIME_100MS, + 200: IntegrationTime.INTEGRATION_TIME_200MS, + 400: IntegrationTime.INTEGRATION_TIME_400MS, +} + +MeasurementRepeatRate = ltr501_ns.enum("MeasurementRepeatRate") +MEASUREMENT_REPEAT_RATES = { + 50: MeasurementRepeatRate.REPEAT_RATE_50MS, + 100: MeasurementRepeatRate.REPEAT_RATE_100MS, + 200: MeasurementRepeatRate.REPEAT_RATE_200MS, + 500: MeasurementRepeatRate.REPEAT_RATE_500MS, + 1000: MeasurementRepeatRate.REPEAT_RATE_1000MS, + 2000: MeasurementRepeatRate.REPEAT_RATE_2000MS, +} + +PsGain = ltr501_ns.enum("PsGain501") +PS_GAINS = { + "1X": PsGain.PS_GAIN_1, + "4X": PsGain.PS_GAIN_4, + "8X": PsGain.PS_GAIN_8, + "16X": PsGain.PS_GAIN_16, +} + +LTRPsHighTrigger = ltr501_ns.class_("LTRPsHighTrigger", automation.Trigger.template()) +LTRPsLowTrigger = ltr501_ns.class_("LTRPsLowTrigger", automation.Trigger.template()) + + +def validate_integration_time(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(INTEGRATION_TIMES, int=True)(value) + + +def validate_repeat_rate(value): + value = cv.positive_time_period_milliseconds(value).total_milliseconds + return cv.enum(MEASUREMENT_REPEAT_RATES, int=True)(value) + + +def validate_time_and_repeat_rate(config): + integraton_time = config[CONF_INTEGRATION_TIME] + repeat_rate = config[CONF_REPEAT] + if integraton_time > repeat_rate: + raise cv.Invalid( + f"Measurement repeat rate ({repeat_rate}ms) shall be greater or equal to integration time ({integraton_time}ms)" + ) + return config + + +def validate_als_gain_and_integration_time(config): + integraton_time = config[CONF_INTEGRATION_TIME] + if config[CONF_GAIN] == "1X" and integraton_time > 100: + raise cv.Invalid( + "ALS gain 1X can only be used with integration time 50ms or 100ms" + ) + if config[CONF_GAIN] == "200X" and integraton_time == 50: + raise cv.Invalid("ALS gain 200X can not be used with integration time 50ms") + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LTRAlsPsComponent), + cv.Optional(CONF_TYPE, default="ALS_PS"): cv.enum(LTR_TYPES, upper=True), + cv.Optional(CONF_AUTO_MODE, default=True): cv.boolean, + cv.Optional(CONF_GAIN, default="1X"): cv.enum(ALS_GAINS, upper=True), + cv.Optional( + CONF_INTEGRATION_TIME, default="100ms" + ): validate_integration_time, + cv.Optional(CONF_REPEAT, default="500ms"): validate_repeat_rate, + cv.Optional(CONF_GLASS_ATTENUATION_FACTOR, default=1.0): cv.float_range( + min=1.0 + ), + cv.Optional( + CONF_PS_COOLDOWN, default="5s" + ): cv.positive_time_period_seconds, + cv.Optional(CONF_PS_GAIN, default="1X"): cv.enum(PS_GAINS, upper=True), + cv.Optional(CONF_PS_HIGH_THRESHOLD, default=65535): cv.int_range( + min=0, max=65535 + ), + cv.Optional(CONF_PS_LOW_THRESHOLD, default=0): cv.int_range( + min=0, max=65535 + ), + cv.Optional(CONF_ON_PS_HIGH_THRESHOLD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsHighTrigger), + } + ), + cv.Optional(CONF_ON_PS_LOW_THRESHOLD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LTRPsLowTrigger), + } + ), + cv.Optional(CONF_AMBIENT_LIGHT): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_INFRARED_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_FULL_SPECTRUM_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_7, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_PS_COUNTS): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_COUNTS, + icon=ICON_PROXIMITY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTUAL_GAIN): cv.maybe_simple_value( + sensor.sensor_schema( + icon=ICON_GAIN, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + cv.Optional(CONF_ACTUAL_INTEGRATION_TIME): cv.maybe_simple_value( + sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + key=CONF_NAME, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x23)), + validate_time_and_repeat_rate, + validate_als_gain_and_integration_time, +) + + +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 als_config := config.get(CONF_AMBIENT_LIGHT): + sens = await sensor.new_sensor(als_config) + cg.add(var.set_ambient_light_sensor(sens)) + + if infrared_cnt_config := config.get(CONF_INFRARED_COUNTS): + sens = await sensor.new_sensor(infrared_cnt_config) + cg.add(var.set_infrared_counts_sensor(sens)) + + if full_spect_cnt_config := config.get(CONF_FULL_SPECTRUM_COUNTS): + sens = await sensor.new_sensor(full_spect_cnt_config) + cg.add(var.set_full_spectrum_counts_sensor(sens)) + + if act_gain_config := config.get(CONF_ACTUAL_GAIN): + sens = await sensor.new_sensor(act_gain_config) + cg.add(var.set_actual_gain_sensor(sens)) + + if act_itime_config := config.get(CONF_ACTUAL_INTEGRATION_TIME): + sens = await sensor.new_sensor(act_itime_config) + cg.add(var.set_actual_integration_time_sensor(sens)) + + if prox_cnt_config := config.get(CONF_PS_COUNTS): + sens = await sensor.new_sensor(prox_cnt_config) + cg.add(var.set_proximity_counts_sensor(sens)) + + for prox_high_tr in config.get(CONF_ON_PS_HIGH_THRESHOLD, []): + trigger = cg.new_Pvariable(prox_high_tr[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], prox_high_tr) + + for prox_low_tr in config.get(CONF_ON_PS_LOW_THRESHOLD, []): + trigger = cg.new_Pvariable(prox_low_tr[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], prox_low_tr) + + cg.add(var.set_ltr_type(config[CONF_TYPE])) + + cg.add(var.set_als_auto_mode(config[CONF_AUTO_MODE])) + cg.add(var.set_als_gain(config[CONF_GAIN])) + cg.add(var.set_als_integration_time(config[CONF_INTEGRATION_TIME])) + cg.add(var.set_als_meas_repeat_rate(config[CONF_REPEAT])) + cg.add(var.set_als_glass_attenuation_factor(config[CONF_GLASS_ATTENUATION_FACTOR])) + + cg.add(var.set_ps_cooldown_time_s(config[CONF_PS_COOLDOWN])) + cg.add(var.set_ps_gain(config[CONF_PS_GAIN])) + cg.add(var.set_ps_high_threshold(config[CONF_PS_HIGH_THRESHOLD])) + cg.add(var.set_ps_low_threshold(config[CONF_PS_LOW_THRESHOLD])) diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py index ac9f7e6788..e9a5264941 100644 --- a/esphome/components/ltr_als_ps/sensor.py +++ b/esphome/components/ltr_als_ps/sensor.py @@ -4,8 +4,10 @@ from esphome import automation from esphome.components import i2c, sensor from esphome.const import ( CONF_ACTUAL_GAIN, + CONF_ACTUAL_INTEGRATION_TIME, CONF_AMBIENT_LIGHT, CONF_AUTO_MODE, + CONF_FULL_SPECTRUM_COUNTS, CONF_GAIN, CONF_GLASS_ATTENUATION_FACTOR, CONF_ID, @@ -27,8 +29,6 @@ from esphome.const import ( CODEOWNERS = ["@latonita"] DEPENDENCIES = ["i2c"] -CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time" -CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts" CONF_INFRARED_COUNTS = "infrared_counts" CONF_ON_PS_HIGH_THRESHOLD = "on_ps_high_threshold" CONF_ON_PS_LOW_THRESHOLD = "on_ps_low_threshold" diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index c454a61957..64f254cde8 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,5 +1,6 @@ import logging +from esphome.automation import build_automation, register_action, validate_automation import esphome.codegen as cg from esphome.components.display import Display import esphome.config_validation as cv @@ -8,62 +9,124 @@ from esphome.const import ( CONF_BUFFER_SIZE, CONF_ID, CONF_LAMBDA, + CONF_ON_IDLE, CONF_PAGES, + CONF_TIMEOUT, + CONF_TRIGGER_ID, + CONF_TYPE, ) -from esphome.core import CORE, ID, Lambda +from esphome.core import CORE, ID from esphome.cpp_generator import MockObj from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid -from .btn import btn_spec -from .label import label_spec -from .lvcode import ConstantLiteral, LvContext -from .obj import obj_spec -from .schemas import any_widget_schema, obj_schema -from .touchscreens import touchscreen_schema, touchscreens_to_code -from .types import ( +from .automation import disp_update, focused_widgets, update_to_code +from .defines import add_define +from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code +from .gradient import GRADIENT_SCHEMA, gradients_to_code +from .lv_validation import lv_bool, lv_images_used +from .lvcode import LvContext, LvglComponent +from .schemas import ( + DISP_BG_SCHEMA, + FLEX_OBJ_SCHEMA, + GRID_CELL_SCHEMA, + LAYOUT_SCHEMAS, + STYLE_SCHEMA, WIDGET_TYPES, + any_widget_schema, + container_schema, + create_modify_schema, + grid_alignments, + obj_schema, +) +from .styles import add_top_layer, styles_to_code, theme_to_code +from .touchscreens import touchscreen_schema, touchscreens_to_code +from .trigger import generate_triggers +from .types import ( FontEngine, - LvglComponent, - lv_disp_t_ptr, + IdleTrigger, + ObjUpdateAction, lv_font_t, + lv_group_t, + lv_style_t, lvgl_ns, ) -from .widget import LvScrActType, Widget, add_widgets, set_obj_properties +from .widgets import Widget, add_widgets, lv_scr_act, set_obj_properties +from .widgets.animimg import animimg_spec +from .widgets.arc import arc_spec +from .widgets.button import button_spec +from .widgets.buttonmatrix import buttonmatrix_spec +from .widgets.checkbox import checkbox_spec +from .widgets.dropdown import dropdown_spec +from .widgets.img import img_spec +from .widgets.keyboard import keyboard_spec +from .widgets.label import label_spec +from .widgets.led import led_spec +from .widgets.line import line_spec +from .widgets.lv_bar import bar_spec +from .widgets.meter import meter_spec +from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code +from .widgets.obj import obj_spec +from .widgets.page import add_pages, generate_page_triggers, page_spec +from .widgets.roller import roller_spec +from .widgets.slider import slider_spec +from .widgets.spinbox import spinbox_spec +from .widgets.spinner import spinner_spec +from .widgets.switch import switch_spec +from .widgets.tabview import tabview_spec +from .widgets.textarea import textarea_spec +from .widgets.tileview import tileview_spec DOMAIN = "lvgl" -DEPENDENCIES = ("display",) -AUTO_LOAD = ("key_provider",) -CODEOWNERS = ("@clydebarrow",) +DEPENDENCIES = ["display"] +AUTO_LOAD = ["key_provider"] +CODEOWNERS = ["@clydebarrow"] LOGGER = logging.getLogger(__name__) -for w_type in (label_spec, obj_spec, btn_spec): +for w_type in ( + label_spec, + obj_spec, + button_spec, + bar_spec, + slider_spec, + arc_spec, + line_spec, + spinner_spec, + led_spec, + animimg_spec, + checkbox_spec, + img_spec, + switch_spec, + tabview_spec, + buttonmatrix_spec, + meter_spec, + dropdown_spec, + roller_spec, + textarea_spec, + spinbox_spec, + keyboard_spec, + tileview_spec, +): WIDGET_TYPES[w_type.name] = w_type -lv_scr_act_spec = LvScrActType() -lv_scr_act = Widget.create( - None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None -) - WIDGET_SCHEMA = any_widget_schema() - -async def add_init_lambda(lv_component, init): - if init: - lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")]) - cg.add(lv_component.add_init_lambda(lamb)) - - -lv_defines = {} # Dict of #defines to provide as build flags - - -def add_define(macro, value="1"): - if macro in lv_defines and lv_defines[macro] != value: - LOGGER.error( - "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value - ) - lv_defines[macro] = value +LAYOUT_SCHEMAS[df.TYPE_GRID] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA)) +} +LAYOUT_SCHEMAS[df.TYPE_FLEX] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA)) +} +LAYOUT_SCHEMAS[df.TYPE_NONE] = { + cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema()) +} +for w_type in WIDGET_TYPES.values(): + register_action( + f"lvgl.{w_type.name}.update", + ObjUpdateAction, + create_modify_schema(w_type), + )(update_to_code) def as_macro(macro, value): @@ -80,12 +143,15 @@ LV_CONF_H_FORMAT = """\ def generate_lv_conf_h(): - definitions = [as_macro(m, v) for m, v in lv_defines.items()] + definitions = [as_macro(m, v) for m, v in df.lv_defines.items()] definitions.sort() return LV_CONF_H_FORMAT.format("\n".join(definitions)) def final_validation(config): + if pages := config.get(CONF_PAGES): + if all(p[df.CONF_SKIP] for p in pages): + raise cv.Invalid("At least one page must not be skipped") global_config = full_config.get() for display_id in config[df.CONF_DISPLAYS]: path = global_config.get_path_for_id(display_id)[:-1] @@ -99,6 +165,21 @@ def final_validation(config): buffer_frac = config[CONF_BUFFER_SIZE] if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: LOGGER.warning("buffer_size: may need to be reduced without PSRAM") + for image_id in lv_images_used: + path = global_config.get_path_for_id(image_id)[:-1] + image_conf = global_config.get_config_for_path(path) + if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): + raise cv.Invalid( + "Using RGBA or RGB24 in image config not compatible with LVGL", path + ) + for w in focused_widgets: + path = global_config.get_path_for_id(w) + widget_conf = global_config.get_config_for_path(path[:-1]) + if df.CONF_ADJUSTABLE in widget_conf and not widget_conf[df.CONF_ADJUSTABLE]: + raise cv.Invalid( + "A non adjustable arc may not be focused", + path, + ) async def to_code(config): @@ -172,12 +253,30 @@ async def to_code(config): else: add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) - with LvContext(): + async with LvContext(lv_component): await touchscreens_to_code(lv_component, config) + await encoders_to_code(lv_component, config) + await theme_to_code(config) + await styles_to_code(config) + await gradients_to_code(config) await set_obj_properties(lv_scr_act, config) await add_widgets(lv_scr_act, config) + await add_pages(lv_component, config) + await add_top_layer(config) + await msgboxes_to_code(config) + await disp_update(f"{lv_component}->get_disp()", config) + # At this point only the setup code should be generated + assert LvContext.added_lambda_count == 1 Widget.set_completed() - await add_init_lambda(lv_component, LvContext.get_code()) + async with LvContext(lv_component): + await generate_triggers(lv_component) + await generate_page_triggers(lv_component, config) + for conf in config.get(CONF_ON_IDLE, ()): + templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) + idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) + await build_automation(idle_trigger, [], conf) + await initial_focus_to_code(config) + for comp in helpers.lvgl_components_required: CORE.add_define(f"USE_LVGL_{comp.upper()}") for use in helpers.lv_uses: @@ -212,9 +311,42 @@ CONFIG_SCHEMA = ( cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( "big_endian", "little_endian" ), - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), + cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}) + .extend(STYLE_SCHEMA) + .extend( + { + cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + } + ) + ), + cv.Optional(CONF_ON_IDLE): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger), + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), + cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA), + cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( + container_schema(page_spec) + ), + cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), + cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, + cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, - cv.GenerateID(df.CONF_TOUCHSCREENS): touchscreen_schema, + cv.Optional(df.CONF_THEME): cv.Schema( + {cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()} + ), + cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA, + cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, + cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, + cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t), } ) + .extend(DISP_BG_SCHEMA) ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py new file mode 100644 index 0000000000..cdc7553e81 --- /dev/null +++ b/esphome/components/lvgl/automation.py @@ -0,0 +1,317 @@ +from collections.abc import Awaitable +from typing import Callable + +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT +from esphome.cpp_generator import RawExpression, get_variable +from esphome.cpp_types import nullptr + +from .defines import ( + CONF_DISP_BG_COLOR, + CONF_DISP_BG_IMAGE, + CONF_EDITING, + CONF_FREEZE, + CONF_LVGL_ID, + CONF_SHOW_SNOW, + literal, +) +from .lv_validation import lv_bool, lv_color, lv_image +from .lvcode import ( + LVGL_COMP_ARG, + UPDATE_EVENT, + LambdaContext, + LocalVariable, + LvConditional, + LvglComponent, + ReturnStatement, + add_line_marks, + lv, + lv_add, + lv_expr, + lv_obj, + lvgl_comp, + static_cast, +) +from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA +from .types import ( + LV_STATE, + LvglAction, + LvglCondition, + ObjUpdateAction, + lv_disp_t, + lv_group_t, + lv_obj_t, + lv_pseudo_button_t, +) +from .widgets import ( + Widget, + get_widgets, + lv_scr_act, + set_obj_properties, + wait_for_widgets, +) + +# Record widgets that are used in a focused action here +focused_widgets = set() + + +async def action_to_code( + widgets: list[Widget], + action: Callable[[Widget], Awaitable[None]], + action_id, + template_arg, + args, +): + await wait_for_widgets() + async with LambdaContext(parameters=args, where=action_id) as context: + with LvConditional(lv_expr.is_pre_initialise()): + context.add(RawExpression("return")) + for widget in widgets: + await action(widget) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + return var + + +async def update_to_code(config, action_id, template_arg, args): + async def do_update(widget: Widget): + await set_obj_properties(widget, config) + await widget.type.to_code(widget, config) + if ( + widget.type.w_type.value_property is not None + and widget.type.w_type.value_property in config + ): + lv.event_send(widget.obj, UPDATE_EVENT, nullptr) + + widgets = await get_widgets(config[CONF_ID]) + return await action_to_code(widgets, do_update, action_id, template_arg, args) + + +@automation.register_condition( + "lvgl.is_paused", + LvglCondition, + LVGL_SCHEMA, +) +async def lvgl_is_paused(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: + lv_add(ReturnStatement(lvgl_comp.is_paused())) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +@automation.register_condition( + "lvgl.is_idle", + LvglCondition, + LVGL_SCHEMA.extend( + { + cv.Required(CONF_TIMEOUT): cv.templatable( + cv.positive_time_period_milliseconds + ) + } + ), +) +async def lvgl_is_idle(config, condition_id, template_arg, args): + lvgl = config[CONF_LVGL_ID] + timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) + async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: + lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) + var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, lvgl) + return var + + +async def disp_update(disp, config: dict): + if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: + return + with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp: + if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: + lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) + if bg_image := config.get(CONF_DISP_BG_IMAGE): + lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) + + +@automation.register_action( + "lvgl.widget.redraw", + ObjUpdateAction, + cv.Schema( + { + cv.Optional(CONF_ID): cv.use_id(lv_obj_t), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + } + ), +) +async def obj_invalidate_to_code(config, action_id, template_arg, args): + widgets = await get_widgets(config) or [lv_scr_act] + + async def do_invalidate(widget: Widget): + lv_obj.invalidate(widget.obj) + + return await action_to_code(widgets, do_invalidate, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.update", + LvglAction, + DISP_BG_SCHEMA.extend( + { + cv.GenerateID(): cv.use_id(LvglComponent), + } + ).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)), +) +async def lvgl_update_to_code(config, action_id, template_arg, args): + widgets = await get_widgets(config) + w = widgets[0] + disp = f"{w.obj}->get_disp()" + async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: + await disp_update(disp, config) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, w.var) + return var + + +@automation.register_action( + "lvgl.pause", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, + }, +) +async def pause_action_to_code(config, action_id, template_arg, args): + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(where=action_id) + lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "lvgl.resume", + LvglAction, + { + cv.GenerateID(): cv.use_id(LvglComponent), + }, +) +async def resume_action_to_code(config, action_id, template_arg, args): + async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: + lv_add(lvgl_comp.set_paused(False, False)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action("lvgl.widget.disable", ObjUpdateAction, LIST_ACTION_SCHEMA) +async def obj_disable_to_code(config, action_id, template_arg, args): + async def do_disable(widget: Widget): + widget.add_state(LV_STATE.DISABLED) + + return await action_to_code( + await get_widgets(config), do_disable, action_id, template_arg, args + ) + + +@automation.register_action("lvgl.widget.enable", ObjUpdateAction, LIST_ACTION_SCHEMA) +async def obj_enable_to_code(config, action_id, template_arg, args): + async def do_enable(widget: Widget): + widget.clear_state(LV_STATE.DISABLED) + + return await action_to_code( + await get_widgets(config), do_enable, action_id, template_arg, args + ) + + +@automation.register_action("lvgl.widget.hide", ObjUpdateAction, LIST_ACTION_SCHEMA) +async def obj_hide_to_code(config, action_id, template_arg, args): + async def do_hide(widget: Widget): + widget.add_flag("LV_OBJ_FLAG_HIDDEN") + + widgets = [ + widget.outer if widget.outer else widget for widget in await get_widgets(config) + ] + return await action_to_code(widgets, do_hide, action_id, template_arg, args) + + +@automation.register_action("lvgl.widget.show", ObjUpdateAction, LIST_ACTION_SCHEMA) +async def obj_show_to_code(config, action_id, template_arg, args): + async def do_show(widget: Widget): + widget.clear_flag("LV_OBJ_FLAG_HIDDEN") + if widget.move_to_foreground: + lv_obj.move_foreground(widget.obj) + + widgets = [ + widget.outer if widget.outer else widget for widget in await get_widgets(config) + ] + return await action_to_code(widgets, do_show, action_id, template_arg, args) + + +def focused_id(value): + value = cv.use_id(lv_pseudo_button_t)(value) + focused_widgets.add(value) + return value + + +@automation.register_action( + "lvgl.widget.focus", + ObjUpdateAction, + cv.Any( + cv.maybe_simple_value( + { + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), + cv.Required(CONF_ACTION): cv.one_of( + "MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True + ), + cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), + cv.Optional(CONF_FREEZE, default=False): cv.boolean, + }, + key=CONF_ACTION, + ), + cv.maybe_simple_value( + { + cv.Required(CONF_ID): focused_id, + cv.Optional(CONF_FREEZE, default=False): cv.boolean, + cv.Optional(CONF_EDITING, default=False): cv.boolean, + }, + key=CONF_ID, + ), + ), +) +async def widget_focus(config, action_id, template_arg, args): + widget = await get_widgets(config) + if widget: + widget = widget[0] + group = static_cast( + lv_group_t.operator("ptr"), lv_expr.obj_get_group(widget.obj) + ) + elif group := config.get(CONF_GROUP): + group = await get_variable(group) + else: + group = lv_expr.group_get_default() + + async with LambdaContext(parameters=args, where=action_id) as context: + if widget: + lv.group_focus_freeze(group, False) + lv.group_focus_obj(widget.obj) + if config[CONF_EDITING]: + lv.group_set_editing(group, True) + else: + action = config[CONF_ACTION] + lv_comp = await get_variable(config[CONF_LVGL_ID]) + if action == "MARK": + context.add(lv_comp.set_focus_mark(group)) + else: + lv.group_focus_freeze(group, False) + if action == "RESTORE": + context.add(lv_comp.restore_focus_mark(group)) + elif action == "NEXT": + lv.group_focus_next(group) + else: + lv.group_focus_prev(group) + + if config[CONF_FREEZE]: + lv.group_focus_freeze(group, True) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + return var diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py new file mode 100644 index 0000000000..56984405aa --- /dev/null +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import ( + BinarySensor, + binary_sensor_schema, + new_binary_sensor, +) +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import EVENT_ARG, LambdaContext, LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, lv_pseudo_button_t +from ..widgets import Widget, get_widgets, wait_for_widgets + +CONFIG_SCHEMA = ( + binary_sensor_schema(BinarySensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } + ) +) + + +async def to_code(config): + sensor = await new_binary_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + assert isinstance(widget, Widget) + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as pressed_ctx: + pressed_ctx.add(sensor.publish_state(widget.is_pressed())) + async with LvContext(paren) as ctx: + ctx.add(sensor.publish_initial_state(widget.is_pressed())) + ctx.add( + paren.add_event_cb( + widget.obj, + await pressed_ctx.get_lambda(), + LV_EVENT.PRESSING, + LV_EVENT.RELEASED, + ) + ) diff --git a/esphome/components/lvgl/btn.py b/esphome/components/lvgl/btn.py deleted file mode 100644 index 4f5f88d9e6..0000000000 --- a/esphome/components/lvgl/btn.py +++ /dev/null @@ -1,25 +0,0 @@ -from esphome.const import CONF_BUTTON -from esphome.cpp_generator import MockObjClass - -from .defines import CONF_MAIN -from .types import LvBoolean, WidgetType - - -class BtnType(WidgetType): - def __init__(self): - super().__init__(CONF_BUTTON, LvBoolean("lv_btn_t"), (CONF_MAIN,)) - - async def to_code(self, w, config): - return [] - - def obj_creator(self, parent: MockObjClass, config: dict): - """ - LVGL 8 calls buttons `btn` - """ - return f"lv_btn_create({parent})" - - def get_uses(self): - return ("btn",) - - -btn_spec = BtnType() diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index a2b4ac13fb..3db49d26a4 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -4,12 +4,42 @@ Constants already defined in esphome.const are not duplicated here and must be i """ +import logging + from esphome import codegen as cg, config_validation as cv -from esphome.core import ID, Lambda +from esphome.const import CONF_ITEMS +from esphome.core import Lambda +from esphome.cpp_generator import LambdaExpression, MockObj from esphome.cpp_types import uint32 from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor -from .lvcode import ConstantLiteral +from .helpers import requires_component + +LOGGER = logging.getLogger(__name__) +lvgl_ns = cg.esphome_ns.namespace("lvgl") + +lv_defines = {} # Dict of #defines to provide as build flags + + +def add_define(macro, value="1"): + if macro in lv_defines and lv_defines[macro] != value: + LOGGER.error( + "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value + ) + lv_defines[macro] = value + + +def literal(arg): + if isinstance(arg, str): + return MockObj(arg) + return arg + + +def call_lambda(lamb: LambdaExpression): + expr = lamb.content.strip() + if expr.startswith("return") and expr.endswith(";"): + return expr[7:][:-1] + return f"{lamb}()" class LValidator: @@ -18,18 +48,17 @@ class LValidator: has `process()` to convert a value during code generation """ - def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None): + def __init__(self, validator, rtype, retmapper=None, requires=None): self.validator = validator self.rtype = rtype - self.idtype = idtype - self.idexpr = idexpr self.retmapper = retmapper + self.requires = requires def __call__(self, value): + if self.requires: + value = requires_component(self.requires)(value) if isinstance(value, cv.Lambda): return cv.returning_lambda(value) - if self.idtype is not None and isinstance(value, ID): - return cv.use_id(self.idtype)(value) return self.validator(value) async def process(self, value, args=()): @@ -37,10 +66,10 @@ class LValidator: return None if isinstance(value, Lambda): return cg.RawExpression( - f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" + call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) ) - if self.idtype is not None and isinstance(value, ID): - return cg.RawExpression(f"{value}->{self.idexpr}") if self.retmapper is not None: return self.retmapper(value) return cg.safe_exp(value) @@ -68,40 +97,49 @@ class LvConstant(LValidator): return self.prefix + cv.one_of(*choices, upper=True)(value) super().__init__(validator, rtype=uint32) + self.retmapper = self.mapper self.one_of = LValidator(validator, uint32, retmapper=self.mapper) self.several_of = LValidator( cv.ensure_list(self.one_of), uint32, retmapper=self.mapper ) - def mapper(self, value, args=()): - if isinstance(value, list): - value = "|".join(value) - return ConstantLiteral(value) + def mapper(self, value): + if not isinstance(value, list): + value = [value] + return literal( + "|".join( + [ + str(v) if str(v).startswith(self.prefix) else self.prefix + str(v) + for v in value + ] + ).upper() + ) def extend(self, *choices): """ - Extend an LVCconstant with additional choices. + Extend an LVconstant with additional choices. :param choices: The extra choices :return: A new LVConstant instance """ return LvConstant(self.prefix, *(self.choices + choices)) -# Widgets -CONF_LABEL = "label" - # Parts CONF_MAIN = "main" CONF_SCROLLBAR = "scrollbar" CONF_INDICATOR = "indicator" CONF_KNOB = "knob" CONF_SELECTED = "selected" -CONF_ITEMS = "items" CONF_TICKS = "ticks" -CONF_TICK_STYLE = "tick_style" CONF_CURSOR = "cursor" CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder" +# Layout types + +TYPE_FLEX = "flex" +TYPE_GRID = "grid" +TYPE_NONE = "none" + LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ "dejavu_16_persian_hebrew", "simsun_16_cjk", @@ -109,7 +147,7 @@ LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ "unscii_16", ] -LV_EVENT = { +LV_EVENT_MAP = { "PRESS": "PRESSED", "SHORT_CLICK": "SHORT_CLICKED", "LONG_PRESS": "LONG_PRESSED", @@ -123,9 +161,10 @@ LV_EVENT = { "DEFOCUS": "DEFOCUSED", "READY": "READY", "CANCEL": "CANCEL", + "ALL_EVENTS": "ALL", } -LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT) +LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP) LV_ANIM = LvConstant( @@ -147,6 +186,9 @@ LV_ANIM = LvConstant( "OUT_BOTTOM", ) +LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER") +LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF") + LOG_LEVELS = ( "TRACE", "INFO", @@ -280,7 +322,8 @@ OBJ_FLAGS = ( ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") -BTNMATRIX_CTRLS = ( +BUTTONMATRIX_CTRLS = LvConstant( + "LV_BTNMATRIX_CTRL_", "HIDDEN", "NO_REPEAT", "DISABLED", @@ -341,13 +384,13 @@ CONF_ACCEPTED_CHARS = "accepted_chars" CONF_ADJUSTABLE = "adjustable" CONF_ALIGN = "align" CONF_ALIGN_TO = "align_to" -CONF_ANGLE_RANGE = "angle_range" CONF_ANIMATED = "animated" CONF_ANIMATION = "animation" CONF_ANTIALIAS = "antialias" CONF_ARC_LENGTH = "arc_length" CONF_AUTO_START = "auto_start" CONF_BACKGROUND_STYLE = "background_style" +CONF_BUTTON_STYLE = "button_style" CONF_DECIMAL_PLACES = "decimal_places" CONF_COLUMN = "column" CONF_DIGITS = "digits" @@ -359,13 +402,14 @@ CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" CONF_COLOR_DEPTH = "color_depth" -CONF_COLOR_END = "color_end" -CONF_COLOR_START = "color_start" CONF_CONTROL = "control" CONF_DEFAULT = "default" CONF_DEFAULT_FONT = "default_font" +CONF_DEFAULT_GROUP = "default_group" CONF_DIR = "dir" CONF_DISPLAYS = "displays" +CONF_EDITING = "editing" +CONF_ENCODERS = "encoders" CONF_END_ANGLE = "end_angle" CONF_END_VALUE = "end_value" CONF_ENTER_BUTTON = "enter_button" @@ -376,7 +420,9 @@ CONF_FLEX_ALIGN_MAIN = "flex_align_main" CONF_FLEX_ALIGN_CROSS = "flex_align_cross" CONF_FLEX_ALIGN_TRACK = "flex_align_track" CONF_FLEX_GROW = "flex_grow" +CONF_FREEZE = "freeze" CONF_FULL_REFRESH = "full_refresh" +CONF_GRADIENTS = "gradients" CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos" CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos" CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span" @@ -389,9 +435,8 @@ CONF_GRID_ROW_ALIGN = "grid_row_align" CONF_GRID_ROWS = "grid_rows" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" -CONF_INDICATORS = "indicators" +CONF_INITIAL_FOCUS = "initial_focus" CONF_KEY_CODE = "key_code" -CONF_LABEL_GAP = "label_gap" CONF_LAYOUT = "layout" CONF_LEFT_BUTTON = "left_button" CONF_LINE_WIDTH = "line_width" @@ -400,15 +445,17 @@ CONF_LONG_PRESS_TIME = "long_press_time" CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" CONF_LONG_MODE = "long_mode" -CONF_MAJOR = "major" CONF_MSGBOXES = "msgboxes" CONF_OBJ = "obj" CONF_OFFSET_X = "offset_x" CONF_OFFSET_Y = "offset_y" +CONF_ONE_CHECKED = "one_checked" CONF_ONE_LINE = "one_line" CONF_ON_SELECT = "on_select" -CONF_ONE_CHECKED = "one_checked" CONF_NEXT = "next" +CONF_PAD_ROW = "pad_row" +CONF_PAD_COLUMN = "pad_column" +CONF_PAGE = "page" CONF_PAGE_WRAP = "page_wrap" CONF_PASSWORD_MODE = "password_mode" CONF_PIVOT_X = "pivot_x" @@ -417,13 +464,11 @@ CONF_PLACEHOLDER_TEXT = "placeholder_text" CONF_POINTS = "points" CONF_PREVIOUS = "previous" CONF_REPEAT_COUNT = "repeat_count" -CONF_R_MOD = "r_mod" CONF_RECOLOR = "recolor" CONF_RIGHT_BUTTON = "right_button" CONF_ROLLOVER = "rollover" CONF_ROOT_BACK_BTN = "root_back_btn" CONF_ROWS = "rows" -CONF_SCALES = "scales" CONF_SCALE_LINES = "scale_lines" CONF_SCROLLBAR_MODE = "scrollbar_mode" CONF_SELECTED_INDEX = "selected_index" @@ -433,14 +478,15 @@ CONF_SRC = "src" CONF_START_ANGLE = "start_angle" CONF_START_VALUE = "start_value" CONF_STATES = "states" -CONF_STRIDE = "stride" CONF_STYLE = "style" +CONF_STYLES = "styles" +CONF_STYLE_DEFINITIONS = "style_definitions" CONF_STYLE_ID = "style_id" CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" -CONF_TEXT = "text" +CONF_TIME_FORMAT = "time_format" CONF_TILE = "tile" CONF_TILE_ID = "tile_id" CONF_TILES = "tiles" @@ -449,6 +495,7 @@ CONF_TOP_LAYER = "top_layer" CONF_TOUCHSCREENS = "touchscreens" CONF_TRANSPARENCY_KEY = "transparency_key" CONF_THEME = "theme" +CONF_UPDATE_ON_RELEASE = "update_on_release" CONF_VISIBLE_ROW_COUNT = "visible_row_count" CONF_WIDGET = "widget" CONF_WIDGETS = "widgets" @@ -479,4 +526,10 @@ DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" def join_enums(enums, prefix=""): - return ConstantLiteral("|".join(f"(int){prefix}{e.upper()}" for e in enums)) + enums = list(enums) + enums.sort() + # If a prefix is provided, prepend each constant with the prefix, and assume that all the constants are within the + # same namespace, otherwise cast to int to avoid triggering warnings about mixing enum types. + if prefix: + return literal("|".join(f"{prefix}{e.upper()}" for e in enums)) + return literal("|".join(f"(int){e.upper()}" for e in enums)) diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py new file mode 100644 index 0000000000..81bcda95b4 --- /dev/null +++ b/esphome/components/lvgl/encoders.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import BinarySensor +from esphome.components.rotary_encoder.sensor import RotaryEncoderSensor +import esphome.config_validation as cv +from esphome.const import CONF_GROUP, CONF_ID, CONF_SENSOR + +from .defines import ( + CONF_DEFAULT_GROUP, + CONF_ENCODERS, + CONF_ENTER_BUTTON, + CONF_INITIAL_FOCUS, + CONF_LEFT_BUTTON, + CONF_LONG_PRESS_REPEAT_TIME, + CONF_LONG_PRESS_TIME, + CONF_RIGHT_BUTTON, +) +from .helpers import lvgl_components_required, requires_component +from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable +from .schemas import ENCODER_SCHEMA +from .types import lv_group_t, lv_indev_type_t + +ENCODERS_CONFIG = cv.ensure_list( + ENCODER_SCHEMA.extend( + { + cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_SENSOR): cv.Any( + cv.All( + cv.use_id(RotaryEncoderSensor), requires_component("rotary_encoder") + ), + cv.Schema( + { + cv.Required(CONF_LEFT_BUTTON): cv.use_id(BinarySensor), + cv.Required(CONF_RIGHT_BUTTON): cv.use_id(BinarySensor), + } + ), + ), + } + ) +) + + +async def encoders_to_code(var, config): + default_group = lv_Pvariable(lv_group_t, config[CONF_DEFAULT_GROUP]) + lv_assign(default_group, lv_expr.group_create()) + lv.group_set_default(default_group) + for enc_conf in config[CONF_ENCODERS]: + lvgl_components_required.add("KEY_LISTENER") + lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds + lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds + listener = cg.new_Pvariable( + enc_conf[CONF_ID], lv_indev_type_t.LV_INDEV_TYPE_ENCODER, lpt, lprt + ) + await cg.register_parented(listener, var) + if sensor_config := enc_conf.get(CONF_SENSOR): + if isinstance(sensor_config, dict): + b_sensor = await cg.get_variable(sensor_config[CONF_LEFT_BUTTON]) + cg.add(listener.set_left_button(b_sensor)) + b_sensor = await cg.get_variable(sensor_config[CONF_RIGHT_BUTTON]) + cg.add(listener.set_right_button(b_sensor)) + else: + sensor_config = await cg.get_variable(sensor_config) + lv_add(listener.set_sensor(sensor_config)) + b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON]) + cg.add(listener.set_enter_button(b_sensor)) + if group := enc_conf.get(CONF_GROUP): + group = lv_Pvariable(lv_group_t, group) + lv_assign(group, lv_expr.group_create()) + else: + group = default_group + lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + + +async def initial_focus_to_code(config): + for enc_conf in config[CONF_ENCODERS]: + if default_focus := enc_conf.get(CONF_INITIAL_FOCUS): + obj = await cg.get_variable(default_focus) + lv.group_focus_obj(obj) diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py new file mode 100644 index 0000000000..bc89470d47 --- /dev/null +++ b/esphome/components/lvgl/gradient.py @@ -0,0 +1,61 @@ +from esphome import config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_COLOR, + CONF_DIRECTION, + CONF_DITHER, + CONF_ID, + CONF_POSITION, +) +from esphome.cpp_generator import MockObj + +from .defines import CONF_GRADIENTS, LV_DITHER, LV_GRAD_DIR, add_define +from .lv_validation import lv_color, lv_fraction +from .lvcode import lv_assign +from .types import lv_gradient_t + +CONF_STOPS = "stops" + + +def min_stops(value): + if len(value) < 2: + raise cv.Invalid("Must have at least 2 stops") + return value + + +GRADIENT_SCHEMA = cv.ensure_list( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t), + cv.Optional(CONF_DIRECTION, default="NONE"): LV_GRAD_DIR.one_of, + cv.Optional(CONF_DITHER, default="NONE"): LV_DITHER.one_of, + cv.Required(CONF_STOPS): cv.All( + [ + cv.Schema( + { + cv.Required(CONF_COLOR): lv_color, + cv.Required(CONF_POSITION): lv_fraction, + } + ) + ], + min_stops, + ), + } + ) +) + + +async def gradients_to_code(config): + max_stops = 2 + for gradient in config.get(CONF_GRADIENTS, ()): + var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->") + max_stops = max(max_stops, len(gradient[CONF_STOPS])) + lv_assign(var.dir, await LV_GRAD_DIR.process(gradient[CONF_DIRECTION])) + lv_assign(var.dither, await LV_DITHER.process(gradient[CONF_DITHER])) + lv_assign(var.stops_count, len(gradient[CONF_STOPS])) + for index, stop in enumerate(gradient[CONF_STOPS]): + lv_assign(var.stops[index].color, await lv_color.process(stop[CONF_COLOR])) + lv_assign( + var.stops[index].frac, await lv_fraction.process(stop[CONF_POSITION]) + ) + add_define("LV_GRADIENT_MAX_STOPS", max_stops) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index d67739155c..e04a0105d5 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -1,10 +1,7 @@ import re from esphome import config_validation as cv -from esphome.config import Config from esphome.const import CONF_ARGS, CONF_FORMAT -from esphome.core import CORE, ID -from esphome.yaml_util import ESPHomeDataBase lv_uses = { "USER_DATA", @@ -44,23 +41,6 @@ def validate_printf(value): return value -def get_line_marks(value) -> list: - """ - If possible, return a preprocessor directive to identify the line number where the given id was defined. - :param id: The id in question - :return: A list containing zero or more line directives - """ - path = None - if isinstance(value, ESPHomeDataBase): - path = value.esp_range - elif isinstance(value, ID) and isinstance(CORE.config, Config): - path = CORE.config.get_path_for_id(value)[:-1] - path = CORE.config.get_deepest_document_range_for_path(path) - if path is None: - return [] - return [path.start_mark.as_line_directive] - - def requires_component(comp): def validator(value): lvgl_components_required.add(comp) diff --git a/esphome/components/lvgl/light/__init__.py b/esphome/components/lvgl/light/__init__.py new file mode 100644 index 0000000000..a0eeded349 --- /dev/null +++ b/esphome/components/lvgl/light/__init__.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import light +from esphome.components.light import LightOutput +import esphome.config_validation as cv +from esphome.const import CONF_GAMMA_CORRECT, CONF_LED, CONF_OUTPUT_ID + +from ..defines import CONF_LVGL_ID +from ..lvcode import LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LvType, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +lv_led_t = LvType("lv_led_t") +LVLight = lvgl_ns.class_("LVLight", LightOutput) +CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( + { + cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float, + cv.Required(CONF_LED): cv.use_id(lv_led_t), + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight), + } +).extend(LVGL_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await light.register_light(var, config) + + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_LED) + widget = widget[0] + await wait_for_widgets() + async with LvContext(paren) as ctx: + ctx.add(var.set_obj(widget.obj)) diff --git a/esphome/components/lvgl/light/lvgl_light.h b/esphome/components/lvgl/light/lvgl_light.h new file mode 100644 index 0000000000..50ae4c5327 --- /dev/null +++ b/esphome/components/lvgl/light/lvgl_light.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/light_output.h" +#include "../lvgl_esphome.h" + +namespace esphome { +namespace lvgl { + +class LVLight : public light::LightOutput { + public: + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + void write_state(light::LightState *state) override { + float red, green, blue; + state->current_values_as_rgb(&red, &green, &blue, false); + auto color = lv_color_make(red * 255, green * 255, blue * 255); + if (this->obj_ != nullptr) { + this->set_value_(color); + } else { + this->initial_value_ = color; + } + } + + void set_obj(lv_obj_t *obj) { + this->obj_ = obj; + if (this->initial_value_) { + lv_led_set_color(obj, this->initial_value_.value()); + lv_led_on(obj); + this->initial_value_.reset(); + } + } + + protected: + void set_value_(lv_color_t value) { + lv_led_set_color(this->obj_, value); + lv_led_on(this->obj_); + lv_event_send(this->obj_, lv_api_event, nullptr); + } + lv_obj_t *obj_{}; + optional initial_value_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 533dc582f0..8593deb869 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,34 +1,38 @@ +from typing import Union + import esphome.codegen as cg -from esphome.components.binary_sensor import BinarySensor -from esphome.components.color import ColorStruct +from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw from esphome.components.font import Font -from esphome.components.sensor import Sensor -from esphome.components.text_sensor import TextSensor +from esphome.components.image import Image_ import esphome.config_validation as cv -from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT -from esphome.core import HexInt +from esphome.const import ( + CONF_ARGS, + CONF_COLOR, + CONF_FORMAT, + CONF_ID, + CONF_TIME, + CONF_VALUE, +) +from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj -from esphome.cpp_types import uint32 +from esphome.cpp_types import ESPTime, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from . import types as ty -from .defines import LV_FONTS, LValidator, LvConstant -from .helpers import ( - esphome_fonts_used, - lv_fonts_used, - lvgl_components_required, - requires_component, +from .defines import ( + CONF_END_VALUE, + CONF_START_VALUE, + CONF_TIME_FORMAT, + LV_FONTS, + LValidator, + LvConstant, + call_lambda, + literal, ) -from .lvcode import ConstantLiteral, lv_expr -from .types import lv_font_t - - -def literal_mapper(value, args=()): - if isinstance(value, str): - return ConstantLiteral(value) - return value - +from .helpers import esphome_fonts_used, lv_fonts_used, requires_component +from .lvcode import lv_expr +from .types import lv_font_t, lv_gradient_t, lv_img_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -43,27 +47,38 @@ def opacity_validator(value): return value -opacity = LValidator(opacity_validator, uint32, retmapper=literal_mapper) +opacity = LValidator(opacity_validator, uint32, retmapper=literal) @schema_extractor("one_of") def color(value): if value == SCHEMA_EXTRACT: return ["hex color value", "color ID"] - if isinstance(value, int): - return value - return cv.use_id(ColorStruct)(value) + return cv.Any(cv.int_, cv.use_id(ColorStruct))(value) def color_retmapper(value): if isinstance(value, cv.Lambda): return cv.returning_lambda(value) if isinstance(value, int): - hexval = HexInt(value) - return lv_expr.color_hex(hexval) - # Must be an id - lvgl_components_required.add(CONF_COLOR) - return lv_expr.color_from(MockObj(value)) + return literal( + f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})" + ) + if isinstance(value, ID): + cval = [x for x in CORE.config[CONF_COLOR] if x[CONF_ID] == value][0] + if CONF_HEX in cval: + r, g, b = cval[CONF_HEX] + else: + r, g, b, _ = from_rgbw(cval) + return literal(f"lv_color_make({r}, {g}, {b})") + assert False + + +def option_string(value): + value = cv.string(value).strip() + if value.find("\n") != -1: + raise cv.Invalid("Options strings must not contain newlines") + return value lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) @@ -73,15 +88,13 @@ def pixels_or_percent_validator(value): """A length in one axis - either a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: return ["pixels", "..%"] + value = cv.Any(cv.int_, cv.percentage)(value) if isinstance(value, int): - return cv.int_(value) - # Will throw an exception if not a percentage. - return f"lv_pct({int(cv.percentage(value) * 100)})" + return value + return f"lv_pct({int(value * 100)})" -pixels_or_percent = LValidator( - pixels_or_percent_validator, uint32, retmapper=literal_mapper -) +pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal) def zoom(value): @@ -102,26 +115,32 @@ def angle(value): def size_validator(value): """A size in one axis - one of "size_content", a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: - return ["size_content", "pixels", "..%"] + return ["SIZE_CONTENT", "number of pixels", "percentage"] if isinstance(value, str) and value.lower().endswith("px"): value = cv.int_(value[:-2]) if isinstance(value, str) and not value.endswith("%"): if value.upper() == "SIZE_CONTENT": return "LV_SIZE_CONTENT" - raise cv.Invalid("must be 'size_content', a pixel position or a percentage") - if isinstance(value, int): - return cv.int_(value) - # Will throw an exception if not a percentage. - return f"lv_pct({int(cv.percentage(value) * 100)})" + raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)") + return pixels_or_percent_validator(value) -size = LValidator(size_validator, uint32, retmapper=literal_mapper) +size = LValidator(size_validator, uint32, retmapper=literal) + + +def pixels_validator(value): + if isinstance(value, str) and value.lower().endswith("px"): + return cv.int_(value[:-2]) + return cv.int_(value) + + +pixels = LValidator(pixels_validator, uint32, retmapper=literal) radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") @schema_extractor("one_of") -def radius_validator(value): +def fraction_validator(value): if value == SCHEMA_EXTRACT: return radius_consts.choices value = cv.Any(size, cv.percentage, radius_consts.one_of)(value) @@ -130,22 +149,42 @@ def radius_validator(value): return value +lv_fraction = LValidator(fraction_validator, uint32, retmapper=literal) + + def id_name(value): if value == SCHEMA_EXTRACT: return "id" return cv.validate_id_name(value) -radius = LValidator(radius_validator, uint32, retmapper=literal_mapper) - - def stop_value(value): return cv.int_range(0, 255)(value) -lv_bool = LValidator( - cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal_mapper +lv_images_used = set() + + +def image_validator(value): + value = requires_component("image")(value) + value = cv.use_id(Image_)(value) + lv_images_used.add(value) + return value + + +lv_image = LValidator( + image_validator, + lv_img_t, + retmapper=lambda x: lv_expr.img_from(MockObj(x)), + requires="image", ) +lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) + + +def lv_pct(value: Union[int, float]): + if isinstance(value, float): + value = int(value * 100) + return literal(f"lv_pct({value})") def lvms_validator_(value): @@ -155,39 +194,75 @@ def lvms_validator_(value): lv_milliseconds = LValidator( - lvms_validator_, - cg.int32, - retmapper=lambda x: x.total_milliseconds, + lvms_validator_, cg.int32, retmapper=lambda x: x.total_milliseconds ) class TextValidator(LValidator): def __init__(self): - super().__init__( - cv.string, - cg.const_char_ptr, - TextSensor, - "get_state().c_str()", - lambda s: cg.safe_exp(f"{s}"), - ) + super().__init__(cv.string, cg.std_string, lambda s: cg.safe_exp(f"{s}")) def __call__(self, value): - if isinstance(value, dict): + if isinstance(value, dict) and CONF_FORMAT in value: return value return super().__call__(value) async def process(self, value, args=()): if isinstance(value, dict): - args = [str(x) for x in value[CONF_ARGS]] - arg_expr = cg.RawExpression(",".join(args)) - format_str = cpp_string_escape(value[CONF_FORMAT]) - return f"str_sprintf({format_str}, {arg_expr}).c_str()" + if format_str := value.get(CONF_FORMAT): + args = [str(x) for x in value[CONF_ARGS]] + arg_expr = cg.RawExpression(",".join(args)) + format_str = cpp_string_escape(format_str) + return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + if time_format := value.get(CONF_TIME_FORMAT): + source = value[CONF_TIME] + if isinstance(source, Lambda): + time_format = cpp_string_escape(time_format) + return cg.RawExpression( + call_lambda( + await cg.process_lambda(source, args, return_type=ESPTime) + ) + + f".strftime({time_format}).c_str()" + ) + # must be an ID + source = await cg.get_variable(source) + return source.now().strftime(time_format).c_str() + if isinstance(value, Lambda): + value = call_lambda( + await cg.process_lambda(value, args, return_type=self.rtype) + ) + + # Was the lambda call reduced to a string? + if value.endswith("c_str()") or ( + value.endswith('"') and value.startswith('"') + ): + pass + else: + # Either a std::string or a lambda call returning that. We need const char* + value = f"({value}).c_str()" + return cg.RawExpression(value) return await super().process(value, args) lv_text = TextValidator() -lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") -lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") +lv_float = LValidator(cv.float_, cg.float_) +lv_int = LValidator(cv.int_, cg.int_) +lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) + + +def gradient_mapper(value): + return MockObj(value) + + +def gradient_validator(value): + return cv.use_id(lv_gradient_t)(value) + + +lv_gradient = LValidator( + validator=gradient_validator, + rtype=lv_gradient_t, + retmapper=gradient_mapper, +) def is_lv_font(font): @@ -214,8 +289,33 @@ class LvFont(LValidator): async def process(self, value, args=()): if is_lv_font(value): - return ConstantLiteral(f"&lv_font_{value}") - return ConstantLiteral(f"{value}_engine->get_lv_font()") + return literal(f"&lv_font_{value}") + return literal(f"{value}_engine->get_lv_font()") lv_font = LvFont() + + +def animated(value): + if isinstance(value, bool): + value = "ON" if value else "OFF" + return LvConstant("LV_ANIM_", "OFF", "ON").one_of(value) + + +def key_code(value): + value = cv.Any(cv.All(cv.string_strict, cv.Length(min=1, max=1)), cv.uint8_t)(value) + if isinstance(value, str): + return ord(value[0]) + return value + + +async def get_end_value(config): + return await lv_int.process(config.get(CONF_END_VALUE)) + + +async def get_start_value(config): + if CONF_START_VALUE in config: + value = config[CONF_START_VALUE] + else: + value = config.get(CONF_VALUE) + return await lv_int.process(value) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 13b4862b4d..3a080d63e9 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -1,15 +1,15 @@ import abc -import logging from typing import Union from esphome import codegen as cg -from esphome.core import ID, Lambda +from esphome.config import Config +from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import ( AssignmentExpression, CallExpression, Expression, + ExpressionStatement, LambdaExpression, - Literal, MockObj, RawExpression, RawStatement, @@ -18,10 +18,51 @@ from esphome.cpp_generator import ( VariableDeclarationExpression, statement, ) +from esphome.yaml_util import ESPHomeDataBase -from .helpers import get_line_marks +from .defines import literal, lvgl_ns -_LOGGER = logging.getLogger(__name__) +LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp() + +# Argument tuple for use in lambdas +LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) +LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)] +lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr") +EVENT_ARG = [(lv_event_t_ptr, "event")] +# Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction; +# UPDATE_EVENT is fired when an entity is programmatically updated locally. +# VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction. +API_EVENT = literal("lvgl::lv_api_event") +UPDATE_EVENT = literal("lvgl::lv_update_event") + + +def get_line_marks(value) -> list: + """ + If possible, return a preprocessor directive to identify the line number where the given id was defined. + :param value: The id or other token to get the line number for + :return: A list containing zero or more line directives + """ + path = None + if isinstance(value, ESPHomeDataBase): + path = value.esp_range + elif isinstance(value, ID) and isinstance(CORE.config, Config): + path = CORE.config.get_path_for_id(value)[:-1] + path = CORE.config.get_deepest_document_range_for_path(path) + if path is None: + return [] + return [path.start_mark.as_line_directive] + + +class IndentedStatement(Statement): + def __init__(self, stmt: Statement, indent: int): + self.statement = stmt + self.indent = indent + + def __str__(self): + result = " " * self.indent * 4 + str(self.statement).strip() + if not isinstance(self.statement, RawStatement): + result += ";" + return result class CodeContext(abc.ABC): @@ -37,6 +78,16 @@ class CodeContext(abc.ABC): def add(self, expression: Union[Expression, Statement]): pass + @staticmethod + def start_block(): + CodeContext.append(RawStatement("{")) + CodeContext.code_context.indent() + + @staticmethod + def end_block(): + CodeContext.code_context.detent() + CodeContext.append(RawStatement("}")) + @staticmethod def append(expression: Union[Expression, Statement]): if CodeContext.code_context is not None: @@ -45,14 +96,25 @@ class CodeContext(abc.ABC): def __init__(self): self.previous: Union[CodeContext | None] = None + self.indent_level = 0 - def __enter__(self): + async def __aenter__(self): self.previous = CodeContext.code_context CodeContext.code_context = self + return self - def __exit__(self, *args): + async def __aexit__(self, *args): CodeContext.code_context = self.previous + def indent(self): + self.indent_level += 1 + + def detent(self): + self.indent_level -= 1 + + def indented_statement(self, stmt): + return IndentedStatement(stmt, self.indent_level) + class MainContext(CodeContext): """ @@ -60,42 +122,7 @@ class MainContext(CodeContext): """ def add(self, expression: Union[Expression, Statement]): - return cg.add(expression) - - -class LvContext(CodeContext): - """ - Code generation into the LVGL initialisation code (called in `setup()`) - """ - - lv_init_code: list["Statement"] = [] - - @staticmethod - def lv_add(expression: Union[Expression, Statement]): - if isinstance(expression, Expression): - expression = statement(expression) - if not isinstance(expression, Statement): - raise ValueError( - f"Add '{expression}' must be expression or statement, not {type(expression)}" - ) - LvContext.lv_init_code.append(expression) - _LOGGER.debug("LV Adding: %s", expression) - return expression - - @staticmethod - def get_code(): - code = [] - for exp in LvContext.lv_init_code: - text = str(statement(exp)) - text = text.rstrip() - code.append(text) - return "\n".join(code) + "\n\n" - - def add(self, expression: Union[Expression, Statement]): - return LvContext.lv_add(expression) - - def set_style(self, prop): - return MockObj("lv_set_style_{prop}", "") + return cg.add(self.indented_statement(expression)) class LambdaContext(CodeContext): @@ -105,29 +132,72 @@ class LambdaContext(CodeContext): def __init__( self, - parameters: list[tuple[SafeExpType, str]], - return_type: SafeExpType = None, + parameters: list[tuple[SafeExpType, str]] = None, + return_type: SafeExpType = cg.void, + capture: str = "", + where=None, ): super().__init__() self.code_list: list[Statement] = [] - self.parameters = parameters + self.parameters = parameters or [] self.return_type = return_type + self.capture = capture + self.where = where def add(self, expression: Union[Expression, Statement]): - self.code_list.append(expression) + self.code_list.append(self.indented_statement(expression)) return expression - async def code(self) -> LambdaExpression: + async def get_lambda(self) -> LambdaExpression: + code_text = self.get_code() + return await cg.process_lambda( + Lambda("\n".join(code_text) + "\n"), + self.parameters, + capture=self.capture, + return_type=self.return_type, + ) + + def get_code(self): code_text = [] for exp in self.code_list: text = str(statement(exp)) text = text.rstrip() code_text.append(text) - return await cg.process_lambda( - Lambda("\n".join(code_text) + "\n\n"), - self.parameters, - return_type=self.return_type, - ) + return code_text + + async def __aenter__(self): + await super().__aenter__() + add_line_marks(self.where) + return self + + +class LvContext(LambdaContext): + """ + Code generation into the LVGL initialisation code (called in `setup()`) + """ + + added_lambda_count = 0 + + def __init__(self, lv_component, args=None): + self.args = args or LVGL_COMP_ARG + super().__init__(parameters=self.args) + self.lv_component = lv_component + + async def add_init_lambda(self): + if self.code_list: + cg.add(self.lv_component.add_init_lambda(await self.get_lambda())) + LvContext.added_lambda_count += 1 + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await super().__aexit__(exc_type, exc_val, exc_tb) + await self.add_init_lambda() + + def add(self, expression: Union[Expression, Statement]): + self.code_list.append(self.indented_statement(expression)) + return expression + + def __call__(self, *args): + return self.add(*args) class LocalVariable(MockObj): @@ -135,23 +205,23 @@ class LocalVariable(MockObj): Create a local variable and enclose the code using it within a block. """ - def __init__(self, name, type, modifier=None, rhs=None): - base = ID(name, True, type) + def __init__(self, name, type, rhs=None, modifier="*"): + base = ID(name + "_VAR_", True, type) super().__init__(base, "") self.modifier = modifier self.rhs = rhs def __enter__(self): - CodeContext.append(RawStatement("{")) + CodeContext.start_block() CodeContext.append( VariableDeclarationExpression(self.base.type, self.modifier, self.base.id) ) if self.rhs is not None: CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs)) - return self.base + return MockObj(self.base) def __exit__(self, *args): - CodeContext.append(RawStatement("}")) + CodeContext.end_block() class MockLv: @@ -186,14 +256,32 @@ class MockLv: self.append(result) return result - def cond_if(self, expression: Expression): - CodeContext.append(RawExpression(f"if({expression}) {{")) - def cond_else(self): - CodeContext.append(RawExpression("} else {")) +class LvConditional: + def __init__(self, condition): + self.condition = condition - def cond_endif(self): - CodeContext.append(RawExpression("}")) + def __enter__(self): + if self.condition is not None: + CodeContext.append(RawStatement(f"if ({self.condition}) {{")) + CodeContext.code_context.indent() + return self + + def __exit__(self, *args): + if self.condition is not None: + CodeContext.code_context.detent() + CodeContext.append(RawStatement("}")) + + def else_(self): + assert self.condition is not None + CodeContext.code_context.detent() + CodeContext.append(RawStatement("} else {")) + CodeContext.code_context.indent() + + +class ReturnStatement(ExpressionStatement): + def __str__(self): + return f"return {self.expression};" class LvExpr(MockLv): @@ -204,34 +292,66 @@ class LvExpr(MockLv): pass +def static_cast(type, value): + return literal(f"static_cast<{type}>({value})") + + # Top level mock for generic lv_ calls to be recorded lv = MockLv("lv_") # Just generate an expression lv_expr = LvExpr("lv_") # Mock for lv_obj_ calls lv_obj = MockLv("lv_obj_") +# Operations on the LVGL component +lvgl_comp = MockObj(LVGL_COMP, "->") -# equivalent to cg.add() for the lvgl init context +# equivalent to cg.add() for the current code context def lv_add(expression: Union[Expression, Statement]): return CodeContext.append(expression) def add_line_marks(where): + """ + Add line marks for the current code context + :param where: An object to identify the source of the line marks + :return: + """ for mark in get_line_marks(where): lv_add(cg.RawStatement(mark)) def lv_assign(target, expression): - lv_add(RawExpression(f"{target} = {expression}")) + lv_add(AssignmentExpression("", "", target, expression)) -class ConstantLiteral(Literal): - __slots__ = ("constant",) +def lv_Pvariable(type, name): + """ + Create but do not initialise a pointer variable + :param type: Type of the variable target + :param name: name of the variable, or an ID + :return: A MockObj of the variable + """ + if isinstance(name, str): + name = ID(name, True, type) + decl = VariableDeclarationExpression(type, "*", name) + CORE.add_global(decl) + var = MockObj(name, "->") + CORE.register_variable(name, var) + return var - def __init__(self, constant: str): - super().__init__() - self.constant = constant - def __str__(self): - return self.constant +def lv_variable(type, name): + """ + Create but do not initialise a variable + :param type: Type of the variable target + :param name: name of the variable, or an ID + :return: A MockObj of the variable + """ + if isinstance(name, str): + name = ID(name, True, type) + decl = VariableDeclarationExpression(type, "", name) + CORE.add_global(decl) + var = MockObj(name, ".") + CORE.register_variable(name, var) + return var diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 74a1b0e7af..89c9828740 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -9,8 +9,130 @@ namespace esphome { namespace lvgl { static const char *const TAG = "lvgl"; -lv_event_code_t lv_custom_event; // NOLINT +#if LV_USE_LOG +static void log_cb(const char *buf) { + esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); +} +#endif // LV_USE_LOG + +static const char *const EVENT_NAMES[] = { + "NONE", + "PRESSED", + "PRESSING", + "PRESS_LOST", + "SHORT_CLICKED", + "LONG_PRESSED", + "LONG_PRESSED_REPEAT", + "CLICKED", + "RELEASED", + "SCROLL_BEGIN", + "SCROLL_END", + "SCROLL", + "GESTURE", + "KEY", + "FOCUSED", + "DEFOCUSED", + "LEAVE", + "HIT_TEST", + "COVER_CHECK", + "REFR_EXT_DRAW_SIZE", + "DRAW_MAIN_BEGIN", + "DRAW_MAIN", + "DRAW_MAIN_END", + "DRAW_POST_BEGIN", + "DRAW_POST", + "DRAW_POST_END", + "DRAW_PART_BEGIN", + "DRAW_PART_END", + "VALUE_CHANGED", + "INSERT", + "REFRESH", + "READY", + "CANCEL", + "DELETE", + "CHILD_CHANGED", + "CHILD_CREATED", + "CHILD_DELETED", + "SCREEN_UNLOAD_START", + "SCREEN_LOAD_START", + "SCREEN_LOADED", + "SCREEN_UNLOADED", + "SIZE_CHANGED", + "STYLE_CHANGED", + "LAYOUT_CHANGED", + "GET_SELF_SIZE", +}; + +std::string lv_event_code_name_for(uint8_t event_code) { + if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) { + return EVENT_NAMES[event_code]; + } + return str_sprintf("%2d", event_code); +} +static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { + // make sure all coordinates are even + if (area->x1 & 1) + area->x1--; + if (!(area->x2 & 1)) + area->x2++; + if (area->y1 & 1) + area->y1--; + if (!(area->y2 & 1)) + area->y2++; +} + +lv_event_code_t lv_api_event; // NOLINT +lv_event_code_t lv_update_event; // NOLINT void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); } +void LvglComponent::set_paused(bool paused, bool show_snow) { + this->paused_ = paused; + this->show_snow_ = show_snow; + this->snow_line_ = 0; + if (!paused && lv_scr_act() != nullptr) { + lv_disp_trig_activity(this->disp_); // resets the inactivity time + lv_obj_invalidate(lv_scr_act()); + } +} +void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { + lv_obj_add_event_cb(obj, callback, event, this); +} +void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, + lv_event_code_t event2) { + this->add_event_cb(obj, callback, event1); + this->add_event_cb(obj, callback, event2); +} +void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, + lv_event_code_t event2, lv_event_code_t event3) { + this->add_event_cb(obj, callback, event1); + this->add_event_cb(obj, callback, event2); + this->add_event_cb(obj, callback, event3); +} +void LvglComponent::add_page(LvPageType *page) { + this->pages_.push_back(page); + page->setup(this->pages_.size() - 1); +} +void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { + if (index >= this->pages_.size()) + return; + this->current_page_ = index; + lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false); +} +void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) { + if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_)) + return; + do { + this->current_page_ = (this->current_page_ + 1) % this->pages_.size(); + } while (this->pages_[this->current_page_]->skip); // skip empty pages() + this->show_page(this->current_page_, anim, time); +} +void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { + if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_)) + return; + do { + this->current_page_ = (this->current_page_ + this->pages_.size() - 1) % this->pages_.size(); + } while (this->pages_[this->current_page_]->skip); // skip empty pages() + this->show_page(this->current_page_, anim, time); +} void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { for (auto *display : this->displays_) { display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr, @@ -19,12 +141,144 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { } void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - auto now = millis(); - this->draw_buffer_(area, (const uint8_t *) color_p); - ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), - lv_area_get_height(area), (int) (millis() - now)); + if (!this->paused_) { + auto now = millis(); + this->draw_buffer_(area, (const uint8_t *) color_p); + ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), + lv_area_get_height(area), (int) (millis() - now)); + } lv_disp_flush_ready(disp_drv); } +IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue timeout) : timeout_(std::move(timeout)) { + parent->add_on_idle_callback([this](uint32_t idle_time) { + if (!this->is_idle_ && idle_time > this->timeout_.value()) { + this->is_idle_ = true; + this->trigger(); + } else if (this->is_idle_ && idle_time < this->timeout_.value()) { + this->is_idle_ = false; + } + }); +} + +#ifdef USE_LVGL_TOUCHSCREEN +LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { + lv_indev_drv_init(&this->drv_); + this->drv_.long_press_repeat_time = long_press_repeat_time; + this->drv_.long_press_time = long_press_time; + this->drv_.type = LV_INDEV_TYPE_POINTER; + this->drv_.user_data = this; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + if (l->touch_pressed_) { + data->point.x = l->touch_point_.x; + data->point.y = l->touch_point_.y; + data->state = LV_INDEV_STATE_PRESSED; + } else { + data->state = LV_INDEV_STATE_RELEASED; + } + }; +} +void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { + this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); + if (this->touch_pressed_) + this->touch_point_ = tpoints[0]; +} +#endif // USE_LVGL_TOUCHSCREEN + +#ifdef USE_LVGL_KEY_LISTENER +LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt) { + lv_indev_drv_init(&this->drv_); + this->drv_.type = type; + this->drv_.user_data = this; + this->drv_.long_press_time = lpt; + this->drv_.long_press_repeat_time = lprt; + this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { + auto *l = static_cast(d->user_data); + data->state = l->pressed_ ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + data->key = l->key_; + data->enc_diff = (int16_t) (l->count_ - l->last_count_); + l->last_count_ = l->count_; + data->continue_reading = false; + }; +} +#endif // USE_LVGL_KEY_LISTENER + +#ifdef USE_LVGL_BUTTONMATRIX +void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) { + LvCompound::set_obj(lv_obj); + lv_obj_add_event_cb( + lv_obj, + [](lv_event_t *event) { + auto *self = static_cast(event->user_data); + if (self->key_callback_.size() == 0) + return; + auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); + if (key_idx == LV_BTNMATRIX_BTN_NONE) + return; + if (self->key_map_.count(key_idx) != 0) { + self->send_key_(self->key_map_[key_idx]); + return; + } + const auto *str = lv_btnmatrix_get_btn_text(self->obj, key_idx); + auto len = strlen(str); + while (len--) + self->send_key_(*str++); + }, + LV_EVENT_PRESSED, this); +} +#endif // USE_LVGL_BUTTONMATRIX + +#ifdef USE_LVGL_KEYBOARD +static const char *const KB_SPECIAL_KEYS[] = { + "abc", "ABC", "1#", + // maybe add other special keys here +}; + +void LvKeyboardType::set_obj(lv_obj_t *lv_obj) { + LvCompound::set_obj(lv_obj); + lv_obj_add_event_cb( + lv_obj, + [](lv_event_t *event) { + auto *self = static_cast(event->user_data); + if (self->key_callback_.size() == 0) + return; + + auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); + if (key_idx == LV_BTNMATRIX_BTN_NONE) + return; + const char *txt = lv_btnmatrix_get_btn_text(self->obj, key_idx); + if (txt == nullptr) + return; + for (const auto *kb_special_key : KB_SPECIAL_KEYS) { + if (strcmp(txt, kb_special_key) == 0) + return; + } + while (*txt != 0) + self->send_key_(*txt++); + }, + LV_EVENT_PRESSED, this); +} +#endif // USE_LVGL_KEYBOARD + +void LvglComponent::write_random_() { + // length of 2 lines in 32 bit units + // we write 2 lines for the benefit of displays that won't write one line at a time. + size_t line_len = this->disp_drv_.hor_res * LV_COLOR_DEPTH / 8 / 4 * 2; + for (size_t i = 0; i != line_len; i++) { + ((uint32_t *) (this->draw_buf_.buf1))[i] = random_uint32(); + } + lv_area_t area; + area.x1 = 0; + area.x2 = this->disp_drv_.hor_res - 1; + if (this->snow_line_ == this->disp_drv_.ver_res / 2) { + area.y1 = static_cast(random_uint32() % (this->disp_drv_.ver_res / 2) * 2); + } else { + area.y1 = this->snow_line_++ * 2; + } + // write 2 lines + area.y2 = area.y1 + 1; + this->draw_buffer_(&area, (const uint8_t *) this->draw_buf_.buf1); +} void LvglComponent::setup() { ESP_LOGCONFIG(TAG, "LVGL Setup starts"); @@ -32,7 +286,8 @@ void LvglComponent::setup() { lv_log_register_print_cb(log_cb); #endif lv_init(); - lv_custom_event = static_cast(lv_event_register_id()); + lv_update_event = static_cast(lv_event_register_id()); + lv_api_event = static_cast(lv_event_register_id()); auto *display = this->displays_[0]; size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_; auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; @@ -74,10 +329,88 @@ void LvglComponent::setup() { ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated); this->disp_ = lv_disp_drv_register(&this->disp_drv_); for (const auto &v : this->init_lambdas_) - v(this->disp_); + v(this); + this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0); lv_disp_trig_activity(this->disp_); ESP_LOGCONFIG(TAG, "LVGL Setup complete"); } +void LvglComponent::update() { + // update indicators + if (this->paused_) { + return; + } + this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_)); +} +void LvglComponent::loop() { + if (this->paused_) { + if (this->show_snow_) + this->write_random_(); + } + lv_timer_handler_run_in_period(5); +} +bool lv_is_pre_initialise() { + if (!lv_is_initialized()) { + ESP_LOGE(TAG, "LVGL call before component is initialised"); + return true; + } + return false; +} + +#ifdef USE_LVGL_IMAGE +lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc) { + if (img_dsc == nullptr) + img_dsc = new lv_img_dsc_t(); // NOLINT + img_dsc->header.always_zero = 0; + img_dsc->header.reserved = 0; + img_dsc->header.w = src->get_width(); + img_dsc->header.h = src->get_height(); + img_dsc->data = src->get_data_start(); + img_dsc->data_size = image_type_to_width_stride(img_dsc->header.w * img_dsc->header.h, src->get_type()); + switch (src->get_type()) { + case image::IMAGE_TYPE_BINARY: + img_dsc->header.cf = LV_IMG_CF_ALPHA_1BIT; + break; + + case image::IMAGE_TYPE_GRAYSCALE: + img_dsc->header.cf = LV_IMG_CF_ALPHA_8BIT; + break; + + case image::IMAGE_TYPE_RGB24: + img_dsc->header.cf = LV_IMG_CF_RGB888; + break; + + case image::IMAGE_TYPE_RGB565: +#if LV_COLOR_DEPTH == 16 + img_dsc->header.cf = src->has_transparency() ? LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED : LV_IMG_CF_TRUE_COLOR; +#else + img_dsc->header.cf = LV_IMG_CF_RGB565; +#endif + break; + + case image::IMAGE_TYPE_RGBA: +#if LV_COLOR_DEPTH == 32 + img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR; +#else + img_dsc->header.cf = LV_IMG_CF_RGBA8888; +#endif + break; + } + return img_dsc; +} +#endif // USE_LVGL_IMAGE + +#ifdef USE_LVGL_ANIMIMG +void lv_animimg_stop(lv_obj_t *obj) { + auto *animg = (lv_animimg_t *) obj; + int32_t duration = animg->anim.time; + lv_animimg_set_duration(obj, 0); + lv_animimg_start(obj); + lv_animimg_set_duration(obj, duration); +} +#endif +void LvglComponent::static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { + reinterpret_cast(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); +} } // namespace lvgl } // namespace esphome diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index a884a27042..d5cff51de2 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -1,34 +1,47 @@ #pragma once #include "esphome/core/defines.h" -#ifdef USE_LVGL + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif // USE_BINARY_SENSOR +#ifdef USE_LVGL_ROTARY_ENCODER +#include "esphome/components/rotary_encoder/rotary_encoder.h" +#endif // USE_LVGL_ROTARY_ENCODER // required for clang-tidy #ifndef LV_CONF_H #define LV_CONF_SKIP 1 // NOLINT -#endif +#endif // LV_CONF_H #include "esphome/components/display/display.h" #include "esphome/components/display/display_color_utils.h" #include "esphome/core/component.h" -#include "esphome/core/hal.h" #include "esphome/core/log.h" #include #include +#include +#ifdef USE_LVGL_IMAGE +#include "esphome/components/image/image.h" +#endif // USE_LVGL_IMAGE #ifdef USE_LVGL_FONT #include "esphome/components/font/font.h" -#endif +#endif // USE_LVGL_FONT #ifdef USE_LVGL_TOUCHSCREEN #include "esphome/components/touchscreen/touchscreen.h" #endif // USE_LVGL_TOUCHSCREEN +#if defined(USE_LVGL_BUTTONMATRIX) || defined(USE_LVGL_KEYBOARD) +#include "esphome/components/key_provider/key_provider.h" +#endif // USE_LVGL_BUTTONMATRIX + namespace esphome { namespace lvgl { -extern lv_event_code_t lv_custom_event; // NOLINT -#ifdef USE_LVGL_COLOR -inline lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } -#endif // USE_LVGL_COLOR +extern lv_event_code_t lv_api_event; // NOLINT +extern lv_event_code_t lv_update_event; // NOLINT +extern std::string lv_event_code_name_for(uint8_t event_code); +extern bool lv_is_pre_initialise(); #if LV_COLOR_DEPTH == 16 static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; #elif LV_COLOR_DEPTH == 32 @@ -38,17 +51,39 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT #endif // LV_COLOR_DEPTH // Parent class for things that wrap an LVGL object -class LvCompound final { +class LvCompound { public: virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } lv_obj_t *obj{}; }; +class LvPageType { + public: + LvPageType(bool skip) : skip(skip) {} + + void setup(size_t index) { + this->index = index; + this->obj = lv_obj_create(nullptr); + } + lv_obj_t *obj{}; + size_t index{}; + bool skip; +}; + using LvLambdaType = std::function; using set_value_lambda_t = std::function; using event_callback_t = void(_lv_event_t *); using text_lambda_t = std::function; +template class ObjUpdateAction : public Action { + public: + explicit ObjUpdateAction(std::function &&lamb) : lamb_(std::move(lamb)) {} + + void play(Ts... x) override { this->lamb_(x...); } + + protected: + std::function lamb_; +}; #ifdef USE_LVGL_FONT class FontEngine { public: @@ -67,52 +102,55 @@ class FontEngine { lv_font_t lv_font_{}; }; #endif // USE_LVGL_FONT +#ifdef USE_LVGL_IMAGE +lv_img_dsc_t *lv_img_from(image::Image *src, lv_img_dsc_t *img_dsc = nullptr); +#endif // USE_LVGL_IMAGE + +#ifdef USE_LVGL_ANIMIMG +void lv_animimg_stop(lv_obj_t *obj); +#endif // USE_LVGL_ANIMIMG class LvglComponent : public PollingComponent { constexpr static const char *const TAG = "lvgl"; public: - static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { - reinterpret_cast(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); - } + static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); float get_setup_priority() const override { return setup_priority::PROCESSOR; } - static void log_cb(const char *buf) { - esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); - } - static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { - // make sure all coordinates are even - if (area->x1 & 1) - area->x1--; - if (!(area->x2 & 1)) - area->x2++; - if (area->y1 & 1) - area->y1--; - if (!(area->y2 & 1)) - area->y2++; - } - - void loop() override { lv_timer_handler_run_in_period(5); } void setup() override; - - void update() override {} - + void update() override; + void loop() override; + void add_on_idle_callback(std::function &&callback) { + this->idle_callbacks_.add(std::move(callback)); + } void add_display(display::Display *display) { this->displays_.push_back(display); } - void add_init_lambda(const std::function &lamb) { this->init_lambdas_.push_back(lamb); } + void add_init_lambda(const std::function &lamb) { this->init_lambdas_.push_back(lamb); } void dump_config() override; void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; } + bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } lv_disp_t *get_disp() { return this->disp_; } - void set_paused(bool paused, bool show_snow) { - this->paused_ = paused; - if (!paused && lv_scr_act() != nullptr) { - lv_disp_trig_activity(this->disp_); // resets the inactivity time - lv_obj_invalidate(lv_scr_act()); + void set_paused(bool paused, bool show_snow); + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); + void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, + lv_event_code_t event3); + bool is_paused() const { return this->paused_; } + void add_page(LvPageType *page); + void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); + void show_next_page(lv_scr_load_anim_t anim, uint32_t time); + void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); + void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } + void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); } + void restore_focus_mark(lv_group_t *group) { + auto *mark = this->focus_marks_[group]; + if (mark != nullptr) { + lv_group_focus_obj(mark); } } - bool is_paused() const { return this->paused_; } protected: + void write_random_(); void draw_buffer_(const lv_area_t *area, const uint8_t *ptr); void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); std::vector displays_{}; @@ -120,37 +158,52 @@ class LvglComponent : public PollingComponent { lv_disp_drv_t disp_drv_{}; lv_disp_t *disp_{}; bool paused_{}; + std::vector pages_{}; + size_t current_page_{0}; + bool show_snow_{}; + lv_coord_t snow_line_{}; + bool page_wrap_{true}; + std::map focus_marks_{}; - std::vector> init_lambdas_; + std::vector> init_lambdas_; + CallbackManager idle_callbacks_{}; size_t buffer_frac_{1}; bool full_refresh_{}; }; +class IdleTrigger : public Trigger<> { + public: + explicit IdleTrigger(LvglComponent *parent, TemplatableValue timeout); + + protected: + TemplatableValue timeout_; + bool is_idle_{}; +}; + +template class LvglAction : public Action, public Parented { + public: + explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} + void play(Ts... x) override { this->action_(this->parent_); } + + protected: + std::function action_{}; +}; + +template class LvglCondition : public Condition, public Parented { + public: + LvglCondition(std::function &&condition_lambda) + : condition_lambda_(std::move(condition_lambda)) {} + bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } + + protected: + std::function condition_lambda_{}; +}; + #ifdef USE_LVGL_TOUCHSCREEN class LVTouchListener : public touchscreen::TouchListener, public Parented { public: - LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { - lv_indev_drv_init(&this->drv_); - this->drv_.long_press_repeat_time = long_press_repeat_time; - this->drv_.long_press_time = long_press_time; - this->drv_.type = LV_INDEV_TYPE_POINTER; - this->drv_.user_data = this; - this->drv_.read_cb = [](lv_indev_drv_t *d, lv_indev_data_t *data) { - auto *l = static_cast(d->user_data); - if (l->touch_pressed_) { - data->point.x = l->touch_point_.x; - data->point.y = l->touch_point_.y; - data->state = LV_INDEV_STATE_PRESSED; - } else { - data->state = LV_INDEV_STATE_RELEASED; - } - }; - } - void update(const touchscreen::TouchPoints_t &tpoints) override { - this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); - if (this->touch_pressed_) - this->touch_point_ = tpoints[0]; - } + LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time); + void update(const touchscreen::TouchPoints_t &tpoints) override; void release() override { touch_pressed_ = false; } lv_indev_drv_t *get_drv() { return &this->drv_; } @@ -160,7 +213,69 @@ class LVTouchListener : public touchscreen::TouchListener, public Parented { + public: + LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt); + + void set_left_button(binary_sensor::BinarySensor *left_button) { + left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); + } + void set_right_button(binary_sensor::BinarySensor *right_button) { + right_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_RIGHT, state); }); + } + + void set_enter_button(binary_sensor::BinarySensor *enter_button) { + enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); }); + } + +#ifdef USE_LVGL_ROTARY_ENCODER + void set_sensor(rotary_encoder::RotaryEncoderSensor *sensor) { + sensor->register_listener([this](int32_t count) { this->set_count(count); }); + } +#endif // USE_LVGL_ROTARY_ENCODER + + void event(int key, bool pressed) { + if (!this->parent_->is_paused()) { + this->pressed_ = pressed; + this->key_ = key; + } + } + + void set_count(int32_t count) { + if (!this->parent_->is_paused()) + this->count_ = count; + } + + lv_indev_drv_t *get_drv() { return &this->drv_; } + + protected: + lv_indev_drv_t drv_{}; + bool pressed_{}; + int32_t count_{}; + int32_t last_count_{}; + int key_{}; +}; +#endif // USE_LVGL_KEY_LISTENER + +#ifdef USE_LVGL_BUTTONMATRIX +class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound { + public: + void set_obj(lv_obj_t *lv_obj) override; + uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } + void set_key(size_t idx, uint8_t key) { this->key_map_[idx] = key; } + + protected: + std::map key_map_{}; +}; +#endif // USE_LVGL_BUTTONMATRIX + +#ifdef USE_LVGL_KEYBOARD +class LvKeyboardType : public key_provider::KeyProvider, public LvCompound { + public: + void set_obj(lv_obj_t *lv_obj) override; +}; +#endif // USE_LVGL_KEYBOARD } // namespace lvgl } // namespace esphome - -#endif // USE_LVGL diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py new file mode 100644 index 0000000000..07f92635b5 --- /dev/null +++ b/esphome/components/lvgl/number/__init__.py @@ -0,0 +1,68 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.cpp_generator import MockObj + +from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_UPDATE_ON_RELEASE, CONF_WIDGET +from ..lv_validation import animated +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvNumber, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number) + +CONFIG_SCHEMA = ( + number.number_schema(LVGLNumber) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + cv.Optional(CONF_ANIMATED, default=True): animated, + cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, + } + ) +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + var = await number.new_number( + config, + max_value=widget.get_max(), + min_value=widget.get_min(), + step=widget.get_step(), + ) + + await wait_for_widgets() + async with LambdaContext([(cg.float_, "v")]) as control: + await widget.set_property( + "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] + ) + lv.event_send(widget.obj, API_EVENT, cg.nullptr) + control.add(var.publish_state(widget.get_value())) + async with LambdaContext(EVENT_ARG) as event: + event.add(var.publish_state(widget.get_value())) + event_code = ( + LV_EVENT.VALUE_CHANGED + if not config[CONF_UPDATE_ON_RELEASE] + else LV_EVENT.RELEASED + ) + async with LvContext(paren): + lv_add(var.set_control_lambda(await control.get_lambda())) + lv_add( + paren.add_event_cb( + widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code + ) + ) + lv_add(var.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h new file mode 100644 index 0000000000..77fadd2a29 --- /dev/null +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLNumber : public number::Number { + public: + void set_control_lambda(std::function control_lambda) { + this->control_lambda_ = std::move(control_lambda); + if (this->initial_state_.has_value()) { + this->control_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void control(float value) override { + if (this->control_lambda_ != nullptr) { + this->control_lambda_(value); + } else { + this->initial_state_ = value; + } + } + std::function control_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/obj.py b/esphome/components/lvgl/obj.py deleted file mode 100644 index 92c4f63d2d..0000000000 --- a/esphome/components/lvgl/obj.py +++ /dev/null @@ -1,17 +0,0 @@ -from .defines import CONF_MAIN, CONF_OBJ -from .types import WidgetType, lv_obj_t - - -class ObjType(WidgetType): - """ - The base LVGL object. All other widgets inherit from this. - """ - - def __init__(self): - super().__init__(CONF_OBJ, lv_obj_t, (CONF_MAIN,), schema={}, modify_schema={}) - - async def to_code(self, w, config): - return [] - - -obj_spec = ObjType() diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 9f6d984545..780057623a 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -1,16 +1,43 @@ from esphome import config_validation as cv -from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE +from esphome.automation import Trigger, validate_automation +from esphome.components.time import RealTimeClock +from esphome.const import ( + CONF_ARGS, + CONF_FORMAT, + CONF_GROUP, + CONF_ID, + CONF_ON_VALUE, + CONF_STATE, + CONF_TEXT, + CONF_TIME, + CONF_TRIGGER_ID, + CONF_TYPE, +) +from esphome.core import TimePeriod from esphome.schema_extractors import SCHEMA_EXTRACT -from . import defines as df, lv_validation as lvalid, types as ty +from . import defines as df, lv_validation as lvalid +from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR from .helpers import add_lv_use, requires_component, validate_printf -from .lv_validation import lv_font -from .types import WIDGET_TYPES, WidgetType +from .lv_validation import lv_color, lv_font, lv_gradient, lv_image +from .lvcode import LvglComponent, lv_event_t_ptr +from .types import ( + LVEncoderListener, + LvType, + WidgetType, + lv_group_t, + lv_obj_t, + lv_pseudo_button_t, + lv_style_t, +) + +# this will be populated later, in __init__.py to avoid circular imports. +WIDGET_TYPES: dict = {} # A schema for text properties TEXT_SCHEMA = cv.Schema( { - cv.Optional(df.CONF_TEXT): cv.Any( + cv.Optional(CONF_TEXT): cv.Any( cv.All( cv.Schema( { @@ -22,11 +49,42 @@ TEXT_SCHEMA = cv.Schema( ), validate_printf, ), - lvalid.lv_text, + cv.Schema( + { + cv.Required(CONF_TIME_FORMAT): cv.string, + cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)), + } + ), + cv.templatable(cv.string), ) } ) +LIST_ACTION_SCHEMA = cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_pseudo_button_t), + }, + key=CONF_ID, + ) +) + +PRESS_TIME = cv.All( + lvalid.lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535)) +) + +ENCODER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.All( + cv.declare_id(LVEncoderListener), requires_component("binary_sensor") + ), + cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t), + cv.Optional(df.CONF_INITIAL_FOCUS): cv.use_id(lv_obj_t), + cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME, + cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME, + } +) + # All LVGL styles and their validators STYLE_PROPS = { "align": df.CHILD_ALIGNMENTS.one_of, @@ -36,13 +94,15 @@ STYLE_PROPS = { "arc_width": cv.positive_int, "anim_time": lvalid.lv_milliseconds, "bg_color": lvalid.lv_color, + "bg_grad": lv_gradient, "bg_grad_color": lvalid.lv_color, "bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of, - "bg_grad_dir": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of, + "bg_grad_dir": LV_GRAD_DIR.one_of, "bg_grad_stop": lvalid.stop_value, "bg_image_opa": lvalid.opacity, "bg_image_recolor": lvalid.lv_color, "bg_image_recolor_opa": lvalid.opacity, + "bg_image_src": lvalid.lv_image, "bg_main_stop": lvalid.stop_value, "bg_opa": lvalid.opacity, "border_color": lvalid.lv_color, @@ -53,6 +113,7 @@ STYLE_PROPS = { ).several_of, "border_width": cv.positive_int, "clip_corner": lvalid.lv_bool, + "color_filter_opa": lvalid.opacity, "height": lvalid.size, "image_recolor": lvalid.lv_color, "image_recolor_opa": lvalid.opacity, @@ -65,15 +126,13 @@ STYLE_PROPS = { "opa_layered": lvalid.opacity, "outline_color": lvalid.lv_color, "outline_opa": lvalid.opacity, - "outline_pad": lvalid.size, - "outline_width": lvalid.size, - "pad_all": lvalid.size, - "pad_bottom": lvalid.size, - "pad_column": lvalid.size, - "pad_left": lvalid.size, - "pad_right": lvalid.size, - "pad_row": lvalid.size, - "pad_top": lvalid.size, + "outline_pad": lvalid.pixels, + "outline_width": lvalid.pixels, + "pad_all": lvalid.pixels, + "pad_bottom": lvalid.pixels, + "pad_left": lvalid.pixels, + "pad_right": lvalid.pixels, + "pad_top": lvalid.pixels, "shadow_color": lvalid.lv_color, "shadow_ofs_x": cv.int_, "shadow_ofs_y": cv.int_, @@ -102,7 +161,7 @@ STYLE_PROPS = { "max_width": lvalid.pixels_or_percent, "min_height": lvalid.pixels_or_percent, "min_width": lvalid.pixels_or_percent, - "radius": lvalid.radius, + "radius": lvalid.lv_fraction, "width": lvalid.size, "x": lvalid.pixels_or_percent, "y": lvalid.pixels_or_percent, @@ -120,6 +179,7 @@ STYLE_REMAP = { # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { + cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(lv_style_t)), cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" ).one_of, @@ -151,6 +211,44 @@ def part_schema(widget_type: WidgetType): ) +def automation_schema(typ: LvType): + if typ.has_on_value: + events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,) + else: + events = df.LV_EVENT_TRIGGERS + args = [typ.get_arg_type()] if isinstance(typ, LvType) else [] + args.append(lv_event_t_ptr) + return { + cv.Optional(event): validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template(*args)), + } + ) + for event in events + } + + +def create_modify_schema(widget_type): + return ( + part_schema(widget_type) + .extend( + { + cv.Required(CONF_ID): cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(widget_type), + }, + key=CONF_ID, + ) + ), + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + } + ) + .extend(FLAG_SCHEMA) + .extend(widget_type.modify_schema) + ) + + def obj_schema(widget_type: WidgetType): """ Create a schema for a widget type itself i.e. no allowance for children @@ -160,21 +258,26 @@ def obj_schema(widget_type: WidgetType): return ( part_schema(widget_type) .extend(FLAG_SCHEMA) + .extend(LAYOUT_SCHEMA) .extend(ALIGN_TO_SCHEMA) + .extend(automation_schema(widget_type.w_type)) .extend( cv.Schema( { cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), } ) ) ) +LAYOUT_SCHEMAS = {} + ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( { - cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t), + cv.Required(CONF_ID): cv.use_id(lv_obj_t), cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of, cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent, cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent, @@ -183,13 +286,86 @@ ALIGN_TO_SCHEMA = { } +def grid_free_space(value): + value = cv.Upper(value) + if value.startswith("FR(") and value.endswith(")"): + value = value.removesuffix(")").removeprefix("FR(") + return f"LV_GRID_FR({cv.positive_int(value)})" + raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") + + +grid_spec = cv.Any( + lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space +) + +cell_alignments = df.LV_CELL_ALIGNMENTS.one_of +grid_alignments = df.LV_GRID_ALIGNMENTS.one_of +flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of + +LAYOUT_SCHEMA = { + cv.Optional(df.CONF_LAYOUT): cv.typed_schema( + { + df.TYPE_GRID: { + cv.Required(df.CONF_GRID_ROWS): [grid_spec], + cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], + cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + }, + df.TYPE_FLEX: { + cv.Optional( + df.CONF_FLEX_FLOW, default="row_wrap" + ): df.FLEX_FLOWS.one_of, + cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, + cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, + cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, + }, + }, + lower=True, + ) +} + +GRID_CELL_SCHEMA = { + cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, +} + +FLEX_OBJ_SCHEMA = { + cv.Optional(df.CONF_FLEX_GROW): cv.int_, +} + +DISP_BG_SCHEMA = cv.Schema( + { + cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image, + cv.Optional(df.CONF_DISP_BG_COLOR): lv_color, + } +) + # A style schema that can include text STYLED_TEXT_SCHEMA = cv.maybe_simple_value( - STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT + STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT +) + +# For use by platform components +LVGL_SCHEMA = cv.Schema( + { + cv.GenerateID(df.CONF_LVGL_ID): cv.use_id(LvglComponent), + } ) ALL_STYLES = { **STYLE_PROPS, + **GRID_CELL_SCHEMA, + **FLEX_OBJ_SCHEMA, + cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, + cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, } @@ -205,16 +381,17 @@ def container_validator(schema, widget_type: WidgetType): result = schema if w_sch := widget_type.schema: result = result.extend(w_sch) + ltype = df.TYPE_NONE if value and (layout := value.get(df.CONF_LAYOUT)): if not isinstance(layout, dict): raise cv.Invalid("Layout value must be a dict") ltype = layout.get(CONF_TYPE) + if not ltype: + raise (cv.Invalid("Layout schema requires type:")) add_lv_use(ltype) - result = result.extend( - {cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())} - ) if value == SCHEMA_EXTRACT: return result + result = result.extend(LAYOUT_SCHEMAS[ltype.lower()]) return result(value) return validator diff --git a/esphome/components/lvgl/select/__init__.py b/esphome/components/lvgl/select/__init__.py new file mode 100644 index 0000000000..73ac50aa55 --- /dev/null +++ b/esphome/components/lvgl/select/__init__.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import CONF_OPTIONS + +from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvSelect, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select) + +CONFIG_SCHEMA = ( + select.select_schema(LVGLSelect) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvSelect), + cv.Optional(CONF_ANIMATED, default=False): cv.boolean, + } + ) +) + + +async def to_code(config): + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + options = widget.config.get(CONF_OPTIONS, []) + selector = await select.new_select(config, options=options) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as pub_ctx: + pub_ctx.add(selector.publish_index(widget.get_value())) + async with LambdaContext([(cg.uint16, "v")]) as control: + await widget.set_property("selected", "v", animated=config[CONF_ANIMATED]) + lv.event_send(widget.obj, API_EVENT, cg.nullptr) + control.add(selector.publish_index(widget.get_value())) + async with LvContext(paren) as ctx: + lv_add(selector.set_control_lambda(await control.get_lambda())) + ctx.add( + paren.add_event_cb( + widget.obj, + await pub_ctx.get_lambda(), + LV_EVENT.VALUE_CHANGED, + UPDATE_EVENT, + ) + ) + lv_add(selector.publish_index(widget.get_value())) diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h new file mode 100644 index 0000000000..97cc8697eb --- /dev/null +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include "esphome/components/select/select.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +static std::vector split_string(const std::string &str) { + std::vector strings; + auto delimiter = std::string("\n"); + + std::string::size_type pos; + std::string::size_type prev = 0; + while ((pos = str.find(delimiter, prev)) != std::string::npos) { + strings.push_back(str.substr(prev, pos - prev)); + prev = pos + delimiter.size(); + } + + // To get the last substring (or only, if delimiter is not found) + strings.push_back(str.substr(prev)); + + return strings; +} + +class LVGLSelect : public select::Select { + public: + void set_control_lambda(std::function lambda) { + this->control_lambda_ = std::move(lambda); + if (this->initial_state_.has_value()) { + this->control(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + void publish_index(size_t index) { + auto value = this->at(index); + if (value) + this->publish_state(value.value()); + } + + void set_options(const char *str) { this->traits.set_options(split_string(str)); } + + protected: + void control(const std::string &value) override { + if (this->control_lambda_ != nullptr) { + auto index = index_of(value); + if (index) + this->control_lambda_(index.value()); + } else { + this->initial_state_ = value.c_str(); + } + } + + std::function control_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py new file mode 100644 index 0000000000..a2a2298c27 --- /dev/null +++ b/esphome/components/lvgl/sensor/__init__.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +from esphome.components.sensor import Sensor, new_sensor, sensor_schema +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + LVGL_COMP_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvNumber +from ..widgets import Widget, get_widgets, wait_for_widgets + +CONFIG_SCHEMA = ( + sensor_schema(Sensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + } + ) +) + + +async def to_code(config): + sensor = await new_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + assert isinstance(widget, Widget) + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as lamb: + lv_add(sensor.publish_state(widget.get_value())) + async with LvContext(paren, LVGL_COMP_ARG): + lv_add( + paren.add_event_cb( + widget.obj, + await lamb.get_lambda(), + LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, + ) + ) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py new file mode 100644 index 0000000000..26c2694a52 --- /dev/null +++ b/esphome/components/lvgl/styles.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +from esphome.const import CONF_ID +from esphome.core import ID +from esphome.cpp_generator import MockObj + +from .defines import ( + CONF_STYLE_DEFINITIONS, + CONF_THEME, + CONF_TOP_LAYER, + LValidator, + literal, +) +from .helpers import add_lv_use +from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable +from .schemas import ALL_STYLES +from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr +from .widgets import Widget, add_widgets, set_obj_properties, theme_widget_map +from .widgets.obj import obj_spec + +TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())") + + +async def styles_to_code(config): + """Convert styles to C__ code.""" + for style in config.get(CONF_STYLE_DEFINITIONS, ()): + svar = cg.new_Pvariable(style[CONF_ID]) + lv.style_init(svar) + for prop, validator in ALL_STYLES.items(): + if (value := style.get(prop)) is not None: + if isinstance(validator, LValidator): + value = await validator.process(value) + if isinstance(value, list): + value = "|".join(value) + lv.call(f"style_set_{prop}", svar, literal(value)) + + +async def theme_to_code(config): + if theme := config.get(CONF_THEME): + add_lv_use(CONF_THEME) + for w_name, style in theme.items(): + if not isinstance(style, dict): + continue + + lname = "lv_theme_apply_" + w_name + apply = lv_variable(lv_lambda_t, lname) + theme_widget_map[w_name] = apply + ow = Widget.create("obj", MockObj(ID("obj")), obj_spec) + async with LambdaContext([(lv_obj_t_ptr, "obj")], where=w_name) as context: + await set_obj_properties(ow, style) + lv_assign(apply, await context.get_lambda()) + + +async def add_top_layer(config): + if top_conf := config.get(CONF_TOP_LAYER): + with LocalVariable("top_layer", lv_obj_t, TOP_LAYER) as top_layer_obj: + top_w = Widget(top_layer_obj, obj_spec, top_conf) + await set_obj_properties(top_w, top_conf) + await add_widgets(top_w, top_conf) diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py new file mode 100644 index 0000000000..8c090543f9 --- /dev/null +++ b/esphome/components/lvgl/switch/__init__.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +from esphome.components.switch import Switch, new_switch, switch_schema +import esphome.config_validation as cv +from esphome.cpp_generator import MockObj + +from ..defines import CONF_LVGL_ID, CONF_WIDGET, literal +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvConditional, + LvContext, + lv, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch) +CONFIG_SCHEMA = ( + switch_schema(LVGLSwitch) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } + ) +) + + +async def to_code(config): + switch = await new_switch(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as checked_ctx: + checked_ctx.add(switch.publish_state(widget.get_value())) + async with LambdaContext([(cg.bool_, "v")]) as control: + with LvConditional(MockObj("v")) as cond: + widget.add_state(LV_STATE.CHECKED) + cond.else_() + widget.clear_state(LV_STATE.CHECKED) + lv.event_send(widget.obj, API_EVENT, cg.nullptr) + control.add(switch.publish_state(literal("v"))) + async with LvContext(paren) as ctx: + lv_add(switch.set_control_lambda(await control.get_lambda())) + ctx.add( + paren.add_event_cb( + widget.obj, + await checked_ctx.get_lambda(), + LV_EVENT.VALUE_CHANGED, + UPDATE_EVENT, + ) + ) + lv_add(switch.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/switch/lvgl_switch.h b/esphome/components/lvgl/switch/lvgl_switch.h new file mode 100644 index 0000000000..af839b8892 --- /dev/null +++ b/esphome/components/lvgl/switch/lvgl_switch.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "esphome/components/switch/switch.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLSwitch : public switch_::Switch { + public: + void set_control_lambda(std::function state_lambda) { + this->state_lambda_ = std::move(state_lambda); + if (this->initial_state_.has_value()) { + this->state_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void write_state(bool value) override { + if (this->state_lambda_ != nullptr) { + this->state_lambda_(value); + } else { + this->initial_state_ = value; + } + } + std::function state_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py new file mode 100644 index 0000000000..540591d24b --- /dev/null +++ b/esphome/components/lvgl/text/__init__.py @@ -0,0 +1,52 @@ +import esphome.codegen as cg +from esphome.components import text +from esphome.components.text import new_text +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lv, + lv_add, +) +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvText, lvgl_ns +from ..widgets import get_widgets, wait_for_widgets + +LVGLText = lvgl_ns.class_("LVGLText", text.Text) + +CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(LVGLText), + cv.Required(CONF_WIDGET): cv.use_id(LvText), + } +) + + +async def to_code(config): + textvar = await new_text(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + await wait_for_widgets() + async with LambdaContext([(cg.std_string, "text_value")]) as control: + await widget.set_property("text", "text_value.c_str())") + lv.event_send(widget.obj, API_EVENT, None) + control.add(textvar.publish_state(widget.get_value())) + async with LambdaContext(EVENT_ARG) as lamb: + lv_add(textvar.publish_state(widget.get_value())) + async with LvContext(paren): + widget.var.set_control_lambda(await control.get_lambda()) + lv_add( + paren.add_event_cb( + widget.obj, + await lamb.get_lambda(), + LV_EVENT.VALUE_CHANGED, + UPDATE_EVENT, + ) + ) + lv_add(textvar.publish_state(widget.get_value())) diff --git a/esphome/components/lvgl/text/lvgl_text.h b/esphome/components/lvgl/text/lvgl_text.h new file mode 100644 index 0000000000..4c380d69a2 --- /dev/null +++ b/esphome/components/lvgl/text/lvgl_text.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "esphome/components/text/text.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace lvgl { + +class LVGLText : public text::Text { + public: + void set_control_lambda(std::function control_lambda) { + this->control_lambda_ = std::move(control_lambda); + if (this->initial_state_.has_value()) { + this->control_lambda_(this->initial_state_.value()); + this->initial_state_.reset(); + } + } + + protected: + void control(const std::string &value) override { + if (this->control_lambda_ != nullptr) { + this->control_lambda_(value); + } else { + this->initial_state_ = value; + } + } + std::function control_lambda_{}; + optional initial_state_{}; +}; + +} // namespace lvgl +} // namespace esphome diff --git a/esphome/components/lvgl/text_sensor/__init__.py b/esphome/components/lvgl/text_sensor/__init__.py new file mode 100644 index 0000000000..ae39eec291 --- /dev/null +++ b/esphome/components/lvgl/text_sensor/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components.text_sensor import ( + TextSensor, + new_text_sensor, + text_sensor_schema, +) +import esphome.config_validation as cv + +from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..lvcode import API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext +from ..schemas import LVGL_SCHEMA +from ..types import LV_EVENT, LvText +from ..widgets import get_widgets, wait_for_widgets + +CONFIG_SCHEMA = ( + text_sensor_schema(TextSensor) + .extend(LVGL_SCHEMA) + .extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvText), + } + ) +) + + +async def to_code(config): + sensor = await new_text_sensor(config) + paren = await cg.get_variable(config[CONF_LVGL_ID]) + widget = await get_widgets(config, CONF_WIDGET) + widget = widget[0] + await wait_for_widgets() + async with LambdaContext(EVENT_ARG) as pressed_ctx: + pressed_ctx.add(sensor.publish_state(widget.get_value())) + async with LvContext(paren) as ctx: + ctx.add( + paren.add_event_cb( + widget.obj, + await pressed_ctx.get_lambda(), + LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, + ) + ) diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index a0d4a3e4ad..4d430a428e 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components.touchscreen import CONF_TOUCHSCREEN_ID, Touchscreen import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import CORE, TimePeriod +from esphome.core import CORE from .defines import ( CONF_LONG_PRESS_REPEAT_TIME, @@ -10,11 +10,10 @@ from .defines import ( CONF_TOUCHSCREENS, ) from .helpers import lvgl_components_required -from .lv_validation import lv_milliseconds from .lvcode import lv +from .schemas import PRESS_TIME from .types import LVTouchListener -PRESS_TIME = cv.All(lv_milliseconds, cv.Range(max=TimePeriod(milliseconds=65535))) CONF_TOUCHSCREEN = "touchscreen" TOUCHSCREENS_CONFIG = cv.maybe_simple_value( { @@ -35,7 +34,7 @@ def touchscreen_schema(config): async def touchscreens_to_code(var, config): - for tconf in config.get(CONF_TOUCHSCREENS) or (): + for tconf in config[CONF_TOUCHSCREENS]: lvgl_components_required.add(CONF_TOUCHSCREEN) touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py new file mode 100644 index 0000000000..5288745fab --- /dev/null +++ b/esphome/components/lvgl/trigger.py @@ -0,0 +1,75 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_TRIGGER_ID + +from .defines import ( + CONF_ALIGN, + CONF_ALIGN_TO, + CONF_X, + CONF_Y, + LV_EVENT_MAP, + LV_EVENT_TRIGGERS, + literal, +) +from .lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvConditional, + lv, + lv_add, + lv_event_t_ptr, +) +from .types import LV_EVENT +from .widgets import widget_map + + +async def generate_triggers(lv_component): + """ + Generate LVGL triggers for all defined widgets + Must be done after all widgets completed + :param lv_component: The parent component + :return: + """ + + for w in widget_map.values(): + if w.config: + for event, conf in { + event: conf + for event, conf in w.config.items() + if event in LV_EVENT_TRIGGERS + }.items(): + conf = conf[0] + w.add_flag("LV_OBJ_FLAG_CLICKABLE") + event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) + await add_trigger(conf, lv_component, w, event) + for conf in w.config.get(CONF_ON_VALUE, ()): + await add_trigger( + conf, + lv_component, + w, + LV_EVENT.VALUE_CHANGED, + API_EVENT, + UPDATE_EVENT, + ) + + # Generate align to directives while we're here + if align_to := w.config.get(CONF_ALIGN_TO): + target = widget_map[align_to[CONF_ID]].obj + align = literal(align_to[CONF_ALIGN]) + x = align_to[CONF_X] + y = align_to[CONF_Y] + lv.obj_align_to(w.obj, target, align, x, y) + + +async def add_trigger(conf, lv_component, w, *events): + tid = conf[CONF_TRIGGER_ID] + trigger = cg.new_Pvariable(tid) + args = w.get_args() + [(lv_event_t_ptr, "event")] + value = w.get_value() + await automation.build_automation(trigger, args, conf) + async with LambdaContext(EVENT_ARG, where=tid) as context: + with LvConditional(w.is_selected()): + lv_add(trigger.trigger(value, literal("event"))) + lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events)) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 60291ea54a..b452ab5fb3 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,8 +1,11 @@ -from esphome import codegen as cg -from esphome.core import ID -from esphome.cpp_generator import MockObjClass +import sys -from .defines import CONF_TEXT +from esphome import automation, codegen as cg +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE +from esphome.cpp_generator import MockObj, MockObjClass + +from .defines import lvgl_ns +from .lvcode import lv_expr class LvType(cg.MockObjClass): @@ -18,28 +21,49 @@ class LvType(cg.MockObjClass): return self.args[0][0] if len(self.args) else None +class LvNumber(LvType): + def __init__(self, *args): + super().__init__( + *args, + largs=[(cg.float_, "x")], + lvalue=lambda w: w.get_number_value(), + has_on_value=True, + ) + self.value_property = CONF_VALUE + + uint16_t_ptr = cg.uint16.operator("ptr") -lvgl_ns = cg.esphome_ns.namespace("lvgl") char_ptr = cg.global_ns.namespace("char").operator("ptr") void_ptr = cg.void.operator("ptr") -LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) -lv_event_code_t = cg.global_ns.namespace("lv_event_code_t") +lv_coord_t = cg.global_ns.namespace("lv_coord_t") +lv_event_code_t = cg.global_ns.enum("lv_event_code_t") +lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") FontEngine = lvgl_ns.class_("FontEngine") +IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) +ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) +LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) +LvglAction = lvgl_ns.class_("LvglAction", automation.Action) +lv_lambda_t = lvgl_ns.class_("LvLambdaType") LvCompound = lvgl_ns.class_("LvCompound") lv_font_t = cg.global_ns.class_("lv_font_t") lv_style_t = cg.global_ns.struct("lv_style_t") +# fake parent class for first class widgets and matrix buttons lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) lv_obj_t_ptr = lv_obj_base_t.operator("ptr") -lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr") +lv_disp_t = cg.global_ns.struct("lv_disp_t") lv_color_t = cg.global_ns.struct("lv_color_t") +lv_group_t = cg.global_ns.struct("lv_group_t") LVTouchListener = lvgl_ns.class_("LVTouchListener") LVEncoderListener = lvgl_ns.class_("LVEncoderListener") lv_obj_t = LvType("lv_obj_t") +lv_page_t = LvType("LvPageType", parents=(LvCompound,)) +lv_img_t = LvType("lv_img_t") +lv_gradient_t = LvType("lv_grad_dsc_t") - -# this will be populated later, in __init__.py to avoid circular imports. -WIDGET_TYPES: dict = {} +LV_EVENT = MockObj(base="LV_EVENT_", op="") +LV_STATE = MockObj(base="LV_STATE_", op="") +LV_BTNMATRIX_CTRL = MockObj(base="LV_BTNMATRIX_CTRL_", op="") class LvText(LvType): @@ -47,7 +71,8 @@ class LvText(LvType): super().__init__( *args, largs=[(cg.std_string, "text")], - lvalue=lambda w: w.get_property("text")[0], + lvalue=lambda w: w.get_property("text"), + has_on_value=True, **kwargs, ) self.value_property = CONF_TEXT @@ -64,7 +89,15 @@ class LvBoolean(LvType): ) -CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t) +class LvSelect(LvType): + def __init__(self, *args, **kwargs): + super().__init__( + *args, + largs=[(cg.int_, "x")], + lvalue=lambda w: w.get_property("selected"), + has_on_value=True, + **kwargs, + ) class WidgetType: @@ -72,7 +105,15 @@ class WidgetType: Describes a type of Widget, e.g. "bar" or "line" """ - def __init__(self, name, w_type, parts, schema=None, modify_schema=None): + def __init__( + self, + name: str, + w_type: LvType, + parts: tuple, + schema=None, + modify_schema=None, + lv_name=None, + ): """ :param name: The widget name, e.g. "bar" :param w_type: The C type of the widget @@ -81,13 +122,18 @@ class WidgetType: :param modify_schema: A schema to update the widget """ self.name = name + self.lv_name = lv_name or name self.w_type = w_type self.parts = parts - self.schema = schema or {} + if schema is None: + self.schema = {} + else: + self.schema = schema if modify_schema is None: - self.modify_schema = schema + self.modify_schema = self.schema else: self.modify_schema = modify_schema + self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") @property def animated(self): @@ -107,7 +153,7 @@ class WidgetType: :param config: Its configuration :return: Generated code as a list of text lines """ - raise NotImplementedError(f"No to_code defined for {self.name}") + return [] def obj_creator(self, parent: MockObjClass, config: dict): """ @@ -116,7 +162,7 @@ class WidgetType: :param config: Its configuration :return: Generated code as a single text line """ - return f"lv_{self.name}_create({parent})" + return lv_expr.call(f"{self.lv_name}_create", parent) def get_uses(self): """ @@ -124,3 +170,23 @@ class WidgetType: :return: """ return () + + def get_max(self, config: dict): + return sys.maxsize + + def get_min(self, config: dict): + return -sys.maxsize + + def get_step(self, config: dict): + return 1 + + def get_scale(self, config: dict): + return 1.0 + + +class NumberType(WidgetType): + def get_max(self, config: dict): + return int(config[CONF_MAX_VALUE] or 100) + + def get_min(self, config: dict): + return int(config[CONF_MIN_VALUE] or 0) diff --git a/esphome/components/lvgl/widget.py b/esphome/components/lvgl/widget.py deleted file mode 100644 index 4755d8b21d..0000000000 --- a/esphome/components/lvgl/widget.py +++ /dev/null @@ -1,290 +0,0 @@ -import sys -from typing import Any - -from esphome import codegen as cg, config_validation as cv -from esphome.config_validation import Invalid -from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE -from esphome.core import ID, TimePeriod -from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import MockObjClass - -from .defines import ( - CONF_DEFAULT, - CONF_MAIN, - CONF_SCROLLBAR_MODE, - CONF_WIDGETS, - OBJ_FLAGS, - PARTS, - STATES, - LValidator, - join_enums, -) -from .helpers import add_lv_use -from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj -from .schemas import ALL_STYLES, STYLE_REMAP -from .types import WIDGET_TYPES, WidgetType, lv_obj_t - -EVENT_LAMB = "event_lamb__" - - -class LvScrActType(WidgetType): - """ - A "widget" representing the active screen. - """ - - def __init__(self): - super().__init__("lv_scr_act()", lv_obj_t, ()) - - def obj_creator(self, parent: MockObjClass, config: dict): - return [] - - async def to_code(self, w, config: dict): - return [] - - -class Widget: - """ - Represents a Widget. - """ - - widgets_completed = False - - @staticmethod - def set_completed(): - Widget.widgets_completed = True - - def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None): - self.var = var - self.type = wtype - self.config = config - self.scale = 1.0 - self.step = 1.0 - self.range_from = -sys.maxsize - self.range_to = sys.maxsize - self.parent = parent - - @staticmethod - def create(name, var, wtype: WidgetType, config: dict = None, parent=None): - w = Widget(var, wtype, config, parent) - if name is not None: - widget_map[name] = w - return w - - @property - def obj(self): - if self.type.is_compound(): - return f"{self.var}->obj" - return self.var - - def add_state(self, *args): - return lv_obj.add_state(self.obj, *args) - - def clear_state(self, *args): - return lv_obj.clear_state(self.obj, *args) - - def add_flag(self, *args): - return lv_obj.add_flag(self.obj, *args) - - def clear_flag(self, *args): - return lv_obj.clear_flag(self.obj, *args) - - def set_property(self, prop, value, animated: bool = None, ltype=None): - if isinstance(value, dict): - value = value.get(prop) - if value is None: - return - if isinstance(value, TimePeriod): - value = value.total_milliseconds - ltype = ltype or self.__type_base() - if animated is None or self.type.animated is not True: - lv.call(f"{ltype}_set_{prop}", self.obj, value) - else: - lv.call( - f"{ltype}_set_{prop}", - self.obj, - value, - "LV_ANIM_ON" if animated else "LV_ANIM_OFF", - ) - - def get_property(self, prop, ltype=None): - ltype = ltype or self.__type_base() - return f"lv_{ltype}_get_{prop}({self.obj})" - - def set_style(self, prop, value, state): - if value is None: - return [] - return lv.call(f"obj_set_style_{prop}", self.obj, value, state) - - def __type_base(self): - wtype = self.type.w_type - base = str(wtype) - if base.startswith("Lv"): - return f"{wtype}".removeprefix("Lv").removesuffix("Type").lower() - return f"{wtype}".removeprefix("lv_").removesuffix("_t") - - def __str__(self): - return f"({self.var}, {self.type})" - - -# Map of widgets to their config, used for trigger generation -widget_map: dict[Any, Widget] = {} - - -def get_widget_generator(wid): - """ - Used to wait for a widget during code generation. - :param wid: - :return: - """ - while True: - if obj := widget_map.get(wid): - return obj - if Widget.widgets_completed: - raise Invalid( - f"Widget {wid} not found, yet all widgets should be defined by now" - ) - yield - - -async def get_widget(wid: ID) -> Widget: - if obj := widget_map.get(wid): - return obj - return await FakeAwaitable(get_widget_generator(wid)) - - -def collect_props(config): - """ - Collect all properties from a configuration - :param config: - :return: - """ - props = {} - for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]: - if prop in config: - props[prop] = config[prop] - return props - - -def collect_states(config): - """ - Collect prperties for each state of a widget - :param config: - :return: - """ - states = {CONF_DEFAULT: collect_props(config)} - for state in STATES: - if state in config: - states[state] = collect_props(config[state]) - return states - - -def collect_parts(config): - """ - Collect properties and states for all widget parts - :param config: - :return: - """ - parts = {CONF_MAIN: collect_states(config)} - for part in PARTS: - if part in config: - parts[part] = collect_states(config[part]) - return parts - - -async def set_obj_properties(w: Widget, config): - """Generate a list of C++ statements to apply properties to an lv_obj_t""" - parts = collect_parts(config) - for part, states in parts.items(): - for state, props in states.items(): - lv_state = ConstantLiteral( - f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}" - ) - for prop, value in { - k: v for k, v in props.items() if k in ALL_STYLES - }.items(): - if isinstance(ALL_STYLES[prop], LValidator): - value = await ALL_STYLES[prop].process(value) - # Remapping for backwards compatibility of style names - prop_r = STYLE_REMAP.get(prop, prop) - w.set_style(prop_r, value, lv_state) - flag_clr = set() - flag_set = set() - props = parts[CONF_MAIN][CONF_DEFAULT] - for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): - if value: - flag_set.add(prop) - else: - flag_clr.add(prop) - if flag_set: - adds = join_enums(flag_set, "LV_OBJ_FLAG_") - w.add_flag(adds) - if flag_clr: - clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") - w.clear_flag(clrs) - - if states := config.get(CONF_STATE): - adds = set() - clears = set() - lambs = {} - for key, value in states.items(): - if isinstance(value, cv.Lambda): - lambs[key] = value - elif value == "true": - adds.add(key) - else: - clears.add(key) - if adds: - adds = join_enums(adds, "LV_STATE_") - w.add_state(adds) - if clears: - clears = join_enums(clears, "LV_STATE_") - w.clear_state(clears) - for key, value in lambs.items(): - lamb = await cg.process_lambda(value, [], return_type=cg.bool_) - state = ConstantLiteral(f"LV_STATE_{key.upper}") - lv.cond_if(lamb) - w.add_state(state) - lv.cond_else() - w.clear_state(state) - lv.cond_endif() - if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE): - lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode) - - -async def add_widgets(parent: Widget, config: dict): - """ - Add all widgets to an object - :param parent: The enclosing obj - :param config: The configuration - :return: - """ - for w in config.get(CONF_WIDGETS) or (): - w_type, w_cnfig = next(iter(w.items())) - await widget_to_code(w_cnfig, w_type, parent.obj) - - -async def widget_to_code(w_cnfig, w_type, parent): - """ - Converts a Widget definition to C code. - :param w_cnfig: The widget configuration - :param w_type: The Widget type - :param parent: The parent to which the widget should be added - :return: - """ - spec: WidgetType = WIDGET_TYPES[w_type] - creator = spec.obj_creator(parent, w_cnfig) - add_lv_use(spec.name) - add_lv_use(*spec.get_uses()) - wid = w_cnfig[CONF_ID] - add_line_marks(wid) - if spec.is_compound(): - var = cg.new_Pvariable(wid) - lv_add(var.set_obj(creator)) - else: - var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t) - lv_assign(var, creator) - - widget = Widget.create(wid, var, spec, w_cnfig, parent) - await set_obj_properties(widget, w_cnfig) - await add_widgets(widget, w_cnfig) - await spec.to_code(widget, w_cnfig) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py new file mode 100644 index 0000000000..e093cebd16 --- /dev/null +++ b/esphome/components/lvgl/widgets/__init__.py @@ -0,0 +1,447 @@ +import sys +from typing import Any, Union + +from esphome import codegen as cg, config_validation as cv +from esphome.config_validation import Invalid +from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE, CONF_TYPE +from esphome.core import ID, TimePeriod +from esphome.coroutine import FakeAwaitable +from esphome.cpp_generator import CallExpression, MockObj + +from ..defines import ( + CONF_DEFAULT, + CONF_FLEX_ALIGN_CROSS, + CONF_FLEX_ALIGN_MAIN, + CONF_FLEX_ALIGN_TRACK, + CONF_FLEX_FLOW, + CONF_GRID_COLUMN_ALIGN, + CONF_GRID_COLUMNS, + CONF_GRID_ROW_ALIGN, + CONF_GRID_ROWS, + CONF_LAYOUT, + CONF_MAIN, + CONF_PAD_COLUMN, + CONF_PAD_ROW, + CONF_SCROLLBAR_MODE, + CONF_STYLES, + CONF_WIDGETS, + OBJ_FLAGS, + PARTS, + STATES, + TYPE_FLEX, + TYPE_GRID, + LValidator, + call_lambda, + join_enums, + literal, +) +from ..helpers import add_lv_use +from ..lvcode import ( + LvConditional, + add_line_marks, + lv, + lv_add, + lv_assign, + lv_expr, + lv_obj, + lv_Pvariable, +) +from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES +from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr + +EVENT_LAMB = "event_lamb__" + +theme_widget_map = {} + + +class LvScrActType(WidgetType): + """ + A "widget" representing the active screen. + """ + + def __init__(self): + super().__init__("lv_scr_act()", lv_obj_t, ()) + + async def to_code(self, w, config: dict): + return [] + + +class Widget: + """ + Represents a Widget. + """ + + widgets_completed = False + + @staticmethod + def set_completed(): + Widget.widgets_completed = True + + def __init__(self, var, wtype: WidgetType, config: dict = None): + self.var = var + self.type = wtype + self.config = config + self.scale = 1.0 + self.step = 1.0 + self.range_from = -sys.maxsize + self.range_to = sys.maxsize + if wtype.is_compound(): + self.obj = MockObj(f"{self.var}->obj") + else: + self.obj = var + self.outer = None + self.move_to_foreground = False + + @staticmethod + def create(name, var, wtype: WidgetType, config: dict = None): + w = Widget(var, wtype, config) + if name is not None: + widget_map[name] = w + return w + + def add_state(self, state): + return lv_obj.add_state(self.obj, literal(state)) + + def clear_state(self, state): + return lv_obj.clear_state(self.obj, literal(state)) + + def has_state(self, state): + return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0 + + def is_pressed(self): + return self.has_state(LV_STATE.PRESSED) + + def is_checked(self): + return self.has_state(LV_STATE.CHECKED) + + def add_flag(self, flag): + return lv_obj.add_flag(self.obj, literal(flag)) + + def clear_flag(self, flag): + return lv_obj.clear_flag(self.obj, literal(flag)) + + async def set_property(self, prop, value, animated: bool = None, lv_name=None): + """ + Set a property of the widget. + :param prop: The property name + :param value: The value + :param animated: If the change should be animated + :param lv_name: The base type of the widget e.g. "obj" + """ + if isinstance(value, dict): + value = value.get(prop) + if isinstance(ALL_STYLES.get(prop), LValidator): + value = await ALL_STYLES[prop].process(value) + else: + value = literal(value) + if value is None: + return + if isinstance(value, TimePeriod): + value = value.total_milliseconds + if isinstance(value, str): + value = literal(value) + lv_name = lv_name or self.type.lv_name + if animated is None or self.type.animated is not True: + lv.call(f"{lv_name}_set_{prop}", self.obj, value) + else: + lv.call( + f"{lv_name}_set_{prop}", + self.obj, + value, + literal("LV_ANIM_ON" if animated else "LV_ANIM_OFF"), + ) + + def get_property(self, prop, ltype=None): + ltype = ltype or self.__type_base() + return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})") + + def set_style(self, prop, value, state): + if value is None: + return + lv.call(f"obj_set_style_{prop}", self.obj, value, state) + + def __type_base(self): + wtype = self.type.w_type + base = str(wtype) + if base.startswith("Lv"): + return f"{wtype}".removeprefix("Lv").removesuffix("Type").lower() + return f"{wtype}".removeprefix("lv_").removesuffix("_t") + + def __str__(self): + return f"({self.var}, {self.type})" + + def get_args(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.args + return [(lv_obj_t_ptr, "obj")] + + def get_value(self): + if isinstance(self.type.w_type, LvType): + return self.type.w_type.value(self) + return self.obj + + def get_number_value(self): + value = self.type.mock_obj.get_value(self.obj) + if self.scale == 1.0: + return value + return value / float(self.scale) + + def is_selected(self): + """ + Overridable property to determine if the widget is selected. Will be None except + for matrix buttons + :return: + """ + return None + + def get_max(self): + return self.type.get_max(self.config) + + def get_min(self): + return self.type.get_min(self.config) + + def get_step(self): + return self.type.get_step(self.config) + + def get_scale(self): + return self.type.get_scale(self.config) + + +# Map of widgets to their config, used for trigger generation +widget_map: dict[Any, Widget] = {} + + +def get_widget_generator(wid): + """ + Used to wait for a widget during code generation. + :param wid: + :return: + """ + while True: + if obj := widget_map.get(wid): + return obj + if Widget.widgets_completed: + raise Invalid( + f"Widget {wid} not found, yet all widgets should be defined by now" + ) + yield + + +async def get_widget_(wid): + if obj := widget_map.get(wid): + return obj + return await FakeAwaitable(get_widget_generator(wid)) + + +def widgets_wait_generator(): + while True: + if Widget.widgets_completed: + return + yield + + +async def wait_for_widgets(): + if Widget.widgets_completed: + return + await FakeAwaitable(widgets_wait_generator()) + + +async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]: + if not config: + return [] + if not isinstance(config, list): + config = [config] + return [await get_widget_(c[id]) for c in config if id in c] + + +def collect_props(config): + """ + Collect all properties from a configuration + :param config: + :return: + """ + props = {} + for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_STYLES, CONF_GROUP]: + if prop in config: + props[prop] = config[prop] + return props + + +def collect_states(config): + """ + Collect prperties for each state of a widget + :param config: + :return: + """ + states = {CONF_DEFAULT: collect_props(config)} + for state in STATES: + if state in config: + states[state] = collect_props(config[state]) + return states + + +def collect_parts(config): + """ + Collect properties and states for all widget parts + :param config: + :return: + """ + parts = {CONF_MAIN: collect_states(config)} + for part in PARTS: + if part in config: + parts[part] = collect_states(config[part]) + return parts + + +async def set_obj_properties(w: Widget, config): + """Generate a list of C++ statements to apply properties to an lv_obj_t""" + if layout := config.get(CONF_LAYOUT): + layout_type: str = layout[CONF_TYPE] + add_lv_use(layout_type) + lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) + if (pad_row := layout.get(CONF_PAD_ROW)) is not None: + w.set_style(CONF_PAD_ROW, pad_row, 0) + if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: + w.set_style(CONF_PAD_COLUMN, pad_column, 0) + if layout_type == TYPE_GRID: + wid = config[CONF_ID] + rows = [str(x) for x in layout[CONF_GRID_ROWS]] + rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}" + row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) + row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) + w.set_style("grid_row_dsc_array", row_array, 0) + columns = [str(x) for x in layout[CONF_GRID_COLUMNS]] + columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}" + column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) + column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) + w.set_style("grid_column_dsc_array", column_array, 0) + w.set_style( + CONF_GRID_COLUMN_ALIGN, literal(layout.get(CONF_GRID_COLUMN_ALIGN)), 0 + ) + w.set_style( + CONF_GRID_ROW_ALIGN, literal(layout.get(CONF_GRID_ROW_ALIGN)), 0 + ) + if layout_type == TYPE_FLEX: + lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) + main = literal(layout[CONF_FLEX_ALIGN_MAIN]) + cross = literal(layout[CONF_FLEX_ALIGN_CROSS]) + track = literal(layout[CONF_FLEX_ALIGN_TRACK]) + lv_obj.set_flex_align(w.obj, main, cross, track) + parts = collect_parts(config) + for part, states in parts.items(): + part = "LV_PART_" + part.upper() + for state, props in states.items(): + state = "LV_STATE_" + state.upper() + if state == "LV_STATE_DEFAULT": + lv_state = literal(part) + elif part == "LV_PART_MAIN": + lv_state = literal(state) + else: + lv_state = join_enums((state, part)) + for style_id in props.get(CONF_STYLES, ()): + lv_obj.add_style(w.obj, MockObj(style_id), lv_state) + for prop, value in { + k: v for k, v in props.items() if k in ALL_STYLES + }.items(): + if isinstance(ALL_STYLES[prop], LValidator): + value = await ALL_STYLES[prop].process(value) + prop_r = STYLE_REMAP.get(prop, prop) + w.set_style(prop_r, value, lv_state) + if group := config.get(CONF_GROUP): + group = await cg.get_variable(group) + lv.group_add_obj(group, w.obj) + props = parts[CONF_MAIN][CONF_DEFAULT] + lambs = {} + flag_set = set() + flag_clr = set() + for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): + if isinstance(value, cv.Lambda): + lambs[prop] = value + elif value: + flag_set.add(prop) + else: + flag_clr.add(prop) + if flag_set: + adds = join_enums(flag_set, "LV_OBJ_FLAG_") + w.add_flag(adds) + if flag_clr: + clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") + w.clear_flag(clrs) + for key, value in lambs.items(): + lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + flag = f"LV_OBJ_FLAG_{key.upper()}" + with LvConditional(call_lambda(lamb)) as cond: + w.add_flag(flag) + cond.else_() + w.clear_flag(flag) + + if states := config.get(CONF_STATE): + adds = set() + clears = set() + lambs = {} + for key, value in states.items(): + if isinstance(value, cv.Lambda): + lambs[key] = value + elif value: + adds.add(key) + else: + clears.add(key) + if adds: + adds = join_enums(adds, "LV_STATE_") + w.add_state(adds) + if clears: + clears = join_enums(clears, "LV_STATE_") + w.clear_state(clears) + for key, value in lambs.items(): + lamb = await cg.process_lambda(value, [], return_type=cg.bool_) + state = f"LV_STATE_{key.upper()}" + with LvConditional(call_lambda(lamb)) as cond: + w.add_state(state) + cond.else_() + w.clear_state(state) + await w.set_property(CONF_SCROLLBAR_MODE, config, lv_name="obj") + + +async def add_widgets(parent: Widget, config: dict): + """ + Add all widgets to an object + :param parent: The enclosing obj + :param config: The configuration + :return: + """ + for w in config.get(CONF_WIDGETS, ()): + w_type, w_cnfig = next(iter(w.items())) + await widget_to_code(w_cnfig, w_type, parent.obj) + + +async def widget_to_code(w_cnfig, w_type: WidgetType, parent): + """ + Converts a Widget definition to C code. + :param w_cnfig: The widget configuration + :param w_type: The Widget type + :param parent: The parent to which the widget should be added + :return: + """ + spec: WidgetType = WIDGET_TYPES[w_type] + creator = spec.obj_creator(parent, w_cnfig) + add_lv_use(spec.name) + add_lv_use(*spec.get_uses()) + wid = w_cnfig[CONF_ID] + add_line_marks(wid) + if spec.is_compound(): + var = cg.new_Pvariable(wid) + lv_add(var.set_obj(creator)) + else: + var = lv_Pvariable(lv_obj_t, wid) + lv_assign(var, creator) + + w = Widget.create(wid, var, spec, w_cnfig) + if theme := theme_widget_map.get(w_type): + lv_add(CallExpression(theme, w.obj)) + await set_obj_properties(w, w_cnfig) + await add_widgets(w, w_cnfig) + await spec.to_code(w, w_cnfig) + + +lv_scr_act_spec = LvScrActType() +lv_scr_act = Widget.create(None, literal("lv_scr_act()"), lv_scr_act_spec, {}) diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py new file mode 100644 index 0000000000..a973ca0702 --- /dev/null +++ b/esphome/components/lvgl/widgets/animimg.py @@ -0,0 +1,117 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_DURATION, CONF_ID +from esphome.cpp_generator import MockObj + +from ..automation import action_to_code +from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC +from ..helpers import lvgl_components_required +from ..lv_validation import lv_image, lv_milliseconds +from ..lvcode import lv, lv_expr +from ..types import LvType, ObjUpdateAction, void_ptr +from . import Widget, WidgetType, get_widgets +from .img import CONF_IMAGE +from .label import CONF_LABEL + +CONF_ANIMIMG = "animimg" +CONF_SRC_LIST_ID = "src_list_id" + + +def lv_repeat_count(value): + if isinstance(value, str) and value.lower() in ("forever", "infinite"): + value = 0xFFFF + return cv.int_range(min=0, max=0xFFFF)(value) + + +ANIMIMG_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_REPEAT_COUNT, default="forever"): lv_repeat_count, + cv.Optional(CONF_AUTO_START, default=True): cv.boolean, + } +) +ANIMIMG_SCHEMA = ANIMIMG_BASE_SCHEMA.extend( + { + cv.Required(CONF_DURATION): lv_milliseconds, + cv.Required(CONF_SRC): cv.ensure_list(lv_image), + cv.GenerateID(CONF_SRC_LIST_ID): cv.declare_id(void_ptr), + } +) + +ANIMIMG_MODIFY_SCHEMA = ANIMIMG_BASE_SCHEMA.extend( + { + cv.Optional(CONF_DURATION): lv_milliseconds, + } +) + +lv_animimg_t = LvType("lv_animimg_t") + + +class AnimimgType(WidgetType): + def __init__(self): + super().__init__( + CONF_ANIMIMG, + lv_animimg_t, + (CONF_MAIN,), + ANIMIMG_SCHEMA, + ANIMIMG_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + lvgl_components_required.add(CONF_IMAGE) + lvgl_components_required.add(CONF_ANIMIMG) + if CONF_SRC in config: + for x in config[CONF_SRC]: + await cg.get_variable(x) + srcs = [lv_expr.img_from(MockObj(x)) for x in config[CONF_SRC]] + src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs) + count = len(config[CONF_SRC]) + lv.animimg_set_src(w.obj, src_id, count) + lv.animimg_set_repeat_count(w.obj, config[CONF_REPEAT_COUNT]) + lv.animimg_set_duration(w.obj, config[CONF_DURATION]) + if config.get(CONF_AUTO_START): + lv.animimg_start(w.obj) + + def get_uses(self): + return CONF_IMAGE, CONF_LABEL + + +animimg_spec = AnimimgType() + + +@automation.register_action( + "lvgl.animimg.start", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_animimg_t), + }, + key=CONF_ID, + ), +) +async def animimg_start(config, action_id, template_arg, args): + widget = await get_widgets(config) + + async def do_start(w: Widget): + lv.animimg_start(w.obj) + + return await action_to_code(widget, do_start, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.animimg.stop", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_animimg_t), + }, + key=CONF_ID, + ), +) +async def animimg_stop(config, action_id, template_arg, args): + widget = await get_widgets(config) + + async def do_stop(w: Widget): + lv.animimg_stop(w.obj) + + return await action_to_code(widget, do_stop, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/arc.py b/esphome/components/lvgl/widgets/arc.py new file mode 100644 index 0000000000..dc120e4cbb --- /dev/null +++ b/esphome/components/lvgl/widgets/arc.py @@ -0,0 +1,82 @@ +import esphome.config_validation as cv +from esphome.const import ( + CONF_GROUP, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MODE, + CONF_ROTATION, + CONF_VALUE, +) +from esphome.cpp_types import nullptr + +from ..defines import ( + ARC_MODES, + CONF_ADJUSTABLE, + CONF_CHANGE_RATE, + CONF_END_ANGLE, + CONF_INDICATOR, + CONF_KNOB, + CONF_MAIN, + CONF_START_ANGLE, + literal, +) +from ..lv_validation import angle, get_start_value, lv_float +from ..lvcode import lv, lv_expr, lv_obj +from ..types import LvNumber, NumberType +from . import Widget + +CONF_ARC = "arc" +ARC_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_START_ANGLE, default=135): angle, + cv.Optional(CONF_END_ANGLE, default=45): angle, + cv.Optional(CONF_ROTATION, default=0.0): angle, + cv.Optional(CONF_ADJUSTABLE, default=False): bool, + cv.Optional(CONF_MODE, default="NORMAL"): ARC_MODES.one_of, + cv.Optional(CONF_CHANGE_RATE, default=720): cv.uint16_t, + } +) + +ARC_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + } +) + + +class ArcType(NumberType): + def __init__(self): + super().__init__( + CONF_ARC, + LvNumber("lv_arc_t"), + parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + schema=ARC_SCHEMA, + modify_schema=ARC_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if CONF_MIN_VALUE in config: + lv.arc_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.arc_set_bg_angles( + w.obj, config[CONF_START_ANGLE] // 10, config[CONF_END_ANGLE] // 10 + ) + lv.arc_set_rotation(w.obj, config[CONF_ROTATION] // 10) + lv.arc_set_mode(w.obj, literal(config[CONF_MODE])) + lv.arc_set_change_rate(w.obj, config[CONF_CHANGE_RATE]) + + if config.get(CONF_ADJUSTABLE) is False: + lv_obj.remove_style(w.obj, nullptr, literal("LV_PART_KNOB")) + w.clear_flag("LV_OBJ_FLAG_CLICKABLE") + elif CONF_GROUP not in config: + # For some reason arc does not get automatically added to the default group + lv.group_add_obj(lv_expr.group_get_default(), w.obj) + + value = await get_start_value(config) + if value is not None: + lv.arc_set_value(w.obj, value) + + +arc_spec = ArcType() diff --git a/esphome/components/lvgl/widgets/button.py b/esphome/components/lvgl/widgets/button.py new file mode 100644 index 0000000000..b59884ee67 --- /dev/null +++ b/esphome/components/lvgl/widgets/button.py @@ -0,0 +1,20 @@ +from esphome.const import CONF_BUTTON + +from ..defines import CONF_MAIN +from ..types import LvBoolean, WidgetType + +lv_button_t = LvBoolean("lv_btn_t") + + +class ButtonType(WidgetType): + def __init__(self): + super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn") + + def get_uses(self): + return ("btn",) + + async def to_code(self, w, config): + return [] + + +button_spec = ButtonType() diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py new file mode 100644 index 0000000000..c65bb4b354 --- /dev/null +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -0,0 +1,279 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components.key_provider import KeyProvider +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH +from esphome.cpp_generator import MockObj + +from ..automation import action_to_code +from ..defines import ( + BUTTONMATRIX_CTRLS, + CONF_BUTTONS, + CONF_CONTROL, + CONF_KEY_CODE, + CONF_MAIN, + CONF_ONE_CHECKED, + CONF_PAD_COLUMN, + CONF_PAD_ROW, + CONF_ROWS, + CONF_SELECTED, +) +from ..helpers import lvgl_components_required +from ..lv_validation import key_code, lv_bool, pixels +from ..lvcode import lv, lv_add, lv_expr +from ..schemas import automation_schema +from ..types import ( + LV_BTNMATRIX_CTRL, + LV_STATE, + LvBoolean, + LvCompound, + LvType, + ObjUpdateAction, + char_ptr, + lv_pseudo_button_t, +) +from . import Widget, WidgetType, get_widgets, widget_map +from .button import lv_button_t + +CONF_BUTTONMATRIX = "buttonmatrix" +CONF_BUTTON_TEXT_LIST_ID = "button_text_list_id" + +LvButtonMatrixButton = LvBoolean( + str(cg.uint16), + parents=(lv_pseudo_button_t,), +) +BUTTONMATRIX_BUTTON_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TEXT): cv.string, + cv.Optional(CONF_KEY_CODE): key_code, + cv.GenerateID(): cv.declare_id(LvButtonMatrixButton), + cv.Optional(CONF_WIDTH, default=1): cv.positive_int, + cv.Optional(CONF_CONTROL): cv.ensure_list( + cv.Schema( + {cv.Optional(k.lower()): cv.boolean for k in BUTTONMATRIX_CTRLS.choices} + ) + ), + } +).extend(automation_schema(lv_button_t)) + +BUTTONMATRIX_SCHEMA = cv.Schema( + { + cv.Optional(CONF_ONE_CHECKED, default=False): lv_bool, + cv.Optional(CONF_PAD_ROW): pixels, + cv.Optional(CONF_PAD_COLUMN): pixels, + cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), + cv.Required(CONF_ROWS): cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_BUTTONS): cv.ensure_list( + BUTTONMATRIX_BUTTON_SCHEMA + ), + } + ) + ), + } +) + + +class ButtonmatrixButtonType(WidgetType): + """ + A pseudo-widget for the matrix buttons + """ + + def __init__(self): + super().__init__("btnmatrix_btn", LvButtonMatrixButton, (), {}, {}) + + async def to_code(self, w, config: dict): + return [] + + +btn_btn_spec = ButtonmatrixButtonType() + + +class MatrixButton(Widget): + """ + Describes a button within a button matrix. + """ + + @staticmethod + def create_button(id, parent, config: dict, index): + w = MatrixButton(id, parent, config, index) + widget_map[id] = w + return w + + def __init__(self, id, parent: Widget, config, index): + super().__init__(id, btn_btn_spec, config) + self.parent = parent + self.index = index + self.obj = parent.obj + + def is_selected(self): + return self.parent.var.get_selected() == MockObj(self.var) + + @staticmethod + def map_ctrls(state): + state = str(state).upper().removeprefix("LV_STATE_") + assert state in BUTTONMATRIX_CTRLS.choices + return getattr(LV_BTNMATRIX_CTRL, state) + + def has_state(self, state): + state = self.map_ctrls(state) + return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state) + + def add_state(self, state): + state = self.map_ctrls(state) + return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state) + + def clear_state(self, state): + state = self.map_ctrls(state) + return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state) + + def is_pressed(self): + return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED) + + def is_checked(self): + return self.has_state(LV_STATE.CHECKED) + + def get_value(self): + return self.is_checked() + + def check_null(self): + return None + + +async def get_button_data(config, buttonmatrix: Widget): + """ + Process a button matrix button list + :param config: The row list + :param buttonmatrix: The parent variable + :return: text array id, control list, width list + """ + text_list = [] + ctrl_list = [] + width_list = [] + key_list = [] + for row in config: + for button_conf in row.get(CONF_BUTTONS, ()): + bid = button_conf[CONF_ID] + index = len(width_list) + MatrixButton.create_button(bid, buttonmatrix, button_conf, index) + cg.new_variable(bid, index) + text_list.append(button_conf.get(CONF_TEXT) or "") + key_list.append(button_conf.get(CONF_KEY_CODE) or 0) + width_list.append(button_conf[CONF_WIDTH]) + ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"] + for item in button_conf.get(CONF_CONTROL, ()): + ctrl.extend([k for k, v in item.items() if v]) + ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl)) + text_list.append("\n") + text_list = text_list[:-1] + text_list.append(cg.nullptr) + return text_list, ctrl_list, width_list, key_list + + +lv_buttonmatrix_t = LvType( + "LvButtonMatrixType", + parents=(KeyProvider, LvCompound), + largs=[(cg.uint16, "x")], + lvalue=lambda w: w.var.get_selected(), +) + + +class ButtonMatrixType(WidgetType): + def __init__(self): + super().__init__( + CONF_BUTTONMATRIX, + lv_buttonmatrix_t, + (CONF_MAIN, CONF_ITEMS), + BUTTONMATRIX_SCHEMA, + {}, + lv_name="btnmatrix", + ) + + async def to_code(self, w: Widget, config): + lvgl_components_required.add("BUTTONMATRIX") + if CONF_ROWS not in config: + return [] + text_list, ctrl_list, width_list, key_list = await get_button_data( + config[CONF_ROWS], w + ) + text_id = config[CONF_BUTTON_TEXT_LIST_ID] + text_id = cg.static_const_array(text_id, text_list) + lv.btnmatrix_set_map(w.obj, text_id) + set_btn_data(w.obj, ctrl_list, width_list) + lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED]) + for index, key in enumerate(key_list): + if key != 0: + lv_add(w.var.set_key(index, key)) + + def get_uses(self): + return ("btnmatrix",) + + +def set_btn_data(obj, ctrl_list, width_list): + for index, ctrl in enumerate(ctrl_list): + lv.btnmatrix_set_btn_ctrl(obj, index, ctrl) + for index, width in enumerate(width_list): + lv.btnmatrix_set_btn_width(obj, index, width) + + +buttonmatrix_spec = ButtonMatrixType() + + +@automation.register_action( + "lvgl.matrix.button.update", + ObjUpdateAction, + cv.Schema( + { + cv.Optional(CONF_WIDTH): cv.positive_int, + cv.Optional(CONF_CONTROL): cv.ensure_list( + cv.Schema( + { + cv.Optional(k.lower()): cv.boolean + for k in BUTTONMATRIX_CTRLS.choices + } + ), + ), + cv.Required(CONF_ID): cv.ensure_list( + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(LvButtonMatrixButton), + }, + key=CONF_ID, + ) + ), + cv.Optional(CONF_SELECTED): lv_bool, + } + ), +) +async def button_update_to_code(config, action_id, template_arg, args): + widgets = await get_widgets(config[CONF_ID]) + assert all(isinstance(w, MatrixButton) for w in widgets) + + async def do_button_update(w: MatrixButton): + if (width := config.get(CONF_WIDTH)) is not None: + lv.btnmatrix_set_btn_width(w.obj, w.index, width) + if config.get(CONF_SELECTED): + lv.btnmatrix_set_selected_btn(w.obj, w.index) + if controls := config.get(CONF_CONTROL): + adds = [] + clrs = [] + for item in controls: + adds.extend( + [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v] + ) + clrs.extend( + [f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v] + ) + if adds: + lv.btnmatrix_set_btn_ctrl( + w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds) + ) + if clrs: + lv.btnmatrix_clear_btn_ctrl( + w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs) + ) + + return await action_to_code( + widgets, do_button_update, action_id, template_arg, args + ) diff --git a/esphome/components/lvgl/widgets/checkbox.py b/esphome/components/lvgl/widgets/checkbox.py new file mode 100644 index 0000000000..75f4142eb1 --- /dev/null +++ b/esphome/components/lvgl/widgets/checkbox.py @@ -0,0 +1,32 @@ +from esphome.config_validation import Optional +from esphome.const import CONF_TEXT + +from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_PAD_COLUMN +from ..lv_validation import lv_text, pixels +from ..lvcode import lv +from ..schemas import TEXT_SCHEMA +from ..types import LvBoolean +from . import Widget, WidgetType + +CONF_CHECKBOX = "checkbox" + + +class CheckboxType(WidgetType): + def __init__(self): + super().__init__( + CONF_CHECKBOX, + LvBoolean("lv_checkbox_t"), + (CONF_MAIN, CONF_INDICATOR), + TEXT_SCHEMA.extend( + { + Optional(CONF_PAD_COLUMN): pixels, + } + ), + ) + + async def to_code(self, w: Widget, config): + if (value := config.get(CONF_TEXT)) is not None: + lv.checkbox_set_text(w.obj, await lv_text.process(value)) + + +checkbox_spec = CheckboxType() diff --git a/esphome/components/lvgl/widgets/dropdown.py b/esphome/components/lvgl/widgets/dropdown.py new file mode 100644 index 0000000000..dc0346b080 --- /dev/null +++ b/esphome/components/lvgl/widgets/dropdown.py @@ -0,0 +1,76 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_OPTIONS + +from ..defines import ( + CONF_DIR, + CONF_INDICATOR, + CONF_MAIN, + CONF_SELECTED_INDEX, + CONF_SYMBOL, + DIRECTIONS, + literal, +) +from ..lv_validation import lv_int, lv_text, option_string +from ..lvcode import LocalVariable, lv, lv_expr +from ..schemas import part_schema +from ..types import LvSelect, LvType, lv_obj_t +from . import Widget, WidgetType, set_obj_properties +from .label import CONF_LABEL + +CONF_DROPDOWN = "dropdown" +CONF_DROPDOWN_LIST = "dropdown_list" + +lv_dropdown_t = LvSelect("lv_dropdown_t") +lv_dropdown_list_t = LvType("lv_dropdown_list_t") +dropdown_list_spec = WidgetType(CONF_DROPDOWN_LIST, lv_dropdown_list_t, (CONF_MAIN,)) + +DROPDOWN_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_SYMBOL): lv_text, + cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_), + cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, + cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec), + } +) + +DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), + } +) + + +class DropdownType(WidgetType): + def __init__(self): + super().__init__( + CONF_DROPDOWN, + lv_dropdown_t, + (CONF_MAIN, CONF_INDICATOR), + DROPDOWN_SCHEMA, + DROPDOWN_BASE_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if options := config.get(CONF_OPTIONS): + text = cg.safe_exp("\n".join(options)) + lv.dropdown_set_options(w.obj, text) + if symbol := config.get(CONF_SYMBOL): + lv.dropdown_set_symbol(w.obj, await lv_text.process(symbol)) + if (selected := config.get(CONF_SELECTED_INDEX)) is not None: + value = await lv_int.process(selected) + lv.dropdown_set_selected(w.obj, value) + if dirn := config.get(CONF_DIR): + lv.dropdown_set_dir(w.obj, literal(dirn)) + if dlist := config.get(CONF_DROPDOWN_LIST): + with LocalVariable( + "dropdown_list", lv_obj_t, lv_expr.dropdown_get_list(w.obj) + ) as dlist_obj: + dwid = Widget(dlist_obj, dropdown_list_spec, dlist) + await set_obj_properties(dwid, dlist) + + def get_uses(self): + return (CONF_LABEL,) + + +dropdown_spec = DropdownType() diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py new file mode 100644 index 0000000000..59b2c97c63 --- /dev/null +++ b/esphome/components/lvgl/widgets/img.py @@ -0,0 +1,85 @@ +import esphome.config_validation as cv +from esphome.const import CONF_ANGLE, CONF_MODE + +from ..defines import ( + CONF_ANTIALIAS, + CONF_MAIN, + CONF_OFFSET_X, + CONF_OFFSET_Y, + CONF_PIVOT_X, + CONF_PIVOT_Y, + CONF_SRC, + CONF_ZOOM, + LvConstant, +) +from ..lv_validation import angle, lv_bool, lv_image, size, zoom +from ..lvcode import lv +from ..types import lv_img_t +from . import Widget, WidgetType +from .label import CONF_LABEL + +CONF_IMAGE = "image" + +BASE_IMG_SCHEMA = cv.Schema( + { + cv.Optional(CONF_PIVOT_X, default="50%"): size, + cv.Optional(CONF_PIVOT_Y, default="50%"): size, + cv.Optional(CONF_ANGLE): angle, + cv.Optional(CONF_ZOOM): zoom, + cv.Optional(CONF_OFFSET_X): size, + cv.Optional(CONF_OFFSET_Y): size, + cv.Optional(CONF_ANTIALIAS): lv_bool, + cv.Optional(CONF_MODE): LvConstant( + "LV_IMG_SIZE_MODE_", "VIRTUAL", "REAL" + ).one_of, + } +) + +IMG_SCHEMA = BASE_IMG_SCHEMA.extend( + { + cv.Required(CONF_SRC): lv_image, + } +) + +IMG_MODIFY_SCHEMA = BASE_IMG_SCHEMA.extend( + { + cv.Optional(CONF_SRC): lv_image, + } +) + + +class ImgType(WidgetType): + def __init__(self): + super().__init__( + CONF_IMAGE, + lv_img_t, + (CONF_MAIN,), + IMG_SCHEMA, + IMG_MODIFY_SCHEMA, + lv_name="img", + ) + + def get_uses(self): + return "img", CONF_LABEL + + async def to_code(self, w: Widget, config): + if src := config.get(CONF_SRC): + lv.img_set_src(w.obj, await lv_image.process(src)) + if (cf_angle := config.get(CONF_ANGLE)) is not None: + pivot_x = config[CONF_PIVOT_X] + pivot_y = config[CONF_PIVOT_Y] + lv.img_set_pivot(w.obj, pivot_x, pivot_y) + lv.img_set_angle(w.obj, cf_angle) + if (img_zoom := config.get(CONF_ZOOM)) is not None: + lv.img_set_zoom(w.obj, img_zoom) + if (offset := config.get(CONF_OFFSET_X)) is not None: + lv.img_set_offset_x(w.obj, offset) + if (offset := config.get(CONF_OFFSET_Y)) is not None: + lv.img_set_offset_y(w.obj, offset) + if CONF_ANTIALIAS in config: + lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) + if mode := config.get(CONF_MODE): + lv.img_set_mode(w.obj, mode) + + +img_spec = ImgType() diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py new file mode 100644 index 0000000000..ba7edb302e --- /dev/null +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -0,0 +1,49 @@ +from esphome.components.key_provider import KeyProvider +import esphome.config_validation as cv +from esphome.const import CONF_ITEMS, CONF_MODE +from esphome.cpp_types import std_string + +from ..defines import CONF_MAIN, KEYBOARD_MODES, literal +from ..helpers import add_lv_use, lvgl_components_required +from ..types import LvCompound, LvType +from . import Widget, WidgetType, get_widgets +from .textarea import CONF_TEXTAREA, lv_textarea_t + +CONF_KEYBOARD = "keyboard" + +KEYBOARD_SCHEMA = { + cv.Optional(CONF_MODE, default="TEXT_UPPER"): KEYBOARD_MODES.one_of, + cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t), +} + +lv_keyboard_t = LvType( + "LvKeyboardType", + parents=(KeyProvider, LvCompound), + largs=[(std_string, "text")], + has_on_value=True, + lvalue=lambda w: literal(f"lv_textarea_get_text({w.obj})"), +) + + +class KeyboardType(WidgetType): + def __init__(self): + super().__init__( + CONF_KEYBOARD, + lv_keyboard_t, + (CONF_MAIN, CONF_ITEMS), + KEYBOARD_SCHEMA, + ) + + def get_uses(self): + return CONF_KEYBOARD, CONF_TEXTAREA + + async def to_code(self, w: Widget, config: dict): + lvgl_components_required.add("KEY_LISTENER") + lvgl_components_required.add(CONF_KEYBOARD) + add_lv_use("btnmatrix") + await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE])) + if ta := await get_widgets(config, CONF_TEXTAREA): + await w.set_property(CONF_TEXTAREA, ta[0].obj) + + +keyboard_spec = KeyboardType() diff --git a/esphome/components/lvgl/label.py b/esphome/components/lvgl/widgets/label.py similarity index 64% rename from esphome/components/lvgl/label.py rename to esphome/components/lvgl/widgets/label.py index 0498f39474..6b04235674 100644 --- a/esphome/components/lvgl/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -1,19 +1,20 @@ import esphome.config_validation as cv +from esphome.const import CONF_TEXT -from .defines import ( - CONF_LABEL, +from ..defines import ( CONF_LONG_MODE, CONF_MAIN, CONF_RECOLOR, CONF_SCROLLBAR, CONF_SELECTED, - CONF_TEXT, LV_LONG_MODES, ) -from .lv_validation import lv_bool, lv_text -from .schemas import TEXT_SCHEMA -from .types import LvText, WidgetType -from .widget import Widget +from ..lv_validation import lv_bool, lv_text +from ..schemas import TEXT_SCHEMA +from ..types import LvText, WidgetType +from . import Widget + +CONF_LABEL = "label" class LabelType(WidgetType): @@ -33,9 +34,9 @@ class LabelType(WidgetType): async def to_code(self, w: Widget, config): """For a text object, create and set text""" if value := config.get(CONF_TEXT): - w.set_property(CONF_TEXT, await lv_text.process(value)) - w.set_property(CONF_LONG_MODE, config) - w.set_property(CONF_RECOLOR, config) + await w.set_property(CONF_TEXT, await lv_text.process(value)) + await w.set_property(CONF_LONG_MODE, config) + await w.set_property(CONF_RECOLOR, config) label_spec = LabelType() diff --git a/esphome/components/lvgl/widgets/led.py b/esphome/components/lvgl/widgets/led.py new file mode 100644 index 0000000000..647973c9b7 --- /dev/null +++ b/esphome/components/lvgl/widgets/led.py @@ -0,0 +1,29 @@ +import esphome.config_validation as cv +from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED + +from ..defines import CONF_MAIN +from ..lv_validation import lv_brightness, lv_color +from ..lvcode import lv +from ..types import LvType +from . import Widget, WidgetType + +LED_SCHEMA = cv.Schema( + { + cv.Optional(CONF_COLOR): lv_color, + cv.Optional(CONF_BRIGHTNESS): lv_brightness, + } +) + + +class LedType(WidgetType): + def __init__(self): + super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA) + + async def to_code(self, w: Widget, config): + if (color := config.get(CONF_COLOR)) is not None: + lv.led_set_color(w.obj, await lv_color.process(color)) + if (brightness := config.get(CONF_BRIGHTNESS)) is not None: + lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) + + +led_spec = LedType() diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py new file mode 100644 index 0000000000..4c6439fde4 --- /dev/null +++ b/esphome/components/lvgl/widgets/line.py @@ -0,0 +1,52 @@ +import functools + +import esphome.codegen as cg +import esphome.config_validation as cv + +from ..defines import CONF_MAIN +from ..lvcode import lv +from ..types import LvType +from . import Widget, WidgetType + +CONF_LINE = "line" +CONF_POINTS = "points" +CONF_POINT_LIST_ID = "point_list_id" + +lv_point_t = cg.global_ns.struct("lv_point_t") + + +def point_list(il): + il = cv.string(il) + nl = il.replace(" ", "").split(",") + return [int(n) for n in nl] + + +def cv_point_list(value): + if not isinstance(value, list): + raise cv.Invalid("List of points required") + values = [point_list(v) for v in value] + if not functools.reduce(lambda f, v: f and len(v) == 2, values, True): + raise cv.Invalid("Points must be a list of x,y integer pairs") + return values + + +LINE_SCHEMA = { + cv.Required(CONF_POINTS): cv_point_list, + cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t), +} + + +class LineType(WidgetType): + def __init__(self): + super().__init__( + CONF_LINE, LvType("lv_line_t"), (CONF_MAIN,), LINE_SCHEMA, modify_schema={} + ) + + async def to_code(self, w: Widget, config): + """For a line object, create and add the points""" + if data := config.get(CONF_POINTS): + points = cg.static_const_array(config[CONF_POINT_LIST_ID], data) + lv.line_set_points(w.obj, points, len(data)) + + +line_spec = LineType() diff --git a/esphome/components/lvgl/widgets/lv_bar.py b/esphome/components/lvgl/widgets/lv_bar.py new file mode 100644 index 0000000000..57209370c0 --- /dev/null +++ b/esphome/components/lvgl/widgets/lv_bar.py @@ -0,0 +1,55 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE + +from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal +from ..lv_validation import animated, get_start_value, lv_float +from ..lvcode import lv +from ..types import LvNumber, NumberType +from . import Widget + +# Note this file cannot be called "bar.py" because that name is disallowed. + +CONF_BAR = "bar" +BAR_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + +BAR_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + + +class BarType(NumberType): + def __init__(self): + super().__init__( + CONF_BAR, + LvNumber("lv_bar_t"), + parts=(CONF_MAIN, CONF_INDICATOR), + schema=BAR_SCHEMA, + modify_schema=BAR_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + var = w.obj + if CONF_MIN_VALUE in config: + lv.bar_set_range(var, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.bar_set_mode(var, literal(config[CONF_MODE])) + value = await get_start_value(config) + if value is not None: + lv.bar_set_value(var, value, literal(config[CONF_ANIMATED])) + + @property + def animated(self): + return True + + +bar_spec = BarType() diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py new file mode 100644 index 0000000000..36f6643022 --- /dev/null +++ b/esphome/components/lvgl/widgets/meter.py @@ -0,0 +1,309 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import ( + CONF_COLOR, + CONF_COUNT, + CONF_ID, + CONF_ITEMS, + CONF_LENGTH, + CONF_LOCAL, + CONF_RANGE_FROM, + CONF_RANGE_TO, + CONF_ROTATION, + CONF_VALUE, + CONF_WIDTH, +) + +from ..automation import action_to_code +from ..defines import ( + CONF_END_VALUE, + CONF_INDICATOR, + CONF_MAIN, + CONF_PIVOT_X, + CONF_PIVOT_Y, + CONF_SRC, + CONF_START_VALUE, + CONF_TICKS, +) +from ..helpers import add_lv_use +from ..lv_validation import ( + angle, + get_end_value, + get_start_value, + lv_bool, + lv_color, + lv_float, + lv_image, + requires_component, + size, +) +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..types import LvType, ObjUpdateAction +from . import Widget, WidgetType, get_widgets +from .arc import CONF_ARC +from .img import CONF_IMAGE +from .line import CONF_LINE +from .obj import obj_spec + +CONF_ANGLE_RANGE = "angle_range" +CONF_COLOR_END = "color_end" +CONF_COLOR_START = "color_start" +CONF_INDICATORS = "indicators" +CONF_LABEL_GAP = "label_gap" +CONF_MAJOR = "major" +CONF_METER = "meter" +CONF_R_MOD = "r_mod" +CONF_SCALES = "scales" +CONF_STRIDE = "stride" +CONF_TICK_STYLE = "tick_style" + +lv_meter_t = LvType("lv_meter_t") +lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t") +lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr") + + +def pixels(value): + """A size in one axis in pixels""" + if isinstance(value, str) and value.lower().endswith("px"): + return cv.int_(value[:-2]) + return cv.int_(value) + + +INDICATOR_LINE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_R_MOD, default=0): size, + cv.Optional(CONF_VALUE): lv_float, + } +) +INDICATOR_IMG_SCHEMA = cv.Schema( + { + cv.Required(CONF_SRC): lv_image, + cv.Required(CONF_PIVOT_X): pixels, + cv.Required(CONF_PIVOT_Y): pixels, + cv.Optional(CONF_VALUE): lv_float, + } +) +INDICATOR_ARC_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_R_MOD, default=0): size, + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + } +) +INDICATOR_TICKS_SCHEMA = cv.Schema( + { + cv.Optional(CONF_WIDTH, default=4): size, + cv.Optional(CONF_COLOR_START, default=0): lv_color, + cv.Optional(CONF_COLOR_END): lv_color, + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + cv.Optional(CONF_LOCAL, default=False): lv_bool, + } +) +INDICATOR_SCHEMA = cv.Schema( + { + cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All( + INDICATOR_IMG_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + requires_component("image"), + ), + cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(lv_meter_indicator_t), + } + ), + } +) + +SCALE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TICKS): cv.Schema( + { + cv.Optional(CONF_COUNT, default=12): cv.positive_int, + cv.Optional(CONF_WIDTH, default=2): size, + cv.Optional(CONF_LENGTH, default=10): size, + cv.Optional(CONF_COLOR, default=0x808080): lv_color, + cv.Optional(CONF_MAJOR): cv.Schema( + { + cv.Optional(CONF_STRIDE, default=3): cv.positive_int, + cv.Optional(CONF_WIDTH, default=5): size, + cv.Optional(CONF_LENGTH, default="15%"): size, + cv.Optional(CONF_COLOR, default=0): lv_color, + cv.Optional(CONF_LABEL_GAP, default=4): size, + } + ), + } + ), + cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, + cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, + cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), + cv.Optional(CONF_ROTATION): angle, + cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), + } +) + +METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)} + + +class MeterType(WidgetType): + def __init__(self): + super().__init__( + CONF_METER, + lv_meter_t, + (CONF_MAIN, CONF_INDICATOR, CONF_TICKS, CONF_ITEMS), + METER_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + """For a meter object, create and set parameters""" + + var = w.obj + for scale_conf in config.get(CONF_SCALES, ()): + rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 + if CONF_ROTATION in scale_conf: + rotation = scale_conf[CONF_ROTATION] // 10 + with LocalVariable( + "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) + ) as meter_var: + lv.meter_set_scale_range( + var, + meter_var, + scale_conf[CONF_RANGE_FROM], + scale_conf[CONF_RANGE_TO], + scale_conf[CONF_ANGLE_RANGE], + rotation, + ) + if ticks := scale_conf.get(CONF_TICKS): + color = await lv_color.process(ticks[CONF_COLOR]) + lv.meter_set_scale_ticks( + var, + meter_var, + ticks[CONF_COUNT], + ticks[CONF_WIDTH], + ticks[CONF_LENGTH], + color, + ) + if CONF_MAJOR in ticks: + major = ticks[CONF_MAJOR] + color = await lv_color.process(major[CONF_COLOR]) + lv.meter_set_scale_major_ticks( + var, + meter_var, + major[CONF_STRIDE], + major[CONF_WIDTH], + major[CONF_LENGTH], + color, + major[CONF_LABEL_GAP], + ) + for indicator in scale_conf.get(CONF_INDICATORS, ()): + (t, v) = next(iter(indicator.items())) + iid = v[CONF_ID] + ivar = cg.new_variable( + iid, cg.nullptr, type_=lv_meter_indicator_t_ptr + ) + # Enable getting the meter to which this belongs. + wid = Widget.create(iid, var, obj_spec, v) + wid.obj = ivar + if t == CONF_LINE: + color = await lv_color.process(v[CONF_COLOR]) + lv_assign( + ivar, + lv_expr.meter_add_needle_line( + var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + ), + ) + if t == CONF_ARC: + color = await lv_color.process(v[CONF_COLOR]) + lv_assign( + ivar, + lv_expr.meter_add_arc( + var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + ), + ) + if t == CONF_TICK_STYLE: + color_start = await lv_color.process(v[CONF_COLOR_START]) + color_end = await lv_color.process( + v.get(CONF_COLOR_END) or color_start + ) + lv_assign( + ivar, + lv_expr.meter_add_scale_lines( + var, + meter_var, + color_start, + color_end, + v[CONF_LOCAL], + v[CONF_WIDTH], + ), + ) + if t == CONF_IMAGE: + add_lv_use("img") + lv_assign( + ivar, + lv_expr.meter_add_needle_img( + var, + meter_var, + await lv_image.process(v[CONF_SRC]), + v[CONF_PIVOT_X], + v[CONF_PIVOT_Y], + ), + ) + start_value = await get_start_value(v) + end_value = await get_end_value(v) + set_indicator_values(var, ivar, start_value, end_value) + + +meter_spec = MeterType() + + +@automation.register_action( + "lvgl.indicator.update", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_meter_indicator_t), + cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float, + cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float, + cv.Optional(CONF_END_VALUE): lv_float, + } + ), +) +async def indicator_update_to_code(config, action_id, template_arg, args): + widget = await get_widgets(config) + start_value = await get_start_value(config) + end_value = await get_end_value(config) + + async def set_value(w: Widget): + set_indicator_values(w.var, w.obj, start_value, end_value) + + return await action_to_code(widget, set_value, action_id, template_arg, args) + + +def set_indicator_values(meter, indicator, start_value, end_value): + if start_value is not None: + if end_value is None: + lv.meter_set_indicator_value(meter, indicator, start_value) + else: + lv.meter_set_indicator_start_value(meter, indicator, start_value) + if end_value is not None: + lv.meter_set_indicator_end_value(meter, indicator, end_value) diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py new file mode 100644 index 0000000000..1af4ed6e05 --- /dev/null +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -0,0 +1,146 @@ +from esphome import config_validation as cv +from esphome.const import CONF_BUTTON, CONF_ID, CONF_ITEMS, CONF_TEXT +from esphome.core import ID +from esphome.cpp_generator import new_Pvariable, static_const_array +from esphome.cpp_types import nullptr + +from ..defines import ( + CONF_BODY, + CONF_BUTTON_STYLE, + CONF_BUTTONS, + CONF_CLOSE_BUTTON, + CONF_MSGBOXES, + CONF_TITLE, + TYPE_FLEX, + literal, +) +from ..helpers import add_lv_use, lvgl_components_required +from ..lv_validation import lv_bool, lv_pct, lv_text +from ..lvcode import ( + EVENT_ARG, + LambdaContext, + LocalVariable, + lv_add, + lv_assign, + lv_expr, + lv_obj, + lv_Pvariable, +) +from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema, part_schema +from ..styles import TOP_LAYER +from ..types import LV_EVENT, char_ptr, lv_obj_t +from . import Widget, set_obj_properties +from .button import button_spec +from .buttonmatrix import ( + BUTTONMATRIX_BUTTON_SCHEMA, + CONF_BUTTON_TEXT_LIST_ID, + buttonmatrix_spec, + get_button_data, + lv_buttonmatrix_t, + set_btn_data, +) +from .label import CONF_LABEL +from .obj import obj_spec + +CONF_MSGBOX = "msgbox" +MSGBOX_SCHEMA = container_schema( + obj_spec, + STYLE_SCHEMA.extend( + { + cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t), + cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, + cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA, + cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA), + cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool, + cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), + } + ), +) + + +async def msgbox_to_code(conf): + """ + Construct a message box. This consists of a full-screen translucent background enclosing a centered container + with an optional title, body, close button and a button matrix. And any other widgets the user cares to add + :param conf: The config data + :return: code to add to the init lambda + """ + add_lv_use( + TYPE_FLEX, + CONF_BUTTON, + CONF_LABEL, + CONF_MSGBOX, + *buttonmatrix_spec.get_uses(), + *button_spec.get_uses(), + ) + lvgl_components_required.add("BUTTONMATRIX") + messagebox_id = conf[CONF_ID] + outer_id = f"{messagebox_id.id}_outer" + outer = lv_Pvariable(lv_obj_t, messagebox_id.id + "_outer") + buttonmatrix = new_Pvariable( + ID( + f"{messagebox_id.id}_buttonmatrix_", + is_declaration=True, + type=lv_buttonmatrix_t, + ) + ) + msgbox = lv_Pvariable(lv_obj_t, messagebox_id.id) + outer_widget = Widget.create(outer_id, outer, obj_spec, conf) + outer_widget.move_to_foreground = True + msgbox_widget = Widget.create(messagebox_id, msgbox, obj_spec, conf) + msgbox_widget.outer = outer_widget + buttonmatrix_widget = Widget.create( + str(buttonmatrix), buttonmatrix, buttonmatrix_spec, conf + ) + text_list, ctrl_list, width_list, _ = await get_button_data( + (conf,), buttonmatrix_widget + ) + text_id = conf[CONF_BUTTON_TEXT_LIST_ID] + text_list = static_const_array(text_id, text_list) + text = await lv_text.process(conf[CONF_BODY].get(CONF_TEXT, "")) + title = await lv_text.process(conf[CONF_TITLE].get(CONF_TEXT, "")) + close_button = conf[CONF_CLOSE_BUTTON] + lv_assign(outer, lv_expr.obj_create(TOP_LAYER)) + lv_obj.set_width(outer, lv_pct(100)) + lv_obj.set_height(outer, lv_pct(100)) + lv_obj.set_style_bg_opa(outer, 128, 0) + lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0) + lv_obj.set_style_border_width(outer, 0, 0) + lv_obj.set_style_pad_all(outer, 0, 0) + lv_obj.set_style_radius(outer, 0, 0) + outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") + lv_assign( + msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button) + ) + lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0) + lv_add(buttonmatrix.set_obj(lv_expr.msgbox_get_btns(msgbox))) + if button_style := conf.get(CONF_BUTTON_STYLE): + button_style = {CONF_ITEMS: button_style} + await set_obj_properties(buttonmatrix_widget, button_style) + await set_obj_properties(msgbox_widget, conf) + async with LambdaContext(EVENT_ARG, where=messagebox_id) as close_action: + outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN") + if close_button: + with LocalVariable( + "close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox) + ) as close_btn: + lv_obj.remove_event_cb(close_btn, nullptr) + lv_obj.add_event_cb( + close_btn, + await close_action.get_lambda(), + LV_EVENT.CLICKED, + nullptr, + ) + else: + lv_obj.add_event_cb( + outer, await close_action.get_lambda(), LV_EVENT.CLICKED, nullptr + ) + + if len(ctrl_list) != 0 or len(width_list) != 0: + set_btn_data(buttonmatrix.obj, ctrl_list, width_list) + + +async def msgboxes_to_code(config): + for conf in config.get(CONF_MSGBOXES, ()): + await msgbox_to_code(conf) diff --git a/esphome/components/lvgl/widgets/obj.py b/esphome/components/lvgl/widgets/obj.py new file mode 100644 index 0000000000..20a24c86f6 --- /dev/null +++ b/esphome/components/lvgl/widgets/obj.py @@ -0,0 +1,28 @@ +from esphome import automation + +from ..automation import update_to_code +from ..defines import CONF_MAIN, CONF_OBJ +from ..schemas import create_modify_schema +from ..types import ObjUpdateAction, WidgetType, lv_obj_t + + +class ObjType(WidgetType): + """ + The base LVGL object. All other widgets inherit from this. + """ + + def __init__(self): + super().__init__(CONF_OBJ, lv_obj_t, (CONF_MAIN,), schema={}, modify_schema={}) + + async def to_code(self, w, config): + return [] + + +obj_spec = ObjType() + + +@automation.register_action( + "lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec) +) +async def obj_update_to_code(config, action_id, template_arg, args): + return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/page.py b/esphome/components/lvgl/widgets/page.py new file mode 100644 index 0000000000..0e84ab6791 --- /dev/null +++ b/esphome/components/lvgl/widgets/page.py @@ -0,0 +1,157 @@ +from esphome import automation, codegen as cg +from esphome.automation import Trigger +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID + +from ..defines import ( + CONF_ANIMATION, + CONF_LVGL_ID, + CONF_PAGE, + CONF_PAGE_WRAP, + CONF_SKIP, + LV_ANIM, + literal, +) +from ..lv_validation import lv_bool, lv_milliseconds +from ..lvcode import ( + EVENT_ARG, + LVGL_COMP_ARG, + LambdaContext, + add_line_marks, + lv_add, + lvgl_comp, +) +from ..schemas import LVGL_SCHEMA +from ..types import LvglAction, lv_page_t +from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties + +CONF_ON_LOAD = "on_load" +CONF_ON_UNLOAD = "on_unload" + +PAGE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_SKIP, default=False): lv_bool, + cv.Optional(CONF_ON_LOAD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()), + } + ), + cv.Optional(CONF_ON_UNLOAD): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Trigger.template()), + } + ), + } +) + + +class PageType(WidgetType): + def __init__(self): + super().__init__( + CONF_PAGE, + lv_page_t, + (), + PAGE_SCHEMA, + modify_schema={}, + ) + + async def to_code(self, w: Widget, config: dict): + return [] + + +SHOW_SCHEMA = LVGL_SCHEMA.extend( + { + cv.Optional(CONF_ANIMATION, default="NONE"): LV_ANIM.one_of, + cv.Optional(CONF_TIME, default="50ms"): lv_milliseconds, + } +) + +page_spec = PageType() + + +@automation.register_action( + "lvgl.page.next", + LvglAction, + SHOW_SCHEMA, +) +async def page_next_to_code(config, action_id, template_arg, args): + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_next_page(animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +@automation.register_action( + "lvgl.page.previous", + LvglAction, + SHOW_SCHEMA, +) +async def page_previous_to_code(config, action_id, template_arg, args): + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_prev_page(animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +@automation.register_action( + "lvgl.page.show", + LvglAction, + cv.maybe_simple_value( + SHOW_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.use_id(lv_page_t), + } + ), + key=CONF_ID, + ), +) +async def page_show_to_code(config, action_id, template_arg, args): + widget = await cg.get_variable(config[CONF_ID]) + animation = await LV_ANIM.process(config[CONF_ANIMATION]) + time = await lv_milliseconds.process(config[CONF_TIME]) + async with LambdaContext(LVGL_COMP_ARG) as context: + add_line_marks(action_id) + lv_add(lvgl_comp.show_page(widget.index, animation, time)) + var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) + await cg.register_parented(var, config[CONF_LVGL_ID]) + return var + + +async def add_pages(lv_component, config): + lv_add(lv_component.set_page_wrap(config[CONF_PAGE_WRAP])) + for pconf in config.get(CONF_PAGES, ()): + id = pconf[CONF_ID] + skip = pconf[CONF_SKIP] + var = cg.new_Pvariable(id, skip) + page = Widget.create(id, var, page_spec, pconf) + lv_add(lv_component.add_page(var)) + # Set outer config first + await set_obj_properties(page, config) + await set_obj_properties(page, pconf) + await add_widgets(page, pconf) + + +async def generate_page_triggers(lv_component, config): + for pconf in config.get(CONF_PAGES, ()): + page = (await get_widgets(pconf))[0] + for ev in (CONF_ON_LOAD, CONF_ON_UNLOAD): + for loaded in pconf.get(ev, ()): + trigger = cg.new_Pvariable(loaded[CONF_TRIGGER_ID]) + await automation.build_automation(trigger, [], loaded) + async with LambdaContext(EVENT_ARG, where=id) as context: + lv_add(trigger.trigger()) + lv_add( + lv_component.add_event_cb( + page.obj, + await context.get_lambda(), + literal(f"LV_EVENT_SCREEN_{ev[3:].upper()}_START"), + ) + ) diff --git a/esphome/components/lvgl/widgets/roller.py b/esphome/components/lvgl/widgets/roller.py new file mode 100644 index 0000000000..50fdf6113c --- /dev/null +++ b/esphome/components/lvgl/widgets/roller.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_MODE, CONF_OPTIONS + +from ..defines import ( + CONF_ANIMATED, + CONF_MAIN, + CONF_SELECTED, + CONF_SELECTED_INDEX, + CONF_VISIBLE_ROW_COUNT, + ROLLER_MODES, + literal, +) +from ..lv_validation import animated, lv_int, option_string +from ..lvcode import lv +from ..types import LvSelect +from . import WidgetType +from .label import CONF_LABEL + +CONF_ROLLER = "roller" +lv_roller_t = LvSelect("lv_roller_t") + +ROLLER_BASE_SCHEMA = cv.Schema( + { + cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_), + cv.Optional(CONF_VISIBLE_ROW_COUNT): lv_int, + } +) + +ROLLER_SCHEMA = ROLLER_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPTIONS): cv.ensure_list(option_string), + cv.Optional(CONF_MODE, default="NORMAL"): ROLLER_MODES.one_of, + } +) + +ROLLER_MODIFY_SCHEMA = ROLLER_BASE_SCHEMA.extend( + { + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + + +class RollerType(WidgetType): + def __init__(self): + super().__init__( + CONF_ROLLER, + lv_roller_t, + (CONF_MAIN, CONF_SELECTED), + ROLLER_SCHEMA, + ROLLER_MODIFY_SCHEMA, + ) + + async def to_code(self, w, config): + if options := config.get(CONF_OPTIONS): + mode = await ROLLER_MODES.process(config[CONF_MODE]) + text = cg.safe_exp("\n".join(options)) + lv.roller_set_options(w.obj, text, mode) + animopt = literal(config.get(CONF_ANIMATED) or "LV_ANIM_OFF") + if CONF_SELECTED_INDEX in config: + if selected := config[CONF_SELECTED_INDEX]: + value = await lv_int.process(selected) + lv.roller_set_selected(w.obj, value, animopt) + await w.set_property( + CONF_VISIBLE_ROW_COUNT, + await lv_int.process(config.get(CONF_VISIBLE_ROW_COUNT)), + ) + + @property + def animated(self): + return True + + def get_uses(self): + return (CONF_LABEL,) + + +roller_spec = RollerType() diff --git a/esphome/components/lvgl/widgets/slider.py b/esphome/components/lvgl/widgets/slider.py new file mode 100644 index 0000000000..d5017668e4 --- /dev/null +++ b/esphome/components/lvgl/widgets/slider.py @@ -0,0 +1,63 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE + +from ..defines import ( + BAR_MODES, + CONF_ANIMATED, + CONF_INDICATOR, + CONF_KNOB, + CONF_MAIN, + literal, +) +from ..helpers import add_lv_use +from ..lv_validation import animated, get_start_value, lv_float +from ..lvcode import lv +from ..types import LvNumber, NumberType +from . import Widget +from .lv_bar import CONF_BAR + +CONF_SLIDER = "slider" +SLIDER_MODIFY_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + +SLIDER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_, + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_, + cv.Optional(CONF_MODE, default="NORMAL"): BAR_MODES.one_of, + cv.Optional(CONF_ANIMATED, default=True): animated, + } +) + + +class SliderType(NumberType): + def __init__(self): + super().__init__( + CONF_SLIDER, + LvNumber("lv_slider_t"), + parts=(CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + schema=SLIDER_SCHEMA, + modify_schema=SLIDER_MODIFY_SCHEMA, + ) + + @property + def animated(self): + return True + + async def to_code(self, w: Widget, config): + add_lv_use(CONF_BAR) + if CONF_MIN_VALUE in config: + # not modify case + lv.slider_set_range(w.obj, config[CONF_MIN_VALUE], config[CONF_MAX_VALUE]) + lv.slider_set_mode(w.obj, literal(config[CONF_MODE])) + value = await get_start_value(config) + if value is not None: + lv.slider_set_value(w.obj, value, literal(config[CONF_ANIMATED])) + + +slider_spec = SliderType() diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py new file mode 100644 index 0000000000..b84dc7cd23 --- /dev/null +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -0,0 +1,178 @@ +from esphome import automation +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE + +from ..automation import action_to_code, update_to_code +from ..defines import ( + CONF_CURSOR, + CONF_DECIMAL_PLACES, + CONF_DIGITS, + CONF_MAIN, + CONF_ROLLOVER, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_TEXTAREA_PLACEHOLDER, +) +from ..lv_validation import lv_bool, lv_float +from ..lvcode import lv +from ..types import LvNumber, ObjUpdateAction +from . import Widget, WidgetType, get_widgets +from .label import CONF_LABEL +from .textarea import CONF_TEXTAREA + +CONF_SPINBOX = "spinbox" + +lv_spinbox_t = LvNumber("lv_spinbox_t") + +SPIN_ACTIONS = ( + "INCREMENT", + "DECREMENT", + "STEP_NEXT", + "STEP_PREV", + "CLEAR", +) + + +def validate_spinbox(config): + max_val = 2**31 - 1 + min_val = -1 - max_val + range_from = int(config[CONF_RANGE_FROM]) + range_to = int(config[CONF_RANGE_TO]) + step = int(config[CONF_STEP]) + if ( + range_from > max_val + or range_from < min_val + or range_to > max_val + or range_to < min_val + ): + raise cv.Invalid("Range outside allowed limits") + if step <= 0 or step >= (range_to - range_from) / 2: + raise cv.Invalid("Invalid step value") + if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]: + raise cv.Invalid("Number of digits must exceed number of decimal places") + return config + + +SPINBOX_SCHEMA = cv.Schema( + { + cv.Optional(CONF_VALUE): lv_float, + cv.Optional(CONF_RANGE_FROM, default=0): cv.float_, + cv.Optional(CONF_RANGE_TO, default=100): cv.float_, + cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10), + cv.Optional(CONF_STEP, default=1.0): cv.positive_float, + cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6), + cv.Optional(CONF_ROLLOVER, default=False): lv_bool, + } +).add_extra(validate_spinbox) + + +SPINBOX_MODIFY_SCHEMA = { + cv.Required(CONF_VALUE): lv_float, +} + + +class SpinboxType(WidgetType): + def __init__(self): + super().__init__( + CONF_SPINBOX, + lv_spinbox_t, + ( + CONF_MAIN, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_CURSOR, + CONF_TEXTAREA_PLACEHOLDER, + ), + SPINBOX_SCHEMA, + SPINBOX_MODIFY_SCHEMA, + ) + + async def to_code(self, w: Widget, config): + if CONF_DIGITS in config: + digits = config[CONF_DIGITS] + scale = 10 ** config[CONF_DECIMAL_PLACES] + range_from = int(config[CONF_RANGE_FROM]) * scale + range_to = int(config[CONF_RANGE_TO]) * scale + step = int(config[CONF_STEP]) * scale + w.scale = scale + w.step = step + w.range_to = range_to + w.range_from = range_from + lv.spinbox_set_range(w.obj, range_from, range_to) + await w.set_property(CONF_STEP, step) + await w.set_property(CONF_ROLLOVER, config) + lv.spinbox_set_digit_format( + w.obj, digits, digits - config[CONF_DECIMAL_PLACES] + ) + if (value := config.get(CONF_VALUE)) is not None: + lv.spinbox_set_value(w.obj, await lv_float.process(value)) + + def get_scale(self, config): + return 10 ** config[CONF_DECIMAL_PLACES] + + def get_uses(self): + return CONF_TEXTAREA, CONF_LABEL + + def get_max(self, config: dict): + return config[CONF_RANGE_TO] + + def get_min(self, config: dict): + return config[CONF_RANGE_FROM] + + def get_step(self, config: dict): + return config[CONF_STEP] + + +spinbox_spec = SpinboxType() + + +@automation.register_action( + "lvgl.spinbox.increment", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + }, + key=CONF_ID, + ), +) +async def spinbox_increment(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_increment(w: Widget): + lv.spinbox_increment(w.obj) + + return await action_to_code(widgets, do_increment, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.spinbox.decrement", + ObjUpdateAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + }, + key=CONF_ID, + ), +) +async def spinbox_decrement(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_increment(w: Widget): + lv.spinbox_decrement(w.obj) + + return await action_to_code(widgets, do_increment, action_id, template_arg, args) + + +@automation.register_action( + "lvgl.spinbox.update", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), + cv.Required(CONF_VALUE): lv_float, + } + ), +) +async def spinbox_update_to_code(config, action_id, template_arg, args): + return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/spinner.py b/esphome/components/lvgl/widgets/spinner.py new file mode 100644 index 0000000000..2940feb594 --- /dev/null +++ b/esphome/components/lvgl/widgets/spinner.py @@ -0,0 +1,43 @@ +import esphome.config_validation as cv +from esphome.cpp_generator import MockObjClass + +from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME +from ..lv_validation import angle +from ..lvcode import lv_expr +from ..types import LvType +from . import Widget, WidgetType +from .arc import CONF_ARC + +CONF_SPINNER = "spinner" + +SPINNER_SCHEMA = cv.Schema( + { + cv.Required(CONF_ARC_LENGTH): angle, + cv.Required(CONF_SPIN_TIME): cv.positive_time_period_milliseconds, + } +) + + +class SpinnerType(WidgetType): + def __init__(self): + super().__init__( + CONF_SPINNER, + LvType("lv_spinner_t"), + (CONF_MAIN, CONF_INDICATOR), + SPINNER_SCHEMA, + {}, + ) + + async def to_code(self, w: Widget, config): + return [] + + def get_uses(self): + return (CONF_ARC,) + + def obj_creator(self, parent: MockObjClass, config: dict): + spin_time = config[CONF_SPIN_TIME].total_milliseconds + arc_length = config[CONF_ARC_LENGTH] // 10 + return lv_expr.call("spinner_create", parent, spin_time, arc_length) + + +spinner_spec = SpinnerType() diff --git a/esphome/components/lvgl/widgets/switch.py b/esphome/components/lvgl/widgets/switch.py new file mode 100644 index 0000000000..a7c1356bf2 --- /dev/null +++ b/esphome/components/lvgl/widgets/switch.py @@ -0,0 +1,20 @@ +from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN +from ..types import LvBoolean +from . import WidgetType + +CONF_SWITCH = "switch" + + +class SwitchType(WidgetType): + def __init__(self): + super().__init__( + CONF_SWITCH, + LvBoolean("lv_switch_t"), + (CONF_MAIN, CONF_INDICATOR, CONF_KNOB), + ) + + async def to_code(self, w, config): + return [] + + +switch_spec = SwitchType() diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py new file mode 100644 index 0000000000..226fc3f286 --- /dev/null +++ b/esphome/components/lvgl/widgets/tabview.py @@ -0,0 +1,114 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE +from esphome.cpp_generator import MockObjClass + +from ..automation import action_to_code +from ..defines import ( + CONF_ANIMATED, + CONF_MAIN, + CONF_TAB_ID, + CONF_TABS, + DIRECTIONS, + TYPE_FLEX, + literal, +) +from ..lv_validation import animated, lv_int, size +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr +from ..schemas import container_schema, part_schema +from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr +from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from .buttonmatrix import buttonmatrix_spec +from .obj import obj_spec + +CONF_TABVIEW = "tabview" +CONF_TAB_STYLE = "tab_style" + +lv_tab_t = LvType("lv_obj_t") + +TABVIEW_SCHEMA = cv.Schema( + { + cv.Required(CONF_TABS): cv.ensure_list( + container_schema( + obj_spec, + { + cv.Required(CONF_NAME): cv.string, + cv.GenerateID(): cv.declare_id(lv_tab_t), + }, + ) + ), + cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of, + cv.Optional(CONF_SIZE, default="10%"): size, + } +) + + +class TabviewType(WidgetType): + def __init__(self): + super().__init__( + CONF_TABVIEW, + LvType( + "lv_tabview_t", + largs=[(lv_obj_t_ptr, "tab")], + lvalue=lambda w: lv_expr.obj_get_child( + lv_expr.tabview_get_content(w.obj), + lv_expr.tabview_get_tab_act(w.obj), + ), + has_on_value=True, + ), + parts=(CONF_MAIN,), + schema=TABVIEW_SCHEMA, + modify_schema={}, + ) + + def get_uses(self): + return "btnmatrix", TYPE_FLEX + + async def to_code(self, w: Widget, config: dict): + for tab_conf in config[CONF_TABS]: + w_id = tab_conf[CONF_ID] + tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t) + tab_widget = Widget.create(w_id, tab_obj, obj_spec) + lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME])) + await set_obj_properties(tab_widget, tab_conf) + await add_widgets(tab_widget, tab_conf) + if button_style := config.get(CONF_TAB_STYLE): + with LocalVariable( + "tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj) + ) as btnmatrix_obj: + await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style) + + def obj_creator(self, parent: MockObjClass, config: dict): + return lv_expr.call( + "tabview_create", + parent, + literal(config[CONF_POSITION]), + literal(config[CONF_SIZE]), + ) + + +tabview_spec = TabviewType() + + +@automation.register_action( + "lvgl.tabview.select", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(tabview_spec.w_type), + cv.Optional(CONF_ANIMATED, default=False): animated, + cv.Required(CONF_INDEX): lv_int, + }, + ).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)), +) +async def tabview_select(config, action_id, template_arg, args): + widget = await get_widgets(config) + index = config[CONF_INDEX] + + async def do_select(w: Widget): + lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED])) + lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + + return await action_to_code(widget, do_select, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/textarea.py b/esphome/components/lvgl/widgets/textarea.py new file mode 100644 index 0000000000..23d50b3894 --- /dev/null +++ b/esphome/components/lvgl/widgets/textarea.py @@ -0,0 +1,66 @@ +import esphome.config_validation as cv +from esphome.const import CONF_MAX_LENGTH, CONF_TEXT + +from ..defines import ( + CONF_ACCEPTED_CHARS, + CONF_CURSOR, + CONF_MAIN, + CONF_ONE_LINE, + CONF_PASSWORD_MODE, + CONF_PLACEHOLDER_TEXT, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_TEXTAREA_PLACEHOLDER, +) +from ..lv_validation import lv_bool, lv_int, lv_text +from ..schemas import TEXT_SCHEMA +from ..types import LvText +from . import Widget, WidgetType + +CONF_TEXTAREA = "textarea" + +lv_textarea_t = LvText("lv_textarea_t") + +TEXTAREA_SCHEMA = TEXT_SCHEMA.extend( + { + cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, + cv.Optional(CONF_ACCEPTED_CHARS): lv_text, + cv.Optional(CONF_ONE_LINE): lv_bool, + cv.Optional(CONF_PASSWORD_MODE): lv_bool, + cv.Optional(CONF_MAX_LENGTH): lv_int, + } +) + + +class TextareaType(WidgetType): + def __init__(self): + super().__init__( + CONF_TEXTAREA, + lv_textarea_t, + ( + CONF_MAIN, + CONF_SCROLLBAR, + CONF_SELECTED, + CONF_CURSOR, + CONF_TEXTAREA_PLACEHOLDER, + ), + TEXTAREA_SCHEMA, + ) + + async def to_code(self, w: Widget, config: dict): + for prop in (CONF_TEXT, CONF_PLACEHOLDER_TEXT, CONF_ACCEPTED_CHARS): + if (value := config.get(prop)) is not None: + await w.set_property(prop, await lv_text.process(value)) + await w.set_property( + CONF_MAX_LENGTH, await lv_int.process(config.get(CONF_MAX_LENGTH)) + ) + await w.set_property( + CONF_PASSWORD_MODE, + await lv_bool.process(config.get(CONF_PASSWORD_MODE)), + ) + await w.set_property( + CONF_ONE_LINE, await lv_bool.process(config.get(CONF_ONE_LINE)) + ) + + +textarea_spec = TextareaType() diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py new file mode 100644 index 0000000000..05259fbd3c --- /dev/null +++ b/esphome/components/lvgl/widgets/tileview.py @@ -0,0 +1,122 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_ROW + +from ..automation import action_to_code +from ..defines import ( + CONF_ANIMATED, + CONF_COLUMN, + CONF_DIR, + CONF_MAIN, + CONF_TILE_ID, + CONF_TILES, + TILE_DIRECTIONS, + literal, +) +from ..lv_validation import animated, lv_int +from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable +from ..schemas import container_schema +from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr +from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from .obj import obj_spec + +CONF_TILEVIEW = "tileview" + +lv_tile_t = LvType("lv_tileview_tile_t") + +lv_tileview_t = LvType( + "lv_tileview_t", + largs=[(lv_obj_t_ptr, "tile")], + lvalue=lambda w: w.get_property("tile_act"), + has_on_value=True, +) + +tile_spec = WidgetType("lv_tileview_tile_t", lv_tile_t, (CONF_MAIN,), {}) + +TILEVIEW_SCHEMA = cv.Schema( + { + cv.Required(CONF_TILES): cv.ensure_list( + container_schema( + obj_spec, + { + cv.Required(CONF_ROW): lv_int, + cv.Required(CONF_COLUMN): lv_int, + cv.GenerateID(): cv.declare_id(lv_tile_t), + cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of, + }, + ) + ), + } +) + + +class TileviewType(WidgetType): + def __init__(self): + super().__init__( + CONF_TILEVIEW, + lv_tileview_t, + (CONF_MAIN,), + schema=TILEVIEW_SCHEMA, + modify_schema={}, + ) + + async def to_code(self, w: Widget, config: dict): + for tile_conf in config.get(CONF_TILES, ()): + w_id = tile_conf[CONF_ID] + tile_obj = lv_Pvariable(lv_obj_t, w_id) + tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) + dirs = tile_conf[CONF_DIR] + if isinstance(dirs, list): + dirs = "|".join(dirs) + lv_assign( + tile_obj, + lv_expr.tileview_add_tile( + w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs) + ), + ) + await set_obj_properties(tile, tile_conf) + await add_widgets(tile, tile_conf) + + +tileview_spec = TileviewType() + + +def tile_select_validate(config): + row = CONF_ROW in config + column = CONF_COLUMN in config + tile = CONF_TILE_ID in config + if tile and (row or column) or not tile and not (row and column): + raise cv.Invalid("Specify either a tile id, or both a row and a column") + return config + + +@automation.register_action( + "lvgl.tileview.select", + ObjUpdateAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lv_tileview_t), + cv.Optional(CONF_ANIMATED, default=False): animated, + cv.Optional(CONF_ROW): lv_int, + cv.Optional(CONF_COLUMN): lv_int, + cv.Optional(CONF_TILE_ID): cv.use_id(lv_tile_t), + }, + ).add_extra(tile_select_validate), +) +async def tileview_select(config, action_id, template_arg, args): + widgets = await get_widgets(config) + + async def do_select(w: Widget): + if tile := config.get(CONF_TILE_ID): + tile = await cg.get_variable(tile) + lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED])) + else: + row = await lv_int.process(config[CONF_ROW]) + column = await lv_int.process(config[CONF_COLUMN]) + lv_obj.set_tile_id( + widgets[0].obj, column, row, literal(config[CONF_ANIMATED]) + ) + lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr) + + return await action_to_code(widgets, do_select, action_id, template_arg, args) diff --git a/esphome/components/matrix_keypad/matrix_keypad.cpp b/esphome/components/matrix_keypad/matrix_keypad.cpp index 902e574846..f62c75c869 100644 --- a/esphome/components/matrix_keypad/matrix_keypad.cpp +++ b/esphome/components/matrix_keypad/matrix_keypad.cpp @@ -8,6 +8,7 @@ static const char *const TAG = "matrix_keypad"; void MatrixKeypad::setup() { for (auto *pin : this->rows_) { + pin->setup(); if (!has_diodes_) { pin->pin_mode(gpio::FLAG_INPUT); } else { @@ -15,6 +16,7 @@ void MatrixKeypad::setup() { } } for (auto *pin : this->columns_) { + pin->setup(); if (has_pulldowns_) { pin->pin_mode(gpio::FLAG_INPUT); } else { diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index 8ae4be6657..6a4d34b430 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -32,6 +32,12 @@ void MAX31856Sensor::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); ESP_LOGCONFIG(TAG, " Mains Filter: %s", (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); + if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) { + ESP_LOGCONFIG(TAG, " Thermocouple Type: Unknown"); + } else { + ESP_LOGCONFIG(TAG, " Thermocouple Type: %c", "BEJKNRST"[this->thermocouple_type_]); + } + LOG_UPDATE_INTERVAL(this); } @@ -129,7 +135,12 @@ void MAX31856Sensor::clear_fault_() { } void MAX31856Sensor::set_thermocouple_type_() { - MAX31856ThermocoupleType type = MAX31856_TCTYPE_K; + MAX31856ThermocoupleType type; + if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) { + type = MAX31856_TCTYPE_K; + } else { + type = this->thermocouple_type_; + } ESP_LOGCONFIG(TAG, "set_thermocouple_type_: 0x%02X", type); uint8_t t = this->read_register_(MAX31856_CR1_REG); t &= 0xF0; // mask off bottom 4 bits diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h index 4deb6bc855..8d64cfe8bc 100644 --- a/esphome/components/max31856/max31856.h +++ b/esphome/components/max31856/max31856.h @@ -50,7 +50,6 @@ enum MAX31856Registers { /** * Multiple types of thermocouples supported by the chip. - * Currently only K type implemented here. */ enum MAX31856ThermocoupleType { MAX31856_TCTYPE_B = 0b0000, // 0x00 @@ -78,11 +77,15 @@ class MAX31856Sensor : public sensor::Sensor, void setup() override; void dump_config() override; float get_setup_priority() const override; - void set_filter(MAX31856ConfigFilter filter) { filter_ = filter; } + void set_filter(MAX31856ConfigFilter filter) { this->filter_ = filter; } + void set_thermocouple_type(MAX31856ThermocoupleType thermocouple_type) { + this->thermocouple_type_ = thermocouple_type; + } void update() override; protected: MAX31856ConfigFilter filter_; + MAX31856ThermocoupleType thermocouple_type_; uint8_t read_register_(uint8_t reg); uint32_t read_register24_(uint8_t reg); diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 71f1f3bfa5..679e02b11d 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, spi +import esphome.config_validation as cv from esphome.const import ( CONF_MAINS_FILTER, + CONF_THERMOCOUPLE_TYPE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -15,8 +16,19 @@ MAX31856Sensor = max31856_ns.class_( MAX31865ConfigFilter = max31856_ns.enum("MAX31856ConfigFilter") FILTER = { - "50HZ": MAX31865ConfigFilter.FILTER_50HZ, - "60HZ": MAX31865ConfigFilter.FILTER_60HZ, + 50: MAX31865ConfigFilter.FILTER_50HZ, + 60: MAX31865ConfigFilter.FILTER_60HZ, +} +MAX31856ThermocoupleType = max31856_ns.enum("MAX31856ThermocoupleType") +THERMOCOUPLE_TYPE = { + "B": MAX31856ThermocoupleType.MAX31856_TCTYPE_B, + "E": MAX31856ThermocoupleType.MAX31856_TCTYPE_E, + "J": MAX31856ThermocoupleType.MAX31856_TCTYPE_J, + "K": MAX31856ThermocoupleType.MAX31856_TCTYPE_K, + "N": MAX31856ThermocoupleType.MAX31856_TCTYPE_N, + "R": MAX31856ThermocoupleType.MAX31856_TCTYPE_R, + "S": MAX31856ThermocoupleType.MAX31856_TCTYPE_S, + "T": MAX31856ThermocoupleType.MAX31856_TCTYPE_T, } CONFIG_SCHEMA = ( @@ -29,8 +41,15 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Optional(CONF_MAINS_FILTER, default="60HZ"): cv.enum( - FILTER, upper=True, space="" + cv.Optional(CONF_MAINS_FILTER, default="60Hz"): cv.All( + cv.frequency, cv.enum(FILTER, int=True) + ), + } + ) + .extend( + { + cv.Optional(CONF_THERMOCOUPLE_TYPE, default="K"): cv.enum( + THERMOCOUPLE_TYPE, upper=True, space="" ), } ) @@ -44,3 +63,4 @@ async def to_code(config): await cg.register_component(var, config) await spi.register_spi_device(var, config) cg.add(var.set_filter(config[CONF_MAINS_FILTER])) + cg.add(var.set_thermocouple_type(config[CONF_THERMOCOUPLE_TYPE])) diff --git a/esphome/components/mcp9600/sensor.py b/esphome/components/mcp9600/sensor.py index 8557c7205e..65ae5f2eec 100644 --- a/esphome/components/mcp9600/sensor.py +++ b/esphome/components/mcp9600/sensor.py @@ -1,14 +1,14 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import i2c, sensor +import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_THERMOCOUPLE_TYPE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) -CONF_THERMOCOUPLE_TYPE = "thermocouple_type" CONF_HOT_JUNCTION = "hot_junction" CONF_COLD_JUNCTION = "cold_junction" diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index fb90986314..dd68fbb93c 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,17 +1,17 @@ +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_component +import esphome.config_validation as cv from esphome.const import ( + CONF_DISABLED, CONF_ID, CONF_PORT, CONF_PROTOCOL, - CONF_SERVICES, CONF_SERVICE, + CONF_SERVICES, KEY_CORE, KEY_FRAMEWORK_VERSION, - CONF_DISABLED, ) -import esphome.codegen as cg -import esphome.config_validation as cv from esphome.core import CORE, coroutine_with_priority -from esphome.components.esp32 import add_idf_component CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] @@ -91,7 +91,7 @@ async def to_code(config): add_idf_component( name="mdns", repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.2.5", + ref="mdns-v1.3.2", path="components/mdns", ) diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index 320014e355..423cb065dc 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -1,20 +1,18 @@ from esphome import automation -import esphome.config_validation as cv -import esphome.codegen as cg - from esphome.automation import maybe_simple_id +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_ON_IDLE, CONF_ON_STATE, CONF_TRIGGER_ID, CONF_VOLUME, - CONF_ON_IDLE, ) from esphome.core import CORE from esphome.coroutine import coroutine_with_priority from esphome.cpp_helpers import setup_entity - CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index fc3ce7a764..f0e0a5dd31 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -7,30 +7,24 @@ namespace esphome { namespace media_player { -#define MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ACTION_CLASS, ACTION_COMMAND) \ - template class ACTION_CLASS : public Action, public Parented { \ - void play(Ts... x) override { \ - this->parent_->make_call().set_command(MediaPlayerCommand::MEDIA_PLAYER_COMMAND_##ACTION_COMMAND).perform(); \ - } \ - }; +template +class MediaPlayerCommandAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->make_call().set_command(Command).perform(); } +}; -#define MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(TRIGGER_CLASS, TRIGGER_STATE) \ - class TRIGGER_CLASS : public Trigger<> { \ - public: \ - explicit TRIGGER_CLASS(MediaPlayer *player) { \ - player->add_on_state_callback([this, player]() { \ - if (player->state == MediaPlayerState::MEDIA_PLAYER_STATE_##TRIGGER_STATE) \ - this->trigger(); \ - }); \ - } \ - }; - -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PlayAction, PLAY) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(PauseAction, PAUSE) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(StopAction, STOP) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(ToggleAction, TOGGLE) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeUpAction, VOLUME_UP) -MEDIA_PLAYER_SIMPLE_COMMAND_ACTION(VolumeDownAction, VOLUME_DOWN) +template +using PlayAction = MediaPlayerCommandAction; +template +using PauseAction = MediaPlayerCommandAction; +template +using StopAction = MediaPlayerCommandAction; +template +using ToggleAction = MediaPlayerCommandAction; +template +using VolumeUpAction = MediaPlayerCommandAction; +template +using VolumeDownAction = MediaPlayerCommandAction; template class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) @@ -49,10 +43,20 @@ class StateTrigger : public Trigger<> { } }; -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(IdleTrigger, IDLE) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PlayTrigger, PLAYING) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(PauseTrigger, PAUSED) -MEDIA_PLAYER_SIMPLE_STATE_TRIGGER(AnnouncementTrigger, ANNOUNCING) +template class MediaPlayerStateTrigger : public Trigger<> { + public: + explicit MediaPlayerStateTrigger(MediaPlayer *player) { + player->add_on_state_callback([this, player]() { + if (player->state == State) + this->trigger(); + }); + } +}; + +using IdleTrigger = MediaPlayerStateTrigger; +using PlayTrigger = MediaPlayerStateTrigger; +using PauseTrigger = MediaPlayerStateTrigger; +using AnnouncementTrigger = MediaPlayerStateTrigger; template class IsIdleCondition : public Condition, public Parented { public: diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index 77746e1808..78b3ed6216 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -27,6 +27,19 @@ enum MediaPlayerCommand : uint8_t { }; const char *media_player_command_to_string(MediaPlayerCommand command); +enum class MediaPlayerFormatPurpose : uint8_t { + PURPOSE_DEFAULT = 0, + PURPOSE_ANNOUNCEMENT = 1, +}; + +struct MediaPlayerSupportedFormat { + std::string format; + uint32_t sample_rate; + uint32_t num_channels; + MediaPlayerFormatPurpose purpose; + uint32_t sample_bytes; +}; + class MediaPlayer; class MediaPlayerTraits { @@ -37,8 +50,11 @@ class MediaPlayerTraits { bool get_supports_pause() const { return this->supports_pause_; } + std::vector &get_supported_formats() { return this->supported_formats_; } + protected: bool supports_pause_{false}; + std::vector supported_formats_{}; }; class MediaPlayerCall { diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index c2faca25f4..a8aa590951 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -1,39 +1,31 @@ -import logging - -import json import hashlib -from urllib.parse import urljoin +import json +import logging from pathlib import Path -import requests +from urllib.parse import urljoin -import esphome.config_validation as cv -import esphome.codegen as cg - -from esphome.core import CORE, HexInt - -from esphome.components import esp32, microphone -from esphome import automation, git, external_files +from esphome import automation, external_files, git from esphome.automation import register_action, register_condition - - +import esphome.codegen as cg +from esphome.components import esp32, microphone +import esphome.config_validation as cv from esphome.const import ( - __version__, + CONF_FILE, CONF_ID, CONF_MICROPHONE, CONF_MODEL, - CONF_URL, - CONF_FILE, + CONF_PASSWORD, CONF_PATH, + CONF_RAW_DATA_ID, CONF_REF, CONF_REFRESH, CONF_TYPE, + CONF_URL, CONF_USERNAME, - CONF_PASSWORD, - CONF_RAW_DATA_ID, TYPE_GIT, TYPE_LOCAL, ) - +from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) @@ -174,36 +166,16 @@ def _convert_manifest_v1_to_v2(v1_manifest): CONF_SLIDING_WINDOW_AVERAGE_SIZE ] del v2_manifest[KEY_MICRO][CONF_SLIDING_WINDOW_AVERAGE_SIZE] - v2_manifest[KEY_MICRO][ - CONF_TENSOR_ARENA_SIZE - ] = 45672 # Original Inception-based V1 manifest models require a minimum of 45672 bytes - v2_manifest[KEY_MICRO][ - CONF_FEATURE_STEP_SIZE - ] = 20 # Original Inception-based V1 manifest models use a 20 ms feature step size + + # Original Inception-based V1 manifest models require a minimum of 45672 bytes + v2_manifest[KEY_MICRO][CONF_TENSOR_ARENA_SIZE] = 45672 + + # Original Inception-based V1 manifest models use a 20 ms feature step size + v2_manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE] = 20 return v2_manifest -def _download_file(url: str, path: Path) -> bytes: - if not external_files.has_remote_file_changed(url, path): - _LOGGER.debug("Remote file has not changed, skipping download") - return path.read_bytes() - - try: - req = requests.get( - url, - timeout=external_files.NETWORK_TIMEOUT, - headers={"User-agent": f"ESPHome/{__version__} (https://esphome.io)"}, - ) - req.raise_for_status() - except requests.exceptions.RequestException as e: - raise cv.Invalid(f"Could not download file from {url}: {e}") from e - - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(req.content) - return req.content - - def _validate_manifest_version(manifest_data): if manifest_version := manifest_data.get(KEY_VERSION): if manifest_version == 1: @@ -228,7 +200,7 @@ def _process_http_source(config): json_path = path / "manifest.json" - json_contents = _download_file(url, json_path) + json_contents = external_files.download_content(url, json_path) manifest_data = json.loads(json_contents) if not isinstance(manifest_data, dict): @@ -239,7 +211,7 @@ def _process_http_source(config): model_path = path / model - _download_file(str(model_url), model_path) + external_files.download_content(str(model_url), model_path) return config @@ -502,7 +474,7 @@ async def to_code(config): ) cg.add(var.set_features_step_size(manifest[KEY_MICRO][CONF_FEATURE_STEP_SIZE])) - cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.0.0") + cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.1.0") MICRO_WAKE_WORD_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(MicroWakeWord)}) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index 013fa2ce6e..d0d2e2df05 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -148,7 +148,7 @@ WakeWordModel::WakeWordModel(const uint8_t *model_start, float probability_cutof }; bool WakeWordModel::determine_detected() { - int32_t sum = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { sum += prob; } @@ -175,12 +175,14 @@ VADModel::VADModel(const uint8_t *model_start, float probability_cutoff, size_t }; bool VADModel::determine_detected() { - uint8_t max = 0; + uint32_t sum = 0; for (auto &prob : this->recent_streaming_probabilities_) { - max = std::max(prob, max); + sum += prob; } - return max > this->probability_cutoff_; + float sliding_window_average = static_cast(sum) / static_cast(255 * this->sliding_window_size_); + + return sliding_window_average > this->probability_cutoff_; } } // namespace micro_wake_word diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index e01a10e15c..914ad80bea 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -1,6 +1,9 @@ #pragma once -#include "esphome/core/entity_base.h" +#include +#include +#include +#include #include "esphome/core/helpers.h" namespace esphome { diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index a14d7f2f80..ed2fc6c826 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -70,72 +70,62 @@ void MICS4514Component::update() { if (this->carbon_monoxide_sensor_ != nullptr) { float co = 0.0f; - if (red_f <= 0.425f) { - co = (0.425f - red_f) / 0.000405f; - if (co < 1.0f) - co = 0.0f; - if (co > 1000.0f) - co = 1000.0f; + if (red_f > 3.4f) { + co = 0.0; + } else if (red_f < 0.01) { + co = 1000.0; + } else { + co = 4.2 / pow(red_f, 1.2); } this->carbon_monoxide_sensor_->publish_state(co); } if (this->nitrogen_dioxide_sensor_ != nullptr) { float nitrogendioxide = 0.0f; - if (ox_f >= 1.1f) { - nitrogendioxide = (ox_f - 0.045f) / 6.13f; - if (nitrogendioxide < 0.1f) - nitrogendioxide = 0.0f; - if (nitrogendioxide > 10.0f) - nitrogendioxide = 10.0f; + if (ox_f < 0.3f) { + nitrogendioxide = 0.0; + } else { + nitrogendioxide = 0.164 * pow(ox_f, 0.975); } this->nitrogen_dioxide_sensor_->publish_state(nitrogendioxide); } if (this->methane_sensor_ != nullptr) { float methane = 0.0f; - if (red_f <= 0.786f) { - methane = (0.786f - red_f) / 0.000023f; - if (methane < 1000.0f) - methane = 0.0f; - if (methane > 25000.0f) - methane = 25000.0f; + if (red_f > 0.9f || red_f < 0.5) { // outside the range->unlikely + methane = 0.0; + } else { + methane = 630 / pow(red_f, 4.4); } this->methane_sensor_->publish_state(methane); } if (this->ethanol_sensor_ != nullptr) { float ethanol = 0.0f; - if (red_f <= 0.306f) { - ethanol = (0.306f - red_f) / 0.00057f; - if (ethanol < 10.0f) - ethanol = 0.0f; - if (ethanol > 500.0f) - ethanol = 500.0f; + if (red_f > 1.0f || red_f < 0.02) { // outside the range->unlikely + ethanol = 0.0; + } else { + ethanol = 1.52 / pow(red_f, 1.55); } this->ethanol_sensor_->publish_state(ethanol); } if (this->hydrogen_sensor_ != nullptr) { float hydrogen = 0.0f; - if (red_f <= 0.279f) { - hydrogen = (0.279f - red_f) / 0.00026f; - if (hydrogen < 1.0f) - hydrogen = 0.0f; - if (hydrogen > 1000.0f) - hydrogen = 1000.0f; + if (red_f > 0.9f || red_f < 0.02) { // outside the range->unlikely + hydrogen = 0.0; + } else { + hydrogen = 0.85 / pow(red_f, 1.75); } this->hydrogen_sensor_->publish_state(hydrogen); } if (this->ammonia_sensor_ != nullptr) { float ammonia = 0.0f; - if (red_f <= 0.8f) { - ammonia = (0.8f - red_f) / 0.0015f; - if (ammonia < 1.0f) - ammonia = 0.0f; - if (ammonia > 500.0f) - ammonia = 500.0f; + if (red_f > 0.98f || red_f < 0.2532) { // outside the ammonia range->unlikely + ammonia = 0.0; + } else { + ammonia = 0.9 / pow(red_f, 4.6); } this->ammonia_sensor_->publish_state(ammonia); } diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 1d0f406783..488baa245a 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -1,25 +1,29 @@ import binascii -import esphome.codegen as cg -import esphome.config_validation as cv + from esphome import automation +import esphome.codegen as cg from esphome.components import modbus +import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, CONF_ID, - CONF_NAME, CONF_LAMBDA, + CONF_NAME, CONF_OFFSET, CONF_TRIGGER_ID, ) from esphome.cpp_helpers import logging + from .const import ( + CONF_ALLOW_DUPLICATE_COMMANDS, CONF_BITMASK, CONF_BYTE_OFFSET, CONF_COMMAND_THROTTLE, - CONF_OFFLINE_SKIP_UPDATES, CONF_CUSTOM_COMMAND, CONF_FORCE_NEW_RANGE, + CONF_MAX_CMD_RETRIES, CONF_MODBUS_CONTROLLER_ID, + CONF_OFFLINE_SKIP_UPDATES, CONF_ON_COMMAND_SENT, CONF_REGISTER_COUNT, CONF_REGISTER_TYPE, @@ -126,9 +130,11 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(ModbusController), + cv.Optional(CONF_ALLOW_DUPLICATE_COMMANDS, default=False): cv.boolean, cv.Optional( CONF_COMMAND_THROTTLE, default="0ms" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int, cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int, cv.Optional( CONF_SERVER_REGISTERS, @@ -253,7 +259,9 @@ async def add_modbus_base_properties( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS])) cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE])) + cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) if CONF_SERVER_REGISTERS in config: for server_register in config[CONF_SERVER_REGISTERS]: diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index 5315167479..2ae008f630 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -1,16 +1,16 @@ +import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -import esphome.codegen as cg - from esphome.const import CONF_ADDRESS, CONF_ID + from .. import ( - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, - validate_modbus_register, + MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, - MODBUS_REGISTER_TYPE, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, ) from ..const import ( CONF_BITMASK, diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index 1f5c39895c..5cf7d230f1 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -1,9 +1,11 @@ +CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands" CONF_BITMASK = "bitmask" CONF_BYTE_OFFSET = "byte_offset" CONF_COMMAND_THROTTLE = "command_throttle" CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates" CONF_CUSTOM_COMMAND = "custom_command" CONF_FORCE_NEW_RANGE = "force_new_range" +CONF_MAX_CMD_RETRIES = "max_cmd_retries" CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" CONF_ON_COMMAND_SENT = "on_command_sent" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 378e5c06c0..1dcb533629 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -18,11 +18,11 @@ void ModbusController::setup() { this->create_register_ranges_(); } bool ModbusController::send_next_command_() { uint32_t last_send = millis() - this->last_command_timestamp_; - if ((last_send > this->command_throttle_) && !waiting_for_response() && !command_queue_.empty()) { - auto &command = command_queue_.front(); + if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) { + auto &command = this->command_queue_.front(); // remove from queue if command was sent too often - if (command->send_countdown < 1) { + if (!command->should_retry(this->max_cmd_retries_)) { if (!this->module_offline_) { ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_); @@ -34,11 +34,9 @@ bool ModbusController::send_next_command_() { } } this->module_offline_ = true; - ESP_LOGD( - TAG, - "Modbus command to device=%d register=0x%02X countdown=%d no response received - removed from send queue", - this->address_, command->register_address, command->send_countdown); - command_queue_.pop_front(); + ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue", + this->address_, command->register_address); + this->command_queue_.pop_front(); } else { ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_, command->register_address, command->register_count); @@ -50,11 +48,11 @@ bool ModbusController::send_next_command_() { // remove from queue if no handler is defined if (!command->on_data_func) { - command_queue_.pop_front(); + this->command_queue_.pop_front(); } } } - return (!command_queue_.empty()); + return (!this->command_queue_.empty()); } // Queue incoming response @@ -77,7 +75,7 @@ void ModbusController::on_modbus_data(const std::vector &data) { current_command->payload = data; this->incoming_queue_.push(std::move(current_command)); ESP_LOGV(TAG, "Modbus response queued"); - command_queue_.pop_front(); + this->command_queue_.pop_front(); } } @@ -99,7 +97,7 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_ "payload size=%zu", function_code, current_command->register_address, current_command->register_count, current_command->payload.size()); - command_queue_.pop_front(); + this->command_queue_.pop_front(); } } @@ -175,19 +173,21 @@ void ModbusController::on_register_data(ModbusRegisterType register_type, uint16 } void ModbusController::queue_command(const ModbusCommandItem &command) { - // check if this command is already qeued. - // not very effective but the queue is never really large - for (auto &item : command_queue_) { - if (item->is_equal(command)) { - ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u", - static_cast(command.register_type), command.register_address, command.register_count); - // update the payload of the queued command - // replaces a previous command - item->payload = command.payload; - return; + if (!this->allow_duplicate_commands_) { + // check if this command is already qeued. + // not very effective but the queue is never really large + for (auto &item : this->command_queue_) { + if (item->is_equal(command)) { + ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u", + static_cast(command.register_type), command.register_address, command.register_count); + // update the payload of the queued command + // replaces a previous command + item->payload = command.payload; + return; + } } } - command_queue_.push_back(make_unique(command)); + this->command_queue_.push_back(make_unique(command)); } void ModbusController::update_range_(RegisterRange &r) { @@ -222,8 +222,8 @@ void ModbusController::update_range_(RegisterRange &r) { // Once we get a response to the command it is removed from the queue and the next command is send // void ModbusController::update() { - if (!command_queue_.empty()) { - ESP_LOGV(TAG, "%zu modbus commands already in queue", command_queue_.size()); + if (!this->command_queue_.empty()) { + ESP_LOGV(TAG, "%zu modbus commands already in queue", this->command_queue_.size()); } else { ESP_LOGV(TAG, "Updating modbus component"); } @@ -344,6 +344,8 @@ size_t ModbusController::create_register_ranges_() { void ModbusController::dump_config() { ESP_LOGCONFIG(TAG, "ModbusController:"); ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + ESP_LOGCONFIG(TAG, " Max Command Retries: %d", this->max_cmd_retries_); + ESP_LOGCONFIG(TAG, " Offline Skip Updates: %d", this->offline_skip_updates_); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); for (auto &it : sensorset_) { @@ -558,8 +560,9 @@ bool ModbusCommandItem::send() { } else { modbusdevice->send_raw(this->payload); } - ESP_LOGV(TAG, "Command sent %d 0x%X %d", uint8_t(this->function_code), this->register_address, this->register_count); - send_countdown--; + this->send_count_++; + ESP_LOGV(TAG, "Command sent %d 0x%X %d send_count: %d", uint8_t(this->function_code), this->register_address, + this->register_count, this->send_count_); return true; } diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 3bc11da879..1fa35e1535 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -312,7 +312,6 @@ struct RegisterRange { class ModbusCommandItem { public: static const size_t MAX_PAYLOAD_BYTES = 240; - static const uint8_t MAX_SEND_REPEATS = 5; ModbusController *modbusdevice; uint16_t register_address; uint16_t register_count; @@ -322,9 +321,9 @@ class ModbusCommandItem { on_data_func; std::vector payload = {}; bool send(); - // wrong commands (esp. custom commands) can block the send queue - // limit the number of repeats - uint8_t send_countdown{MAX_SEND_REPEATS}; + /// Check if the command should be retried based on the max_retries parameter + bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; }; + /// factory methods /** Create modbus read command * Function code 02-04 @@ -413,6 +412,11 @@ class ModbusCommandItem { &&handler = nullptr); bool is_equal(const ModbusCommandItem &other); + + protected: + // wrong commands (esp. custom commands) can block the send queue, limit the number of repeats. + /// How many times this command has been sent + uint8_t send_count_{0}; }; /** Modbus controller class. @@ -448,6 +452,12 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { /// incoming queue void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, const std::vector &data); + /// Allow a duplicate command to be sent + void set_allow_duplicate_commands(bool allow_duplicate_commands) { + this->allow_duplicate_commands_ = allow_duplicate_commands; + } + /// get if a duplicate command can be sent + bool get_allow_duplicate_commands() { return this->allow_duplicate_commands_; } /// called by esphome generated code to set the command_throttle period void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; } /// called by esphome generated code to set the offline_skip_updates @@ -458,6 +468,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { bool get_module_offline() { return module_offline_; } /// Set callback for commands void add_on_command_sent_callback(std::function &&callback); + /// called by esphome generated code to set the max_cmd_retries. + void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; } + /// get how many times a command will be (re)sent if no response is received + uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; } protected: /// parse sensormap_ and create range of sequential addresses @@ -482,6 +496,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { std::list> command_queue_; /// modbus response data waiting to get processed std::queue> incoming_queue_; + /// if duplicate commands can be sent + bool allow_duplicate_commands_; /// when was the last send operation uint32_t last_command_timestamp_; /// min time in ms between sending modbus commands @@ -490,6 +506,8 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { bool module_offline_; /// how many updates to skip if module is offline uint16_t offline_skip_updates_; + /// How many times we will retry a command if we get no response + uint8_t max_cmd_retries_{4}; CallbackManager command_sent_callback_{}; }; diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index fe99b28a00..b5efd7abf0 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import number +import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, CONF_ID, @@ -12,14 +12,13 @@ from esphome.const import ( from .. import ( MODBUS_WRITE_REGISTER_TYPE, - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, + SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, - SENSOR_VALUE_TYPE, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, ) - from ..const import ( CONF_BITMASK, CONF_CUSTOM_COMMAND, diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 1bf989ce8b..1800a90d57 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,20 +1,15 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import output -from esphome.const import ( - CONF_ADDRESS, - CONF_ID, - CONF_MULTIPLY, -) +import esphome.config_validation as cv +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_MULTIPLY from .. import ( - modbus_controller_ns, - modbus_calc_properties, + SENSOR_VALUE_TYPE, ModbusItemBaseSchema, SensorItem, - SENSOR_VALUE_TYPE, + modbus_calc_properties, + modbus_controller_ns, ) - from ..const import ( CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, @@ -65,6 +60,7 @@ CONFIG_SCHEMA = cv.typed_schema( async def to_code(config): byte_offset, reg_count = modbus_calc_properties(config) # Binary Output + write_template = None if config[CONF_REGISTER_TYPE] == "coil": var = cg.new_Pvariable( config[CONF_ID], @@ -72,7 +68,7 @@ async def to_code(config): byte_offset, ) if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( + write_template = await cg.process_lambda( config[CONF_WRITE_LAMBDA], [ (ModbusBinaryOutput.operator("ptr"), "item"), @@ -92,7 +88,7 @@ async def to_code(config): ) cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( + write_template = await cg.process_lambda( config[CONF_WRITE_LAMBDA], [ (ModbusFloatOutput.operator("ptr"), "item"), @@ -105,5 +101,5 @@ async def to_code(config): parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) cg.add(var.set_parent(parent)) - if CONF_WRITE_LAMBDA in config: - cg.add(var.set_write_template(template_)) + if write_template: + cg.add(var.set_write_template(write_template)) diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index 5692fea3e3..c94532da51 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import select +import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC from .. import ( diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index 0e4588cfef..d8fce54ece 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -1,17 +1,17 @@ +import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -import esphome.codegen as cg +from esphome.const import CONF_ADDRESS, CONF_ID -from esphome.const import CONF_ID, CONF_ADDRESS from .. import ( - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, - validate_modbus_register, - ModbusItemBaseSchema, - SensorItem, MODBUS_REGISTER_TYPE, SENSOR_VALUE_TYPE, + ModbusItemBaseSchema, + SensorItem, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, ) from ..const import ( CONF_BITMASK, diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index 9490325968..258d87fd25 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -1,17 +1,16 @@ +import esphome.codegen as cg from esphome.components import switch import esphome.config_validation as cv -import esphome.codegen as cg +from esphome.const import CONF_ADDRESS, CONF_ID - -from esphome.const import CONF_ID, CONF_ADDRESS from .. import ( - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, - validate_modbus_register, + MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, - MODBUS_REGISTER_TYPE, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, ) from ..const import ( CONF_BITMASK, diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 81d6453c6f..35cae645e1 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -1,26 +1,25 @@ +import esphome.codegen as cg from esphome.components import text_sensor import esphome.config_validation as cv -import esphome.codegen as cg - - from esphome.const import CONF_ADDRESS, CONF_ID + from .. import ( - add_modbus_base_properties, - modbus_controller_ns, - modbus_calc_properties, - validate_modbus_register, + MODBUS_REGISTER_TYPE, ModbusItemBaseSchema, SensorItem, - MODBUS_REGISTER_TYPE, + add_modbus_base_properties, + modbus_calc_properties, + modbus_controller_ns, + validate_modbus_register, ) from ..const import ( CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, + CONF_RAW_ENCODE, CONF_REGISTER_COUNT, + CONF_REGISTER_TYPE, CONF_RESPONSE_SIZE, CONF_SKIP_UPDATES, - CONF_RAW_ENCODE, - CONF_REGISTER_TYPE, ) DEPENDENCIES = ["modbus_controller"] diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f4bd34bfd3..240b407819 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -1,10 +1,11 @@ import re -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +import esphome.codegen as cg from esphome.components import logger +from esphome.components.esp32 import add_idf_sdkconfig_option +import esphome.config_validation as cv from esphome.const import ( CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, @@ -13,21 +14,21 @@ from esphome.const import ( CONF_CLIENT_CERTIFICATE, CONF_CLIENT_CERTIFICATE_KEY, CONF_CLIENT_ID, - CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, + CONF_COMMAND_TOPIC, CONF_DISCOVERY, + CONF_DISCOVERY_OBJECT_ID_GENERATOR, CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, CONF_DISCOVERY_UNIQUE_ID_GENERATOR, - CONF_DISCOVERY_OBJECT_ID_GENERATOR, CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, CONF_LOG_TOPIC, - CONF_ON_JSON_MESSAGE, - CONF_ON_MESSAGE, CONF_ON_CONNECT, CONF_ON_DISCONNECT, + CONF_ON_JSON_MESSAGE, + CONF_ON_MESSAGE, CONF_PASSWORD, CONF_PAYLOAD, CONF_PAYLOAD_AVAILABLE, @@ -45,12 +46,11 @@ from esphome.const import ( CONF_USE_ABBREVIATIONS, CONF_USERNAME, CONF_WILL_MESSAGE, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_BK72XX, ) -from esphome.core import coroutine_with_priority, CORE -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.core import CORE, coroutine_with_priority DEPENDENCIES = ["network"] @@ -110,6 +110,9 @@ MQTTDisconnectTrigger = mqtt_ns.class_( MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) +MQTTAlarmControlPanelComponent = mqtt_ns.class_( + "MQTTAlarmControlPanelComponent", MQTTComponent +) MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent) MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent) MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp new file mode 100644 index 0000000000..660a030d11 --- /dev/null +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -0,0 +1,128 @@ +#include "mqtt_alarm_control_panel.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_ALARM_CONTROL_PANEL + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.alarm_control_panel"; + +using namespace esphome::alarm_control_panel; + +MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) + : alarm_control_panel_(alarm_control_panel) {} +void MQTTAlarmControlPanelComponent::setup() { + this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); }); + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { + auto call = this->alarm_control_panel_->make_call(); + if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) { + call.arm_away(); + } else if (strcasecmp(payload.c_str(), "ARM_HOME") == 0) { + call.arm_home(); + } else if (strcasecmp(payload.c_str(), "ARM_NIGHT") == 0) { + call.arm_night(); + } else if (strcasecmp(payload.c_str(), "ARM_VACATION") == 0) { + call.arm_vacation(); + } else if (strcasecmp(payload.c_str(), "ARM_CUSTOM_BYPASS") == 0) { + call.arm_custom_bypass(); + } else if (strcasecmp(payload.c_str(), "DISARM") == 0) { + call.disarm(); + } else if (strcasecmp(payload.c_str(), "PENDING") == 0) { + call.pending(); + } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { + call.triggered(); + } else { + ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); + } + call.perform(); + }); +} + +void MQTTAlarmControlPanelComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true) + ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->alarm_control_panel_->get_supported_features()); + ESP_LOGCONFIG(TAG, " Requires Code to Disarm: %s", YESNO(this->alarm_control_panel_->get_requires_code())); + ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->alarm_control_panel_->get_requires_code_to_arm())); +} + +void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); + const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); + if (acp_supported_features & ACP_FEAT_ARM_AWAY) { + supported_features.add("arm_away"); + } + if (acp_supported_features & ACP_FEAT_ARM_HOME) { + supported_features.add("arm_home"); + } + if (acp_supported_features & ACP_FEAT_ARM_NIGHT) { + supported_features.add("arm_night"); + } + if (acp_supported_features & ACP_FEAT_ARM_VACATION) { + supported_features.add("arm_vacation"); + } + if (acp_supported_features & ACP_FEAT_ARM_CUSTOM_BYPASS) { + supported_features.add("arm_custom_bypass"); + } + if (acp_supported_features & ACP_FEAT_TRIGGER) { + supported_features.add("trigger"); + } + root[MQTT_CODE_DISARM_REQUIRED] = this->alarm_control_panel_->get_requires_code(); + root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm(); +} + +std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; } +const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; } + +bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } +bool MQTTAlarmControlPanelComponent::publish_state() { + bool success = true; + const char *state_s = ""; + switch (this->alarm_control_panel_->get_state()) { + case ACP_STATE_DISARMED: + state_s = "disarmed"; + break; + case ACP_STATE_ARMED_HOME: + state_s = "armed_home"; + break; + case ACP_STATE_ARMED_AWAY: + state_s = "armed_away"; + break; + case ACP_STATE_ARMED_NIGHT: + state_s = "armed_night"; + break; + case ACP_STATE_ARMED_VACATION: + state_s = "armed_vacation"; + break; + case ACP_STATE_ARMED_CUSTOM_BYPASS: + state_s = "armed_custom_bypass"; + break; + case ACP_STATE_PENDING: + state_s = "pending"; + break; + case ACP_STATE_ARMING: + state_s = "arming"; + break; + case ACP_STATE_DISARMING: + state_s = "disarming"; + break; + case ACP_STATE_TRIGGERED: + state_s = "triggered"; + break; + default: + state_s = "unknown"; + } + if (!this->publish(this->get_state_topic_(), state_s)) + success = false; + return success; +} + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h new file mode 100644 index 0000000000..4ad37b7314 --- /dev/null +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_ALARM_CONTROL_PANEL + +#include "mqtt_component.h" +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" + +namespace esphome { +namespace mqtt { + +class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { + public: + explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel); + + void setup() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(); + + void dump_config() override; + + protected: + std::string component_type() const override; + const EntityBase *get_entity() const override; + + alarm_control_panel::AlarmControlPanel *alarm_control_panel_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h index d23cda578d..3962c40a42 100644 --- a/esphome/components/mqtt/mqtt_backend.h +++ b/esphome/components/mqtt/mqtt_backend.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_MQTT #include #include #include "esphome/components/network/ip_address.h" @@ -67,3 +68,4 @@ class MQTTBackend { } // namespace mqtt } // namespace esphome +#endif diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 9c2e487ae7..ed500c6d44 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -1,7 +1,9 @@ +#include "mqtt_backend_esp32.h" + +#ifdef USE_MQTT #ifdef USE_ESP32 #include -#include "mqtt_backend_esp32.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" @@ -189,3 +191,4 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b } // namespace mqtt } // namespace esphome #endif // USE_ESP32 +#endif diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index b1f672da10..9054702115 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -1,5 +1,7 @@ #pragma once +#include "mqtt_backend.h" +#ifdef USE_MQTT #ifdef USE_ESP32 #include @@ -7,7 +9,6 @@ #include #include "esphome/components/network/ip_address.h" #include "esphome/core/helpers.h" -#include "mqtt_backend.h" namespace esphome { namespace mqtt { @@ -174,3 +175,4 @@ class MQTTBackendESP32 final : public MQTTBackend { } // namespace esphome #endif +#endif diff --git a/esphome/components/mqtt/mqtt_backend_esp8266.h b/esphome/components/mqtt/mqtt_backend_esp8266.h index 06d4993bdf..a979634bf4 100644 --- a/esphome/components/mqtt/mqtt_backend_esp8266.h +++ b/esphome/components/mqtt/mqtt_backend_esp8266.h @@ -1,8 +1,9 @@ #pragma once +#include "mqtt_backend.h" +#ifdef USE_MQTT #ifdef USE_ESP8266 -#include "mqtt_backend.h" #include namespace esphome { @@ -70,3 +71,4 @@ class MQTTBackendESP8266 final : public MQTTBackend { } // namespace esphome #endif // defined(USE_ESP8266) +#endif diff --git a/esphome/components/mqtt/mqtt_backend_libretiny.h b/esphome/components/mqtt/mqtt_backend_libretiny.h index ac4d4298fc..2578ae9941 100644 --- a/esphome/components/mqtt/mqtt_backend_libretiny.h +++ b/esphome/components/mqtt/mqtt_backend_libretiny.h @@ -1,8 +1,9 @@ #pragma once +#include "mqtt_backend.h" +#ifdef USE_MQTT #ifdef USE_LIBRETINY -#include "mqtt_backend.h" #include namespace esphome { @@ -70,3 +71,4 @@ class MQTTBackendLibreTiny final : public MQTTBackend { } // namespace esphome #endif // defined(USE_LIBRETINY) +#endif diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 876367aaea..c19b24c0cf 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -632,6 +632,7 @@ void MQTTClientComponent::disable_discovery() { this->discovery_info_ = MQTTDiscoveryInfo{ .prefix = "", .retain = false, + .discover_ip = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index bb46ce732d..295fbba5e5 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -150,12 +150,40 @@ bool MQTTComponent::send_discovery_() { const std::string &node_area = App.get_area(); JsonObject device_info = root.createNestedObject(MQTT_DEVICE); - device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address(); + const auto mac = get_mac_address(); + device_info[MQTT_DEVICE_IDENTIFIERS] = mac; device_info[MQTT_DEVICE_NAME] = node_friendly_name; - device_info[MQTT_DEVICE_SW_VERSION] = "esphome v" ESPHOME_VERSION " " + App.get_compilation_time(); +#ifdef ESPHOME_PROJECT_NAME + device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")"; + const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.'); + if (model == nullptr) { // must never happen but check anyway + device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; + device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME; + } else { + device_info[MQTT_DEVICE_MODEL] = model + 1; + device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME); + } +#else + device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")"; device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD; - device_info[MQTT_DEVICE_MANUFACTURER] = "espressif"; - device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area; +#if defined(USE_ESP8266) || defined(USE_ESP32) + device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif"; +#elif defined(USE_RP2040) + device_info[MQTT_DEVICE_MANUFACTURER] = "Raspberry Pi"; +#elif defined(USE_BK72XX) + device_info[MQTT_DEVICE_MANUFACTURER] = "Beken"; +#elif defined(USE_RTL87XX) + device_info[MQTT_DEVICE_MANUFACTURER] = "Realtek"; +#elif defined(USE_HOST) + device_info[MQTT_DEVICE_MANUFACTURER] = "Host"; +#endif +#endif + if (!node_area.empty()) { + device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area; + } + + device_info[MQTT_DEVICE_CONNECTIONS][0][0] = "mac"; + device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; }, this->qos_, discovery_info.retain); } diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 0e063c66d2..71f169fbe8 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -62,6 +62,7 @@ constexpr const char *const MQTT_DEVICE_MODEL = "mdl"; constexpr const char *const MQTT_DEVICE_NAME = "name"; constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "sa"; constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw"; +constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw"; constexpr const char *const MQTT_DOCKED_TEMPLATE = "dock_tpl"; constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "fx_cmd_t"; @@ -322,6 +323,7 @@ constexpr const char *const MQTT_DEVICE_MODEL = "model"; constexpr const char *const MQTT_DEVICE_NAME = "name"; constexpr const char *const MQTT_DEVICE_SUGGESTED_AREA = "suggested_area"; constexpr const char *const MQTT_DEVICE_SW_VERSION = "sw_version"; +constexpr const char *const MQTT_DEVICE_HW_VERSION = "hw_version"; constexpr const char *const MQTT_DOCKED_TEMPLATE = "docked_template"; constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; constexpr const char *const MQTT_EFFECT_COMMAND_TOPIC = "effect_command_topic"; diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 9ef75e0fb9..be4e102930 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,15 +1,8 @@ -from esphome.core import CORE import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components.esp32 import add_idf_sdkconfig_option - -from esphome.const import ( - CONF_ENABLE_IPV6, - CONF_MIN_IPV6_ADDR_COUNT, - PLATFORM_ESP32, - PLATFORM_ESP8266, - PLATFORM_RP2040, -) +import esphome.config_validation as cv +from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] @@ -24,8 +17,19 @@ CONFIG_SCHEMA = cv.Schema( esp8266=False, esp32=False, rp2040=False, + bk72xx=False, ): cv.All( - cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]) + cv.boolean, + cv.Any( + cv.require_framework_version( + esp_idf=cv.Version(0, 0, 0), + esp32_arduino=cv.Version(0, 0, 0), + esp8266_arduino=cv.Version(0, 0, 0), + rp2040_arduino=cv.Version(0, 0, 0), + bk72xx_libretiny=cv.Version(1, 7, 0), + ), + cv.boolean_false, + ), ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, } @@ -33,6 +37,7 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): + cg.add_define("USE_NETWORK") if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: cg.add_define("USE_NETWORK_IPV6", enable_ipv6) if enable_ipv6: @@ -42,11 +47,12 @@ async def to_code(config): if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_LWIP_IPV6", enable_ipv6) add_idf_sdkconfig_option("CONFIG_LWIP_IPV6_AUTOCONFIG", enable_ipv6) - else: - if enable_ipv6: - cg.add_build_flag("-DCONFIG_LWIP_IPV6") - cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") - if CORE.is_rp2040: - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") - if CORE.is_esp8266: - cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") + elif enable_ipv6: + cg.add_build_flag("-DCONFIG_LWIP_IPV6") + cg.add_build_flag("-DCONFIG_LWIP_IPV6_AUTOCONFIG") + if CORE.is_rp2040: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6") + if CORE.is_esp8266: + cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY") + if CORE.is_bk72xx: + cg.add_build_flag("-DCONFIG_IPV6") diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 30a426e458..941934cf0a 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -1,4 +1,6 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include #include @@ -140,3 +142,4 @@ using IPAddresses = std::array; } // namespace network } // namespace esphome +#endif diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 445485b644..ed519f738a 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -1,6 +1,6 @@ #include "util.h" #include "esphome/core/defines.h" - +#ifdef USE_NETWORK #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" #endif @@ -63,3 +63,4 @@ std::string get_use_address() { } // namespace network } // namespace esphome +#endif diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 5377d44f2f..b518696e68 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include "ip_address.h" @@ -16,3 +17,4 @@ IPAddresses get_ip_addresses(); } // namespace network } // namespace esphome +#endif diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py index 784da35371..d12434ec8f 100644 --- a/esphome/components/nextion/base_component.py +++ b/esphome/components/nextion/base_component.py @@ -1,12 +1,11 @@ from string import ascii_letters, digits -import esphome.config_validation as cv + import esphome.codegen as cg from esphome.components import color -from esphome.const import ( - CONF_VISIBLE, -) -from . import CONF_NEXTION_ID -from . import Nextion +import esphome.config_validation as cv +from esphome.const import CONF_BACKGROUND_COLOR, CONF_FOREGROUND_COLOR, CONF_VISIBLE + +from . import CONF_NEXTION_ID, Nextion CONF_VARIABLE_NAME = "variable_name" CONF_COMPONENT_NAME = "component_name" @@ -24,9 +23,7 @@ CONF_WAKE_UP_PAGE = "wake_up_page" CONF_START_UP_PAGE = "start_up_page" CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" CONF_WAVE_MAX_LENGTH = "wave_max_length" -CONF_BACKGROUND_COLOR = "background_color" CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" -CONF_FOREGROUND_COLOR = "foreground_color" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_FONT_ID = "font_id" CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index d9c16fd7a9..ece738af49 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -1,24 +1,23 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import mqtt -from esphome.components import web_server +import esphome.codegen as cg +from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ABOVE, CONF_BELOW, + CONF_CYCLE, CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, - CONF_ID, CONF_ICON, + CONF_ID, CONF_MODE, + CONF_MQTT_ID, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, + CONF_OPERATION, CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, - CONF_MQTT_ID, CONF_VALUE, - CONF_OPERATION, - CONF_CYCLE, CONF_WEB_SERVER_ID, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, @@ -72,8 +71,8 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ) from esphome.core import CORE, coroutine_with_priority -from esphome.cpp_helpers import setup_entity from esphome.cpp_generator import MockObjClass +from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] DEVICE_CLASSES = [ diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py new file mode 100644 index 0000000000..d9a7609543 --- /dev/null +++ b/esphome/components/online_image/__init__.py @@ -0,0 +1,167 @@ +import logging + +from esphome import automation +import esphome.codegen as cg +from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent +from esphome.components.image import ( + CONF_USE_TRANSPARENCY, + IMAGE_TYPE, + Image_, + validate_cross_dependencies, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BUFFER_SIZE, + CONF_FORMAT, + CONF_ID, + CONF_ON_ERROR, + CONF_RESIZE, + CONF_TRIGGER_ID, + CONF_TYPE, + CONF_URL, +) + +AUTO_LOAD = ["image"] +DEPENDENCIES = ["display", "http_request"] +CODEOWNERS = ["@guillempages"] +MULTI_CONF = True + +CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" +CONF_PLACEHOLDER = "placeholder" + +_LOGGER = logging.getLogger(__name__) + +online_image_ns = cg.esphome_ns.namespace("online_image") + +ImageFormat = online_image_ns.enum("ImageFormat") + +FORMAT_PNG = "PNG" + +IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here + +OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) + +# Actions +SetUrlAction = online_image_ns.class_( + "OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage) +) +ReleaseImageAction = online_image_ns.class_( + "OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage) +) + +# Triggers +DownloadFinishedTrigger = online_image_ns.class_( + "DownloadFinishedTrigger", automation.Trigger.template() +) +DownloadErrorTrigger = online_image_ns.class_( + "DownloadErrorTrigger", automation.Trigger.template() +) + +ONLINE_IMAGE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(OnlineImage), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + # + # Common image options + # + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), + # Not setting default here on purpose; the default depends on the image type, + # and thus will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, + # + # Online Image specific options + # + cv.Required(CONF_URL): cv.url, + cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger), + } + ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), + } + ), + } +).extend(cv.polling_component_schema("never")) + +CONFIG_SCHEMA = cv.Schema( + cv.All( + ONLINE_IMAGE_SCHEMA, + validate_cross_dependencies, + cv.require_framework_version( + # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed + # esp8266_arduino=cv.Version(2, 7, 0), + esp32_arduino=cv.Version(0, 0, 0), + esp_idf=cv.Version(4, 0, 0), + ), + ) +) + +SET_URL_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(OnlineImage), + cv.Required(CONF_URL): cv.templatable(cv.url), + } +) + +RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(OnlineImage), + } +) + + +@automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA) +@automation.register_action( + "online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA +) +async def online_image_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + if CONF_URL in config: + template_ = await cg.templatable(config[CONF_URL], args, cg.const_char_ptr) + cg.add(var.set_url(template_)) + return var + + +async def to_code(config): + format = config[CONF_FORMAT] + if format in [FORMAT_PNG]: + cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") + cg.add_library("pngle", "1.0.2") + + url = config[CONF_URL] + width, height = config.get(CONF_RESIZE, (0, 0)) + transparent = config[CONF_USE_TRANSPARENCY] + + var = cg.new_Pvariable( + config[CONF_ID], + url, + width, + height, + format, + config[CONF_TYPE], + config[CONF_BUFFER_SIZE], + ) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) + + cg.add(var.set_transparency(transparent)) + + if placeholder_id := config.get(CONF_PLACEHOLDER): + placeholder = await cg.get_variable(placeholder_id) + cg.add(var.set_placeholder(placeholder)) + + for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp new file mode 100644 index 0000000000..50ec39dfcc --- /dev/null +++ b/esphome/components/online_image/image_decoder.cpp @@ -0,0 +1,44 @@ +#include "image_decoder.h" +#include "online_image.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace online_image { + +static const char *const TAG = "online_image.decoder"; + +void ImageDecoder::set_size(int width, int height) { + this->image_->resize_(width, height); + this->x_scale_ = static_cast(this->image_->buffer_width_) / width; + this->y_scale_ = static_cast(this->image_->buffer_height_) / height; +} + +void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { + auto width = std::min(this->image_->buffer_width_, static_cast(std::ceil((x + w) * this->x_scale_))); + auto height = std::min(this->image_->buffer_height_, static_cast(std::ceil((y + h) * this->y_scale_))); + for (int i = x * this->x_scale_; i < width; i++) { + for (int j = y * this->y_scale_; j < height; j++) { + this->image_->draw_pixel_(i, j, color); + } + } +} + +uint8_t *DownloadBuffer::data(size_t offset) { + if (offset > this->size_) { + ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!"); + return this->buffer_; + } + return this->buffer_ + offset; +} + +size_t DownloadBuffer::read(size_t len) { + this->unread_ -= len; + if (this->unread_ > 0) { + memmove(this->data(), this->data(len), this->unread_); + } + return this->unread_; +} + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h new file mode 100644 index 0000000000..908efab987 --- /dev/null +++ b/esphome/components/online_image/image_decoder.h @@ -0,0 +1,112 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" + +namespace esphome { +namespace online_image { + +class OnlineImage; + +/** + * @brief Class to abstract decoding different image formats. + */ +class ImageDecoder { + public: + /** + * @brief Construct a new Image Decoder object + * + * @param image The image to decode the stream into. + */ + ImageDecoder(OnlineImage *image) : image_(image) {} + virtual ~ImageDecoder() = default; + + /** + * @brief Initialize the decoder. + * + * @param download_size The total number of bytes that need to be download for the image. + */ + virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } + + /** + * @brief Decode a part of the image. It will try reading from the buffer. + * There is no guarantee that the whole available buffer will be read/decoded; + * the method will return the amount of bytes actually decoded, so that the + * unread content can be moved to the beginning. + * + * @param buffer The buffer to read from. + * @param size The maximum amount of bytes that can be read from the buffer. + * @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully + * decode anything, or negative in case of a decoding error. + */ + virtual int decode(uint8_t *buffer, size_t size); + + /** + * @brief Request the image to be resized once the actual dimensions are known. + * Called by the callback functions, to be able to access the parent Image class. + * + * @param width The image's width. + * @param height The image's height. + */ + void set_size(int width, int height); + + /** + * @brief Draw a rectangle on the display_buffer using the defined color. + * Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly. + * In case of binary displays, the color will be converted to binary as well. + * Called by the callback functions, to be able to access the parent Image class. + * + * @param x The left-most coordinate of the rectangle. + * @param y The top-most coordinate of the rectangle. + * @param w The width of the rectangle. + * @param h The height of the rectangle. + * @param color The color to draw the rectangle with. + */ + void draw(int x, int y, int w, int h, const Color &color); + + bool is_finished() const { return this->decoded_bytes_ == this->download_size_; } + + protected: + OnlineImage *image_; + // Initializing to 1, to ensure it is different than initial "decoded_bytes_". + // Will be overwritten anyway once the download size is known. + uint32_t download_size_ = 1; + uint32_t decoded_bytes_ = 0; + double x_scale_ = 1.0; + double y_scale_ = 1.0; +}; + +class DownloadBuffer { + public: + DownloadBuffer(size_t size) : size_(size) { + this->buffer_ = this->allocator_.allocate(size); + this->reset(); + } + + virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); } + + uint8_t *data(size_t offset = 0); + + uint8_t *append() { return this->data(this->unread_); } + + size_t unread() const { return this->unread_; } + size_t size() const { return this->size_; } + size_t free_capacity() const { return this->size_ - this->unread_; } + + size_t read(size_t len); + size_t write(size_t len) { + this->unread_ += len; + return this->unread_; + } + + void reset() { this->unread_ = 0; } + + protected: + ExternalRAMAllocator allocator_; + uint8_t *buffer_; + size_t size_; + /** Total number of downloaded bytes not yet read. */ + size_t unread_; +}; + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp new file mode 100644 index 0000000000..480bad6aca --- /dev/null +++ b/esphome/components/online_image/online_image.cpp @@ -0,0 +1,283 @@ +#include "online_image.h" + +#include "esphome/core/log.h" + +static const char *const TAG = "online_image"; + +#include "image_decoder.h" + +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_image.h" +#endif + +namespace esphome { +namespace online_image { + +using image::ImageType; + +inline bool is_color_on(const Color &color) { + // This produces the most accurate monochrome conversion, but is slightly slower. + // return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127; + + // Approximation using fast integer computations; produces acceptable results + // Equivalent to 0.25 * R + 0.5 * G + 0.25 * B + return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80; +} + +OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, + uint32_t download_buffer_size) + : Image(nullptr, 0, 0, type), + buffer_(nullptr), + download_buffer_(download_buffer_size), + format_(format), + fixed_width_(width), + fixed_height_(height) { + this->set_url(url); +} + +void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, Color color_off) { + if (this->data_start_) { + Image::draw(x, y, display, color_on, color_off); + } else if (this->placeholder_) { + this->placeholder_->draw(x, y, display, color_on, color_off); + } +} + +void OnlineImage::release() { + if (this->buffer_) { + ESP_LOGD(TAG, "Deallocating old buffer..."); + this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); + this->data_start_ = nullptr; + this->buffer_ = nullptr; + this->width_ = 0; + this->height_ = 0; + this->buffer_width_ = 0; + this->buffer_height_ = 0; + this->end_connection_(); + } +} + +bool OnlineImage::resize_(int width_in, int height_in) { + int width = this->fixed_width_; + int height = this->fixed_height_; + if (this->auto_resize_()) { + width = width_in; + height = height_in; + if (this->width_ != width && this->height_ != height) { + this->release(); + } + } + if (this->buffer_) { + return false; + } + auto new_size = this->get_buffer_size_(width, height); + ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); + delay_microseconds_safe(2000); + this->buffer_ = this->allocator_.allocate(new_size); + if (this->buffer_) { + this->buffer_width_ = width; + this->buffer_height_ = height; + this->width_ = width; + ESP_LOGD(TAG, "New size: (%d, %d)", width, height); + } else { +#if defined(USE_ESP8266) + // NOLINTNEXTLINE(readability-static-accessed-through-instance) + int max_block = ESP.getMaxFreeBlockSize(); +#elif defined(USE_ESP32) + int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); +#else + int max_block = -1; +#endif + ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block); + this->end_connection_(); + return false; + } + return true; +} + +void OnlineImage::update() { + if (this->decoder_) { + ESP_LOGW(TAG, "Image already being updated."); + return; + } else { + ESP_LOGI(TAG, "Updating image"); + } + + this->downloader_ = this->parent_->get(this->url_); + + if (this->downloader_ == nullptr) { + ESP_LOGE(TAG, "Download failed."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + int http_code = this->downloader_->status_code; + if (http_code == HTTP_CODE_NOT_MODIFIED) { + // Image hasn't changed on server. Skip download. + this->end_connection_(); + return; + } + if (http_code != HTTP_CODE_OK) { + ESP_LOGE(TAG, "HTTP result: %d", http_code); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + + ESP_LOGD(TAG, "Starting download"); + size_t total_size = this->downloader_->content_length; + +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT + if (this->format_ == ImageFormat::PNG) { + this->decoder_ = esphome::make_unique(this); + } +#endif // ONLINE_IMAGE_PNG_SUPPORT + + if (!this->decoder_) { + ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + this->decoder_->prepare(total_size); + ESP_LOGI(TAG, "Downloading image"); +} + +void OnlineImage::loop() { + if (!this->decoder_) { + // Not decoding at the moment => nothing to do. + return; + } + if (!this->downloader_ || this->decoder_->is_finished()) { + ESP_LOGD(TAG, "Image fully downloaded"); + this->data_start_ = buffer_; + this->width_ = buffer_width_; + this->height_ = buffer_height_; + this->end_connection_(); + this->download_finished_callback_.call(); + return; + } + if (this->downloader_ == nullptr) { + ESP_LOGE(TAG, "Downloader not instantiated; cannot download"); + return; + } + size_t available = this->download_buffer_.free_capacity(); + if (available) { + auto len = this->downloader_->read(this->download_buffer_.append(), available); + if (len > 0) { + this->download_buffer_.write(len); + auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread()); + if (fed < 0) { + ESP_LOGE(TAG, "Error when decoding image."); + this->end_connection_(); + this->download_error_callback_.call(); + return; + } + this->download_buffer_.read(fed); + } + } +} + +void OnlineImage::draw_pixel_(int x, int y, Color color) { + if (!this->buffer_) { + ESP_LOGE(TAG, "Buffer not allocated!"); + return; + } + if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) { + ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y); + return; + } + uint32_t pos = this->get_position_(x, y); + switch (this->type_) { + case ImageType::IMAGE_TYPE_BINARY: { + const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; + const uint32_t pos = x + y * width_8; + if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { + this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u)); + } else { + this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); + } + break; + } + case ImageType::IMAGE_TYPE_GRAYSCALE: { + uint8_t gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); + if (this->has_transparency()) { + if (gray == 1) { + gray = 0; + } + if (color.w < 0x80) { + gray = 1; + } + } + this->buffer_[pos] = gray; + break; + } + case ImageType::IMAGE_TYPE_RGB565: { + uint16_t col565 = display::ColorUtil::color_to_565(color); + if (this->has_transparency()) { + if (col565 == 0x0020) { + col565 = 0; + } + if (color.w < 0x80) { + col565 = 0x0020; + } + } + this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); + this->buffer_[pos + 1] = static_cast(col565 & 0xFF); + break; + } + case ImageType::IMAGE_TYPE_RGBA: { + this->buffer_[pos + 0] = color.r; + this->buffer_[pos + 1] = color.g; + this->buffer_[pos + 2] = color.b; + this->buffer_[pos + 3] = color.w; + break; + } + case ImageType::IMAGE_TYPE_RGB24: + default: { + if (this->has_transparency()) { + if (color.b == 1 && color.r == 0 && color.g == 0) { + color.b = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = 0; + color.b = 1; + } + } + this->buffer_[pos + 0] = color.r; + this->buffer_[pos + 1] = color.g; + this->buffer_[pos + 2] = color.b; + break; + } + } +} + +void OnlineImage::end_connection_() { + if (this->downloader_) { + this->downloader_->end(); + this->downloader_ = nullptr; + } + this->decoder_.reset(); + this->download_buffer_.reset(); +} + +bool OnlineImage::validate_url_(const std::string &url) { + if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { + ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); + return false; + } + return true; +} + +void OnlineImage::add_on_finished_callback(std::function &&callback) { + this->download_finished_callback_.add(std::move(callback)); +} + +void OnlineImage::add_on_error_callback(std::function &&callback) { + this->download_error_callback_.add(std::move(callback)); +} + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h new file mode 100644 index 0000000000..775cc46e0b --- /dev/null +++ b/esphome/components/online_image/online_image.h @@ -0,0 +1,195 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "esphome/components/http_request/http_request.h" +#include "esphome/components/image/image.h" + +#include "image_decoder.h" + +namespace esphome { +namespace online_image { + +using t_http_codes = enum { + HTTP_CODE_OK = 200, + HTTP_CODE_NOT_MODIFIED = 304, + HTTP_CODE_NOT_FOUND = 404, +}; + +/** + * @brief Format that the image is encoded with. + */ +enum ImageFormat { + /** Automatically detect from MIME type. Not supported yet. */ + AUTO, + /** JPEG format. Not supported yet. */ + JPEG, + /** PNG format. */ + PNG, +}; + +/** + * @brief Download an image from a given URL, and decode it using the specified decoder. + * The image will then be stored in a buffer, so that it can be re-displayed without the + * need to re-download or re-decode. + */ +class OnlineImage : public PollingComponent, + public image::Image, + public Parented { + public: + /** + * @brief Construct a new OnlineImage object. + * + * @param url URL to download the image from. + * @param width Desired width of the target image area. + * @param height Desired height of the target image area. + * @param format Format that the image is encoded in (@see ImageFormat). + * @param buffer_size Size of the buffer used to download the image. + */ + OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, + uint32_t buffer_size); + + void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; + + void update() override; + void loop() override; + + /** Set the URL to download the image from. */ + void set_url(const std::string &url) { + if (this->validate_url_(url)) { + this->url_ = url; + } + } + + /** + * @brief Set the image that needs to be shown as long as the downloaded image + * is not available. + * + * @param placeholder Pointer to the (@link Image) to show as placeholder. + */ + void set_placeholder(image::Image *placeholder) { this->placeholder_ = placeholder; } + + /** + * Release the buffer storing the image. The image will need to be downloaded again + * to be able to be displayed. + */ + void release(); + + void add_on_finished_callback(std::function &&callback); + void add_on_error_callback(std::function &&callback); + + protected: + bool validate_url_(const std::string &url); + + using Allocator = ExternalRAMAllocator; + Allocator allocator_{Allocator::Flags::ALLOW_FAILURE}; + + uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } + int get_buffer_size_(int width, int height) const { + return std::ceil(image::image_type_to_bpp(this->type_) * width * height / 8.0); + } + + int get_position_(int x, int y) const { + return ((x + y * this->buffer_width_) * image::image_type_to_bpp(this->type_)) / 8; + } + + ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; } + + bool resize_(int width, int height); + + /** + * @brief Draw a pixel into the buffer. + * + * This is used by the decoder to fill the buffer that will later be displayed + * by the `draw` method. This will internally convert the supplied 32 bit RGBA + * color into the requested image storage format. + * + * @param x Horizontal pixel position. + * @param y Vertical pixel position. + * @param color 32 bit color to put into the pixel. + */ + void draw_pixel_(int x, int y, Color color); + + void end_connection_(); + + CallbackManager download_finished_callback_{}; + CallbackManager download_error_callback_{}; + + std::shared_ptr downloader_{nullptr}; + std::unique_ptr decoder_{nullptr}; + + uint8_t *buffer_; + DownloadBuffer download_buffer_; + + const ImageFormat format_; + image::Image *placeholder_{nullptr}; + + std::string url_{""}; + + /** width requested on configuration, or 0 if non specified. */ + const int fixed_width_; + /** height requested on configuration, or 0 if non specified. */ + const int fixed_height_; + /** + * Actual width of the current image. If fixed_width_ is specified, + * this will be equal to it; otherwise it will be set once the decoding + * starts and the original size is known. + * This needs to be separate from "BaseImage::get_width()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). + */ + int buffer_width_; + /** + * Actual height of the current image. If fixed_height_ is specified, + * this will be equal to it; otherwise it will be set once the decoding + * starts and the original size is known. + * This needs to be separate from "BaseImage::get_height()" because the latter + * must return 0 until the image has been decoded (to avoid showing partially + * decoded images). + */ + int buffer_height_; + + friend void ImageDecoder::set_size(int width, int height); + friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); +}; + +template class OnlineImageSetUrlAction : public Action { + public: + OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(const char *, url) + void play(Ts... x) override { + this->parent_->set_url(this->url_.value(x...)); + this->parent_->update(); + } + + protected: + OnlineImage *parent_; +}; + +template class OnlineImageReleaseAction : public Action { + public: + OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(const char *, url) + void play(Ts... x) override { this->parent_->release(); } + + protected: + OnlineImage *parent_; +}; + +class DownloadFinishedTrigger : public Trigger<> { + public: + explicit DownloadFinishedTrigger(OnlineImage *parent) { + parent->add_on_finished_callback([this]() { this->trigger(); }); + } +}; + +class DownloadErrorTrigger : public Trigger<> { + public: + explicit DownloadErrorTrigger(OnlineImage *parent) { + parent->add_on_error_callback([this]() { this->trigger(); }); + } +}; + +} // namespace online_image +} // namespace esphome diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp new file mode 100644 index 0000000000..c8e215a91d --- /dev/null +++ b/esphome/components/online_image/png_image.cpp @@ -0,0 +1,68 @@ +#include "png_image.h" +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT + +#include "esphome/components/display/display_buffer.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +static const char *const TAG = "online_image.png"; + +namespace esphome { +namespace online_image { + +/** + * @brief Callback method that will be called by the PNGLE engine when the basic + * data of the image is received (i.e. width and height); + * + * @param pngle The PNGLE object, including the context data. + * @param w The width of the image. + * @param h The height of the image. + */ +static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) { + PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); + decoder->set_size(w, h); +} + +/** + * @brief Callback method that will be called by the PNGLE engine when a chunk + * of the image is decoded. + * + * @param pngle The PNGLE object, including the context data. + * @param x The X coordinate to draw the rectangle on. + * @param y The Y coordinate to draw the rectangle on. + * @param w The width of the rectangle to draw. + * @param h The height of the rectangle to draw. + * @param rgba The color to paint the rectangle in. + */ +static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) { + PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); + Color color(rgba[0], rgba[1], rgba[2], rgba[3]); + decoder->draw(x, y, w, h, color); +} + +void PngDecoder::prepare(uint32_t download_size) { + ImageDecoder::prepare(download_size); + pngle_set_user_data(this->pngle_, this); + pngle_set_init_callback(this->pngle_, init_callback); + pngle_set_draw_callback(this->pngle_, draw_callback); +} + +int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { + if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { + ESP_LOGD(TAG, "Waiting for data"); + return 0; + } + auto fed = pngle_feed(this->pngle_, buffer, size); + if (fed < 0) { + ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_)); + } else { + this->decoded_bytes_ += fed; + } + return fed; +} + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_PNG_SUPPORT diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h new file mode 100644 index 0000000000..a928276dcc --- /dev/null +++ b/esphome/components/online_image/png_image.h @@ -0,0 +1,33 @@ +#pragma once + +#include "image_decoder.h" +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include + +namespace esphome { +namespace online_image { + +/** + * @brief Image decoder specialization for PNG images. + */ +class PngDecoder : public ImageDecoder { + public: + /** + * @brief Construct a new PNG Decoder object. + * + * @param display The image to decode the stream into. + */ + PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} + ~PngDecoder() override { pngle_destroy(this->pngle_); } + + void prepare(uint32_t download_size) override; + int HOT decode(uint8_t *buffer, size_t size) override; + + protected: + pngle_t *pngle_; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_PNG_SUPPORT diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py new file mode 100644 index 0000000000..23443a4028 --- /dev/null +++ b/esphome/components/opentherm/__init__.py @@ -0,0 +1,57 @@ +from typing import Any + +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 + +CODEOWNERS = ["@olegtarasov"] +MULTI_CONF = True + +CONF_IN_PIN = "in_pin" +CONF_OUT_PIN = "out_pin" +CONF_CH_ENABLE = "ch_enable" +CONF_DHW_ENABLE = "dhw_enable" +CONF_COOLING_ENABLE = "cooling_enable" +CONF_OTC_ACTIVE = "otc_active" +CONF_CH2_ACTIVE = "ch2_active" +CONF_SYNC_MODE = "sync_mode" + +opentherm_ns = cg.esphome_ns.namespace("opentherm") +OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(OpenthermHub), + cv.Required(CONF_IN_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_OUT_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_CH_ENABLE, True): cv.boolean, + cv.Optional(CONF_DHW_ENABLE, True): cv.boolean, + cv.Optional(CONF_COOLING_ENABLE, False): cv.boolean, + cv.Optional(CONF_OTC_ACTIVE, False): cv.boolean, + cv.Optional(CONF_CH2_ACTIVE, False): cv.boolean, + cv.Optional(CONF_SYNC_MODE, False): cv.boolean, + } + ).extend(cv.COMPONENT_SCHEMA), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), +) + + +async def to_code(config: dict[str, Any]) -> None: + # Create the hub, passing the two callbacks defined below + # Since the hub is used in the callbacks, we need to define it first + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + # Set pins + in_pin = await cg.gpio_pin_expression(config[CONF_IN_PIN]) + cg.add(var.set_in_pin(in_pin)) + + out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN]) + cg.add(var.set_out_pin(out_pin)) + + non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN} + for key, value in config.items(): + if key not in non_sensors: + cg.add(getattr(var, f"set_{key}")(value)) diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp new file mode 100644 index 0000000000..c26fbced32 --- /dev/null +++ b/esphome/components/opentherm/hub.cpp @@ -0,0 +1,277 @@ +#include "hub.h" +#include "esphome/core/helpers.h" + +#include + +namespace esphome { +namespace opentherm { + +static const char *const TAG = "opentherm"; + +OpenthermData OpenthermHub::build_request_(MessageId request_id) { + OpenthermData data; + data.type = 0; + data.id = 0; + data.valueHB = 0; + data.valueLB = 0; + + // First, handle the status request. This requires special logic, because we + // wouldn't want to inadvertently disable domestic hot water, for example. + // It is also included in the macro-generated code below, but that will + // never be executed, because we short-circuit it here. + if (request_id == MessageId::STATUS) { + bool const ch_enabled = this->ch_enable; + bool dhw_enabled = this->dhw_enable; + bool cooling_enabled = this->cooling_enable; + bool otc_enabled = this->otc_active; + bool ch2_enabled = this->ch2_active; + + data.type = MessageType::READ_DATA; + data.id = MessageId::STATUS; + data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4); + +// Disable incomplete switch statement warnings, because the cases in each +// switch are generated based on the configured sensors and inputs. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" + + // TODO: This is a placeholder for an auto-generated switch statement which builds request structure based on + // which sensors are enabled in config. + +#pragma GCC diagnostic pop + + return data; + } + return OpenthermData(); +} + +OpenthermHub::OpenthermHub() : Component() {} + +void OpenthermHub::process_response(OpenthermData &data) { + ESP_LOGD(TAG, "Received OpenTherm response with id %d (%s)", data.id, + this->opentherm_->message_id_to_str((MessageId) data.id)); + ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(data).c_str()); +} + +void OpenthermHub::setup() { + ESP_LOGD(TAG, "Setting up OpenTherm component"); + this->opentherm_ = make_unique(this->in_pin_, this->out_pin_); + if (!this->opentherm_->initialize()) { + ESP_LOGE(TAG, "Failed to initialize OpenTherm protocol. See previous log messages for details."); + this->mark_failed(); + return; + } + + // Ensure that there is at least one request, as we are required to + // communicate at least once every second. Sending the status request is + // good practice anyway. + this->add_repeating_message(MessageId::STATUS); + + this->current_message_iterator_ = this->initial_messages_.begin(); +} + +void OpenthermHub::on_shutdown() { this->opentherm_->stop(); } + +void OpenthermHub::loop() { + if (this->sync_mode_) { + this->sync_loop_(); + return; + } + + auto cur_time = millis(); + auto const cur_mode = this->opentherm_->get_mode(); + switch (cur_mode) { + case OperationMode::WRITE: + case OperationMode::READ: + case OperationMode::LISTEN: + if (!this->check_timings_(cur_time)) { + break; + } + this->last_mode_ = cur_mode; + break; + case OperationMode::ERROR_PROTOCOL: + if (this->last_mode_ == OperationMode::WRITE) { + this->handle_protocol_write_error_(); + } else if (this->last_mode_ == OperationMode::READ) { + this->handle_protocol_read_error_(); + } + + this->stop_opentherm_(); + break; + case OperationMode::ERROR_TIMEOUT: + this->handle_timeout_error_(); + this->stop_opentherm_(); + break; + case OperationMode::IDLE: + if (this->should_skip_loop_(cur_time)) { + break; + } + this->start_conversation_(); + break; + case OperationMode::SENT: + // Message sent, now listen for the response. + this->opentherm_->listen(); + break; + case OperationMode::RECEIVED: + this->read_response_(); + break; + } +} + +void OpenthermHub::sync_loop_() { + if (!this->opentherm_->is_idle()) { + ESP_LOGE(TAG, "OpenTherm is not idle at the start of the loop"); + return; + } + + auto cur_time = millis(); + + this->check_timings_(cur_time); + + if (this->should_skip_loop_(cur_time)) { + return; + } + + this->start_conversation_(); + + if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { + ESP_LOGE(TAG, "Hub timeout triggered during send"); + this->stop_opentherm_(); + return; + } + + if (this->opentherm_->is_error()) { + this->handle_protocol_write_error_(); + this->stop_opentherm_(); + return; + } else if (!this->opentherm_->is_sent()) { + ESP_LOGW(TAG, "Unexpected state after sending request: %s", + this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); + this->stop_opentherm_(); + return; + } + + // Listen for the response + this->opentherm_->listen(); + if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { + ESP_LOGE(TAG, "Hub timeout triggered during receive"); + this->stop_opentherm_(); + return; + } + + if (this->opentherm_->is_timeout()) { + this->handle_timeout_error_(); + this->stop_opentherm_(); + return; + } else if (this->opentherm_->is_protocol_error()) { + this->handle_protocol_read_error_(); + this->stop_opentherm_(); + return; + } else if (!this->opentherm_->has_message()) { + ESP_LOGW(TAG, "Unexpected state after receiving response: %s", + this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); + this->stop_opentherm_(); + return; + } + + this->read_response_(); +} + +bool OpenthermHub::check_timings_(uint32_t cur_time) { + if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) { + ESP_LOGW(TAG, + "%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other " + "components that might slow the loop down.", + (int) (cur_time - this->last_conversation_start_)); + this->stop_opentherm_(); + return false; + } + + return true; +} + +bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { + if (this->last_conversation_end_ > 0 && (cur_time - this->last_conversation_end_) < 100) { + ESP_LOGV(TAG, "Less than 100 ms elapsed since last convo, skipping this iteration"); + return true; + } + + return false; +} + +void OpenthermHub::start_conversation_() { + if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) { + this->sending_initial_ = false; + this->current_message_iterator_ = this->repeating_messages_.begin(); + } else if (this->current_message_iterator_ == this->repeating_messages_.end()) { + this->current_message_iterator_ = this->repeating_messages_.begin(); + } + + auto request = this->build_request_(*this->current_message_iterator_); + + ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id, + this->opentherm_->message_id_to_str((MessageId) request.id)); + ESP_LOGD(TAG, "%s", this->opentherm_->debug_data(request).c_str()); + // Send the request + this->last_conversation_start_ = millis(); + this->opentherm_->send(request); +} + +void OpenthermHub::read_response_() { + OpenthermData response; + if (!this->opentherm_->get_message(response)) { + ESP_LOGW(TAG, "Couldn't get the response, but flags indicated success. This is a bug."); + this->stop_opentherm_(); + return; + } + + this->stop_opentherm_(); + + this->process_response(response); + + this->current_message_iterator_++; +} + +void OpenthermHub::stop_opentherm_() { + this->opentherm_->stop(); + this->last_conversation_end_ = millis(); +} + +void OpenthermHub::handle_protocol_write_error_() { + ESP_LOGW(TAG, "Error while sending request: %s", + this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); + ESP_LOGW(TAG, "%s", this->opentherm_->debug_data(this->last_request_).c_str()); +} + +void OpenthermHub::handle_protocol_read_error_() { + OpenThermError error; + this->opentherm_->get_protocol_error(error); + ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", this->opentherm_->debug_error(error).c_str()); +} + +void OpenthermHub::handle_timeout_error_() { + ESP_LOGW(TAG, "Receive response timed out at a protocol level"); + this->stop_opentherm_(); +} + +#define ID(x) x +#define SHOW2(x) #x +#define SHOW(x) SHOW2(x) + +void OpenthermHub::dump_config() { + ESP_LOGCONFIG(TAG, "OpenTherm:"); + LOG_PIN(" In: ", this->in_pin_); + LOG_PIN(" Out: ", this->out_pin_); + ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_); + ESP_LOGCONFIG(TAG, " Initial requests:"); + for (auto type : this->initial_messages_) { + ESP_LOGCONFIG(TAG, " - %d", type); + } + ESP_LOGCONFIG(TAG, " Repeating requests:"); + for (auto type : this->repeating_messages_) { + ESP_LOGCONFIG(TAG, " - %d", type); + } +} + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h new file mode 100644 index 0000000000..ce9f09fe33 --- /dev/null +++ b/esphome/components/opentherm/hub.h @@ -0,0 +1,110 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" +#include "esphome/core/component.h" +#include "esphome/core/log.h" + +#include "opentherm.h" + +#include +#include +#include +#include + +namespace esphome { +namespace opentherm { + +// OpenTherm component for ESPHome +class OpenthermHub : public Component { + protected: + // Communication pins for the OpenTherm interface + InternalGPIOPin *in_pin_, *out_pin_; + // The OpenTherm interface + std::unique_ptr opentherm_; + + // The set of initial messages to send on starting communication with the boiler + std::unordered_set initial_messages_; + // and the repeating messages which are sent repeatedly to update various sensors + // and boiler parameters (like the setpoint). + std::unordered_set repeating_messages_; + // Indicates if we are still working on the initial requests or not + bool sending_initial_ = true; + // Index for the current request in one of the _requests sets. + std::unordered_set::const_iterator current_message_iterator_; + + uint32_t last_conversation_start_ = 0; + uint32_t last_conversation_end_ = 0; + OperationMode last_mode_ = IDLE; + OpenthermData last_request_; + + // Synchronous communication mode prevents other components from disabling interrupts while + // we are talking to the boiler. Enable if you experience random intermittent invalid response errors. + // Very likely to happen while using Dallas temperature sensors. + bool sync_mode_ = false; + + // Create OpenTherm messages based on the message id + OpenthermData build_request_(MessageId request_id); + void handle_protocol_write_error_(); + void handle_protocol_read_error_(); + void handle_timeout_error_(); + void stop_opentherm_(); + void start_conversation_(); + void read_response_(); + bool check_timings_(uint32_t cur_time); + bool should_skip_loop_(uint32_t cur_time) const; + void sync_loop_(); + + template bool spin_wait_(uint32_t timeout, F func) { + auto start_time = millis(); + while (func()) { + yield(); + auto cur_time = millis(); + if (cur_time - start_time >= timeout) { + return false; + } + } + return true; + } + + public: + // Constructor with references to the global interrupt handlers + OpenthermHub(); + + // Handle responses from the OpenTherm interface + void process_response(OpenthermData &data); + + // Setters for the input and output OpenTherm interface pins + void set_in_pin(InternalGPIOPin *in_pin) { this->in_pin_ = in_pin; } + void set_out_pin(InternalGPIOPin *out_pin) { this->out_pin_ = out_pin; } + + // Add a request to the set of initial requests + void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); } + // Add a request to the set of repeating requests. Note that a large number of repeating + // requests will slow down communication with the boiler. Each request may take up to 1 second, + // so with all sensors enabled, it may take about half a minute before a change in setpoint + // will be processed. + void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); } + + // There are five status variables, which can either be set as a simple variable, + // or using a switch. ch_enable and dhw_enable default to true, the others to false. + bool ch_enable = true, dhw_enable = true, cooling_enable = false, otc_active = false, ch2_active = false; + + // Setters for the status variables + void set_ch_enable(bool value) { this->ch_enable = value; } + void set_dhw_enable(bool value) { this->dhw_enable = value; } + void set_cooling_enable(bool value) { this->cooling_enable = value; } + void set_otc_active(bool value) { this->otc_active = value; } + void set_ch2_active(bool value) { this->ch2_active = value; } + void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void setup() override; + void on_shutdown() override; + void loop() override; + void dump_config() override; +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp new file mode 100644 index 0000000000..b830cc01d3 --- /dev/null +++ b/esphome/components/opentherm/opentherm.cpp @@ -0,0 +1,568 @@ +/* + * OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but + * heavily modified to comply with ESPHome coding standards and provide better logging. + * Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International + * Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project. + */ + +#include "opentherm.h" +#include "esphome/core/helpers.h" +#if defined(ESP32) || defined(USE_ESP_IDF) +#include "driver/timer.h" +#include "esp_err.h" +#endif +#ifdef ESP8266 +#include "Arduino.h" +#endif +#include +#include +#include + +namespace esphome { +namespace opentherm { + +using std::string; +using std::bitset; +using std::stringstream; +using std::to_string; + +static const char *const TAG = "opentherm"; + +#ifdef ESP8266 +OpenTherm *OpenTherm::instance_ = nullptr; +#endif + +OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout) + : in_pin_(in_pin), + out_pin_(out_pin), +#if defined(ESP32) || defined(USE_ESP_IDF) + timer_group_(TIMER_GROUP_0), + timer_idx_(TIMER_0), +#endif + mode_(OperationMode::IDLE), + error_type_(ProtocolErrorType::NO_ERROR), + capture_(0), + clock_(0), + data_(0), + bit_pos_(0), + timeout_counter_(-1), + device_timeout_(device_timeout) { + this->isr_in_pin_ = in_pin->to_isr(); + this->isr_out_pin_ = out_pin->to_isr(); +} + +bool OpenTherm::initialize() { +#ifdef ESP8266 + OpenTherm::instance_ = this; +#endif + this->in_pin_->pin_mode(gpio::FLAG_INPUT); + this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->out_pin_->digital_write(true); + +#if defined(ESP32) || defined(USE_ESP_IDF) + return this->init_esp32_timer_(); +#else + return true; +#endif +} + +void OpenTherm::listen() { + this->stop_timer_(); + this->timeout_counter_ = this->device_timeout_ * 5; // timer_ ticks at 5 ticks/ms + + this->mode_ = OperationMode::LISTEN; + this->data_ = 0; + this->bit_pos_ = 0; + + this->start_read_timer_(); +} + +void OpenTherm::send(OpenthermData &data) { + this->stop_timer_(); + this->data_ = data.type; + this->data_ = (this->data_ << 12) | data.id; + this->data_ = (this->data_ << 8) | data.valueHB; + this->data_ = (this->data_ << 8) | data.valueLB; + if (!check_parity_(this->data_)) { + this->data_ = this->data_ | 0x80000000; + } + + this->clock_ = 1; // clock starts at HIGH + this->bit_pos_ = 33; // count down (33 == start bit, 32-1 data, 0 == stop bit) + this->mode_ = OperationMode::WRITE; + + this->start_write_timer_(); +} + +bool OpenTherm::get_message(OpenthermData &data) { + if (this->mode_ == OperationMode::RECEIVED) { + data.type = (this->data_ >> 28) & 0x7; + data.id = (this->data_ >> 16) & 0xFF; + data.valueHB = (this->data_ >> 8) & 0xFF; + data.valueLB = this->data_ & 0xFF; + return true; + } + return false; +} + +bool OpenTherm::get_protocol_error(OpenThermError &error) { + if (this->mode_ != OperationMode::ERROR_PROTOCOL) { + return false; + } + + error.error_type = this->error_type_; + error.bit_pos = this->bit_pos_; + error.capture = this->capture_; + error.clock = this->clock_; + error.data = this->data_; + + return true; +} + +void OpenTherm::stop() { + this->stop_timer_(); + this->mode_ = OperationMode::IDLE; +} + +void IRAM_ATTR OpenTherm::read_() { + this->data_ = 0; + this->bit_pos_ = 0; + this->mode_ = OperationMode::READ; + this->capture_ = 1; // reset counter and add as if read start bit + this->clock_ = 1; // clock is high at the start of comm + this->start_read_timer_(); // get us into 1/4 of manchester code. 5 timer ticks constitute 1 ms, which is 1 bit + // period in OpenTherm. +} + +bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) { + if (arg->mode_ == OperationMode::LISTEN) { + if (arg->timeout_counter_ == 0) { + arg->mode_ = OperationMode::ERROR_TIMEOUT; + arg->stop_timer_(); + return false; + } + bool const value = arg->isr_in_pin_.digital_read(); + if (value) { // incoming data (rising signal) + arg->read_(); + } + if (arg->timeout_counter_ > 0) { + arg->timeout_counter_--; + } + } else if (arg->mode_ == OperationMode::READ) { + bool const value = arg->isr_in_pin_.digital_read(); + uint8_t const last = (arg->capture_ & 1); + if (value != last) { + // transition of signal from last sampling + if (arg->clock_ == 1 && arg->capture_ > 0xF) { + // no transition in the middle of the bit + arg->mode_ = OperationMode::ERROR_PROTOCOL; + arg->error_type_ = ProtocolErrorType::NO_TRANSITION; + arg->stop_timer_(); + return false; + } else if (arg->clock_ == 1 || arg->capture_ > 0xF) { + // transition in the middle of the bit OR no transition between two bit, both are valid data points + if (arg->bit_pos_ == BitPositions::STOP_BIT) { + // expecting stop bit + auto stop_bit_error = arg->verify_stop_bit_(last); + if (stop_bit_error == ProtocolErrorType::NO_ERROR) { + arg->mode_ = OperationMode::RECEIVED; + arg->stop_timer_(); + return false; + } else { + // end of data not verified, invalid data + arg->mode_ = OperationMode::ERROR_PROTOCOL; + arg->error_type_ = stop_bit_error; + arg->stop_timer_(); + return false; + } + } else { + // normal data point at clock high + arg->bit_read_(last); + arg->clock_ = 0; + } + } else { + // clock low, not a data point, switch clock + arg->clock_ = 1; + } + arg->capture_ = 1; // reset counter + } else if (arg->capture_ > 0xFF) { + // no change for too long, invalid mancheter encoding + arg->mode_ = OperationMode::ERROR_PROTOCOL; + arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; + arg->stop_timer_(); + return false; + } + arg->capture_ = (arg->capture_ << 1) | value; + } else if (arg->mode_ == OperationMode::WRITE) { + // write data to pin + if (arg->bit_pos_ == 33 || arg->bit_pos_ == 0) { // start bit + arg->write_bit_(1, arg->clock_); + } else { // data bits + arg->write_bit_(read_bit(arg->data_, arg->bit_pos_ - 1), arg->clock_); + } + if (arg->clock_ == 0) { + if (arg->bit_pos_ <= 0) { // check termination + arg->mode_ = OperationMode::SENT; // all data written + arg->stop_timer_(); + } + arg->bit_pos_--; + arg->clock_ = 1; + } else { + arg->clock_ = 0; + } + } + + return false; +} + +#ifdef ESP8266 +void IRAM_ATTR OpenTherm::esp8266_timer_isr() { OpenTherm::timer_isr(OpenTherm::instance_); } +#endif + +void IRAM_ATTR OpenTherm::bit_read_(uint8_t value) { + this->data_ = (this->data_ << 1) | value; + this->bit_pos_++; +} + +ProtocolErrorType OpenTherm::verify_stop_bit_(uint8_t value) { + if (value) { // stop bit detected + return check_parity_(this->data_) ? ProtocolErrorType::NO_ERROR : ProtocolErrorType::PARITY_ERROR; + } else { // no stop bit detected, error + return ProtocolErrorType::INVALID_STOP_BIT; + } +} + +void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) { + if (clock == 1) { // left part of manchester encoding + this->isr_out_pin_.digital_write(!high); // low means logical 1 to protocol + } else { // right part of manchester encoding + this->isr_out_pin_.digital_write(high); // high means logical 0 to protocol + } +} + +#if defined(ESP32) || defined(USE_ESP_IDF) + +bool OpenTherm::init_esp32_timer_() { + // Search for a free timer. Maybe unstable, we'll see. + int cur_timer = 0; + timer_group_t timer_group = TIMER_GROUP_0; + timer_idx_t timer_idx = TIMER_0; + bool timer_found = false; + + for (; cur_timer < SOC_TIMER_GROUP_TOTAL_TIMERS; cur_timer++) { + timer_config_t temp_config; + timer_group = cur_timer < 2 ? TIMER_GROUP_0 : TIMER_GROUP_1; + timer_idx = cur_timer < 2 ? (timer_idx_t) cur_timer : (timer_idx_t) (cur_timer - 2); + + auto err = timer_get_config(timer_group, timer_idx, &temp_config); + if (err == ESP_ERR_INVALID_ARG) { + // Error means timer was not initialized (or other things, but we are careful with our args) + timer_found = true; + break; + } + + ESP_LOGD(TAG, "Timer %d:%d seems to be occupied, will try another", timer_group, timer_idx); + } + + if (!timer_found) { + ESP_LOGE(TAG, "No free timer was found! OpenTherm cannot function without a timer."); + return false; + } + + ESP_LOGD(TAG, "Found free timer %d:%d", timer_group, timer_idx); + this->timer_group_ = timer_group; + this->timer_idx_ = timer_idx; + + timer_config_t const config = { + .alarm_en = TIMER_ALARM_EN, + .counter_en = TIMER_PAUSE, + .intr_type = TIMER_INTR_LEVEL, + .counter_dir = TIMER_COUNT_UP, + .auto_reload = TIMER_AUTORELOAD_EN, +#if ESP_IDF_VERSION_MAJOR >= 5 + .clk_src = TIMER_SRC_CLK_DEFAULT, +#endif + .divider = 80, + }; + + esp_err_t result; + + result = timer_init(this->timer_group_, this->timer_idx_, &config); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to init timer. Error: %s", error); + return false; + } + + result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to set counter value. Error: %s", error); + return false; + } + + result = timer_isr_callback_add(this->timer_group_, this->timer_idx_, reinterpret_cast(timer_isr), + this, 0); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to register timer interrupt. Error: %s", error); + return false; + } + + return true; +} + +void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { + esp_err_t result; + + result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error); + return; + } + + result = timer_start(this->timer_group_, this->timer_idx_); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error); + return; + } +} + +// 5 kHz timer_ +void IRAM_ATTR OpenTherm::start_read_timer_() { + InterruptLock const lock; + this->start_esp32_timer_(200); +} + +// 2 kHz timer_ +void IRAM_ATTR OpenTherm::start_write_timer_() { + InterruptLock const lock; + this->start_esp32_timer_(500); +} + +void IRAM_ATTR OpenTherm::stop_timer_() { + InterruptLock const lock; + + esp_err_t result; + + result = timer_pause(this->timer_group_, this->timer_idx_); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error); + return; + } + + result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); + if (result != ESP_OK) { + const auto *error = esp_err_to_name(result); + ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error); + return; + } +} + +#endif // END ESP32 + +#ifdef ESP8266 +// 5 kHz timer_ +void OpenTherm::start_read_timer_() { + InterruptLock const lock; + timer1_attachInterrupt(OpenTherm::esp8266_timer_isr); + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max) + timer1_write(1000); // 5kHz +} + +// 2 kHz timer_ +void OpenTherm::start_write_timer_() { + InterruptLock const lock; + timer1_attachInterrupt(OpenTherm::esp8266_timer_isr); + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); // 5MHz (5 ticks/us - 1677721.4 us max) + timer1_write(2500); // 2kHz +} + +void OpenTherm::stop_timer_() { + InterruptLock const lock; + timer1_disable(); + timer1_detachInterrupt(); +} + +#endif // END ESP8266 + +// https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd +bool OpenTherm::check_parity_(uint32_t val) { + val ^= val >> 16; + val ^= val >> 8; + val ^= val >> 4; + val ^= val >> 2; + val ^= val >> 1; + return (~val) & 1; +} + +#define TO_STRING_MEMBER(name) \ + case name: \ + return #name; + +const char *OpenTherm::operation_mode_to_str(OperationMode mode) { + switch (mode) { + TO_STRING_MEMBER(IDLE) + TO_STRING_MEMBER(LISTEN) + TO_STRING_MEMBER(READ) + TO_STRING_MEMBER(RECEIVED) + TO_STRING_MEMBER(WRITE) + TO_STRING_MEMBER(SENT) + TO_STRING_MEMBER(ERROR_PROTOCOL) + TO_STRING_MEMBER(ERROR_TIMEOUT) + default: + return ""; + } +} +const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { + switch (error_type) { + TO_STRING_MEMBER(NO_ERROR) + TO_STRING_MEMBER(NO_TRANSITION) + TO_STRING_MEMBER(INVALID_STOP_BIT) + TO_STRING_MEMBER(PARITY_ERROR) + TO_STRING_MEMBER(NO_CHANGE_TOO_LONG) + default: + return ""; + } +} +const char *OpenTherm::message_type_to_str(MessageType message_type) { + switch (message_type) { + TO_STRING_MEMBER(READ_DATA) + TO_STRING_MEMBER(READ_ACK) + TO_STRING_MEMBER(WRITE_DATA) + TO_STRING_MEMBER(WRITE_ACK) + TO_STRING_MEMBER(INVALID_DATA) + TO_STRING_MEMBER(DATA_INVALID) + TO_STRING_MEMBER(UNKNOWN_DATAID) + default: + return ""; + } +} + +const char *OpenTherm::message_id_to_str(MessageId id) { + switch (id) { + TO_STRING_MEMBER(STATUS) + TO_STRING_MEMBER(CH_SETPOINT) + TO_STRING_MEMBER(CONTROLLER_CONFIG) + TO_STRING_MEMBER(DEVICE_CONFIG) + TO_STRING_MEMBER(COMMAND_CODE) + TO_STRING_MEMBER(FAULT_FLAGS) + TO_STRING_MEMBER(REMOTE) + TO_STRING_MEMBER(COOLING_CONTROL) + TO_STRING_MEMBER(CH2_SETPOINT) + TO_STRING_MEMBER(CH_SETPOINT_OVERRIDE) + TO_STRING_MEMBER(TSP_COUNT) + TO_STRING_MEMBER(TSP_COMMAND) + TO_STRING_MEMBER(FHB_SIZE) + TO_STRING_MEMBER(FHB_COMMAND) + TO_STRING_MEMBER(MAX_MODULATION_LEVEL) + TO_STRING_MEMBER(MAX_BOILER_CAPACITY) + TO_STRING_MEMBER(ROOM_SETPOINT) + TO_STRING_MEMBER(MODULATION_LEVEL) + TO_STRING_MEMBER(CH_WATER_PRESSURE) + TO_STRING_MEMBER(DHW_FLOW_RATE) + TO_STRING_MEMBER(DAY_TIME) + TO_STRING_MEMBER(DATE) + TO_STRING_MEMBER(YEAR) + TO_STRING_MEMBER(ROOM_SETPOINT_CH2) + TO_STRING_MEMBER(ROOM_TEMP) + TO_STRING_MEMBER(FEED_TEMP) + TO_STRING_MEMBER(DHW_TEMP) + TO_STRING_MEMBER(OUTSIDE_TEMP) + TO_STRING_MEMBER(RETURN_WATER_TEMP) + TO_STRING_MEMBER(SOLAR_STORE_TEMP) + TO_STRING_MEMBER(SOLAR_COLLECT_TEMP) + TO_STRING_MEMBER(FEED_TEMP_CH2) + TO_STRING_MEMBER(DHW2_TEMP) + TO_STRING_MEMBER(EXHAUST_TEMP) + TO_STRING_MEMBER(FAN_SPEED) + TO_STRING_MEMBER(FLAME_CURRENT) + TO_STRING_MEMBER(DHW_BOUNDS) + TO_STRING_MEMBER(CH_BOUNDS) + TO_STRING_MEMBER(OTC_CURVE_BOUNDS) + TO_STRING_MEMBER(DHW_SETPOINT) + TO_STRING_MEMBER(MAX_CH_SETPOINT) + TO_STRING_MEMBER(OTC_CURVE_RATIO) + TO_STRING_MEMBER(HVAC_STATUS) + TO_STRING_MEMBER(REL_VENT_SETPOINT) + TO_STRING_MEMBER(DEVICE_VENT) + TO_STRING_MEMBER(REL_VENTILATION) + TO_STRING_MEMBER(REL_HUMID_EXHAUST) + TO_STRING_MEMBER(SUPPLY_INLET_TEMP) + TO_STRING_MEMBER(SUPPLY_OUTLET_TEMP) + TO_STRING_MEMBER(EXHAUST_INLET_TEMP) + TO_STRING_MEMBER(EXHAUST_OUTLET_TEMP) + TO_STRING_MEMBER(NOM_REL_VENTILATION) + TO_STRING_MEMBER(OVERRIDE_FUNC) + TO_STRING_MEMBER(OEM_DIAGNOSTIC) + TO_STRING_MEMBER(BURNER_STARTS) + TO_STRING_MEMBER(CH_PUMP_STARTS) + TO_STRING_MEMBER(DHW_PUMP_STARTS) + TO_STRING_MEMBER(DHW_BURNER_STARTS) + TO_STRING_MEMBER(BURNER_HOURS) + TO_STRING_MEMBER(CH_PUMP_HOURS) + TO_STRING_MEMBER(DHW_PUMP_HOURS) + TO_STRING_MEMBER(DHW_BURNER_HOURS) + TO_STRING_MEMBER(OT_VERSION_CONTROLLER) + TO_STRING_MEMBER(OT_VERSION_DEVICE) + TO_STRING_MEMBER(VERSION_CONTROLLER) + TO_STRING_MEMBER(VERSION_DEVICE) + default: + return ""; + } +} + +string OpenTherm::debug_data(OpenthermData &data) { + stringstream result; + result << bitset<8>(data.type) << " " << bitset<8>(data.id) << " " << bitset<8>(data.valueHB) << " " + << bitset<8>(data.valueLB) << "\n"; + result << "type: " << this->message_type_to_str((MessageType) data.type) << "; "; + result << "id: " << to_string(data.id) << "; "; + result << "HB: " << to_string(data.valueHB) << "; "; + result << "LB: " << to_string(data.valueLB) << "; "; + result << "uint_16: " << to_string(data.u16()) << "; "; + result << "float: " << to_string(data.f88()); + + return result.str(); +} +std::string OpenTherm::debug_error(OpenThermError &error) { + stringstream result; + result << "type: " << this->protocol_error_to_to_str(error.error_type) << "; "; + result << "data: "; + result << format_hex(error.data); + result << "; clock: " << to_string(clock_); + result << "; capture: " << bitset<32>(error.capture); + result << "; bit_pos: " << to_string(error.bit_pos); + + return result.str(); +} + +float OpenthermData::f88() { return ((float) this->s16()) / 256.0; } + +void OpenthermData::f88(float value) { this->s16((int16_t) (value * 256)); } + +uint16_t OpenthermData::u16() { + uint16_t const value = this->valueHB; + return (value << 8) | this->valueLB; +} + +void OpenthermData::u16(uint16_t value) { + this->valueLB = value & 0xFF; + this->valueHB = (value >> 8) & 0xFF; +} + +int16_t OpenthermData::s16() { + int16_t const value = this->valueHB; + return (value << 8) | this->valueLB; +} + +void OpenthermData::s16(int16_t value) { + this->valueLB = value & 0xFF; + this->valueHB = (value >> 8) & 0xFF; +} + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h new file mode 100644 index 0000000000..609cfb6243 --- /dev/null +++ b/esphome/components/opentherm/opentherm.h @@ -0,0 +1,347 @@ +/* + * OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but + * heavily modified to comply with ESPHome coding standards and provide better logging. + * Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International + * Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project. + */ + +#pragma once + +#include +#include +#include +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#if defined(ESP32) || defined(USE_ESP_IDF) +#include "driver/timer.h" +#endif + +namespace esphome { +namespace opentherm { + +// TODO: Account for immutable semantics change in hub.cpp when doing later installments of OpenTherm PR +template constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; } + +template constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); } + +template constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); } + +template constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) { + return bit_value ? setBit(value, bit) : clearBit(value, bit); +} + +enum OperationMode { + IDLE = 0, // no operation + + LISTEN = 1, // waiting for transmission to start + READ = 2, // reading 32-bit data frame + RECEIVED = 3, // data frame received with valid start and stop bit + + WRITE = 4, // writing data with timer_ + SENT = 5, // all data written to output + + ERROR_PROTOCOL = 8, // manchester protocol data transfer error + ERROR_TIMEOUT = 9 // read timeout +}; + +enum ProtocolErrorType { + NO_ERROR = 0, // No error + NO_TRANSITION = 1, // No transition in the middle of the bit + INVALID_STOP_BIT = 2, // Stop bit wasn't present when expected + PARITY_ERROR = 3, // Parity check didn't pass + NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks +}; + +enum MessageType { + READ_DATA = 0, + READ_ACK = 4, + WRITE_DATA = 1, + WRITE_ACK = 5, + INVALID_DATA = 2, + DATA_INVALID = 6, + UNKNOWN_DATAID = 7 +}; + +enum MessageId { + STATUS = 0, + CH_SETPOINT = 1, + CONTROLLER_CONFIG = 2, + DEVICE_CONFIG = 3, + COMMAND_CODE = 4, + FAULT_FLAGS = 5, + REMOTE = 6, + COOLING_CONTROL = 7, + CH2_SETPOINT = 8, + CH_SETPOINT_OVERRIDE = 9, + TSP_COUNT = 10, + TSP_COMMAND = 11, + FHB_SIZE = 12, + FHB_COMMAND = 13, + MAX_MODULATION_LEVEL = 14, + MAX_BOILER_CAPACITY = 15, // u8_hb - u8_lb gives min modulation level + ROOM_SETPOINT = 16, + MODULATION_LEVEL = 17, + CH_WATER_PRESSURE = 18, + DHW_FLOW_RATE = 19, + DAY_TIME = 20, + DATE = 21, + YEAR = 22, + ROOM_SETPOINT_CH2 = 23, + ROOM_TEMP = 24, + FEED_TEMP = 25, + DHW_TEMP = 26, + OUTSIDE_TEMP = 27, + RETURN_WATER_TEMP = 28, + SOLAR_STORE_TEMP = 29, + SOLAR_COLLECT_TEMP = 30, + FEED_TEMP_CH2 = 31, + DHW2_TEMP = 32, + EXHAUST_TEMP = 33, + FAN_SPEED = 35, + FLAME_CURRENT = 36, + DHW_BOUNDS = 48, + CH_BOUNDS = 49, + OTC_CURVE_BOUNDS = 50, + DHW_SETPOINT = 56, + MAX_CH_SETPOINT = 57, + OTC_CURVE_RATIO = 58, + + // HVAC Specific Message IDs + HVAC_STATUS = 70, + REL_VENT_SETPOINT = 71, + DEVICE_VENT = 74, + REL_VENTILATION = 77, + REL_HUMID_EXHAUST = 78, + SUPPLY_INLET_TEMP = 80, + SUPPLY_OUTLET_TEMP = 81, + EXHAUST_INLET_TEMP = 82, + EXHAUST_OUTLET_TEMP = 83, + NOM_REL_VENTILATION = 87, + + OVERRIDE_FUNC = 100, + OEM_DIAGNOSTIC = 115, + BURNER_STARTS = 116, + CH_PUMP_STARTS = 117, + DHW_PUMP_STARTS = 118, + DHW_BURNER_STARTS = 119, + BURNER_HOURS = 120, + CH_PUMP_HOURS = 121, + DHW_PUMP_HOURS = 122, + DHW_BURNER_HOURS = 123, + OT_VERSION_CONTROLLER = 124, + OT_VERSION_DEVICE = 125, + VERSION_CONTROLLER = 126, + VERSION_DEVICE = 127 +}; + +enum BitPositions { STOP_BIT = 33 }; + +/** + * Structure to hold Opentherm data packet content. + * Use f88(), u16() or s16() functions to get appropriate value of data packet accoridng to id of message. + */ +struct OpenthermData { + uint8_t type; + uint8_t id; + uint8_t valueHB; + uint8_t valueLB; + + OpenthermData() : type(0), id(0), valueHB(0), valueLB(0) {} + + /** + * @return float representation of data packet value + */ + float f88(); + + /** + * @param float number to set as value of this data packet + */ + void f88(float value); + + /** + * @return unsigned 16b integer representation of data packet value + */ + uint16_t u16(); + + /** + * @param unsigned 16b integer number to set as value of this data packet + */ + void u16(uint16_t value); + + /** + * @return signed 16b integer representation of data packet value + */ + int16_t s16(); + + /** + * @param signed 16b integer number to set as value of this data packet + */ + void s16(int16_t value); +}; + +struct OpenThermError { + ProtocolErrorType error_type; + uint32_t capture; + uint8_t clock; + uint32_t data; + uint8_t bit_pos; +}; + +/** + * Opentherm static class that supports either listening or sending Opentherm data packets in the same time + */ +class OpenTherm { + public: + OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout = 800); + + /** + * Setup pins. + */ + bool initialize(); + + /** + * Start listening for Opentherm data packet comming from line connected to given pin. + * If data packet is received then has_message() function returns true and data packet can be retrieved by calling + * get_message() function. If timeout > 0 then this function waits for incomming data package for timeout millis and + * if no data packet is recevived, error state is indicated by is_error() function. If either data packet is received + * or timeout is reached listening is stopped. + */ + void listen(); + + /** + * Use this function to check whether listen() function already captured a valid data packet. + * + * @return true if data packet has been captured from line by listen() function. + */ + bool has_message() { return mode_ == OperationMode::RECEIVED; } + + /** + * Use this to retrive data packed captured by listen() function. Data packet is ready when has_message() function + * returns true. This function can be called multiple times until stop() is called. + * + * @param data reference to data structure to which fill the data packet data. + * @return true if packet was ready and was filled into data structure passed, false otherwise. + */ + bool get_message(OpenthermData &data); + + /** + * Immediately send out Opentherm data packet to line connected on given pin. + * Completed data transfer is indicated by is_sent() function. + * Error state is indicated by is_error() function. + * + * @param data Opentherm data packet. + */ + void send(OpenthermData &data); + + /** + * Stops listening for data packet or sending out data packet and resets internal state of this class. + * Stops all timers and unattaches all interrupts. + */ + void stop(); + + /** + * Get protocol error details in case a protocol error occured. + * @param error reference to data structure to which fill the error details + * @return true if protocol error occured during last conversation, false otherwise. + */ + bool get_protocol_error(OpenThermError &error); + + /** + * Use this function to check whether send() function already finished sending data packed to line. + * + * @return true if data packet has been sent, false otherwise. + */ + bool is_sent() { return mode_ == OperationMode::SENT; } + + /** + * Indicates whether listinig or sending is not in progress. + * That also means that no timers are running and no interrupts are attached. + * + * @return true if listening nor sending is in progress. + */ + bool is_idle() { return mode_ == OperationMode::IDLE; } + + /** + * Indicates whether last listen() or send() operation ends up with an error. Includes both timeout and + * protocol errors. + * + * @return true if last listen() or send() operation ends up with an error. + */ + bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; } + + /** + * Indicates whether last listen() or send() operation ends up with a *timeout* error + * @return true if last listen() or send() operation ends up with a *timeout* error. + */ + bool is_timeout() { return mode_ == OperationMode::ERROR_TIMEOUT; } + + /** + * Indicates whether last listen() or send() operation ends up with a *protocol* error + * @return true if last listen() or send() operation ends up with a *protocol* error. + */ + bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; } + + bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; } + + OperationMode get_mode() { return mode_; } + + std::string debug_data(OpenthermData &data); + std::string debug_error(OpenThermError &error); + + const char *protocol_error_to_to_str(ProtocolErrorType error_type); + const char *message_type_to_str(MessageType message_type); + const char *operation_mode_to_str(OperationMode mode); + const char *message_id_to_str(MessageId id); + + static bool timer_isr(OpenTherm *arg); + +#ifdef ESP8266 + static void esp8266_timer_isr(); +#endif + + private: + InternalGPIOPin *in_pin_; + InternalGPIOPin *out_pin_; + ISRInternalGPIOPin isr_in_pin_; + ISRInternalGPIOPin isr_out_pin_; + +#if defined(ESP32) || defined(USE_ESP_IDF) + timer_group_t timer_group_; + timer_idx_t timer_idx_; +#endif + + OperationMode mode_; + ProtocolErrorType error_type_; + uint32_t capture_; + uint8_t clock_; + uint32_t data_; + uint8_t bit_pos_; + int32_t timeout_counter_; // <0 no timeout + + int32_t device_timeout_; + +#if defined(ESP32) || defined(USE_ESP_IDF) + bool init_esp32_timer_(); + void start_esp32_timer_(uint64_t alarm_value); +#endif + + void stop_timer_(); + + void read_(); // data detected start reading + void start_read_timer_(); // reading timer_ to sample at 1/5 of manchester code bit length (at 5kHz) + void start_write_timer_(); // writing timer_ to send manchester code (at 2kHz) + bool check_parity_(uint32_t val); + + void bit_read_(uint8_t value); + ProtocolErrorType verify_stop_bit_(uint8_t value); + void write_bit_(uint8_t high, uint8_t clock); + +#ifdef ESP8266 + // ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm + static OpenTherm *instance_; +#endif +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 2cd1aeba44..c4bc018b75 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -136,6 +136,9 @@ void Pipsolar::loop() { if (this->output_source_priority_battery_switch_) { this->output_source_priority_battery_switch_->publish_state(value_output_source_priority_ == 2); } + if (this->output_source_priority_hybrid_switch_) { + this->output_source_priority_hybrid_switch_->publish_state(value_output_source_priority_ == 3); + } if (this->charger_source_priority_) { this->charger_source_priority_->publish_state(value_charger_source_priority_); } diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index f20f44f095..373911b2d7 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -174,6 +174,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { PIPSOLAR_SWITCH(output_source_priority_utility_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_solar_switch, QPIRI) PIPSOLAR_SWITCH(output_source_priority_battery_switch, QPIRI) + PIPSOLAR_SWITCH(output_source_priority_hybrid_switch, QPIRI) PIPSOLAR_SWITCH(input_voltage_range_switch, QPIRI) PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI) PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI) diff --git a/esphome/components/pipsolar/switch/__init__.py b/esphome/components/pipsolar/switch/__init__.py index 7658c7d4f8..80bcdad62e 100644 --- a/esphome/components/pipsolar/switch/__init__.py +++ b/esphome/components/pipsolar/switch/__init__.py @@ -9,6 +9,7 @@ DEPENDENCIES = ["uart"] CONF_OUTPUT_SOURCE_PRIORITY_UTILITY = "output_source_priority_utility" CONF_OUTPUT_SOURCE_PRIORITY_SOLAR = "output_source_priority_solar" CONF_OUTPUT_SOURCE_PRIORITY_BATTERY = "output_source_priority_battery" +CONF_OUTPUT_SOURCE_PRIORITY_HYBRID = "output_source_priority_hybrid" CONF_INPUT_VOLTAGE_RANGE = "input_voltage_range" CONF_PV_OK_CONDITION_FOR_PARALLEL = "pv_ok_condition_for_parallel" CONF_PV_POWER_BALANCE = "pv_power_balance" @@ -17,6 +18,7 @@ TYPES = { CONF_OUTPUT_SOURCE_PRIORITY_UTILITY: ("POP00", None), CONF_OUTPUT_SOURCE_PRIORITY_SOLAR: ("POP01", None), CONF_OUTPUT_SOURCE_PRIORITY_BATTERY: ("POP02", None), + CONF_OUTPUT_SOURCE_PRIORITY_HYBRID: ("POP03", None), CONF_INPUT_VOLTAGE_RANGE: ("PGR01", "PGR00"), CONF_PV_OK_CONDITION_FOR_PARALLEL: ("PPVOKC1", "PPVOKC0"), CONF_PV_POWER_BALANCE: ("PSPB1", "PSPB0"), diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 09913bd713..3e9cf81e6e 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -1,4 +1,5 @@ #include "prometheus_handler.h" +#ifdef USE_NETWORK #include "esphome/core/application.h" namespace esphome { @@ -350,3 +351,4 @@ void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) } // namespace prometheus } // namespace esphome +#endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index a9505a3572..f5e49a1419 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include @@ -117,3 +118,4 @@ class PrometheusHandler : public AsyncWebHandler, public Component { } // namespace prometheus } // namespace esphome +#endif diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 625af76235..35fd782248 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -201,9 +201,6 @@ std::string ProntoProtocol::compensate_and_dump_sequence_(const RawTimings &data out += dump_duration_(t_duration, timebase); } - // append minimum gap - out += dump_duration_(PRONTO_DEFAULT_GAP, timebase, true); - return out; } diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index b897fa8fab..a5896796c0 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -49,7 +49,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #ifdef USE_ESP32 void configure_rmt_(); - uint32_t current_carrier_frequency_{UINT32_MAX}; + uint32_t current_carrier_frequency_{38000}; bool initialized_{false}; std::vector rmt_temp_; esp_err_t error_code_{ESP_OK}; diff --git a/esphome/components/rgbct/rgbct_light_output.h b/esphome/components/rgbct/rgbct_light_output.h index 9257d67cd1..9e23f783ae 100644 --- a/esphome/components/rgbct/rgbct_light_output.h +++ b/esphome/components/rgbct/rgbct_light_output.h @@ -23,10 +23,11 @@ class RGBCTLightOutput : public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLOR_TEMPERATURE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); + } traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index 0f55775608..a2ab17b75d 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -16,10 +16,11 @@ class RGBWLightOutput : public light::LightOutput { void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + } return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index 5a86b88595..9687360059 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -20,10 +20,11 @@ class RGBWWLightOutput : public light::LightOutput { void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - if (this->color_interlock_) + if (this->color_interlock_) { traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLD_WARM_WHITE}); - else + } else { traits.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE}); + } traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index f5c3b8bda2..8f1d26f780 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -1,6 +1,5 @@ import logging import os - from string import ascii_letters, digits import esphome.codegen as cg @@ -8,6 +7,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_FRAMEWORK, + CONF_PLATFORM_VERSION, CONF_SOURCE, CONF_VERSION, KEY_CORE, @@ -15,10 +15,9 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, PLATFORM_RP2040, - CONF_PLATFORM_VERSION, ) -from esphome.core import CORE, coroutine_with_priority, EsphomeError -from esphome.helpers import mkdir_p, write_file, copy_file_if_changed +from esphome.core import CORE, EsphomeError, coroutine_with_priority +from esphome.helpers import copy_file_if_changed, mkdir_p, write_file from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns @@ -81,19 +80,19 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases # - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 7, 2) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 9, 4) # The platformio/raspberrypi version to use for arduino frameworks # - https://github.com/platformio/platform-raspberrypi/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi -ARDUINO_PLATFORM_VERSION = cv.Version(1, 12, 0) +ARDUINO_PLATFORM_VERSION = cv.Version(1, 13, 0) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(3, 4, 0), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(3, 4, 0), None), + "dev": (cv.Version(3, 9, 4), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(3, 9, 4), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp index 3e5e82898d..2aaa2ceb19 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.cpp +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -7,8 +7,10 @@ #include #include +#include #include #include +#include namespace esphome { namespace rp2040_pio_led_strip { @@ -23,6 +25,19 @@ static std::map conf_count_ = { {CHIPSET_WS2812, false}, {CHIPSET_WS2812B, false}, {CHIPSET_SK6812, false}, {CHIPSET_SM16703, false}, {CHIPSET_CUSTOM, false}, }; +static bool dma_chan_active_[12]; +static struct semaphore dma_write_complete_sem_[12]; + +// DMA interrupt service routine +void RP2040PIOLEDStripLightOutput::dma_write_complete_handler_() { + uint32_t channel = dma_hw->ints0; + for (uint dma_chan = 0; dma_chan < 12; ++dma_chan) { + if (RP2040PIOLEDStripLightOutput::dma_chan_active_[dma_chan] && (channel & (1u << dma_chan))) { + dma_hw->ints0 = (1u << dma_chan); // Clear the interrupt + sem_release(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[dma_chan]); // Handle the interrupt + } + } +} void RP2040PIOLEDStripLightOutput::setup() { ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip..."); @@ -57,22 +72,22 @@ void RP2040PIOLEDStripLightOutput::setup() { // but there are only 4 state machines on each PIO so we can only have 4 strips per PIO uint offset = 0; - if (num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { + if (RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1] > 4) { ESP_LOGE(TAG, "Too many instances of PIO program"); this->mark_failed(); return; } // keep track of how many instances of the PIO program are running on each PIO - num_instance_[this->pio_ == pio0 ? 0 : 1]++; + RP2040PIOLEDStripLightOutput::num_instance_[this->pio_ == pio0 ? 0 : 1]++; // if there are multiple strips of the same chipset, we can reuse the same PIO program and save space if (this->conf_count_[this->chipset_]) { - offset = chipset_offsets_[this->chipset_]; + offset = RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_]; } else { // Load the assembled program into the PIO and get its location in the PIO's instruction memory and save it offset = pio_add_program(this->pio_, this->program_); - chipset_offsets_[this->chipset_] = offset; - conf_count_[this->chipset_] = true; + RP2040PIOLEDStripLightOutput::chipset_offsets_[this->chipset_] = offset; + RP2040PIOLEDStripLightOutput::conf_count_[this->chipset_] = true; } // Configure the state machine's PIO, and start it @@ -93,6 +108,9 @@ void RP2040PIOLEDStripLightOutput::setup() { return; } + // Mark the DMA channel as active + RP2040PIOLEDStripLightOutput::dma_chan_active_[this->dma_chan_] = true; + this->dma_config_ = dma_channel_get_default_config(this->dma_chan_); channel_config_set_transfer_data_size( &this->dma_config_, @@ -109,6 +127,13 @@ void RP2040PIOLEDStripLightOutput::setup() { false // don't start yet ); + // Initialize the semaphore for this DMA channel + sem_init(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_], 1, 1); + + irq_set_exclusive_handler(DMA_IRQ_0, dma_write_complete_handler_); // after DMA all data, raise an interrupt + dma_channel_set_irq0_enabled(this->dma_chan_, true); // map DMA channel to interrupt + irq_set_enabled(DMA_IRQ_0, true); // enable interrupt + this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_); } @@ -126,6 +151,7 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { } // the bits are already in the correct order for the pio program so we can just copy the buffer using DMA + sem_acquire_blocking(&RP2040PIOLEDStripLightOutput::dma_write_complete_sem_[this->dma_chan_]); dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_()); } diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h index 9976842f02..7b62648974 100644 --- a/esphome/components/rp2040_pio_led_strip/led_strip.h +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace esphome { @@ -95,6 +96,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + static void dma_write_complete_handler_(); + uint8_t *buf_{nullptr}; uint8_t *effect_data_{nullptr}; @@ -120,6 +123,8 @@ class RP2040PIOLEDStripLightOutput : public light::AddressableLight { inline static int num_instance_[2]; inline static std::map conf_count_; inline static std::map chipset_offsets_; + inline static bool dma_chan_active_[12]; + inline static struct semaphore dma_write_complete_sem_[12]; }; } // namespace rp2040_pio_led_strip diff --git a/esphome/components/rpi_dpi_rgb/display.py b/esphome/components/rpi_dpi_rgb/display.py index 969b9db78e..c26143d63e 100644 --- a/esphome/components/rpi_dpi_rgb/display.py +++ b/esphome/components/rpi_dpi_rgb/display.py @@ -1,30 +1,28 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import display +from esphome.components.esp32 import const, only_on_variant +import esphome.config_validation as cv from esphome.const import ( - CONF_HSYNC_PIN, - CONF_RESET_PIN, + CONF_BLUE, + CONF_COLOR_ORDER, CONF_DATA_PINS, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_GREEN, + CONF_HEIGHT, + CONF_HSYNC_PIN, CONF_ID, CONF_IGNORE_STRAPPING_WARNING, - CONF_DIMENSIONS, - CONF_VSYNC_PIN, - CONF_WIDTH, - CONF_HEIGHT, + CONF_INVERT_COLORS, CONF_LAMBDA, - CONF_COLOR_ORDER, - CONF_RED, - CONF_GREEN, - CONF_BLUE, CONF_NUMBER, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, - CONF_INVERT_COLORS, -) -from esphome.components.esp32 import ( - only_on_variant, - const, + CONF_RED, + CONF_RESET_PIN, + CONF_VSYNC_PIN, + CONF_WIDTH, ) DEPENDENCIES = ["esp32"] @@ -112,6 +110,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_HSYNC_PULSE_WIDTH, default=10): cv.int_, cv.Optional(CONF_HSYNC_BACK_PORCH, default=10): cv.int_, @@ -164,6 +163,10 @@ async def to_code(config): cg.add(var.add_data_pin(data_pin, index)) index += 1 + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = await cg.gpio_pin_expression(enable_pin) + cg.add(var.set_enable_pin(enable)) + if reset_pin := config.get(CONF_RESET_PIN): reset = await cg.gpio_pin_expression(reset_pin) cg.add(var.set_reset_pin(reset)) diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index 2ffdb3272a..655b469b91 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -6,9 +6,14 @@ namespace esphome { namespace rpi_dpi_rgb { void RpiDpiRgb::setup() { - esph_log_config(TAG, "Setting up RPI_DPI_RGB"); + ESP_LOGCONFIG(TAG, "Setting up RPI_DPI_RGB"); + this->reset_display_(); esp_lcd_rgb_panel_config_t config{}; config.flags.fb_in_psram = 1; +#if ESP_IDF_VERSION_MAJOR >= 5 + config.bounce_buffer_size_px = this->width_ * 10; + config.num_fbs = 1; +#endif // ESP_IDF_VERSION_MAJOR config.timings.h_res = this->width_; config.timings.v_res = this->height_; config.timings.hsync_pulse_width = this->hsync_pulse_width_; @@ -20,7 +25,6 @@ void RpiDpiRgb::setup() { config.timings.flags.pclk_active_neg = this->pclk_inverted_; config.timings.pclk_hz = this->pclk_frequency_; config.clk_src = LCD_CLK_SRC_PLL160M; - config.sram_trans_align = 64; config.psram_trans_align = 64; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { @@ -34,11 +38,19 @@ void RpiDpiRgb::setup() { config.pclk_gpio_num = this->pclk_pin_->get_pin(); esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); if (err != ESP_OK) { - esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); + this->mark_failed(); + return; } ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); - esph_log_config(TAG, "RPI_DPI_RGB setup complete"); + ESP_LOGCONFIG(TAG, "RPI_DPI_RGB setup complete"); +} +void RpiDpiRgb::loop() { +#if ESP_IDF_VERSION_MAJOR >= 5 + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); +#endif // ESP_IDF_VERSION_MAJOR } void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, @@ -53,7 +65,7 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin } x_start += this->offset_x_; y_start += this->offset_y_; - esp_err_t err; + esp_err_t err = ESP_OK; // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. if (x_offset == 0 && x_pad == 0 && y_offset == 0) { // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother @@ -69,7 +81,7 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin } } if (err != ESP_OK) - esph_log_e(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); + ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); } void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) { @@ -104,12 +116,30 @@ void RpiDpiRgb::dump_config() { ESP_LOGCONFIG(TAG, " Height: %u", this->height_); ESP_LOGCONFIG(TAG, " Width: %u", this->width_); LOG_PIN(" DE Pin: ", this->de_pin_); + LOG_PIN(" Enable Pin: ", this->enable_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str()); } +void RpiDpiRgb::reset_display_() const { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + if (this->enable_pin_ != nullptr) { + this->enable_pin_->setup(); + this->enable_pin_->digital_write(false); + } + delay(1); + this->reset_pin_->digital_write(true); + if (this->enable_pin_ != nullptr) { + delay(11); + this->enable_pin_->digital_write(true); + } + } +} + } // namespace rpi_dpi_rgb } // namespace esphome diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h index 0319b46391..10f77a2624 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h @@ -23,6 +23,7 @@ class RpiDpiRgb : public display::Display { public: void update() override { this->do_update_(); } void setup() override; + void loop() override; void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; void draw_pixel_at(int x, int y, Color color) override; @@ -36,6 +37,7 @@ class RpiDpiRgb : public display::Display { void set_pclk_pin(InternalGPIOPin *pclk_pin) { this->pclk_pin_ = pclk_pin; } void set_vsync_pin(InternalGPIOPin *vsync_pin) { this->vsync_pin_ = vsync_pin; } void set_hsync_pin(InternalGPIOPin *hsync_pin) { this->hsync_pin_ = hsync_pin; } + void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_width(uint16_t width) { this->width_ = width; } void set_dimensions(uint16_t width, uint16_t height) { @@ -62,10 +64,12 @@ class RpiDpiRgb : public display::Display { protected: int get_width_internal() override { return this->width_; } int get_height_internal() override { return this->height_; } + void reset_display_() const; InternalGPIOPin *de_pin_{nullptr}; InternalGPIOPin *pclk_pin_{nullptr}; InternalGPIOPin *hsync_pin_{nullptr}; InternalGPIOPin *vsync_pin_{nullptr}; + GPIOPin *enable_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; InternalGPIOPin *data_pins_[16] = {}; uint16_t hsync_front_porch_ = 8; diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 0bdf65b7bd..495b5c1c8a 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -29,6 +29,13 @@ inline double deg2rad(double degrees) { void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } void Rtttl::play(std::string rtttl) { + if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { + int pos = this->rtttl_.find(':'); + auto name = this->rtttl_.substr(0, pos); + ESP_LOGW(TAG, "RTTTL Component is already playing: %s", name.c_str()); + return; + } + this->rtttl_ = std::move(rtttl); this->default_duration_ = 4; @@ -98,16 +105,24 @@ void Rtttl::play(std::string rtttl) { this->note_duration_ = 1; #ifdef USE_SPEAKER - this->samples_sent_ = 0; - this->samples_count_ = 0; + if (this->speaker_ != nullptr) { + this->set_state_(State::STATE_INIT); + this->samples_sent_ = 0; + this->samples_count_ = 0; + } +#endif +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->set_state_(State::STATE_RUNNING); + } #endif } void Rtttl::stop() { - this->note_duration_ = 0; #ifdef USE_OUTPUT if (this->output_ != nullptr) { this->output_->set_level(0.0); + this->set_state_(STATE_STOPPED); } #endif #ifdef USE_SPEAKER @@ -115,18 +130,37 @@ void Rtttl::stop() { if (this->speaker_->is_running()) { this->speaker_->stop(); } + this->set_state_(STATE_STOPPING); } #endif + this->note_duration_ = 0; } void Rtttl::loop() { - if (this->note_duration_ == 0) + if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) return; #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { + if (this->state_ == State::STATE_STOPPING) { + if (this->speaker_->is_stopped()) { + this->set_state_(State::STATE_STOPPED); + } + } else if (this->state_ == State::STATE_INIT) { + if (this->speaker_->is_stopped()) { + this->speaker_->start(); + this->set_state_(State::STATE_STARTING); + } + } else if (this->state_ == State::STATE_STARTING) { + if (this->speaker_->is_running()) { + this->set_state_(State::STATE_RUNNING); + } + } + if (!this->speaker_->is_running()) { + return; + } if (this->samples_sent_ != this->samples_count_) { - SpeakerSample sample[SAMPLE_BUFFER_SIZE + 1]; + SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2]; int x = 0; double rem = 0.0; @@ -136,7 +170,7 @@ void Rtttl::loop() { if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note// rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_); - int16_t val = (49152 * this->gain_) * sin(deg2rad(rem)); + int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152 sample[x].left = val; sample[x].right = val; @@ -153,9 +187,9 @@ void Rtttl::loop() { x++; } if (x > 0) { - int send = this->speaker_->play((uint8_t *) (&sample), x * 4); + int send = this->speaker_->play((uint8_t *) (&sample), x * 2); if (send != x * 4) { - this->samples_sent_ -= (x - (send / 4)); + this->samples_sent_ -= (x - (send / 2)); } return; } @@ -167,14 +201,7 @@ void Rtttl::loop() { return; #endif if (!this->rtttl_[position_]) { - this->note_duration_ = 0; -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - } -#endif - ESP_LOGD(TAG, "Playback finished"); - this->on_finished_playback_callback_.call(); + this->finish_(); return; } @@ -213,6 +240,7 @@ void Rtttl::loop() { case 'a': note = 10; break; + case 'h': case 'b': note = 12; break; @@ -238,14 +266,21 @@ void Rtttl::loop() { uint8_t scale = get_integer_(); if (scale == 0) scale = this->default_octave_; + + if (scale < 4 || scale > 7) { + ESP_LOGE(TAG, "Octave out of valid range. Should be between 4 and 7. (Octave: %d)", scale); + this->finish_(); + return; + } bool need_note_gap = false; // Now play the note if (note) { auto note_index = (scale - 4) * 12 + note; if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { - ESP_LOGE(TAG, "Note out of valid range"); - this->note_duration_ = 0; + ESP_LOGE(TAG, "Note out of valid range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index, + (int) sizeof(NOTES)); + this->finish_(); return; } auto freq = NOTES[note_index]; @@ -285,14 +320,17 @@ void Rtttl::loop() { this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); } if (this->output_freq_ != 0) { + // make sure there is enough samples to add a full last sinus. + + uint16_t samples_wish = this->samples_count_; this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_; - // make sure there is enough samples to add a full last sinus. uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1; - uint16_t x = this->samples_count_; + this->samples_count_ = (division * this->samples_per_wave_); - ESP_LOGD(TAG, "play time old: %d div: %d new: %d %d", x, division, this->samples_count_, this->samples_per_wave_); this->samples_count_ = this->samples_count_ >> 10; + ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_, + division, this->samples_per_wave_); } // Convert from frequency in Hz to high and low samples in fixed point } @@ -301,5 +339,54 @@ void Rtttl::loop() { this->last_note_ = millis(); } +void Rtttl::finish_() { +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + + this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); + } +#endif + this->note_duration_ = 0; + this->on_finished_playback_callback_.call(); + ESP_LOGD(TAG, "Playback finished"); +} + +static const LogString *state_to_string(State state) { + switch (state) { + case STATE_STOPPED: + return LOG_STR("STATE_STOPPED"); + case STATE_STARTING: + return LOG_STR("STATE_STARTING"); + case STATE_RUNNING: + return LOG_STR("STATE_RUNNING"); + case STATE_STOPPING: + return LOG_STR("STATE_STOPPING"); + case STATE_INIT: + return LOG_STR("STATE_INIT"); + default: + return LOG_STR("UNKNOWN"); + } +}; + +void Rtttl::set_state_(State state) { + State old_state = this->state_; + this->state_ = state; + ESP_LOGD(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)), + LOG_STR_ARG(state_to_string(state))); +} + } // namespace rtttl } // namespace esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index bf089ce980..3cb6e3f5fb 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -14,12 +14,20 @@ namespace esphome { namespace rtttl { +enum State : uint8_t { + STATE_STOPPED = 0, + STATE_INIT, + STATE_STARTING, + STATE_RUNNING, + STATE_STOPPING, +}; + #ifdef USE_SPEAKER -static const size_t SAMPLE_BUFFER_SIZE = 512; +static const size_t SAMPLE_BUFFER_SIZE = 2048; struct SpeakerSample { - int16_t left{0}; - int16_t right{0}; + int8_t left{0}; + int8_t right{0}; }; #endif @@ -42,7 +50,7 @@ class Rtttl : public Component { void stop(); void dump_config() override; - bool is_playing() { return this->note_duration_ != 0; } + bool is_playing() { return this->state_ != State::STATE_STOPPED; } void loop() override; void add_on_finished_playback_callback(std::function callback) { @@ -57,6 +65,8 @@ class Rtttl : public Component { } return ret; } + void finish_(); + void set_state_(State state); std::string rtttl_{""}; size_t position_{0}; @@ -68,13 +78,12 @@ class Rtttl : public Component { uint32_t output_freq_; float gain_{0.6f}; + State state_{State::STATE_STOPPED}; #ifdef USE_OUTPUT output::FloatOutput *output_; #endif - void play_output_(); - #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; int sample_rate_{16000}; diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 073fbef1d4..2bc68d43ec 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -1,20 +1,20 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( + CONF_CYCLE, CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, + CONF_INDEX, + CONF_MODE, + CONF_MQTT_ID, CONF_ON_VALUE, + CONF_OPERATION, CONF_OPTION, CONF_TRIGGER_ID, - CONF_MQTT_ID, CONF_WEB_SERVER_ID, - CONF_CYCLE, - CONF_MODE, - CONF_OPERATION, - CONF_INDEX, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_generator import MockObjClass diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 262e69d75b..867cdc1f48 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -1,22 +1,28 @@ import math -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( - CONF_DEVICE_CLASS, CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, + CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_EXPIRE_AFTER, CONF_FILTERS, + CONF_FORCE_UPDATE, CONF_FROM, CONF_ICON, CONF_ID, CONF_IGNORE_OUT_OF_RANGE, + CONF_MAX_VALUE, + CONF_METHOD, + CONF_MIN_VALUE, + CONF_MQTT_ID, + CONF_MULTIPLE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -29,14 +35,9 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, - CONF_WINDOW_SIZE, - CONF_MQTT_ID, - CONF_WEB_SERVER_ID, - CONF_FORCE_UPDATE, CONF_VALUE, - CONF_MIN_VALUE, - CONF_MAX_VALUE, - CONF_METHOD, + CONF_WEB_SERVER_ID, + CONF_WINDOW_SIZE, DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_AQI, DEVICE_CLASS_ATMOSPHERIC_PRESSURE, @@ -249,6 +250,7 @@ CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) ClampFilter = sensor_ns.class_("ClampFilter", Filter) RoundFilter = sensor_ns.class_("RoundFilter", Filter) +RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter) validate_unit_of_measurement = cv.string_strict validate_accuracy_decimals = cv.int_ @@ -734,6 +736,23 @@ async def round_filter_to_code(config, filter_id): ) +@FILTER_REGISTRY.register( + "round_to_multiple_of", + RoundMultipleFilter, + cv.maybe_simple_value( + { + cv.Required(CONF_MULTIPLE): cv.positive_not_null_float, + }, + key=CONF_MULTIPLE, + ), +) +async def round_multiple_filter_to_code(config, filter_id): + return cg.new_Pvariable( + filter_id, + config[CONF_MULTIPLE], + ) + + async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index eaa909429b..bcf1fc8269 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -472,5 +472,13 @@ optional RoundFilter::new_value(float value) { return value; } +RoundMultipleFilter::RoundMultipleFilter(float multiple) : multiple_(multiple) {} +optional RoundMultipleFilter::new_value(float value) { + if (std::isfinite(value)) { + return value - remainderf(value, this->multiple_); + } + return value; +} + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index c13cb3420a..92b1d8d240 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -431,5 +431,14 @@ class RoundFilter : public Filter { uint8_t precision_; }; +class RoundMultipleFilter : public Filter { + public: + explicit RoundMultipleFilter(float multiple); + optional new_value(float value) override; + + protected: + float multiple_; +}; + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index c782c0fc5e..2cc71e87fa 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -10,7 +10,7 @@ SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { this->pos_ = 0; while (this->pos_ < this->buffer_.size()) { if (this->buffer_[this->pos_] == 0x00) - break; // fill byte detected -> no more messages + break; // EndOfSmlMsg SmlNode message = SmlNode(); if (!this->setup_node(&message)) @@ -20,40 +20,66 @@ SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { } bool SmlFile::setup_node(SmlNode *node) { - uint8_t type = this->buffer_[this->pos_] >> 4; // type including overlength info - uint8_t length = this->buffer_[this->pos_] & 0x0f; // length including TL bytes - bool is_list = (type & 0x07) == SML_LIST; - bool has_extended_length = type & 0x08; // we have a long list/value (>15 entries) - uint8_t parse_length = length; - if (has_extended_length) { - length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); - parse_length = length; + // If the TL field is 0x00, this is the end of the message + // (see 6.3.1 of SML protocol definition) + if (this->buffer_[this->pos_] == 0x00) { + // Increment past this byte and signal that the message is done this->pos_ += 1; + return true; } - if (this->pos_ + parse_length >= this->buffer_.size()) + // Extract data from initial TL field + uint8_t type = (this->buffer_[this->pos_] >> 4) & 0x07; // type without overlength info + bool overlength = (this->buffer_[this->pos_] >> 4) & 0x08; // overlength information + uint8_t length = this->buffer_[this->pos_] & 0x0f; // length (including TL bytes) + + // Check if we need additional length bytes + if (overlength) { + // Shift the current length to the higher nibble + // and add the lower nibble of the next byte to the length + length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); + // We are basically done with the first TL field now, + // so increment past that, we now point to the second TL field + this->pos_ += 1; + // Decrement the length for value fields (not lists), + // since the byte we just handled is counted as part of the field + // in case of values but not for lists + if (type != SML_LIST) + length -= 1; + + // Technically, this is not enough, the standard allows for more than two length fields. + // However I don't think it is very common to have more than 255 entries in a list + } + + // We are done with the last TL field(s), so advance the position + this->pos_ += 1; + // and decrement the length for non-list fields + if (type != SML_LIST) + length -= 1; + + // Check if the buffer length is long enough + if (this->pos_ + length > this->buffer_.size()) return false; - node->type = type & 0x07; + node->type = type; node->nodes.clear(); node->value_bytes.clear(); - // if the list is a has_extended_length list with e.g. 16 elements this is a 0x00 byte but not the end of message - if (!has_extended_length && this->buffer_[this->pos_] == 0x00) { // end of message - this->pos_ += 1; - } else if (is_list) { // list - this->pos_ += 1; - node->nodes.reserve(parse_length); - for (size_t i = 0; i != parse_length; i++) { + if (type == SML_LIST) { + node->nodes.reserve(length); + for (size_t i = 0; i != length; i++) { SmlNode child_node = SmlNode(); if (!this->setup_node(&child_node)) return false; node->nodes.emplace_back(child_node); } - } else { // value - node->value_bytes = - bytes(this->buffer_.begin() + this->pos_ + 1, this->buffer_.begin() + this->pos_ + parse_length); - this->pos_ += parse_length; + } else { + // Value starts at the current position + // Value ends "length" bytes later, + // (since the TL field is counted but already subtracted from length) + node->value_bytes = bytes(this->buffer_.begin() + this->pos_, this->buffer_.begin() + this->pos_ + length); + // Increment the pointer past all consumed bytes + this->pos_ += length; } return true; } @@ -101,7 +127,7 @@ int64_t bytes_to_int(const bytes &buffer) { // see https://stackoverflow.com/questions/42534749/signed-extension-from-24-bit-to-32-bit-in-c if (buffer.size() < 8) { const int bits = buffer.size() * 8; - const uint64_t m = 1u << (bits - 1); + const uint64_t m = 1ull << (bits - 1); tmp = (tmp ^ m) - m; } diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index b200046d7f..e260fce05e 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -1,4 +1,5 @@ #include "socket.h" +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) #include #include #include @@ -19,24 +20,22 @@ std::unique_ptr socket_ip(int type, int protocol) { socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { #if USE_NETWORK_IPV6 - if (addrlen < sizeof(sockaddr_in6)) { - errno = EINVAL; - return 0; - } - auto *server = reinterpret_cast(addr); - memset(server, 0, sizeof(sockaddr_in6)); - server->sin6_family = AF_INET6; - server->sin6_port = htons(port); + if (ip_address.find(':') != std::string::npos) { + if (addrlen < sizeof(sockaddr_in6)) { + errno = EINVAL; + return 0; + } + auto *server = reinterpret_cast(addr); + memset(server, 0, sizeof(sockaddr_in6)); + server->sin6_family = AF_INET6; + server->sin6_port = htons(port); - if (ip_address.find('.') != std::string::npos) { - server->sin6_addr.un.u32_addr[3] = inet_addr(ip_address.c_str()); - } else { ip6_addr_t ip6; inet6_aton(ip_address.c_str(), &ip6); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); + return sizeof(sockaddr_in6); } - return sizeof(sockaddr_in6); -#else +#endif /* USE_NETWORK_IPV6 */ if (addrlen < sizeof(sockaddr_in)) { errno = EINVAL; return 0; @@ -47,7 +46,6 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri server->sin_addr.s_addr = inet_addr(ip_address.c_str()); server->sin_port = htons(port); return sizeof(sockaddr_in); -#endif /* USE_NETWORK_IPV6 */ } socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { @@ -77,3 +75,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po } } // namespace socket } // namespace esphome +#endif diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 5c12210d15..cefdb51e0d 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -5,6 +5,7 @@ #include "esphome/core/optional.h" #include "headers.h" +#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS) namespace esphome { namespace socket { @@ -57,3 +58,4 @@ socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t po } // namespace socket } // namespace esphome +#endif diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 79d5df8c5a..d28b726d1f 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -1,13 +1,11 @@ from esphome import automation -import esphome.config_validation as cv -import esphome.codegen as cg - from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_DATA +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_DATA, CONF_ID from esphome.core import CORE from esphome.coroutine import coroutine_with_priority - CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True @@ -22,8 +20,12 @@ PlayAction = speaker_ns.class_( StopAction = speaker_ns.class_( "StopAction", automation.Action, cg.Parented.template(Speaker) ) +FinishAction = speaker_ns.class_( + "FinishAction", automation.Action, cg.Parented.template(Speaker) +) IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition) +IsStoppedCondition = speaker_ns.class_("IsStoppedCondition", automation.Condition) async def setup_speaker_core_(var, config): @@ -75,11 +77,18 @@ async def speaker_play_action(config, action_id, template_arg, args): automation.register_action("speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA)( speaker_action ) +automation.register_action("speaker.finish", FinishAction, SPEAKER_AUTOMATION_SCHEMA)( + speaker_action +) automation.register_condition( "speaker.is_playing", IsPlayingCondition, SPEAKER_AUTOMATION_SCHEMA )(speaker_action) +automation.register_condition( + "speaker.is_stopped", IsStoppedCondition, SPEAKER_AUTOMATION_SCHEMA +)(speaker_action) + @coroutine_with_priority(100.0) async def to_code(config): diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index e28991a0d1..2716fe6100 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -39,10 +39,20 @@ template class StopAction : public Action, public Parente void play(Ts... x) override { this->parent_->stop(); } }; +template class FinishAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->finish(); } +}; + template class IsPlayingCondition : public Condition, public Parented { public: bool check(Ts... x) override { return this->parent_->is_running(); } }; +template class IsStoppedCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_stopped(); } +}; + } // namespace speaker } // namespace esphome diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index b494873160..375ccc4e8c 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + namespace esphome { namespace speaker { @@ -17,10 +21,15 @@ class Speaker { virtual void start() = 0; virtual void stop() = 0; + // In compare between *STOP()* and *FINISH()*; *FINISH()* will stop after emptying the play buffer, + // while *STOP()* will break directly. + // When finish() is not implemented on the plateform component it should just do a normal stop. + virtual void finish() { this->stop(); } virtual bool has_buffered_data() const = 0; bool is_running() const { return this->state_ == STATE_RUNNING; } + bool is_stopped() const { return this->state_ == STATE_STOPPED; } protected: State state_{STATE_STOPPED}; diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index b13826c443..f9435b0424 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -7,10 +7,6 @@ namespace spi { const char *const TAG = "spi"; -SPIDelegate *const SPIDelegate::NULL_DELEGATE = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - new SPIDelegateDummy(); -// https://bugs.llvm.org/show_bug.cgi?id=48040 - bool SPIDelegate::is_ready() { return true; } GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -79,8 +75,6 @@ void SPIComponent::dump_config() { } } -void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); } - uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); } void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index f581dc3f56..4cd8d3383c 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -163,8 +163,6 @@ class Utility { } }; -class SPIDelegateDummy; - // represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is // a thin wrapper over SPIClass. class SPIDelegate { @@ -250,21 +248,6 @@ class SPIDelegate { uint32_t data_rate_{1000000}; SPIMode mode_{MODE0}; GPIOPin *cs_pin_{NullPin::NULL_PIN}; - static SPIDelegate *const NULL_DELEGATE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -}; - -/** - * A dummy SPIDelegate that complains if it's used. - */ - -class SPIDelegateDummy : public SPIDelegate { - public: - SPIDelegateDummy() = default; - - uint8_t transfer(uint8_t data) override { return 0; } - void end_transaction() override{}; - - void begin_transaction() override; }; /** @@ -382,7 +365,7 @@ class SPIClient { virtual void spi_teardown() { this->parent_->unregister_device(this); - this->delegate_ = SPIDelegate::NULL_DELEGATE; + this->delegate_ = nullptr; } bool spi_is_ready() { return this->delegate_->is_ready(); } @@ -393,7 +376,7 @@ class SPIClient { uint32_t data_rate_{1000000}; SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; - SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; + SPIDelegate *delegate_{nullptr}; }; /** diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h index 0d8c1c1e1c..1b317cdd69 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.h +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -13,7 +13,7 @@ class SpiLedStrip : public light::AddressableLight, public spi::SPIDevice { public: - void setup() { this->spi_setup(); } + void setup() override { this->spi_setup(); } int32_t size() const override { return this->num_leds_; } @@ -43,13 +43,14 @@ class SpiLedStrip : public light::AddressableLight, memset(this->buf_, 0, 4); } - void dump_config() { + void dump_config() override { esph_log_config(TAG, "SPI LED Strip:"); esph_log_config(TAG, " LEDs: %d", this->num_leds_); - if (this->data_rate_ >= spi::DATA_RATE_1MHZ) + if (this->data_rate_ >= spi::DATA_RATE_1MHZ) { esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); - else + } else { esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000)); + } } void write_state(light::LightState *state) override { diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 982d9add1a..59565251c3 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -647,7 +647,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == min_str) { + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -729,7 +729,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == min_str) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 5311ae4c05..c4a8b8aeb8 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -11,7 +11,7 @@ namespace esphome { namespace sprinkler { -const std::string min_str = "min"; +const std::string MIN_STR = "min"; enum SprinklerState : uint8_t { // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index 516d770f8b..9310e9d760 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -1,47 +1,39 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.components import ( - spi, - display, -) -from esphome.const import ( - CONF_DC_PIN, - CONF_HSYNC_PIN, - CONF_RESET_PIN, - CONF_DATA_PINS, - CONF_ID, - CONF_DIMENSIONS, - CONF_VSYNC_PIN, - CONF_WIDTH, - CONF_HEIGHT, - CONF_LAMBDA, - CONF_MIRROR_X, - CONF_MIRROR_Y, - CONF_COLOR_ORDER, - CONF_TRANSFORM, - CONF_OFFSET_HEIGHT, - CONF_OFFSET_WIDTH, - CONF_INVERT_COLORS, - CONF_RED, - CONF_GREEN, - CONF_BLUE, - CONF_NUMBER, - CONF_IGNORE_STRAPPING_WARNING, -) - -from esphome.components.esp32 import ( - only_on_variant, - const, -) +import esphome.codegen as cg +from esphome.components import display, spi +from esphome.components.esp32 import const, only_on_variant from esphome.components.rpi_dpi_rgb.display import ( CONF_PCLK_FREQUENCY, CONF_PCLK_INVERTED, ) -from .init_sequences import ( - ST7701S_INITS, - cmd, +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_ORDER, + CONF_DATA_PINS, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_GREEN, + CONF_HEIGHT, + CONF_HSYNC_PIN, + CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_NUMBER, + CONF_OFFSET_HEIGHT, + CONF_OFFSET_WIDTH, + CONF_RED, + CONF_RESET_PIN, + CONF_TRANSFORM, + CONF_VSYNC_PIN, + CONF_WIDTH, ) +from esphome.core import TimePeriod + +from .init_sequences import ST7701S_INITS, cmd CONF_INIT_SEQUENCE = "init_sequence" CONF_DE_PIN = "de_pin" @@ -59,6 +51,7 @@ DEPENDENCIES = ["spi", "esp32"] st7701s_ns = cg.esphome_ns.namespace("st7701s") ST7701S = st7701s_ns.class_("ST7701S", display.Display, cg.Component, spi.SPIDevice) ColorOrder = display.display_ns.enum("ColorMode") +ST7701S_DELAY_FLAG = 0xFF COLOR_ORDERS = { "RGB": ColorOrder.COLOR_ORDER_RGB, @@ -93,18 +86,23 @@ def map_sequence(value): """ An initialisation sequence can be selected from one of the pre-defined sequences in init_sequences.py, or can be a literal array of data bytes. - The format is a repeated sequence of [CMD, LEN, ] where is LEN bytes. + The format is a repeated sequence of [CMD, ] where is s a sequence of bytes. The length is inferred + from the length of the sequence and should not be explicit. + A delay can be inserted by specifying "- delay N" where N is in ms """ + if isinstance(value, str) and value.lower().startswith("delay "): + value = value.lower()[6:] + delay = cv.All( + cv.positive_time_period_milliseconds, + cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)), + )(value) + return [delay, ST7701S_DELAY_FLAG] if not isinstance(value, list): value = cv.int_(value) value = cv.one_of(*ST7701S_INITS)(value) return ST7701S_INITS[value] - # value = cv.ensure_list(cv.uint8_t)(value) - data_length = len(value) - if data_length == 0: - raise cv.Invalid("Empty sequence") - value = cmd(*value) - return value + value = cv.Length(min=1, max=254)(value) + return cmd(*value) CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 43d8653709..7248bc044e 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -8,8 +8,22 @@ namespace st7701s { void ST7701S::setup() { esph_log_config(TAG, "Setting up ST7701S"); this->spi_setup(); + this->write_init_sequence_(); +} + +// called after a delay after writing the init sequence +void ST7701S::complete_setup_() { + this->write_command_(SLEEP_OUT); + this->write_command_(DISPLAY_ON); + this->spi_teardown(); // SPI not needed after this + delay(10); + esp_lcd_rgb_panel_config_t config{}; config.flags.fb_in_psram = 1; +#if ESP_IDF_VERSION_MAJOR >= 5 + config.bounce_buffer_size_px = this->width_ * 10; + config.num_fbs = 1; +#endif // ESP_IDF_VERSION_MAJOR config.timings.h_res = this->width_; config.timings.v_res = this->height_; config.timings.hsync_pulse_width = this->hsync_pulse_width_; @@ -21,7 +35,6 @@ void ST7701S::setup() { config.timings.flags.pclk_active_neg = this->pclk_inverted_; config.timings.pclk_hz = this->pclk_frequency_; config.clk_src = LCD_CLK_SRC_PLL160M; - config.sram_trans_align = 64; config.psram_trans_align = 64; size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); for (size_t i = 0; i != data_pin_count; i++) { @@ -34,15 +47,21 @@ void ST7701S::setup() { config.de_gpio_num = this->de_pin_->get_pin(); config.pclk_gpio_num = this->pclk_pin_->get_pin(); esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); + ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); if (err != ESP_OK) { esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err)); } - ESP_ERROR_CHECK(esp_lcd_panel_reset(this->handle_)); - ESP_ERROR_CHECK(esp_lcd_panel_init(this->handle_)); - this->write_init_sequence_(); esph_log_config(TAG, "ST7701S setup complete"); } +void ST7701S::loop() { +#if ESP_IDF_VERSION_MAJOR >= 5 + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); +#endif // ESP_IDF_VERSION_MAJOR +} + void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { if (w <= 0 || h <= 0) @@ -138,11 +157,16 @@ void ST7701S::write_init_sequence_() { for (size_t i = 0; i != this->init_sequence_.size();) { uint8_t cmd = this->init_sequence_[i++]; size_t len = this->init_sequence_[i++]; - this->write_sequence_(cmd, len, &this->init_sequence_[i]); - i += len; - esph_log_v(TAG, "Command %X, %d bytes", cmd, len); - if (cmd == SW_RESET_CMD) - delay(6); + if (len == ST7701S_DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + this->write_sequence_(cmd, len, &this->init_sequence_[i]); + i += len; + ESP_LOGV(TAG, "Command %X, %d bytes", cmd, len); + if (cmd == SW_RESET_CMD) + delay(6); + } } // st7701 does not appear to support axis swapping this->write_sequence_(CMD2_BKSEL, sizeof(CMD2_BK0), CMD2_BK0); @@ -153,12 +177,9 @@ void ST7701S::write_init_sequence_() { val |= 0x10; this->write_command_(MADCTL_CMD); this->write_data_(val); - esph_log_d(TAG, "write MADCTL %X", val); + ESP_LOGD(TAG, "write MADCTL %X", val); this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF); - this->set_timeout(120, [this] { - this->write_command_(SLEEP_OUT); - this->write_command_(DISPLAY_ON); - }); + this->set_timeout(120, [this] { this->complete_setup_(); }); } void ST7701S::dump_config() { diff --git a/esphome/components/st7701s/st7701s.h b/esphome/components/st7701s/st7701s.h index 2328bca965..a1e3c2e54a 100644 --- a/esphome/components/st7701s/st7701s.h +++ b/esphome/components/st7701s/st7701s.h @@ -25,6 +25,7 @@ const uint8_t INVERT_ON = 0x21; const uint8_t DISPLAY_ON = 0x29; const uint8_t CMD2_BKSEL = 0xFF; const uint8_t CMD2_BK0[5] = {0x77, 0x01, 0x00, 0x00, 0x10}; +const uint8_t ST7701S_DELAY_FLAG = 0xFF; class ST7701S : public display::Display, public spi::SPIDevicedo_update_(); } void setup() override; + void complete_setup_(); + void loop() override; void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; diff --git a/esphome/components/statsd/__init__.py b/esphome/components/statsd/__init__.py new file mode 100644 index 0000000000..3623338aec --- /dev/null +++ b/esphome/components/statsd/__init__.py @@ -0,0 +1,65 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor +from esphome.const import ( + CONF_ID, + CONF_PORT, + CONF_NAME, + CONF_SENSORS, + CONF_BINARY_SENSORS, +) + +AUTO_LOAD = ["socket"] +CODEOWNERS = ["@Links2004"] +DEPENDENCIES = ["network"] + +CONF_HOST = "host" +CONF_PREFIX = "prefix" + +statsd_component_ns = cg.esphome_ns.namespace("statsd") +StatsdComponent = statsd_component_ns.class_("StatsdComponent", cg.PollingComponent) + +CONFIG_SENSORS_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_NAME): cv.string_strict, + } +) + +CONFIG_BINARY_SENSORS_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor), + cv.Required(CONF_NAME): cv.string_strict, + } +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(StatsdComponent), + cv.Required(CONF_HOST): cv.string_strict, + cv.Optional(CONF_PORT, default=8125): cv.port, + cv.Optional(CONF_PREFIX, default=""): cv.string_strict, + cv.Optional(CONF_SENSORS): cv.ensure_list(CONFIG_SENSORS_SCHEMA), + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list(CONFIG_BINARY_SENSORS_SCHEMA), + } +).extend(cv.polling_component_schema("10s")) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add( + var.configure( + config.get(CONF_HOST), + config.get(CONF_PORT), + config.get(CONF_PREFIX), + ) + ) + + for sensor_cfg in config.get(CONF_SENSORS, []): + s = await cg.get_variable(sensor_cfg[CONF_ID]) + cg.add(var.register_sensor(sensor_cfg[CONF_NAME], s)) + + for sensor_cfg in config.get(CONF_BINARY_SENSORS, []): + s = await cg.get_variable(sensor_cfg[CONF_ID]) + cg.add(var.register_binary_sensor(sensor_cfg[CONF_NAME], s)) diff --git a/esphome/components/statsd/statsd.cpp b/esphome/components/statsd/statsd.cpp new file mode 100644 index 0000000000..68b24908d2 --- /dev/null +++ b/esphome/components/statsd/statsd.cpp @@ -0,0 +1,156 @@ +#include "esphome/core/log.h" + +#include "statsd.h" + +namespace esphome { +namespace statsd { + +// send UDP packet if we reach 1Kb packed size +// this is needed since statsD does not support fragmented UDP packets +static const uint16_t SEND_THRESHOLD = 1024; + +static const char *const TAG = "statsD"; + +void StatsdComponent::setup() { +#ifndef USE_ESP8266 + this->sock_ = esphome::socket::socket(AF_INET, SOCK_DGRAM, 0); + + struct sockaddr_in source; + source.sin_family = AF_INET; + source.sin_addr.s_addr = htonl(INADDR_ANY); + source.sin_port = htons(this->port_); + this->sock_->bind((struct sockaddr *) &source, sizeof(source)); + + this->destination_.sin_family = AF_INET; + this->destination_.sin_port = htons(this->port_); + this->destination_.sin_addr.s_addr = inet_addr(this->host_); +#endif +} + +StatsdComponent::~StatsdComponent() { +#ifndef USE_ESP8266 + if (!this->sock_) { + return; + } + this->sock_->close(); +#endif +} + +void StatsdComponent::dump_config() { + ESP_LOGCONFIG(TAG, "statsD:"); + ESP_LOGCONFIG(TAG, " host: %s", this->host_); + ESP_LOGCONFIG(TAG, " port: %d", this->port_); + if (this->prefix_) { + ESP_LOGCONFIG(TAG, " prefix: %s", this->prefix_); + } + + ESP_LOGCONFIG(TAG, " metrics:"); + for (sensors_t s : this->sensors_) { + ESP_LOGCONFIG(TAG, " - name: %s", s.name); + ESP_LOGCONFIG(TAG, " type: %d", s.type); + } +} + +float StatsdComponent::get_setup_priority() const { return esphome::setup_priority::AFTER_WIFI; } + +#ifdef USE_SENSOR +void StatsdComponent::register_sensor(const char *name, esphome::sensor::Sensor *sensor) { + sensors_t s; + s.name = name; + s.sensor = sensor; + s.type = TYPE_SENSOR; + this->sensors_.push_back(s); +} +#endif + +#ifdef USE_BINARY_SENSOR +void StatsdComponent::register_binary_sensor(const char *name, esphome::binary_sensor::BinarySensor *binary_sensor) { + sensors_t s; + s.name = name; + s.binary_sensor = binary_sensor; + s.type = TYPE_BINARY_SENSOR; + this->sensors_.push_back(s); +} +#endif + +void StatsdComponent::update() { + std::string out; + out.reserve(SEND_THRESHOLD); + + for (sensors_t s : this->sensors_) { + double val = 0; + switch (s.type) { +#ifdef USE_SENSOR + case TYPE_SENSOR: + if (!s.sensor->has_state()) { + continue; + } + val = s.sensor->state; + break; +#endif +#ifdef USE_BINARY_SENSOR + case TYPE_BINARY_SENSOR: + if (!s.binary_sensor->has_state()) { + continue; + } + // map bool to double + if (s.binary_sensor->state) { + val = 1; + } + break; +#endif + default: + ESP_LOGE(TAG, "type not known, name: %s type: %d", s.name, s.type); + continue; + } + + // statsD gauge: + // https://github.com/statsd/statsd/blob/master/docs/metric_types.md + // This implies you can't explicitly set a gauge to a negative number without first setting it to zero. + if (val < 0) { + if (this->prefix_) { + out.append(str_sprintf("%s.", this->prefix_)); + } + out.append(str_sprintf("%s:0|g\n", s.name)); + } + if (this->prefix_) { + out.append(str_sprintf("%s.", this->prefix_)); + } + out.append(str_sprintf("%s:%f|g\n", s.name, val)); + + if (out.length() > SEND_THRESHOLD) { + this->send_(&out); + out.clear(); + } + } + + this->send_(&out); +} + +void StatsdComponent::send_(std::string *out) { + if (out->empty()) { + return; + } +#ifdef USE_ESP8266 + IPAddress ip; + ip.fromString(this->host_); + + this->sock_.beginPacket(ip, this->port_); + this->sock_.write((const uint8_t *) out->c_str(), out->length()); + this->sock_.endPacket(); + +#else + if (!this->sock_) { + return; + } + + int n_bytes = this->sock_->sendto(out->c_str(), out->length(), 0, reinterpret_cast(&this->destination_), + sizeof(this->destination_)); + if (n_bytes != out->length()) { + ESP_LOGE(TAG, "Failed to send UDP packed (%d of %d)", n_bytes, out->length()); + } +#endif +} + +} // namespace statsd +} // namespace esphome diff --git a/esphome/components/statsd/statsd.h b/esphome/components/statsd/statsd.h new file mode 100644 index 0000000000..ef42579587 --- /dev/null +++ b/esphome/components/statsd/statsd.h @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include "esphome/core/defines.h" +#include "esphome/core/component.h" +#include "esphome/components/socket/socket.h" +#include "esphome/components/network/ip_address.h" + +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif + +#ifdef USE_ESP8266 +#include "WiFiUdp.h" +#include "IPAddress.h" +#endif + +namespace esphome { +namespace statsd { + +using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR }; + +using sensors_t = struct { + const char *name; + sensor_type_t type; + union { +#ifdef USE_SENSOR + esphome::sensor::Sensor *sensor; +#endif +#ifdef USE_BINARY_SENSOR + esphome::binary_sensor::BinarySensor *binary_sensor; +#endif + }; +}; + +class StatsdComponent : public PollingComponent { + public: + ~StatsdComponent(); + + void setup() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override; + + void configure(const char *host, uint16_t port, const char *prefix) { + this->host_ = host; + this->port_ = port; + this->prefix_ = prefix; + } + +#ifdef USE_SENSOR + void register_sensor(const char *name, esphome::sensor::Sensor *sensor); +#endif + +#ifdef USE_BINARY_SENSOR + void register_binary_sensor(const char *name, esphome::binary_sensor::BinarySensor *binary_sensor); +#endif + + private: + const char *host_; + const char *prefix_; + uint16_t port_; + + std::vector sensors_; + +#ifdef USE_ESP8266 + WiFiUDP sock_; +#else + std::unique_ptr sock_; + struct sockaddr_in destination_; +#endif + + void send_(std::string *out); +}; + +} // namespace statsd +} // namespace esphome diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 3539d0e34e..fef4f7f007 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -1,8 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, @@ -10,11 +10,11 @@ from esphome.const import ( CONF_ID, CONF_INVERTED, CONF_MQTT_ID, - CONF_WEB_SERVER_ID, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_RESTORE_MODE, CONF_TRIGGER_ID, + CONF_WEB_SERVER_ID, DEVICE_CLASS_EMPTY, DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH, diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index 5a8e763495..386baaf756 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -1,18 +1,18 @@ from typing import Optional -import esphome.codegen as cg -import esphome.config_validation as cv + from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_MODE, + CONF_MQTT_ID, CONF_ON_VALUE, CONF_TRIGGER_ID, - CONF_MQTT_ID, - CONF_WEB_SERVER_ID, CONF_VALUE, + CONF_WEB_SERVER_ID, ) - from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index f4e795924c..ba8a2def41 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -1,21 +1,21 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_FILTERS, + CONF_FROM, CONF_ICON, CONF_ID, - CONF_ON_VALUE, - CONF_ON_RAW_VALUE, - CONF_TRIGGER_ID, CONF_MQTT_ID, - CONF_WEB_SERVER_ID, + CONF_ON_RAW_VALUE, + CONF_ON_VALUE, CONF_STATE, - CONF_FROM, CONF_TO, + CONF_TRIGGER_ID, + CONF_WEB_SERVER_ID, DEVICE_CLASS_DATE, DEVICE_CLASS_EMPTY, DEVICE_CLASS_TIMESTAMP, diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 89d6b13376..a529bbd474 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation +import esphome.codegen as cg from esphome.components import climate, sensor +import esphome.config_validation as cv from esphome.const import ( CONF_AUTO_MODE, CONF_AWAY_CONFIG, @@ -15,15 +15,15 @@ from esphome.const import ( CONF_DRY_ACTION, CONF_DRY_MODE, CONF_FAN_MODE, - CONF_FAN_MODE_ON_ACTION, - CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, + CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_FOCUS_ACTION, + CONF_FAN_MODE_HIGH_ACTION, CONF_FAN_MODE_LOW_ACTION, CONF_FAN_MODE_MEDIUM_ACTION, - CONF_FAN_MODE_HIGH_ACTION, CONF_FAN_MODE_MIDDLE_ACTION, - CONF_FAN_MODE_FOCUS_ACTION, - CONF_FAN_MODE_DIFFUSE_ACTION, + CONF_FAN_MODE_OFF_ACTION, + CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_QUIET_ACTION, CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, @@ -50,8 +50,8 @@ from esphome.const import ( CONF_MIN_HEATING_RUN_TIME, CONF_MIN_IDLE_TIME, CONF_MIN_TEMPERATURE, - CONF_NAME, CONF_MODE, + CONF_NAME, CONF_OFF_MODE, CONF_PRESET, CONF_SENSOR, @@ -892,7 +892,7 @@ async def to_code(config): if name.upper() in climate.CLIMATE_PRESETS: standard_preset = climate.CLIMATE_PRESETS[name.upper()] - if two_points_available is True: + if two_points_available: preset_target_config = ThermostatClimateTargetTempConfig( preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], @@ -905,6 +905,8 @@ async def to_code(config): preset_target_config = ThermostatClimateTargetTempConfig( preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] ) + else: + preset_target_config = None preset_target_variable = cg.new_variable( preset_config[CONF_ID], preset_target_config diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index c888705ba2..6a3368ca73 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -1,32 +1,32 @@ -import logging from importlib import resources +import logging from typing import Optional import tzlocal +from esphome import automation +from esphome.automation import Condition import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation from esphome.const import ( - CONF_ID, + CONF_AT, CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, + CONF_HOUR, CONF_HOURS, + CONF_ID, + CONF_MINUTE, CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_ON_TIME_SYNC, + CONF_SECOND, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, - CONF_AT, - CONF_SECOND, - CONF_HOUR, - CONF_MINUTE, ) from esphome.core import coroutine_with_priority -from esphome.automation import Condition _LOGGER = logging.getLogger(__name__) diff --git a/esphome/components/tm1638/binary_sensor/__init__.py b/esphome/components/tm1638/binary_sensor/__init__.py index 6623228555..de6ea35e54 100644 --- a/esphome/components/tm1638/binary_sensor/__init__.py +++ b/esphome/components/tm1638/binary_sensor/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor +import esphome.config_validation as cv from esphome.const import CONF_KEY -from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns TM1638Key = tm1638_ns.class_("TM1638Key", binary_sensor.BinarySensor) diff --git a/esphome/components/tm1638/display.py b/esphome/components/tm1638/display.py index 2fb8dc7a55..14b70be94d 100644 --- a/esphome/components/tm1638/display.py +++ b/esphome/components/tm1638/display.py @@ -1,13 +1,13 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import display +import esphome.config_validation as cv from esphome.const import ( + CONF_CLK_PIN, + CONF_DIO_PIN, CONF_ID, CONF_INTENSITY, CONF_LAMBDA, - CONF_CLK_PIN, - CONF_DIO_PIN, CONF_STB_PIN, ) @@ -51,4 +51,4 @@ async def to_code(config): config[CONF_LAMBDA], [(TM1638ComponentRef, "it")], return_type=cg.void ) - cg.add(var.set_writer(lambda_)) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1638/output/__init__.py b/esphome/components/tm1638/output/__init__.py index 2d982e409d..b16b08d504 100644 --- a/esphome/components/tm1638/output/__init__.py +++ b/esphome/components/tm1638/output/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import output +import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_LED -from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns TM1638OutputLed = tm1638_ns.class_("TM1638OutputLed", output.BinaryOutput, cg.Component) diff --git a/esphome/components/tm1638/switch/__init__.py b/esphome/components/tm1638/switch/__init__.py index ed6aa91d03..8832cf8b92 100644 --- a/esphome/components/tm1638/switch/__init__.py +++ b/esphome/components/tm1638/switch/__init__.py @@ -1,8 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import switch +import esphome.config_validation as cv from esphome.const import CONF_LED -from ..display import tm1638_ns, TM1638Component, CONF_TM1638_ID + +from ..display import CONF_TM1638_ID, TM1638Component, tm1638_ns TM1638SwitchLed = tm1638_ns.class_("TM1638SwitchLed", switch.Switch, cg.Component) diff --git a/esphome/components/touchscreen/binary_sensor/__init__.py b/esphome/components/touchscreen/binary_sensor/__init__.py index 800bc4c2a9..45fefbf814 100644 --- a/esphome/components/touchscreen/binary_sensor/__init__.py +++ b/esphome/components/touchscreen/binary_sensor/__init__.py @@ -1,10 +1,9 @@ import esphome.codegen as cg -import esphome.config_validation as cv - from esphome.components import binary_sensor, display -from esphome.const import CONF_PAGE_ID +import esphome.config_validation as cv +from esphome.const import CONF_PAGE_ID, CONF_PAGES -from .. import touchscreen_ns, CONF_TOUCHSCREEN_ID, Touchscreen, TouchListener +from .. import CONF_TOUCHSCREEN_ID, TouchListener, Touchscreen, touchscreen_ns DEPENDENCIES = ["touchscreen"] @@ -22,7 +21,7 @@ CONF_Y_MIN = "y_min" CONF_Y_MAX = "y_max" -def validate_coords(config): +def _validate_coords(config): if ( config[CONF_X_MAX] < config[CONF_X_MIN] or config[CONF_Y_MAX] < config[CONF_Y_MIN] @@ -33,6 +32,15 @@ def validate_coords(config): return config +def _set_pages(config: dict) -> dict: + if CONF_PAGES in config or CONF_PAGE_ID not in config: + return config + + config = config.copy() + config[CONF_PAGES] = [config.pop(CONF_PAGE_ID)] + return config + + CONFIG_SCHEMA = cv.All( binary_sensor.binary_sensor_schema(TouchscreenBinarySensor) .extend( @@ -42,11 +50,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_X_MAX): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=2000), - cv.Optional(CONF_PAGE_ID): cv.use_id(display.DisplayPage), + cv.Exclusive(CONF_PAGE_ID, group_of_exclusion=CONF_PAGES): cv.use_id( + display.DisplayPage + ), + cv.Exclusive(CONF_PAGES, group_of_exclusion=CONF_PAGES): cv.ensure_list( + cv.use_id(display.DisplayPage) + ), } ) .extend(cv.COMPONENT_SCHEMA), - validate_coords, + _validate_coords, + _set_pages, ) @@ -64,6 +78,6 @@ async def to_code(config): ) ) - if CONF_PAGE_ID in config: - page = await cg.get_variable(config[CONF_PAGE_ID]) - cg.add(var.set_page(page)) + for page_id in config.get(CONF_PAGES, []): + page = await cg.get_variable(page_id) + cg.add(var.add_page(page)) diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index 6c26ae3626..6cd12d4d0d 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -11,8 +11,9 @@ void TouchscreenBinarySensor::setup() { void TouchscreenBinarySensor::touch(TouchPoint tp) { bool touched = (tp.x >= this->x_min_ && tp.x <= this->x_max_ && tp.y >= this->y_min_ && tp.y <= this->y_max_); - if (this->page_ != nullptr) { - touched &= this->page_ == this->parent_->get_display()->get_active_page(); + if (!this->pages_.empty()) { + auto *current_page = this->parent_->get_display()->get_active_page(); + touched &= std::find(this->pages_.begin(), this->pages_.end(), current_page) != this->pages_.end(); } if (touched) { this->publish_state(true); diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index b56ae562b1..862f41064c 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -6,6 +6,8 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include + namespace esphome { namespace touchscreen { @@ -30,14 +32,14 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, int16_t get_width() { return this->x_max_ - this->x_min_; } int16_t get_height() { return this->y_max_ - this->y_min_; } - void set_page(display::DisplayPage *page) { this->page_ = page; } + void add_page(display::DisplayPage *page) { this->pages_.push_back(page); } void touch(TouchPoint tp) override; void release() override; protected: int16_t x_min_, x_max_, y_min_, y_max_; - display::DisplayPage *page_{nullptr}; + std::vector pages_{}; }; } // namespace touchscreen diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 2eaaa2a625..0738f9b6a4 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -15,6 +15,7 @@ CONF_DATAPOINT_TYPE = "datapoint_type" CONF_STATUS_PIN = "status_pin" tuya_ns = cg.esphome_ns.namespace("tuya") +TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True) Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) DPTYPE_ANY = "any" diff --git a/esphome/components/tuya/number/__init__.py b/esphome/components/tuya/number/__init__.py index 4dae6d8d60..c00ea08d23 100644 --- a/esphome/components/tuya/number/__init__.py +++ b/esphome/components/tuya/number/__init__.py @@ -8,18 +8,37 @@ from esphome.const import ( CONF_MIN_VALUE, CONF_MULTIPLY, CONF_STEP, + CONF_INITIAL_VALUE, + CONF_RESTORE_VALUE, ) -from .. import tuya_ns, CONF_TUYA_ID, Tuya +from .. import tuya_ns, CONF_TUYA_ID, Tuya, TuyaDatapointType DEPENDENCIES = ["tuya"] CODEOWNERS = ["@frankiboy1"] +CONF_DATAPOINT_HIDDEN = "datapoint_hidden" +CONF_DATAPOINT_TYPE = "datapoint_type" + TuyaNumber = tuya_ns.class_("TuyaNumber", number.Number, cg.Component) +DATAPOINT_TYPES = { + "int": TuyaDatapointType.INTEGER, + "uint": TuyaDatapointType.INTEGER, + "enum": TuyaDatapointType.ENUM, +} + def validate_min_max(config): - if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + max_value = config[CONF_MAX_VALUE] + min_value = config[CONF_MIN_VALUE] + if max_value <= min_value: raise cv.Invalid("max_value must be greater than min_value") + if hidden_config := config.get(CONF_DATAPOINT_HIDDEN): + if (initial_value := hidden_config.get(CONF_INITIAL_VALUE, None)) is not None: + if (initial_value > max_value) or (initial_value < min_value): + raise cv.Invalid( + f"{CONF_INITIAL_VALUE} must be a value between {CONF_MAX_VALUE} and {CONF_MIN_VALUE}" + ) return config @@ -33,6 +52,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_DATAPOINT_HIDDEN): cv.All( + cv.Schema( + { + cv.Required(CONF_DATAPOINT_TYPE): cv.enum( + DATAPOINT_TYPES, lower=True + ), + cv.Optional(CONF_INITIAL_VALUE): cv.float_, + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, + } + ) + ), } ) .extend(cv.COMPONENT_SCHEMA), @@ -56,3 +86,10 @@ async def to_code(config): cg.add(var.set_tuya_parent(parent)) cg.add(var.set_number_id(config[CONF_NUMBER_DATAPOINT])) + if hidden_config := config.get(CONF_DATAPOINT_HIDDEN): + cg.add(var.set_datapoint_type(hidden_config[CONF_DATAPOINT_TYPE])) + if ( + hidden_init_value := hidden_config.get(CONF_INITIAL_VALUE, None) + ) is not None: + cg.add(var.set_datapoint_initial_value(hidden_init_value)) + cg.add(var.set_restore_value(hidden_config[CONF_RESTORE_VALUE])) diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index e883c72d3d..68a7f8f2a7 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -7,16 +7,58 @@ namespace tuya { static const char *const TAG = "tuya.number"; void TuyaNumber::setup() { + if (this->restore_value_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + } + this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { if (datapoint.type == TuyaDatapointType::INTEGER) { ESP_LOGV(TAG, "MCU reported number %u is: %d", datapoint.id, datapoint.value_int); - this->publish_state(datapoint.value_int / multiply_by_); + float value = datapoint.value_int / multiply_by_; + this->publish_state(value); + if (this->restore_value_) + this->pref_.save(&value); } else if (datapoint.type == TuyaDatapointType::ENUM) { ESP_LOGV(TAG, "MCU reported number %u is: %u", datapoint.id, datapoint.value_enum); - this->publish_state(datapoint.value_enum); + float value = datapoint.value_enum; + this->publish_state(value); + if (this->restore_value_) + this->pref_.save(&value); + } else { + ESP_LOGW(TAG, "Reported type (%d) is not a number!", static_cast(datapoint.type)); + return; + } + + if ((this->type_) && (this->type_ != datapoint.type)) { + ESP_LOGW(TAG, "Reported type (%d) different than previously set (%d)!", static_cast(datapoint.type), + static_cast(*this->type_)); } this->type_ = datapoint.type; }); + + this->parent_->add_on_initialized_callback([this] { + if (this->type_) { + float value; + if (!this->restore_value_) { + if (this->initial_value_) { + value = *this->initial_value_; + } else { + return; + } + } else { + if (!this->pref_.load(&value)) { + if (this->initial_value_) { + value = *this->initial_value_; + } else { + value = this->traits.get_min_value(); + ESP_LOGW(TAG, "Failed to restore and there is no initial value defined. Setting min_value (%f)", value); + } + } + } + + this->control(value); + } + }); } void TuyaNumber::control(float value) { @@ -28,11 +70,25 @@ void TuyaNumber::control(float value) { this->parent_->set_enum_datapoint_value(this->number_id_, value); } this->publish_state(value); + + if (this->restore_value_) + this->pref_.save(&value); } void TuyaNumber::dump_config() { LOG_NUMBER("", "Tuya Number", this); ESP_LOGCONFIG(TAG, " Number has datapoint ID %u", this->number_id_); + if (this->type_) { + ESP_LOGCONFIG(TAG, " Datapoint type is %d", static_cast(*this->type_)); + } else { + ESP_LOGCONFIG(TAG, " Datapoint type is unknown"); + } + + if (this->initial_value_) { + ESP_LOGCONFIG(TAG, " Initial Value: %f", *this->initial_value_); + } + + ESP_LOGCONFIG(TAG, " Restore Value: %s", YESNO(this->restore_value_)); } } // namespace tuya diff --git a/esphome/components/tuya/number/tuya_number.h b/esphome/components/tuya/number/tuya_number.h index f64dac8957..53137d6f66 100644 --- a/esphome/components/tuya/number/tuya_number.h +++ b/esphome/components/tuya/number/tuya_number.h @@ -1,8 +1,10 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/tuya/tuya.h" #include "esphome/components/number/number.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "esphome/core/preferences.h" namespace esphome { namespace tuya { @@ -13,6 +15,9 @@ class TuyaNumber : public number::Number, public Component { void dump_config() override; void set_number_id(uint8_t number_id) { this->number_id_ = number_id; } void set_write_multiply(float factor) { multiply_by_ = factor; } + void set_datapoint_type(TuyaDatapointType type) { type_ = type; } + void set_datapoint_initial_value(float value) { this->initial_value_ = value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } @@ -22,7 +27,11 @@ class TuyaNumber : public number::Number, public Component { Tuya *parent_; uint8_t number_id_{0}; float multiply_by_{1.0}; - TuyaDatapointType type_{}; + optional type_{}; + optional initial_value_{}; + bool restore_value_{false}; + + ESPPreferenceObject pref_; }; } // namespace tuya diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py new file mode 100644 index 0000000000..ca15be2a80 --- /dev/null +++ b/esphome/components/udp/__init__.py @@ -0,0 +1,158 @@ +import hashlib + +import esphome.codegen as cg +from esphome.components.api import CONF_ENCRYPTION +from esphome.components.binary_sensor import BinarySensor +from esphome.components.sensor import Sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BINARY_SENSORS, + CONF_ID, + CONF_INTERNAL, + CONF_KEY, + CONF_NAME, + CONF_PORT, + CONF_SENSORS, +) +from esphome.cpp_generator import MockObjClass + +CODEOWNERS = ["@clydebarrow"] +DEPENDENCIES = ["network"] +AUTO_LOAD = ["socket"] +MULTI_CONF = True + +udp_ns = cg.esphome_ns.namespace("udp") +UDPComponent = udp_ns.class_("UDPComponent", cg.PollingComponent) + +CONF_BROADCAST = "broadcast" +CONF_BROADCAST_ID = "broadcast_id" +CONF_ADDRESSES = "addresses" +CONF_PROVIDER = "provider" +CONF_PROVIDERS = "providers" +CONF_REMOTE_ID = "remote_id" +CONF_UDP_ID = "udp_id" +CONF_PING_PONG_ENABLE = "ping_pong_enable" +CONF_PING_PONG_RECYCLE_TIME = "ping_pong_recycle_time" +CONF_ROLLING_CODE_ENABLE = "rolling_code_enable" + + +def sensor_validation(cls: MockObjClass): + return cv.maybe_simple_value( + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(cls), + cv.Optional(CONF_BROADCAST_ID): cv.validate_id_name, + } + ), + key=CONF_ID, + ) + + +ENCRYPTION_SCHEMA = { + cv.Optional(CONF_ENCRYPTION): cv.maybe_simple_value( + cv.Schema( + { + cv.Required(CONF_KEY): cv.string, + } + ), + key=CONF_KEY, + ) +} + +PROVIDER_SCHEMA = cv.Schema( + { + cv.Required(CONF_NAME): cv.valid_name, + } +).extend(ENCRYPTION_SCHEMA) + + +def validate_(config): + if CONF_ENCRYPTION in config: + if CONF_SENSORS not in config and CONF_BINARY_SENSORS not in config: + raise cv.Invalid("No sensors or binary sensors to encrypt") + elif config[CONF_ROLLING_CODE_ENABLE]: + raise cv.Invalid("Rolling code requires an encryption key") + if config[CONF_PING_PONG_ENABLE]: + if not any(CONF_ENCRYPTION in p for p in config.get(CONF_PROVIDERS) or ()): + raise cv.Invalid("Ping-pong requires at least one encrypted provider") + return config + + +CONFIG_SCHEMA = cv.All( + cv.polling_component_schema("15s") + .extend( + { + cv.GenerateID(): cv.declare_id(UDPComponent), + cv.Optional(CONF_PORT, default=18511): cv.port, + cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( + cv.ipv4 + ), + cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean, + cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean, + cv.Optional( + CONF_PING_PONG_RECYCLE_TIME, default="600s" + ): cv.positive_time_period_seconds, + cv.Optional(CONF_SENSORS): cv.ensure_list(sensor_validation(Sensor)), + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( + sensor_validation(BinarySensor) + ), + cv.Optional(CONF_PROVIDERS): cv.ensure_list(PROVIDER_SCHEMA), + }, + ) + .extend(ENCRYPTION_SCHEMA), + validate_, +) + +SENSOR_SCHEMA = cv.Schema( + { + cv.Optional(CONF_REMOTE_ID): cv.string_strict, + cv.Required(CONF_PROVIDER): cv.valid_name, + cv.GenerateID(CONF_UDP_ID): cv.use_id(UDPComponent), + } +) + + +def require_internal_with_name(config): + if CONF_NAME in config and CONF_INTERNAL not in config: + raise cv.Invalid("Must provide internal: config when using name:") + return config + + +def hash_encryption_key(config: dict): + return list(hashlib.sha256(config[CONF_KEY].encode()).digest()) + + +async def to_code(config): + cg.add_define("USE_UDP") + cg.add_global(udp_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add(var.set_port(config[CONF_PORT])) + cg.add(var.set_rolling_code_enable(config[CONF_ROLLING_CODE_ENABLE])) + cg.add(var.set_ping_pong_enable(config[CONF_PING_PONG_ENABLE])) + cg.add( + var.set_ping_pong_recycle_time( + config[CONF_PING_PONG_RECYCLE_TIME].total_seconds + ) + ) + for sens_conf in config.get(CONF_SENSORS, ()): + sens_id = sens_conf[CONF_ID] + sensor = await cg.get_variable(sens_id) + bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) + cg.add(var.add_sensor(bcst_id, sensor)) + for sens_conf in config.get(CONF_BINARY_SENSORS, ()): + sens_id = sens_conf[CONF_ID] + sensor = await cg.get_variable(sens_id) + bcst_id = sens_conf.get(CONF_BROADCAST_ID, sens_id.id) + cg.add(var.add_binary_sensor(bcst_id, sensor)) + for address in config[CONF_ADDRESSES]: + cg.add(var.add_address(str(address))) + + if encryption := config.get(CONF_ENCRYPTION): + cg.add(var.set_encryption_key(hash_encryption_key(encryption))) + + for provider in config.get(CONF_PROVIDERS, ()): + name = provider[CONF_NAME] + cg.add(var.add_provider(name)) + if encryption := provider.get(CONF_ENCRYPTION): + cg.add(var.set_provider_encryption(name, hash_encryption_key(encryption))) diff --git a/esphome/components/udp/binary_sensor.py b/esphome/components/udp/binary_sensor.py new file mode 100644 index 0000000000..d90e495527 --- /dev/null +++ b/esphome/components/udp/binary_sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +from esphome.config_validation import All, has_at_least_one_key +from esphome.const import CONF_ID + +from . import ( + CONF_PROVIDER, + CONF_REMOTE_ID, + CONF_UDP_ID, + SENSOR_SCHEMA, + require_internal_with_name, +) + +DEPENDENCIES = ["udp"] + +CONFIG_SCHEMA = All( + binary_sensor.binary_sensor_schema().extend(SENSOR_SCHEMA), + has_at_least_one_key(CONF_ID, CONF_REMOTE_ID), + require_internal_with_name, +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + comp = await cg.get_variable(config[CONF_UDP_ID]) + remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) + cg.add(comp.add_remote_binary_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/udp/sensor.py b/esphome/components/udp/sensor.py new file mode 100644 index 0000000000..860c277c44 --- /dev/null +++ b/esphome/components/udp/sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components.sensor import new_sensor, sensor_schema +from esphome.config_validation import All, has_at_least_one_key +from esphome.const import CONF_ID + +from . import ( + CONF_PROVIDER, + CONF_REMOTE_ID, + CONF_UDP_ID, + SENSOR_SCHEMA, + require_internal_with_name, +) + +DEPENDENCIES = ["udp"] + +CONFIG_SCHEMA = All( + sensor_schema().extend(SENSOR_SCHEMA), + has_at_least_one_key(CONF_ID, CONF_REMOTE_ID), + require_internal_with_name, +) + + +async def to_code(config): + var = await new_sensor(config) + comp = await cg.get_variable(config[CONF_UDP_ID]) + remote_id = str(config.get(CONF_REMOTE_ID) or config.get(CONF_ID)) + cg.add(comp.add_remote_sensor(config[CONF_PROVIDER], remote_id, var)) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp new file mode 100644 index 0000000000..799ed813d3 --- /dev/null +++ b/esphome/components/udp/udp_component.cpp @@ -0,0 +1,616 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/components/network/util.h" +#include "udp_component.h" + +namespace esphome { +namespace udp { + +/** + * Structure of a data packet; everything is little-endian + * + * --- In clear text --- + * MAGIC_NUMBER: 16 bits + * host name length: 1 byte + * host name: (length) bytes + * padding: 0 or more null bytes to a 4 byte boundary + * + * --- Encrypted (if key set) ---- + * DATA_KEY: 1 byte: OR ROLLING_CODE_KEY: + * Rolling code (if enabled): 8 bytes + * Ping keys: if any + * repeat: + * PING_KEY: 1 byte + * ping code: 4 bytes + * Sensors: + * repeat: + * SENSOR_KEY: 1 byte + * float value: 4 bytes + * name length: 1 byte + * name + * Binary Sensors: + * repeat: + * BINARY_SENSOR_KEY: 1 byte + * bool value: 1 bytes + * name length: 1 byte + * name + * + * Padded to a 4 byte boundary with nulls + * + * Structure of a ping request packet: + * --- In clear text --- + * MAGIC_PING: 16 bits + * host name length: 1 byte + * host name: (length) bytes + * Ping key (4 bytes) + * + */ +static const char *const TAG = "udp"; + +/** + * XXTEA implementation, using 256 bit key. + */ + +static const uint32_t DELTA = 0x9e3779b9; +#define MX ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((sum ^ y) + (k[(p ^ e) & 7] ^ z))) + +/** + * Encrypt a block of data in-place + */ + +static void xxtea_encrypt(uint32_t *v, size_t n, const uint32_t *k) { + uint32_t z, y, sum, e; + size_t p; + size_t q = 6 + 52 / n; + sum = 0; + z = v[n - 1]; + while (q-- != 0) { + sum += DELTA; + e = (sum >> 2); + for (p = 0; p != n - 1; p++) { + y = v[p + 1]; + z = v[p] += MX; + } + y = v[0]; + z = v[n - 1] += MX; + } +} + +static void xxtea_decrypt(uint32_t *v, size_t n, const uint32_t *k) { + uint32_t z, y, sum, e; + size_t p; + size_t q = 6 + 52 / n; + sum = q * DELTA; + y = v[0]; + while (q-- != 0) { + e = (sum >> 2); + for (p = n - 1; p != 0; p--) { + z = v[p - 1]; + y = v[p] -= MX; + } + z = v[n - 1]; + y = v[0] -= MX; + sum -= DELTA; + } +} + +inline static size_t round4(size_t value) { return (value + 3) & ~3; } + +union FuData { + uint32_t u32; + float f32; +}; + +static const size_t MAX_PACKET_SIZE = 508; +static const uint16_t MAGIC_NUMBER = 0x4553; +static const uint16_t MAGIC_PING = 0x5048; +static const uint32_t PREF_HASH = 0x45535043; +enum DataKey { + ZERO_FILL_KEY, + DATA_KEY, + SENSOR_KEY, + BINARY_SENSOR_KEY, + PING_KEY, + ROLLING_CODE_KEY, +}; + +static const size_t MAX_PING_KEYS = 4; + +static inline void add(std::vector &vec, uint32_t data) { + vec.push_back(data & 0xFF); + vec.push_back((data >> 8) & 0xFF); + vec.push_back((data >> 16) & 0xFF); + vec.push_back((data >> 24) & 0xFF); +} + +static inline uint32_t get_uint32(uint8_t *&buf) { + uint32_t data = *buf++; + data += *buf++ << 8; + data += *buf++ << 16; + data += *buf++ << 24; + return data; +} + +static inline uint16_t get_uint16(uint8_t *&buf) { + uint16_t data = *buf++; + data += *buf++ << 8; + return data; +} + +static inline void add(std::vector &vec, uint8_t data) { vec.push_back(data); } +static inline void add(std::vector &vec, uint16_t data) { + vec.push_back((uint8_t) data); + vec.push_back((uint8_t) (data >> 8)); +} +static inline void add(std::vector &vec, DataKey data) { vec.push_back(data); } +static void add(std::vector &vec, const char *str) { + auto len = strlen(str); + vec.push_back(len); + for (size_t i = 0; i != len; i++) { + vec.push_back(*str++); + } +} + +void UDPComponent::setup() { + this->name_ = App.get_name().c_str(); + if (strlen(this->name_) > 255) { + this->mark_failed(); + this->status_set_error("Device name exceeds 255 chars"); + return; + } + this->resend_ping_key_ = this->ping_pong_enable_; + // restore the upper 32 bits of the rolling code, increment and save. + this->pref_ = global_preferences->make_preference(PREF_HASH, true); + this->pref_.load(&this->rolling_code_[1]); + this->rolling_code_[1]++; + this->pref_.save(&this->rolling_code_[1]); + this->ping_key_ = random_uint32(); + ESP_LOGV(TAG, "Rolling code incremented, upper part now %u", (unsigned) this->rolling_code_[1]); +#ifdef USE_SENSOR + for (auto &sensor : this->sensors_) { + sensor.sensor->add_on_state_callback([this, &sensor](float x) { + this->updated_ = true; + sensor.updated = true; + }); + } +#endif +#ifdef USE_BINARY_SENSOR + for (auto &sensor : this->binary_sensors_) { + sensor.sensor->add_on_state_callback([this, &sensor](bool value) { + this->updated_ = true; + sensor.updated = true; + }); + } +#endif + this->should_send_ = this->ping_pong_enable_; +#ifdef USE_SENSOR + this->should_send_ |= !this->sensors_.empty(); +#endif +#ifdef USE_BINARY_SENSOR + this->should_send_ |= !this->binary_sensors_.empty(); +#endif + this->should_listen_ = !this->providers_.empty() || this->is_encrypted_(); + // initialise the header. This is invariant. + add(this->header_, MAGIC_NUMBER); + add(this->header_, this->name_); + // pad to a multiple of 4 bytes + while (this->header_.size() & 0x3) + this->header_.push_back(0); +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + for (const auto &address : this->addresses_) { + struct sockaddr saddr {}; + socket::set_sockaddr(&saddr, sizeof(saddr), address, this->port_); + this->sockaddrs_.push_back(saddr); + } + // set up broadcast socket + if (this->should_send_) { + this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (this->broadcast_socket_ == nullptr) { + this->mark_failed(); + this->status_set_error("Could not create socket"); + return; + } + int enable = 1; + auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + this->status_set_warning("Socket unable to set reuseaddr"); + // we can still continue + } + err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); + if (err != 0) { + this->status_set_warning("Socket unable to set broadcast"); + } + } + // create listening socket if we either want to subscribe to providers, or need to listen + // for ping key broadcasts. + if (this->should_listen_) { + this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); + if (this->listen_socket_ == nullptr) { + this->mark_failed(); + this->status_set_error("Could not create socket"); + return; + } + auto err = this->listen_socket_->setblocking(false); + if (err < 0) { + ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno); + this->mark_failed(); + this->status_set_error("Unable to set nonblocking"); + return; + } + int enable = 1; + err = this->listen_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); + if (err != 0) { + this->status_set_warning("Socket unable to set reuseaddr"); + // we can still continue + } + struct sockaddr_in server {}; + + socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); + if (sl == 0) { + ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno); + this->mark_failed(); + this->status_set_error("Unable to set sockaddr"); + return; + } + + err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); + this->status_set_error("Unable to bind socket"); + return; + } + } +#else + // 8266 and RP2040 `Duino + for (const auto &address : this->addresses_) { + auto ipaddr = IPAddress(); + ipaddr.fromString(address.c_str()); + this->ipaddrs_.push_back(ipaddr); + } + if (this->should_listen_) + this->udp_client_.begin(this->port_); +#endif +} + +void UDPComponent::init_data_() { + this->data_.clear(); + if (this->rolling_code_enable_) { + add(this->data_, ROLLING_CODE_KEY); + add(this->data_, this->rolling_code_[0]); + add(this->data_, this->rolling_code_[1]); + this->increment_code_(); + } else { + add(this->data_, DATA_KEY); + } + for (auto pkey : this->ping_keys_) { + add(this->data_, PING_KEY); + add(this->data_, pkey.second); + } +} + +void UDPComponent::flush_() { + if (!network::is_connected() || this->data_.empty()) + return; + uint32_t buffer[MAX_PACKET_SIZE / 4]; + memset(buffer, 0, sizeof buffer); + // len must be a multiple of 4 + auto header_len = round4(this->header_.size()) / 4; + auto len = round4(data_.size()) / 4; + memcpy(buffer, this->header_.data(), this->header_.size()); + memcpy(buffer + header_len, this->data_.data(), this->data_.size()); + if (this->is_encrypted_()) { + xxtea_encrypt(buffer + header_len, len, (uint32_t *) this->encryption_key_.data()); + } + auto total_len = (header_len + len) * 4; + this->send_packet_(buffer, total_len); +} + +void UDPComponent::add_binary_data_(uint8_t key, const char *id, bool data) { + auto len = 1 + 1 + 1 + strlen(id); + if (len + this->header_.size() + this->data_.size() > MAX_PACKET_SIZE) { + this->flush_(); + } + add(this->data_, key); + add(this->data_, (uint8_t) data); + add(this->data_, id); +} +void UDPComponent::add_data_(uint8_t key, const char *id, float data) { + FuData udata{.f32 = data}; + this->add_data_(key, id, udata.u32); +} + +void UDPComponent::add_data_(uint8_t key, const char *id, uint32_t data) { + auto len = 4 + 1 + 1 + strlen(id); + if (len + this->header_.size() + this->data_.size() > MAX_PACKET_SIZE) { + this->flush_(); + } + add(this->data_, key); + add(this->data_, data); + add(this->data_, id); +} +void UDPComponent::send_data_(bool all) { + if (!this->should_send_ || !network::is_connected()) + return; + this->init_data_(); +#ifdef USE_SENSOR + for (auto &sensor : this->sensors_) { + if (all || sensor.updated) { + sensor.updated = false; + this->add_data_(SENSOR_KEY, sensor.id, sensor.sensor->get_state()); + } + } +#endif +#ifdef USE_BINARY_SENSOR + for (auto &sensor : this->binary_sensors_) { + if (all || sensor.updated) { + sensor.updated = false; + this->add_binary_data_(BINARY_SENSOR_KEY, sensor.id, sensor.sensor->state); + } + } +#endif + this->flush_(); + this->updated_ = false; + this->resend_data_ = false; +} + +void UDPComponent::update() { + this->updated_ = true; + this->resend_data_ = this->should_send_; + auto now = millis() / 1000; + if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) { + this->resend_ping_key_ = this->ping_pong_enable_; + this->last_key_time_ = now; + } +} + +void UDPComponent::loop() { + uint8_t buf[MAX_PACKET_SIZE]; + if (this->should_listen_) { + for (;;) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + auto len = this->listen_socket_->read(buf, sizeof(buf)); +#else + auto len = this->udp_client_.parsePacket(); + if (len > 0) + len = this->udp_client_.read(buf, sizeof(buf)); +#endif + if (len > 0) { + this->process_(buf, len); + continue; + } + break; + } + } + if (this->resend_ping_key_) + this->send_ping_pong_request_(); + if (this->updated_) { + this->send_data_(this->resend_data_); + } +} + +void UDPComponent::add_key_(const char *name, uint32_t key) { + if (!this->is_encrypted_()) + return; + if (this->ping_keys_.count(name) == 0 && this->ping_keys_.size() == MAX_PING_KEYS) { + ESP_LOGW(TAG, "Ping key from %s discarded", name); + return; + } + this->ping_keys_[name] = key; + this->resend_data_ = true; + ESP_LOGV(TAG, "Ping key from %s now %X", name, (unsigned) key); +} + +void UDPComponent::process_ping_request_(const char *name, uint8_t *ptr, size_t len) { + if (len != 4) { + ESP_LOGW(TAG, "Bad ping request"); + return; + } + auto key = get_uint32(ptr); + this->add_key_(name, key); + ESP_LOGV(TAG, "Updated ping key for %s to %08X", name, (unsigned) key); +} + +static bool process_rolling_code(Provider &provider, uint8_t *&buf, const uint8_t *end) { + if (end - buf < 8) + return false; + auto code0 = get_uint32(buf); + auto code1 = get_uint32(buf); + if (code1 < provider.last_code[1] || (code1 == provider.last_code[1] && code0 <= provider.last_code[0])) { + ESP_LOGW(TAG, "Rolling code for %s %08lX:%08lX is old", provider.name, (unsigned long) code1, + (unsigned long) code0); + return false; + } + provider.last_code[0] = code0; + provider.last_code[1] = code1; + return true; +} + +/** + * Process a received packet + */ +void UDPComponent::process_(uint8_t *buf, const size_t len) { + auto ping_key_seen = !this->ping_pong_enable_; + if (len < 8) { + return ESP_LOGV(TAG, "Bad length %zu", len); + } + char namebuf[256]{}; + uint8_t byte; + uint8_t *start_ptr = buf; + const uint8_t *end = buf + len; + FuData rdata{}; + auto magic = get_uint16(buf); + if (magic != MAGIC_NUMBER && magic != MAGIC_PING) + return ESP_LOGV(TAG, "Bad magic %X", magic); + + auto hlen = *buf++; + if (hlen > len - 3) { + return ESP_LOGV(TAG, "Bad hostname length %u > %zu", hlen, len - 3); + } + memcpy(namebuf, buf, hlen); + if (strcmp(this->name_, namebuf) == 0) { + return ESP_LOGV(TAG, "Ignoring our own data"); + } + buf += hlen; + if (magic == MAGIC_PING) + return this->process_ping_request_(namebuf, buf, end - buf); + if (round4(len) != len) { + return ESP_LOGW(TAG, "Bad length %zu", len); + } + hlen = round4(hlen + 3); + buf = start_ptr + hlen; + if (buf == end) { + return ESP_LOGV(TAG, "No data after header"); + } + + if (this->providers_.count(namebuf) == 0) { + return ESP_LOGVV(TAG, "Unknown hostname %s", namebuf); + } + auto &provider = this->providers_[namebuf]; + // if encryption not used with this host, ping check is pointless since it would be easily spoofed. + if (provider.encryption_key.empty()) + ping_key_seen = true; + + ESP_LOGV(TAG, "Found hostname %s", namebuf); +#ifdef USE_SENSOR + auto &sensors = this->remote_sensors_[namebuf]; +#endif +#ifdef USE_BINARY_SENSOR + auto &binary_sensors = this->remote_binary_sensors_[namebuf]; +#endif + + if (!provider.encryption_key.empty()) { + xxtea_decrypt((uint32_t *) buf, (end - buf) / 4, (uint32_t *) provider.encryption_key.data()); + } + byte = *buf++; + if (byte == ROLLING_CODE_KEY) { + if (!process_rolling_code(provider, buf, end)) + return; + } else if (byte != DATA_KEY) { + return ESP_LOGV(TAG, "Expected rolling_key or data_key, got %X", byte); + } + while (buf < end) { + byte = *buf++; + if (byte == ZERO_FILL_KEY) + continue; + if (byte == PING_KEY) { + if (end - buf < 4) { + return ESP_LOGV(TAG, "PING_KEY requires 4 more bytes"); + } + auto key = get_uint32(buf); + if (key == this->ping_key_) { + ping_key_seen = true; + ESP_LOGV(TAG, "Found good ping key %X", (unsigned) key); + } else { + ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key); + } + continue; + } + if (!ping_key_seen) { + ESP_LOGW(TAG, "Ping key not seen"); + this->resend_ping_key_ = true; + break; + } + if (byte == BINARY_SENSOR_KEY) { + if (end - buf < 3) { + return ESP_LOGV(TAG, "Binary sensor key requires at least 3 more bytes"); + } + rdata.u32 = *buf++; + } else if (byte == SENSOR_KEY) { + if (end - buf < 6) { + return ESP_LOGV(TAG, "Sensor key requires at least 6 more bytes"); + } + rdata.u32 = get_uint32(buf); + } else { + return ESP_LOGW(TAG, "Unknown key byte %X", byte); + } + + hlen = *buf++; + if (end - buf < hlen) { + return ESP_LOGV(TAG, "Name length of %u not available", hlen); + } + memset(namebuf, 0, sizeof namebuf); + memcpy(namebuf, buf, hlen); + ESP_LOGV(TAG, "Found sensor key %d, id %s, data %lX", byte, namebuf, (unsigned long) rdata.u32); + buf += hlen; +#ifdef USE_SENSOR + if (byte == SENSOR_KEY && sensors.count(namebuf) != 0) + sensors[namebuf]->publish_state(rdata.f32); +#endif +#ifdef USE_BINARY_SENSOR + if (byte == BINARY_SENSOR_KEY && binary_sensors.count(namebuf) != 0) + binary_sensors[namebuf]->publish_state(rdata.u32 != 0); +#endif + } +} + +void UDPComponent::dump_config() { + ESP_LOGCONFIG(TAG, "UDP:"); + ESP_LOGCONFIG(TAG, " Port: %u", this->port_); + ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(this->is_encrypted_())); + ESP_LOGCONFIG(TAG, " Ping-pong: %s", YESNO(this->ping_pong_enable_)); + for (const auto &address : this->addresses_) + ESP_LOGCONFIG(TAG, " Address: %s", address.c_str()); +#ifdef USE_SENSOR + for (auto sensor : this->sensors_) + ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); +#endif +#ifdef USE_BINARY_SENSOR + for (auto sensor : this->binary_sensors_) + ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); +#endif + for (const auto &host : this->providers_) { + ESP_LOGCONFIG(TAG, " Remote host: %s", host.first.c_str()); + ESP_LOGCONFIG(TAG, " Encrypted: %s", YESNO(!host.second.encryption_key.empty())); +#ifdef USE_SENSOR + for (const auto &sensor : this->remote_sensors_[host.first.c_str()]) + ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.first.c_str()); +#endif +#ifdef USE_BINARY_SENSOR + for (const auto &sensor : this->remote_binary_sensors_[host.first.c_str()]) + ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.first.c_str()); +#endif + } +} +void UDPComponent::increment_code_() { + if (this->rolling_code_enable_) { + if (++this->rolling_code_[0] == 0) { + this->rolling_code_[1]++; + this->pref_.save(&this->rolling_code_[1]); + } + } +} +void UDPComponent::send_packet_(void *data, size_t len) { +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + for (const auto &saddr : this->sockaddrs_) { + auto result = this->broadcast_socket_->sendto(data, len, 0, &saddr, sizeof(saddr)); + if (result < 0) + ESP_LOGW(TAG, "sendto() error %d", errno); + } +#else + auto iface = IPAddress(0, 0, 0, 0); + for (const auto &saddr : this->ipaddrs_) { + if (this->udp_client_.beginPacketMulticast(saddr, this->port_, iface, 128) != 0) { + this->udp_client_.write((const uint8_t *) data, len); + auto result = this->udp_client_.endPacket(); + if (result == 0) + ESP_LOGW(TAG, "udp.write() error"); + } + } +#endif +} + +void UDPComponent::send_ping_pong_request_() { + if (!this->ping_pong_enable_ || !network::is_connected()) + return; + this->ping_key_ = random_uint32(); + this->ping_header_.clear(); + add(this->ping_header_, MAGIC_PING); + add(this->ping_header_, this->name_); + add(this->ping_header_, this->ping_key_); + this->send_packet_(this->ping_header_.data(), this->ping_header_.size()); + this->resend_ping_key_ = false; + ESP_LOGV(TAG, "Sent new ping request %08X", (unsigned) this->ping_key_); +} +} // namespace udp +} // namespace esphome diff --git a/esphome/components/udp/udp_component.h b/esphome/components/udp/udp_component.h new file mode 100644 index 0000000000..69bf335a90 --- /dev/null +++ b/esphome/components/udp/udp_component.h @@ -0,0 +1,158 @@ +#pragma once + +#include "esphome/core/component.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) +#include "esphome/components/socket/socket.h" +#else +#include +#endif +#include +#include + +namespace esphome { +namespace udp { + +struct Provider { + std::vector encryption_key; + const char *name; + uint32_t last_code[2]; +}; + +#ifdef USE_SENSOR +struct Sensor { + sensor::Sensor *sensor; + const char *id; + bool updated; +}; +#endif +#ifdef USE_BINARY_SENSOR +struct BinarySensor { + binary_sensor::BinarySensor *sensor; + const char *id; + bool updated; +}; +#endif + +class UDPComponent : public PollingComponent { + public: + void setup() override; + void loop() override; + void update() override; + void dump_config() override; + +#ifdef USE_SENSOR + void add_sensor(const char *id, sensor::Sensor *sensor) { + Sensor st{sensor, id, true}; + this->sensors_.push_back(st); + } + void add_remote_sensor(const char *hostname, const char *remote_id, sensor::Sensor *sensor) { + this->add_provider(hostname); + this->remote_sensors_[hostname][remote_id] = sensor; + } +#endif +#ifdef USE_BINARY_SENSOR + void add_binary_sensor(const char *id, binary_sensor::BinarySensor *sensor) { + BinarySensor st{sensor, id, true}; + this->binary_sensors_.push_back(st); + } + + void add_remote_binary_sensor(const char *hostname, const char *remote_id, binary_sensor::BinarySensor *sensor) { + this->add_provider(hostname); + this->remote_binary_sensors_[hostname][remote_id] = sensor; + } +#endif + void add_address(const char *addr) { this->addresses_.emplace_back(addr); } + void set_port(uint16_t port) { this->port_ = port; } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void add_provider(const char *hostname) { + if (this->providers_.count(hostname) == 0) { + Provider provider; + provider.encryption_key = std::vector{}; + provider.last_code[0] = 0; + provider.last_code[1] = 0; + provider.name = hostname; + this->providers_[hostname] = provider; +#ifdef USE_SENSOR + this->remote_sensors_[hostname] = std::map(); +#endif +#ifdef USE_BINARY_SENSOR + this->remote_binary_sensors_[hostname] = std::map(); +#endif + } + } + + void set_encryption_key(std::vector key) { this->encryption_key_ = std::move(key); } + void set_rolling_code_enable(bool enable) { this->rolling_code_enable_ = enable; } + void set_ping_pong_enable(bool enable) { this->ping_pong_enable_ = enable; } + void set_ping_pong_recycle_time(uint32_t recycle_time) { this->ping_pong_recyle_time_ = recycle_time; } + void set_provider_encryption(const char *name, std::vector key) { + this->providers_[name].encryption_key = std::move(key); + } + + protected: + void send_data_(bool all); + void process_(uint8_t *buf, size_t len); + void flush_(); + void add_data_(uint8_t key, const char *id, float data); + void add_data_(uint8_t key, const char *id, uint32_t data); + void increment_code_(); + void add_binary_data_(uint8_t key, const char *id, bool data); + void init_data_(); + + bool updated_{}; + uint16_t port_{18511}; + uint32_t ping_key_{}; + uint32_t rolling_code_[2]{}; + bool rolling_code_enable_{}; + bool ping_pong_enable_{}; + uint32_t ping_pong_recyle_time_{}; + uint32_t last_key_time_{}; + bool resend_ping_key_{}; + bool resend_data_{}; + bool should_send_{}; + const char *name_{}; + bool should_listen_{}; + ESPPreferenceObject pref_; + +#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) + std::unique_ptr broadcast_socket_ = nullptr; + std::unique_ptr listen_socket_ = nullptr; + std::vector sockaddrs_{}; +#else + std::vector ipaddrs_{}; + WiFiUDP udp_client_{}; +#endif + std::vector encryption_key_{}; + std::vector addresses_{}; + +#ifdef USE_SENSOR + std::vector sensors_{}; + std::map> remote_sensors_{}; +#endif +#ifdef USE_BINARY_SENSOR + std::vector binary_sensors_{}; + std::map> remote_binary_sensors_{}; +#endif + + std::map providers_{}; + std::vector ping_header_{}; + std::vector header_{}; + std::vector data_{}; + std::map ping_keys_{}; + void add_key_(const char *name, uint32_t key); + void send_ping_pong_request_(); + void send_packet_(void *data, size_t len); + void process_ping_request_(const char *name, uint8_t *ptr, size_t len); + + inline bool is_encrypted_() { return !this->encryption_key_.empty(); } +}; + +} // namespace udp +} // namespace esphome diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 45bf082fa4..ba3b2f20df 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -1,10 +1,11 @@ from esphome import automation +import esphome.codegen as cg from esphome.components import mqtt, web_server import esphome.config_validation as cv -import esphome.codegen as cg from esphome.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, + CONF_FORCE_UPDATE, CONF_ID, CONF_MQTT_ID, CONF_WEB_SERVER_ID, @@ -23,8 +24,12 @@ UpdateEntity = update_ns.class_("UpdateEntity", cg.EntityBase) UpdateInfo = update_ns.struct("UpdateInfo") -PerformAction = update_ns.class_("PerformAction", automation.Action) -IsAvailableCondition = update_ns.class_("IsAvailableCondition", automation.Condition) +PerformAction = update_ns.class_( + "PerformAction", automation.Action, cg.Parented.template(UpdateEntity) +) +IsAvailableCondition = update_ns.class_( + "IsAvailableCondition", automation.Condition, cg.Parented.template(UpdateEntity) +) DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, @@ -92,24 +97,37 @@ async def to_code(config): cg.add_global(update_ns.using) -UPDATE_AUTOMATION_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.use_id(UpdateEntity), - } +@automation.register_action( + "update.perform", + PerformAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + cv.Optional(CONF_FORCE_UPDATE, default=False): cv.templatable(cv.boolean), + } + ), ) - - -@automation.register_action("update.perform", PerformAction, UPDATE_AUTOMATION_SCHEMA) async def update_perform_action_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, paren, paren) + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + force = await cg.templatable(config[CONF_FORCE_UPDATE], args, cg.bool_) + cg.add(var.set_force(force)) + return var @automation.register_condition( - "update.is_available", IsAvailableCondition, UPDATE_AUTOMATION_SCHEMA + "update.is_available", + IsAvailableCondition, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(UpdateEntity), + } + ), ) async def update_is_available_condition_to_code( config, condition_id, template_arg, args ): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(condition_id, paren, paren) + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h new file mode 100644 index 0000000000..df50f86a0c --- /dev/null +++ b/esphome/components/update/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "update_entity.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace update { + +template class PerformAction : public Action, public Parented { + TEMPLATABLE_VALUE(bool, force) + + public: + void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); } +}; + +template class IsAvailableCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } +}; + +} // namespace update +} // namespace esphome diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 5984c8e35b..cc269e288f 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -32,7 +32,9 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { void publish_state(); - virtual void perform() = 0; + void perform() { this->perform(false); } + virtual void perform(bool force) = 0; + virtual void check() = 0; const UpdateInfo &update_info = update_info_; const UpdateState &state = state_; diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index a7014dc96c..e058de2852 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -45,11 +45,8 @@ void UponorSmatrixComponent::loop() { // Read incoming data while (this->available()) { - // The controller polls devices every 10 seconds, with around 200 ms between devices. - // Remember timestamps so we can send our own packets when the bus is expected to be silent. - if (now - this->last_rx_ > 500) { - this->last_poll_start_ = now; - } + // The controller polls devices every 10 seconds in some units or continuously in others with around 200 ms between + // devices. Remember timestamps so we can send our own packets when the bus is expected to be silent. this->last_rx_ = now; uint8_t byte; @@ -60,7 +57,8 @@ void UponorSmatrixComponent::loop() { } // Send packets during bus silence - if ((now - this->last_rx_ > 300) && (now - this->last_poll_start_ < 9500) && (now - this->last_tx_ > 200)) { + if (this->rx_buffer_.empty() && (now - this->last_rx_ > 50) && (now - this->last_rx_ < 100) && + (now - this->last_tx_ > 200)) { #ifdef USE_TIME // Only build time packet when bus is silent and queue is empty to make sure we can send it right away if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_()) diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.h b/esphome/components/uponor_smatrix/uponor_smatrix.h index b7667b5b87..e3e19a12fc 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.h +++ b/esphome/components/uponor_smatrix/uponor_smatrix.h @@ -93,7 +93,6 @@ class UponorSmatrixComponent : public uart::UARTDevice, public Component { std::queue> tx_queue_; uint32_t last_rx_; uint32_t last_tx_; - uint32_t last_poll_start_; #ifdef USE_TIME time::RealTimeClock *time_id_{nullptr}; diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index c03d13fec8..3c03bab857 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -1,8 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.automation import maybe_simple_id, Condition +from esphome.automation import Condition, maybe_simple_id +import esphome.codegen as cg from esphome.components import mqtt, web_server +import esphome.config_validation as cv from esphome.const import ( CONF_DEVICE_CLASS, CONF_ID, diff --git a/esphome/components/veml7700/sensor.py b/esphome/components/veml7700/sensor.py index 7b0f75e70c..308f1c1c00 100644 --- a/esphome/components/veml7700/sensor.py +++ b/esphome/components/veml7700/sensor.py @@ -3,9 +3,11 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ACTUAL_GAIN, + CONF_ACTUAL_INTEGRATION_TIME, CONF_AMBIENT_LIGHT, CONF_AUTO_MODE, CONF_FULL_SPECTRUM, + CONF_FULL_SPECTRUM_COUNTS, CONF_GAIN, CONF_GLASS_ATTENUATION_FACTOR, CONF_ID, @@ -28,9 +30,7 @@ UNIT_COUNTS = "#" ICON_MULTIPLICATION = "mdi:multiplication" ICON_BRIGHTNESS_7 = "mdi:brightness-7" -CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time" CONF_AMBIENT_LIGHT_COUNTS = "ambient_light_counts" -CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts" CONF_LUX_COMPENSATION = "lux_compensation" veml7700_ns = cg.esphome_ns.namespace("veml7700") diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 031edbf27a..a4fb572208 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -43,6 +43,8 @@ CONF_VOLUME_MULTIPLIER = "volume_multiplier" CONF_WAKE_WORD = "wake_word" +CONF_CONVERSATION_TIMEOUT = "conversation_timeout" + CONF_ON_TIMER_STARTED = "on_timer_started" CONF_ON_TIMER_UPDATED = "on_timer_updated" CONF_ON_TIMER_CANCELLED = "on_timer_cancelled" @@ -100,6 +102,9 @@ CONFIG_SCHEMA = cv.All( cv.float_with_unit("decibel full scale", "(dBFS|dbfs|DBFS)"), cv.int_range(0, 31), ), + cv.Optional( + CONF_CONVERSATION_TIMEOUT, default="300s" + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_VOLUME_MULTIPLIER, default=1.0): cv.float_range( min=0.0, min_included=False ), @@ -182,6 +187,7 @@ async def to_code(config): cg.add(var.set_noise_suppression_level(config[CONF_NOISE_SUPPRESSION_LEVEL])) cg.add(var.set_auto_gain(config[CONF_AUTO_GAIN])) cg.add(var.set_volume_multiplier(config[CONF_VOLUME_MULTIPLIER])) + cg.add(var.set_conversation_timeout(config[CONF_CONVERSATION_TIMEOUT])) if CONF_ON_LISTENING in config: await automation.build_automation( diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index e4f388db68..a2210f188d 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -171,6 +171,11 @@ void VoiceAssistant::deallocate_buffers_() { #endif } +void VoiceAssistant::reset_conversation_id() { + this->conversation_id_ = ""; + ESP_LOGD(TAG, "reset conversation ID"); +} + int VoiceAssistant::read_microphone_() { size_t bytes_read = 0; if (this->mic_->is_running()) { // Read audio into input buffer @@ -299,7 +304,8 @@ void VoiceAssistant::loop() { break; } this->set_state_(State::STARTING_PIPELINE); - this->set_timeout("reset-conversation_id", 5 * 60 * 1000, [this]() { this->conversation_id_ = ""; }); + this->set_timeout("reset-conversation_id", this->conversation_timeout_, + [this]() { this->reset_conversation_id(); }); break; } case State::STARTING_PIPELINE: { @@ -390,6 +396,10 @@ void VoiceAssistant::loop() { this->set_timeout("playing", 2000, [this]() { this->cancel_timeout("speaker-timeout"); this->set_state_(State::IDLE, State::IDLE); + + api::VoiceAssistantAnnounceFinished msg; + msg.success = true; + this->api_client_->send_voice_assistant_announce_finished(msg); }); } break; @@ -745,7 +755,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { message = std::move(arg.value); } } - if (code == "wake-word-timeout" || code == "wake_word_detection_aborted") { + if (code == "wake-word-timeout" || code == "wake_word_detection_aborted" || code == "no_wake_word") { // Don't change state here since either the "tts-end" or "run-end" events will do it. return; } else if (code == "wake-provider-missing" || code == "wake-engine-missing") { @@ -860,6 +870,18 @@ void VoiceAssistant::timer_tick_() { this->timer_tick_trigger_->trigger(res); } +void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) { +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->tts_start_trigger_->trigger(msg.text); + this->media_player_->make_call().set_media_url(msg.media_id).set_announcement(true).perform(); + this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE); + this->tts_end_trigger_->trigger(msg.media_id); + this->end_trigger_->trigger(); + } +#endif +} + VoiceAssistant *global_voice_assistant = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace voice_assistant diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index a160972e22..b0a172332f 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -132,6 +132,7 @@ class VoiceAssistant : public Component { void on_event(const api::VoiceAssistantEventResponse &msg); void on_audio(const api::VoiceAssistantAudio &msg); void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg); + void on_announce(const api::VoiceAssistantAnnounceRequest &msg); bool is_running() const { return this->state_ != State::IDLE; } void set_continuous(bool continuous) { this->continuous_ = continuous; } @@ -147,6 +148,8 @@ class VoiceAssistant : public Component { } void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; } void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; } + void set_conversation_timeout(uint32_t conversation_timeout) { this->conversation_timeout_ = conversation_timeout; } + void reset_conversation_id(); Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } @@ -262,6 +265,7 @@ class VoiceAssistant : public Component { uint8_t noise_suppression_level_; uint8_t auto_gain_; float volume_multiplier_; + uint32_t conversation_timeout_; uint8_t *send_buffer_; int16_t *input_buffer_; diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index 080e1bbac8..d18cdf89c8 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -1,4 +1,5 @@ #include "wake_on_lan.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" @@ -85,3 +86,4 @@ void WakeOnLanButton::setup() { } // namespace wake_on_lan } // namespace esphome +#endif diff --git a/esphome/components/wake_on_lan/wake_on_lan.h b/esphome/components/wake_on_lan/wake_on_lan.h index 42cb3a9268..f516c4d669 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.h +++ b/esphome/components/wake_on_lan/wake_on_lan.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include "esphome/components/button/button.h" #include "esphome/core/component.h" #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) @@ -32,3 +33,4 @@ class WakeOnLanButton : public button::Button, public Component { } // namespace wake_on_lan } // namespace esphome +#endif diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 24df428e6f..7c1d436673 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -480,7 +480,7 @@ void HOT WaveshareEPaperTypeA::display() { this->start_data_(); switch (this->model_) { case TTGO_EPAPER_2_13_IN_B1: { // block needed because of variable initializations - int16_t wb = ((this->get_width_internal()) >> 3); + int16_t wb = ((this->get_width_controller()) >> 3); for (int i = 0; i < this->get_height_internal(); i++) { for (int j = 0; j < wb; j++) { int idx = j + (this->get_height_internal() - 1 - i) * wb; @@ -766,7 +766,7 @@ void WaveshareEPaper2P7InV2::initialize() { // XRAM_START_AND_END_POSITION this->command(0x44); this->data(0x00); - this->data(((get_width_internal() - 1) >> 3) & 0xFF); + this->data(((this->get_width_controller() - 1) >> 3) & 0xFF); // YRAM_START_AND_END_POSITION this->command(0x45); this->data(0x00); @@ -928,8 +928,8 @@ void HOT WaveshareEPaper2P7InB::display() { // TCON_RESOLUTION this->command(0x61); - this->data(this->get_width_internal() >> 8); - this->data(this->get_width_internal() & 0xff); // 176 + this->data(this->get_width_controller() >> 8); + this->data(this->get_width_controller() & 0xff); // 176 this->data(this->get_height_internal() >> 8); this->data(this->get_height_internal() & 0xff); // 264 @@ -994,7 +994,7 @@ void WaveshareEPaper2P7InBV2::initialize() { // self.SetWindows(0, 0, self.width-1, self.height-1) // SetWindows(self, Xstart, Ystart, Xend, Yend): - uint32_t xend = this->get_width_internal() - 1; + uint32_t xend = this->get_width_controller() - 1; uint32_t yend = this->get_height_internal() - 1; this->command(0x44); this->data(0x00); diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 232ab40d10..02074dcf11 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -1,35 +1,36 @@ from __future__ import annotations import gzip + import esphome.codegen as cg -import esphome.config_validation as cv -import esphome.final_validate as fv from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +import esphome.config_validation as cv from esphome.const import ( + CONF_AUTH, CONF_CSS_INCLUDE, CONF_CSS_URL, + CONF_ENABLE_PRIVATE_NETWORK_ACCESS, CONF_ID, + CONF_INCLUDE_INTERNAL, CONF_JS_INCLUDE, CONF_JS_URL, - CONF_ENABLE_PRIVATE_NETWORK_ACCESS, - CONF_PORT, - CONF_AUTH, - CONF_USERNAME, - CONF_PASSWORD, - CONF_INCLUDE_INTERNAL, - CONF_OTA, - CONF_LOG, - CONF_VERSION, CONF_LOCAL, + CONF_LOG, + CONF_OTA, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERSION, CONF_WEB_SERVER_ID, CONF_WEB_SERVER_SORTING_WEIGHT, + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_BK72XX, PLATFORM_RTL87XX, ) from esphome.core import CORE, coroutine_with_priority +import esphome.final_validate as fv AUTO_LOAD = ["json", "web_server_base"] @@ -208,7 +209,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) - cg.add_define("USE_WEBSERVER") version = config[CONF_VERSION] cg.add(paren.set_port(config[CONF_PORT])) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 332f358352..a02f84c34b 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -1,4 +1,5 @@ #include "list_entities.h" +#ifdef USE_WEBSERVER #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -188,3 +189,4 @@ bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { } // namespace web_server } // namespace esphome +#endif diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 5ff6ec0412..53e5bc3355 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -1,8 +1,9 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_WEBSERVER #include "esphome/core/component.h" #include "esphome/core/component_iterator.h" -#include "esphome/core/defines.h" namespace esphome { namespace web_server { @@ -78,3 +79,4 @@ class ListEntitiesIterator : public ComponentIterator { } // namespace web_server } // namespace esphome +#endif diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6fb04f558a..1bb7c6c249 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,5 +1,5 @@ #include "web_server.h" - +#ifdef USE_WEBSERVER #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" @@ -1659,3 +1659,4 @@ void WebServer::schedule_(std::function &&f) { } // namespace web_server } // namespace esphome +#endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 5b98806af1..3195fa7109 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -3,6 +3,7 @@ #include "list_entities.h" #include "esphome/components/web_server_base/web_server_base.h" +#ifdef USE_WEBSERVER #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/core/entity_base.h" @@ -334,7 +335,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Override the web handler's handleRequest method. void handleRequest(AsyncWebServerRequest *request) override; /// This web handle is not trivial. - bool isRequestHandlerTrivial() override; + bool isRequestHandlerTrivial() override; // NOLINT(readability-identifier-naming) void add_entity_to_sorting_list(EntityBase *entity, float weight); @@ -366,3 +367,4 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { } // namespace web_server } // namespace esphome +#endif diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index f90c7e56a3..7c09022f27 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -1,4 +1,5 @@ #include "web_server_base.h" +#ifdef USE_NETWORK #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" @@ -121,3 +122,4 @@ float WebServerBase::get_setup_priority() const { } // namespace web_server_base } // namespace esphome +#endif diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index c312126472..f876d163bc 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_NETWORK #include #include #include @@ -134,6 +135,7 @@ class OTARequestHandler : public AsyncWebHandler { return request->url() == "/update" && request->method() == HTTP_POST; } + // NOLINTNEXTLINE(readability-identifier-naming) bool isRequestHandlerTrivial() override { return false; } protected: @@ -144,3 +146,4 @@ class OTARequestHandler : public AsyncWebHandler { } // namespace web_server_base } // namespace esphome +#endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8c40f87879..583a27466a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,4 +1,5 @@ #include "wifi_component.h" +#ifdef USE_WIFI #include #include @@ -856,3 +857,4 @@ WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-con } // namespace wifi } // namespace esphome +#endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d79cde0b18..dde0d1d5a5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -1,9 +1,10 @@ #pragma once +#include "esphome/core/defines.h" +#ifdef USE_WIFI #include "esphome/components/network/ip_address.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include @@ -442,3 +443,4 @@ template class WiFiDisableAction : public Action { } // namespace wifi } // namespace esphome +#endif diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 71548b7a3e..b8724838c8 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_ESP32_FRAMEWORK_ARDUINO #include @@ -802,3 +803,4 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddr } // namespace esphome #endif // USE_ESP32_FRAMEWORK_ARDUINO +#endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 997457e2d2..92f80c1e52 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -1,6 +1,7 @@ #include "wifi_component.h" #include "esphome/core/defines.h" +#ifdef USE_WIFI #ifdef USE_ESP8266 #include @@ -834,3 +835,4 @@ void WiFiComponent::wifi_loop_() {} } // namespace esphome #endif +#endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index a8d67ed44d..6008acb95d 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_ESP_IDF #include @@ -1010,3 +1011,4 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { } // namespace esphome #endif // USE_ESP_IDF +#endif diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index f6b0fb2699..afb30c3bcf 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_LIBRETINY #include @@ -84,7 +85,16 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { if (!this->has_sta()) return {}; - return {WiFi.localIP()}; + network::IPAddresses addresses; + addresses[0] = WiFi.localIP(); +#if USE_NETWORK_IPV6 + int i = 1; + auto v6_addresses = WiFi.allLocalIPv6(); + for (auto address : v6_addresses) { + addresses[i++] = network::IPAddress(address.toString().c_str()); + } +#endif /* USE_NETWORK_IPV6 */ + return addresses; } bool WiFiComponent::wifi_apply_hostname_() { @@ -320,6 +330,11 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ s_sta_connecting = false; break; } + case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { + // auto it = info.got_ip.ip_info; + ESP_LOGV(TAG, "Event: Got IPv6"); + break; + } case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: { ESP_LOGV(TAG, "Event: Lost IP"); break; @@ -468,3 +483,4 @@ void WiFiComponent::wifi_loop_() {} } // namespace esphome #endif // USE_LIBRETINY +#endif diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 4afcf2d78b..bac986d899 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -1,6 +1,7 @@ #include "wifi_component.h" +#ifdef USE_WIFI #ifdef USE_RP2040 #include "lwip/dns.h" @@ -218,3 +219,4 @@ void WiFiComponent::wifi_pre_setup_() {} } // namespace esphome #endif +#endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index eeb4985398..150c7229f8 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -1,4 +1,5 @@ #include "wifi_info_text_sensor.h" +#ifdef USE_WIFI #include "esphome/core/log.h" namespace esphome { @@ -15,3 +16,4 @@ void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo DNS Addre } // namespace wifi_info } // namespace esphome +#endif diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 0f31a57cc5..0aa44a0894 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/wifi/wifi_component.h" +#ifdef USE_WIFI #include namespace esphome { @@ -131,3 +132,4 @@ class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { } // namespace wifi_info } // namespace esphome +#endif diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.cpp b/esphome/components/wifi_signal/wifi_signal_sensor.cpp index ba22138e2a..4347295421 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.cpp +++ b/esphome/components/wifi_signal/wifi_signal_sensor.cpp @@ -1,4 +1,5 @@ #include "wifi_signal_sensor.h" +#ifdef USE_WIFI #include "esphome/core/log.h" namespace esphome { @@ -10,3 +11,4 @@ void WiFiSignalSensor::dump_config() { LOG_SENSOR("", "WiFi Signal", this); } } // namespace wifi_signal } // namespace esphome +#endif diff --git a/esphome/components/wifi_signal/wifi_signal_sensor.h b/esphome/components/wifi_signal/wifi_signal_sensor.h index f797aaa590..fbe03a6404 100644 --- a/esphome/components/wifi_signal/wifi_signal_sensor.h +++ b/esphome/components/wifi_signal/wifi_signal_sensor.h @@ -4,7 +4,7 @@ #include "esphome/core/helpers.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/wifi/wifi_component.h" - +#ifdef USE_WIFI namespace esphome { namespace wifi_signal { @@ -19,3 +19,4 @@ class WiFiSignalSensor : public sensor::Sensor, public PollingComponent { } // namespace wifi_signal } // namespace esphome +#endif diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 16d0d0226e..5e34a8a19b 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -1,19 +1,20 @@ -import re import ipaddress +import re + +from esphome import automation import esphome.codegen as cg +from esphome.components import time +from esphome.components.esp32 import CORE, add_idf_sdkconfig_option import esphome.config_validation as cv from esphome.const import ( - CONF_ID, - CONF_TIME_ID, CONF_ADDRESS, + CONF_ID, CONF_REBOOT_TIMEOUT, + CONF_TIME_ID, KEY_CORE, KEY_FRAMEWORK_VERSION, ) -from esphome.components.esp32 import CORE, add_idf_sdkconfig_option -from esphome.components import time from esphome.core import TimePeriod -from esphome import automation CONF_NETMASK = "netmask" CONF_PRIVATE_KEY = "private_key" @@ -91,6 +92,8 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + cg.add_define("USE_WIREGUARD") + cg.add(var.set_address(str(config[CONF_ADDRESS]))) cg.add(var.set_netmask(str(config[CONF_NETMASK]))) cg.add(var.set_private_key(config[CONF_PRIVATE_KEY])) diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 17ebc701e3..7b4011cb79 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -1,5 +1,5 @@ #include "wireguard.h" - +#ifdef USE_WIREGUARD #include #include #include @@ -289,3 +289,4 @@ std::string mask_key(const std::string &key) { return (key.substr(0, 5) + "[...] } // namespace wireguard } // namespace esphome +#endif diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index a0e9e27a1b..5db9a48c90 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -1,5 +1,6 @@ #pragma once - +#include "esphome/core/defines.h" +#ifdef USE_WIREGUARD #include #include #include @@ -170,3 +171,4 @@ template class WireguardDisableAction : public Action, pu } // namespace wireguard } // namespace esphome +#endif diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 95faea0446..85434341cc 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -49,8 +49,8 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ const uint16_t conductivity = encode_uint16(data[1], data[0]); result.conductivity = conductivity; } - // battery, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x100A) && (value_length == 1)) { + // battery / MiaoMiaoce battery, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x100A || value_type == 0x4803) && (value_length == 1)) { result.battery_level = data[0]; } // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % @@ -80,6 +80,17 @@ bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_ result.has_motion = !idle_time; } else if ((value_type == 0x1018) && (value_length == 1)) { result.is_light = data[0]; + } + // MiaoMiaoce temperature, 4 bytes, float, 0.1 °C + else if ((value_type == 0x4C01) && (value_length == 4)) { + const uint32_t int_number = encode_uint32(data[3], data[2], data[1], data[0]); + float temperature; + std::memcpy(&temperature, &int_number, sizeof(temperature)); + result.temperature = temperature; + } + // MiaoMiaoce humidity, 1 byte, 8-bit unsigned integer, 1 % + else if ((value_type == 0x4C02) && (value_length == 1)) { + result.humidity = data[0]; } else { return false; } @@ -111,7 +122,8 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult } while (payload_length > 3) { - if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00) { + if (payload[payload_offset + 1] != 0x10 && payload[payload_offset + 1] != 0x00 && + payload[payload_offset + 1] != 0x4C && payload[payload_offset + 1] != 0x48) { ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); break; } @@ -190,6 +202,11 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service } else if (device_uuid == 0x045b) { // rectangular body, e-ink display result.type = XiaomiParseResult::TYPE_LYWSD02; result.name = "LYWSD02"; + } else if (device_uuid == 0x2542) { // rectangular body, e-ink display — with bindkeys + result.type = XiaomiParseResult::TYPE_LYWSD02MMC; + result.name = "LYWSD02MMC"; + if (raw.size() == 19) + result.raw_offset -= 6; } else if (device_uuid == 0x040a) { // Mosquito Repellent Smart Version result.type = XiaomiParseResult::TYPE_WX08ZM; result.name = "WX08ZM"; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index c1086605d1..6978be97f4 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -17,6 +17,7 @@ struct XiaomiParseResult { TYPE_HHCCPOT002, TYPE_LYWSDCGQ, TYPE_LYWSD02, + TYPE_LYWSD02MMC, TYPE_CGG1, TYPE_LYWSD03MMC, TYPE_CGD1, diff --git a/esphome/components/xiaomi_lywsd02mmc/__init__.py b/esphome/components/xiaomi_lywsd02mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsd02mmc/sensor.py b/esphome/components/xiaomi_lywsd02mmc/sensor.py new file mode 100644 index 0000000000..43784ef698 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/sensor.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_HUMIDITY, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_BATTERY, + CONF_ID, + CONF_BINDKEY, +) + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@juanluss31"] +DEPENDENCIES = ["esp32_ble_tracker"] + +xiaomi_lywsd02mmc_ns = cg.esphome_ns.namespace("xiaomi_lywsd02mmc") +XiaomiLYWSD02MMC = xiaomi_lywsd02mmc_ns.class_( + "XiaomiLYWSD02MMC", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiLYWSD02MMC), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_BINDKEY): cv.bind_key, + 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, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature(sens)) + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity(sens)) + if battery_level_config := config.get(CONF_BATTERY_LEVEL): + sens = await sensor.new_sensor(battery_level_config) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp new file mode 100644 index 0000000000..cc122f2264 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.cpp @@ -0,0 +1,73 @@ +#include "xiaomi_lywsd02mmc.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02mmc { + +static const char *const TAG = "xiaomi_lywsd02mmc"; + +void XiaomiLYWSD02MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiLYWSD02MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + return success; +} + +void XiaomiLYWSD02MMC::set_bindkey(const std::string &bindkey) { + memset(this->bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + this->bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_lywsd02mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h new file mode 100644 index 0000000000..19092aa2a9 --- /dev/null +++ b/esphome/components/xiaomi_lywsd02mmc/xiaomi_lywsd02mmc.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_lywsd02mmc { + +class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { this->address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_lywsd02mmc +} // namespace esphome + +#endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1cd1d6aa31..e55879e37e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -91,7 +91,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=consider-using-f-string VARIABLE_PROG = re.compile( - "\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS) + f"\\$([{VALID_SUBSTITUTIONS_CHARACTERS}]+|\\{{[{VALID_SUBSTITUTIONS_CHARACTERS}]*\\}})" ) # pylint: disable=invalid-name @@ -370,6 +370,20 @@ def boolean(value): ) +def boolean_false(value): + """Validate the given config option to be a boolean, set to False. + + This option allows a bunch of different ways of expressing boolean values: + - instance of boolean + - 'true'/'false' + - 'yes'/'no' + - 'enable'/disable + """ + if boolean(value): + raise Invalid("Expected boolean value to be false") + return False + + @schema_extractor_list def ensure_list(*validators): """Validate this configuration option to be a list. @@ -464,6 +478,7 @@ zero_to_one_float = float_range(min=0, max=1) negative_one_to_one_float = float_range(min=-1, max=1) positive_int = int_range(min=0) positive_not_null_int = int_range(min=0, min_included=False) +positive_not_null_float = float_range(min=0, min_included=False) def validate_id_name(value): @@ -2030,6 +2045,7 @@ def require_framework_version( esp32_arduino=None, esp8266_arduino=None, rp2040_arduino=None, + bk72xx_libretiny=None, host=None, max_version=False, extra_message=None, @@ -2044,6 +2060,13 @@ def require_framework_version( msg += f". {extra_message}" raise Invalid(msg) required = esp_idf + elif CORE.is_bk72xx and framework == "arduino": + if bk72xx_libretiny is None: + msg = "This feature is incompatible with BK72XX" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) + required = bk72xx_libretiny elif CORE.is_esp32 and framework == "arduino": if esp32_arduino is None: msg = "This feature is incompatible with ESP32 using arduino framework" @@ -2181,3 +2204,13 @@ SOURCE_SCHEMA = Any( } ), ) + + +def rename_key(old_key, new_key): + def validator(config: dict) -> dict: + config = config.copy() + if old_key in config: + config[new_key] = config.pop(old_key) + return config + + return validator diff --git a/esphome/const.py b/esphome/const.py index 37844e1047..6e7bbdec98 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,28 +1,28 @@ """Constants used by esphome.""" -__version__ = "2024.8.0-dev" +__version__ = "2024.10.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" ) +PLATFORM_BK72XX = "bk72xx" PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" -PLATFORM_RP2040 = "rp2040" PLATFORM_HOST = "host" -PLATFORM_BK72XX = "bk72xx" -PLATFORM_RTL87XX = "rtl87xx" PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" +PLATFORM_RP2040 = "rp2040" +PLATFORM_RTL87XX = "rtl87xx" TARGET_PLATFORMS = [ + PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, - PLATFORM_RP2040, PLATFORM_HOST, - PLATFORM_BK72XX, - PLATFORM_RTL87XX, PLATFORM_LIBRETINY_OLDSTYLE, + PLATFORM_RP2040, + PLATFORM_RTL87XX, ] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} @@ -37,11 +37,14 @@ CONF_ACCELERATION_Y = "acceleration_y" CONF_ACCELERATION_Z = "acceleration_z" CONF_ACCURACY = "accuracy" CONF_ACCURACY_DECIMALS = "accuracy_decimals" +CONF_ACTION = "action" CONF_ACTION_ID = "action_id" CONF_ACTION_STATE_TOPIC = "action_state_topic" +CONF_ACTIONS = "actions" CONF_ACTIVE = "active" CONF_ACTIVE_POWER = "active_power" CONF_ACTUAL_GAIN = "actual_gain" +CONF_ACTUAL_INTEGRATION_TIME = "actual_integration_time" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ADVANCED = "advanced" @@ -72,6 +75,7 @@ CONF_AWAY = "away" CONF_AWAY_COMMAND_TOPIC = "away_command_topic" CONF_AWAY_CONFIG = "away_config" CONF_AWAY_STATE_TOPIC = "away_state_topic" +CONF_BACKGROUND_COLOR = "background_color" CONF_BACKLIGHT_PIN = "backlight_pin" CONF_BASELINE = "baseline" CONF_BATTERY_LEVEL = "battery_level" @@ -92,6 +96,7 @@ CONF_BOARD_FLASH_MODE = "board_flash_mode" CONF_BORDER = "border" CONF_BRANCH = "branch" CONF_BRIGHTNESS = "brightness" +CONF_BRIGHTNESS_LIMITS = "brightness_limits" CONF_BROKER = "broker" CONF_BSSID = "bssid" CONF_BUFFER_SIZE = "buffer_size" @@ -305,8 +310,10 @@ CONF_FLASH_LENGTH = "flash_length" CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" +CONF_FONT = "font" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" +CONF_FOREGROUND_COLOR = "foreground_color" CONF_FORMALDEHYDE = "formaldehyde" CONF_FORMAT = "format" CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" @@ -317,6 +324,7 @@ CONF_FREQUENCY = "frequency" CONF_FRIENDLY_NAME = "friendly_name" CONF_FROM = "from" CONF_FULL_SPECTRUM = "full_spectrum" +CONF_FULL_SPECTRUM_COUNTS = "full_spectrum_counts" CONF_FULL_UPDATE_EVERY = "full_update_every" CONF_GAIN = "gain" CONF_GAMMA_CORRECT = "gamma_correct" @@ -403,6 +411,7 @@ CONF_INVERTED = "inverted" CONF_IP_ADDRESS = "ip_address" CONF_IRQ_PIN = "irq_pin" CONF_IS_RGBW = "is_rgbw" +CONF_ITEMS = "items" CONF_JS_INCLUDE = "js_include" CONF_JS_URL = "js_url" CONF_JVC = "jvc" @@ -423,6 +432,8 @@ CONF_LIGHT = "light" CONF_LIGHT_ID = "light_id" CONF_LIGHTNING_ENERGY = "lightning_energy" CONF_LIGHTNING_THRESHOLD = "lightning_threshold" +CONF_LIMIT_MODE = "limit_mode" +CONF_LINE_FREQUENCY = "line_frequency" CONF_LINE_THICKNESS = "line_thickness" CONF_LINE_TYPE = "line_type" CONF_LOADED_INTEGRATIONS = "loaded_integrations" @@ -501,6 +512,7 @@ CONF_MOTION = "motion" CONF_MOVEMENT_COUNTER = "movement_counter" CONF_MQTT = "mqtt" CONF_MQTT_ID = "mqtt_id" +CONF_MULTIPLE = "multiple" CONF_MULTIPLEXER = "multiplexer" CONF_MULTIPLY = "multiply" CONF_NAME = "name" @@ -720,6 +732,7 @@ CONF_RW_PIN = "rw_pin" CONF_RX_BUFFER_SIZE = "rx_buffer_size" CONF_RX_ONLY = "rx_only" CONF_RX_PIN = "rx_pin" +CONF_RX_QUEUE_LEN = "rx_queue_len" CONF_SAFE_MODE = "safe_mode" CONF_SAMPLE_RATE = "sample_rate" CONF_SAMSUNG = "samsung" @@ -836,8 +849,10 @@ CONF_TEMPERATURE = "temperature" CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_TEMPERATURE_SOURCE = "temperature_source" CONF_TEMPERATURE_STEP = "temperature_step" +CONF_TEXT = "text" CONF_TEXT_SENSORS = "text_sensors" CONF_THEN = "then" +CONF_THERMOCOUPLE_TYPE = "thermocouple_type" CONF_THRESHOLD = "threshold" CONF_THROTTLE = "throttle" CONF_TILT = "tilt" @@ -870,6 +885,7 @@ CONF_TVOC = "tvoc" CONF_TX_BUFFER_SIZE = "tx_buffer_size" CONF_TX_PIN = "tx_pin" CONF_TX_POWER = "tx_power" +CONF_TX_QUEUE_LEN = "tx_queue_len" CONF_TYPE = "type" CONF_TYPE_ID = "type_id" CONF_UART_ID = "uart_id" @@ -1026,10 +1042,13 @@ UNIT_KELVIN = "K" UNIT_KILOGRAM = "kg" UNIT_KILOMETER = "km" UNIT_KILOMETER_PER_HOUR = "km/h" -UNIT_KILOVOLT_AMPS_REACTIVE = "kVAr" -UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVArh" +UNIT_KILOVOLT_AMPS = "kVA" +UNIT_KILOVOLT_AMPS_HOURS = "kVAh" +UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" UNIT_KILOWATT = "kW" UNIT_KILOWATT_HOURS = "kWh" +UNIT_LITRE = "L" UNIT_LUX = "lx" UNIT_METER = "m" UNIT_METER_PER_SECOND_SQUARED = "m/s²" @@ -1058,6 +1077,7 @@ UNIT_SECOND = "s" UNIT_STEPS = "steps" UNIT_VOLT = "V" UNIT_VOLT_AMPS = "VA" +UNIT_VOLT_AMPS_HOURS = "VAh" UNIT_VOLT_AMPS_REACTIVE = "VAR" UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh" UNIT_WATT = "W" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 9d3d14492e..a97c3b18c9 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -336,7 +336,7 @@ class ID: else: self.is_manual = is_manual self.is_declaration = is_declaration - self.type: Optional["MockObjClass"] = type + self.type: Optional[MockObjClass] = type def resolve(self, registered_ids): from esphome.config_validation import RESERVED_IDS @@ -500,7 +500,7 @@ class EsphomeCore: # The relative path to where all build files are stored self.build_path: Optional[str] = None # The validated configuration, this is None until the config has been validated - self.config: Optional["ConfigType"] = None + self.config: Optional[ConfigType] = None # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -508,17 +508,17 @@ class EsphomeCore: # Task counter for pending tasks self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj - self.variables: dict[str, "MockObj"] = {} + self.variables: dict[str, MockObj] = {} # A list of statements that go in the main setup() block - self.main_statements: list["Statement"] = [] + self.main_statements: list[Statement] = [] # A list of statements to insert in the global block (includes and global variables) - self.global_statements: list["Statement"] = [] + self.global_statements: list[Statement] = [] # A set of platformio libraries to add to the project self.libraries: list[Library] = [] # A set of build flags to set in the platformio project self.build_flags: set[str] = set() # A set of defines to set for the compile process in esphome/core/defines.h - self.defines: set["Define"] = set() + self.defines: set[Define] = set() # A map of all platformio options to apply self.platformio_options: dict[str, Union[str, list[str]]] = {} # A set of strings of names of loaded integrations, used to find namespace ID conflicts diff --git a/esphome/core/application.h b/esphome/core/application.h index 2697357456..462beb1f25 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -246,162 +246,180 @@ class Application { #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->binary_sensors_) + for (auto *obj : this->binary_sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_SWITCH const std::vector &get_switches() { return this->switches_; } switch_::Switch *get_switch_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->switches_) + for (auto *obj : this->switches_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_BUTTON const std::vector &get_buttons() { return this->buttons_; } button::Button *get_button_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->buttons_) + for (auto *obj : this->buttons_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_SENSOR const std::vector &get_sensors() { return this->sensors_; } sensor::Sensor *get_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->sensors_) + for (auto *obj : this->sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_TEXT_SENSOR const std::vector &get_text_sensors() { return this->text_sensors_; } text_sensor::TextSensor *get_text_sensor_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->text_sensors_) + for (auto *obj : this->text_sensors_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_FAN const std::vector &get_fans() { return this->fans_; } fan::Fan *get_fan_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->fans_) + for (auto *obj : this->fans_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_COVER const std::vector &get_covers() { return this->covers_; } cover::Cover *get_cover_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->covers_) + for (auto *obj : this->covers_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_LIGHT const std::vector &get_lights() { return this->lights_; } light::LightState *get_light_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->lights_) + for (auto *obj : this->lights_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_CLIMATE const std::vector &get_climates() { return this->climates_; } climate::Climate *get_climate_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->climates_) + for (auto *obj : this->climates_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_NUMBER const std::vector &get_numbers() { return this->numbers_; } number::Number *get_number_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->numbers_) + for (auto *obj : this->numbers_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_DATETIME_DATE const std::vector &get_dates() { return this->dates_; } datetime::DateEntity *get_date_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->dates_) + for (auto *obj : this->dates_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_DATETIME_TIME const std::vector &get_times() { return this->times_; } datetime::TimeEntity *get_time_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->times_) + for (auto *obj : this->times_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_DATETIME_DATETIME const std::vector &get_datetimes() { return this->datetimes_; } datetime::DateTimeEntity *get_datetime_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->datetimes_) + for (auto *obj : this->datetimes_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_TEXT const std::vector &get_texts() { return this->texts_; } text::Text *get_text_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->texts_) + for (auto *obj : this->texts_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_SELECT const std::vector &get_selects() { return this->selects_; } select::Select *get_select_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->selects_) + for (auto *obj : this->selects_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_LOCK const std::vector &get_locks() { return this->locks_; } lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->locks_) + for (auto *obj : this->locks_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_VALVE const std::vector &get_valves() { return this->valves_; } valve::Valve *get_valve_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->valves_) + for (auto *obj : this->valves_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif #ifdef USE_MEDIA_PLAYER const std::vector &get_media_players() { return this->media_players_; } media_player::MediaPlayer *get_media_player_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->media_players_) + for (auto *obj : this->media_players_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif @@ -411,9 +429,10 @@ class Application { return this->alarm_control_panels_; } alarm_control_panel::AlarmControlPanel *get_alarm_control_panel_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->alarm_control_panels_) + for (auto *obj : this->alarm_control_panels_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif @@ -421,9 +440,10 @@ class Application { #ifdef USE_EVENT const std::vector &get_events() { return this->events_; } event::Event *get_event_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->events_) + for (auto *obj : this->events_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif @@ -431,9 +451,10 @@ class Application { #ifdef USE_UPDATE const std::vector &get_updates() { return this->updates_; } update::UpdateEntity *get_update_by_key(uint32_t key, bool include_internal = false) { - for (auto *obj : this->updates_) + for (auto *obj : this->updates_) { if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) return obj; + } return nullptr; } #endif diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 5a0a17ea1a..e77e453431 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -82,7 +82,7 @@ template class Condition { } protected: - template bool check_tuple_(const std::tuple &tuple, seq) { + template bool check_tuple_(const std::tuple &tuple, seq /*unused*/) { return this->check(std::get(tuple)...); } }; @@ -156,7 +156,7 @@ template class Action { } } } - template void play_next_tuple_(const std::tuple &tuple, seq) { + template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { this->play_next_(std::get(tuple)...); } void play_next_tuple_(const std::tuple &tuple) { @@ -223,7 +223,9 @@ template class ActionList { } protected: - template void play_tuple_(const std::tuple &tuple, seq) { this->play(std::get(tuple)...); } + template void play_tuple_(const std::tuple &tuple, seq /*unused*/) { + this->play(std::get(tuple)...); + } Action *actions_begin_{nullptr}; Action *actions_end_{nullptr}; diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 1bf0efb9a4..dcf7da2f21 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -278,10 +278,11 @@ template class RepeatAction : public Action { this->then_.add_actions(actions); this->then_.add_action(new LambdaAction([this](uint32_t iteration, Ts... x) { iteration++; - if (iteration >= this->count_.value(x...)) + if (iteration >= this->count_.value(x...)) { this->play_next_tuple_(this->var_); - else + } else { this->then_.play(iteration, x...); + } })); } diff --git a/esphome/core/bytebuffer.cpp b/esphome/core/bytebuffer.cpp new file mode 100644 index 0000000000..9dd32bf87a --- /dev/null +++ b/esphome/core/bytebuffer.cpp @@ -0,0 +1,167 @@ +#include "bytebuffer.h" +#include +#include "esphome/core/helpers.h" + +#include +#include + +namespace esphome { + +ByteBuffer ByteBuffer::wrap(const uint8_t *ptr, size_t len, Endian endianness) { + // there is a double copy happening here, could be optimized but at cost of clarity. + std::vector data(ptr, ptr + len); + ByteBuffer buffer = {data}; + buffer.endianness_ = endianness; + return buffer; +} + +ByteBuffer ByteBuffer::wrap(std::vector const &data, Endian endianness) { + ByteBuffer buffer = {data}; + buffer.endianness_ = endianness; + return buffer; +} + +ByteBuffer ByteBuffer::wrap(uint8_t value) { + ByteBuffer buffer = ByteBuffer(1); + buffer.put_uint8(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(uint16_t value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(2, endianness); + buffer.put_uint16(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(uint32_t value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(4, endianness); + buffer.put_uint32(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(uint64_t value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(8, endianness); + buffer.put_uint64(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(float value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(sizeof(float), endianness); + buffer.put_float(value); + buffer.flip(); + return buffer; +} + +ByteBuffer ByteBuffer::wrap(double value, Endian endianness) { + ByteBuffer buffer = ByteBuffer(sizeof(double), endianness); + buffer.put_double(value); + buffer.flip(); + return buffer; +} + +void ByteBuffer::set_limit(size_t limit) { + assert(limit <= this->get_capacity()); + this->limit_ = limit; +} +void ByteBuffer::set_position(size_t position) { + assert(position <= this->get_limit()); + this->position_ = position; +} +void ByteBuffer::clear() { + this->limit_ = this->get_capacity(); + this->position_ = 0; +} +void ByteBuffer::flip() { + this->limit_ = this->position_; + this->position_ = 0; +} + +/// Getters +uint8_t ByteBuffer::get_uint8() { + assert(this->get_remaining() >= 1); + return this->data_[this->position_++]; +} +uint64_t ByteBuffer::get_uint(size_t length) { + assert(this->get_remaining() >= length); + uint64_t value = 0; + if (this->endianness_ == LITTLE) { + this->position_ += length; + auto index = this->position_; + while (length-- != 0) { + value <<= 8; + value |= this->data_[--index]; + } + } else { + while (length-- != 0) { + value <<= 8; + value |= this->data_[this->position_++]; + } + } + return value; +} + +uint32_t ByteBuffer::get_int24() { + auto value = this->get_uint24(); + uint32_t mask = (~static_cast(0)) << 23; + if ((value & mask) != 0) + value |= mask; + return value; +} +float ByteBuffer::get_float() { + assert(this->get_remaining() >= sizeof(float)); + return bit_cast(this->get_uint32()); +} +double ByteBuffer::get_double() { + assert(this->get_remaining() >= sizeof(double)); + return bit_cast(this->get_uint64()); +} + +std::vector ByteBuffer::get_vector(size_t length) { + assert(this->get_remaining() >= length); + auto start = this->data_.begin() + this->position_; + this->position_ += length; + return {start, start + length}; +} + +/// Putters +void ByteBuffer::put_uint8(uint8_t value) { + assert(this->get_remaining() >= 1); + this->data_[this->position_++] = value; +} + +void ByteBuffer::put_uint(uint64_t value, size_t length) { + assert(this->get_remaining() >= length); + if (this->endianness_ == LITTLE) { + while (length-- != 0) { + this->data_[this->position_++] = static_cast(value); + value >>= 8; + } + } else { + this->position_ += length; + auto index = this->position_; + while (length-- != 0) { + this->data_[--index] = static_cast(value); + value >>= 8; + } + } +} +void ByteBuffer::put_float(float value) { + static_assert(sizeof(float) == sizeof(uint32_t), "Float sizes other than 32 bit not supported"); + assert(this->get_remaining() >= sizeof(float)); + this->put_uint32(bit_cast(value)); +} +void ByteBuffer::put_double(double value) { + static_assert(sizeof(double) == sizeof(uint64_t), "Double sizes other than 64 bit not supported"); + assert(this->get_remaining() >= sizeof(double)); + this->put_uint64(bit_cast(value)); +} +void ByteBuffer::put_vector(const std::vector &value) { + assert(this->get_remaining() >= value.size()); + std::copy(value.begin(), value.end(), this->data_.begin() + this->position_); + this->position_ += value.size(); +} +} // namespace esphome diff --git a/esphome/core/bytebuffer.h b/esphome/core/bytebuffer.h new file mode 100644 index 0000000000..d44d01f275 --- /dev/null +++ b/esphome/core/bytebuffer.h @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include + +namespace esphome { + +enum Endian { LITTLE, BIG }; + +/** + * A class modelled on the Java ByteBuffer class. It wraps a vector of bytes and permits putting and getting + * items of various sizes, with an automatically incremented position. + * + * There are three variables maintained pointing into the buffer: + * + * capacity: the maximum amount of data that can be stored - set on construction and cannot be changed + * limit: the limit of the data currently available to get or put + * position: the current insert or extract position + * + * 0 <= position <= limit <= capacity + * + * In addition a mark can be set to the current position with mark(). A subsequent call to reset() will restore + * the position to the mark. + * + * The buffer can be marked to be little-endian (default) or big-endian. All subsequent operations will use that order. + * + * The flip() operation will reset the position to 0 and limit to the current position. This is useful for reading + * data from a buffer after it has been written. + * + */ +class ByteBuffer { + public: + // Default constructor (compatibility with TEMPLATABLE_VALUE) + ByteBuffer() : ByteBuffer(std::vector()) {} + /** + * Create a new Bytebuffer with the given capacity + */ + ByteBuffer(size_t capacity, Endian endianness = LITTLE) + : data_(std::vector(capacity)), endianness_(endianness), limit_(capacity){}; + /** + * Wrap an existing vector in a ByteBufffer + */ + static ByteBuffer wrap(std::vector const &data, Endian endianness = LITTLE); + /** + * Wrap an existing array in a ByteBuffer. Note that this will create a copy of the data. + */ + static ByteBuffer wrap(const uint8_t *ptr, size_t len, Endian endianness = LITTLE); + // Convenience functions to create a ByteBuffer from a value + static ByteBuffer wrap(uint8_t value); + static ByteBuffer wrap(uint16_t value, Endian endianness = LITTLE); + static ByteBuffer wrap(uint32_t value, Endian endianness = LITTLE); + static ByteBuffer wrap(uint64_t value, Endian endianness = LITTLE); + static ByteBuffer wrap(int8_t value) { return wrap(static_cast(value)); } + static ByteBuffer wrap(int16_t value, Endian endianness = LITTLE) { + return wrap(static_cast(value), endianness); + } + static ByteBuffer wrap(int32_t value, Endian endianness = LITTLE) { + return wrap(static_cast(value), endianness); + } + static ByteBuffer wrap(int64_t value, Endian endianness = LITTLE) { + return wrap(static_cast(value), endianness); + } + static ByteBuffer wrap(float value, Endian endianness = LITTLE); + static ByteBuffer wrap(double value, Endian endianness = LITTLE); + static ByteBuffer wrap(bool value) { return wrap(static_cast(value)); } + + // Get an integral value from the buffer, increment position by length + uint64_t get_uint(size_t length); + // Get one byte from the buffer, increment position by 1 + uint8_t get_uint8(); + // Get a 16 bit unsigned value, increment by 2 + uint16_t get_uint16() { return static_cast(this->get_uint(sizeof(uint16_t))); }; + // Get a 24 bit unsigned value, increment by 3 + uint32_t get_uint24() { return static_cast(this->get_uint(3)); }; + // Get a 32 bit unsigned value, increment by 4 + uint32_t get_uint32() { return static_cast(this->get_uint(sizeof(uint32_t))); }; + // Get a 64 bit unsigned value, increment by 8 + uint64_t get_uint64() { return this->get_uint(sizeof(uint64_t)); }; + // Signed versions of the get functions + uint8_t get_int8() { return static_cast(this->get_uint8()); }; + int16_t get_int16() { return static_cast(this->get_uint(sizeof(int16_t))); } + uint32_t get_int24(); + int32_t get_int32() { return static_cast(this->get_uint(sizeof(int32_t))); } + int64_t get_int64() { return static_cast(this->get_uint(sizeof(int64_t))); } + // Get a float value, increment by 4 + float get_float(); + // Get a double value, increment by 8 + double get_double(); + // Get a bool value, increment by 1 + bool get_bool() { return this->get_uint8(); } + // Get vector of bytes, increment by length + std::vector get_vector(size_t length); + + // Put values into the buffer, increment the position accordingly + // put any integral value, length represents the number of bytes + void put_uint(uint64_t value, size_t length); + void put_uint8(uint8_t value); + void put_uint16(uint16_t value) { this->put_uint(value, sizeof(uint16_t)); } + void put_uint24(uint32_t value) { this->put_uint(value, 3); } + void put_uint32(uint32_t value) { this->put_uint(value, sizeof(uint32_t)); } + void put_uint64(uint64_t value) { this->put_uint(value, sizeof(uint64_t)); } + // Signed versions of the put functions + void put_int8(int8_t value) { this->put_uint8(static_cast(value)); } + void put_int16(int32_t value) { this->put_uint(static_cast(value), sizeof(uint16_t)); } + void put_int24(int32_t value) { this->put_uint(static_cast(value), 3); } + void put_int32(int32_t value) { this->put_uint(static_cast(value), sizeof(uint32_t)); } + void put_int64(int64_t value) { this->put_uint(static_cast(value), sizeof(uint64_t)); } + // Extra put functions + void put_float(float value); + void put_double(double value); + void put_bool(bool value) { this->put_uint8(value); } + void put_vector(const std::vector &value); + + inline size_t get_capacity() const { return this->data_.size(); } + inline size_t get_position() const { return this->position_; } + inline size_t get_limit() const { return this->limit_; } + inline size_t get_remaining() const { return this->get_limit() - this->get_position(); } + inline Endian get_endianness() const { return this->endianness_; } + inline void mark() { this->mark_ = this->position_; } + inline void big_endian() { this->endianness_ = BIG; } + inline void little_endian() { this->endianness_ = LITTLE; } + void set_limit(size_t limit); + void set_position(size_t position); + // set position to 0, limit to capacity. + void clear(); + // set limit to current position, postition to zero. Used when swapping from write to read operations. + void flip(); + // retrieve a pointer to the underlying data. + std::vector get_data() { return this->data_; }; + void rewind() { this->position_ = 0; } + void reset() { this->position_ = this->mark_; } + + protected: + ByteBuffer(std::vector const &data) : data_(data), limit_(data.size()) {} + std::vector data_; + Endian endianness_{LITTLE}; + size_t position_{0}; + size_t mark_{0}; + size_t limit_{0}; +}; + +} // namespace esphome diff --git a/esphome/core/color.h b/esphome/core/color.h index 8965d9fc83..1c43fd9d3e 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -85,22 +85,26 @@ struct Color { } inline Color operator+(const Color &add) const ESPHOME_ALWAYS_INLINE { Color ret; - if (uint8_t(add.r + this->r) < this->r) + if (uint8_t(add.r + this->r) < this->r) { ret.r = 255; - else + } else { ret.r = this->r + add.r; - if (uint8_t(add.g + this->g) < this->g) + } + if (uint8_t(add.g + this->g) < this->g) { ret.g = 255; - else + } else { ret.g = this->g + add.g; - if (uint8_t(add.b + this->b) < this->b) + } + if (uint8_t(add.b + this->b) < this->b) { ret.b = 255; - else + } else { ret.b = this->b + add.b; - if (uint8_t(add.w + this->w) < this->w) + } + if (uint8_t(add.w + this->w) < this->w) { ret.w = 255; - else + } else { ret.w = this->w + add.w; + } return ret; } inline Color &operator+=(const Color &add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; } @@ -108,22 +112,26 @@ struct Color { inline Color &operator+=(uint8_t add) ESPHOME_ALWAYS_INLINE { return *this = (*this) + add; } inline Color operator-(const Color &subtract) const ESPHOME_ALWAYS_INLINE { Color ret; - if (subtract.r > this->r) + if (subtract.r > this->r) { ret.r = 0; - else + } else { ret.r = this->r - subtract.r; - if (subtract.g > this->g) + } + if (subtract.g > this->g) { ret.g = 0; - else + } else { ret.g = this->g - subtract.g; - if (subtract.b > this->b) + } + if (subtract.b > this->b) { ret.b = 0; - else + } else { ret.b = this->b - subtract.b; - if (subtract.w > this->w) + } + if (subtract.w > this->w) { ret.w = 0; - else + } else { ret.w = this->w - subtract.w; + } return ret; } inline Color &operator-=(const Color &subtract) ESPHOME_ALWAYS_INLINE { return *this = (*this) - subtract; } diff --git a/esphome/core/config.py b/esphome/core/config.py index 739a8a1aea..f4253bee87 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -100,9 +100,6 @@ def valid_include(value): def valid_project_name(value: str): if value.count(".") != 1: raise cv.Invalid("project name needs to have a namespace") - - value = value.replace(" ", "_") - return value diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 6ba5b64761..ffd5cc6f1b 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -28,6 +28,7 @@ #define USE_DATETIME_DATETIME #define USE_DATETIME_TIME #define USE_DEEP_SLEEP +#define USE_DISPLAY #define USE_EVENT #define USE_FAN #define USE_GRAPH @@ -39,14 +40,22 @@ #define USE_LOCK #define USE_LOGGER #define USE_LVGL +#define USE_LVGL_ANIMIMG +#define USE_LVGL_BINARY_SENSOR +#define USE_LVGL_BUTTONMATRIX #define USE_LVGL_FONT #define USE_LVGL_IMAGE +#define USE_LVGL_KEYBOARD +#define USE_LVGL_KEY_LISTENER #define USE_LVGL_TOUCHSCREEN +#define USE_LVGL_ROTARY_ENCODER #define USE_MDNS #define USE_MEDIA_PLAYER #define USE_MQTT +#define USE_NETWORK #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER +#define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK @@ -67,6 +76,7 @@ #define USE_VALVE #define USE_WIFI #define USE_WIFI_AP +#define USE_WIREGUARD // Arduino-specific feature flags #ifdef USE_ARDUINO @@ -151,6 +161,7 @@ #endif // Disabled feature flags -// #define USE_BSEC // Requires a library with proprietary license. +// #define USE_BSEC // Requires a library with proprietary license +// #define USE_BSEC2 // Requires a library with proprietary license #define USE_DASHBOARD_IMPORT diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 434111de79..4ca21f9ee5 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -63,7 +63,7 @@ class EntityBase { EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; }; -class EntityBase_DeviceClass { +class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. std::string get_device_class(); @@ -74,7 +74,7 @@ class EntityBase_DeviceClass { const char *device_class_{nullptr}; ///< Device class override }; -class EntityBase_UnitOfMeasurement { +class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. std::string get_unit_of_measurement(); diff --git a/esphome/core/gpio.h b/esphome/core/gpio.h index b3f6b00196..1b6f2ba1e6 100644 --- a/esphome/core/gpio.h +++ b/esphome/core/gpio.h @@ -62,24 +62,6 @@ class GPIOPin { virtual bool is_internal() { return false; } }; -/** - * A pin to replace those that don't exist. - */ -class NullPin : public GPIOPin { - public: - void setup() override {} - - void pin_mode(gpio::Flags _) override {} - - bool digital_read() override { return false; } - - void digital_write(bool _) override {} - - std::string dump_summary() const override { return {"Not used"}; } -}; - -static GPIOPin *const NULL_PIN = new NullPin(); - /// Copy of GPIOPin that is safe to use from ISRs (with no virtual functions) class ISRInternalGPIOPin { public: diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b4ad22b083..3e6fe9433e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -680,7 +680,7 @@ template class ExternalRAMAllocator { } private: - Flags flags_{Flags::NONE}; + Flags flags_{Flags::ALLOW_FAILURE}; }; /// @} diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 5b96781e63..1e28ef1354 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -24,7 +24,7 @@ namespace esphome { struct nullopt_t { // NOLINT struct init {}; // NOLINT - nullopt_t(init) {} + nullopt_t(init /*unused*/) {} }; // extra parenthesis to prevent the most vexing parse: @@ -42,13 +42,13 @@ template class optional { // NOLINT optional() {} - optional(nullopt_t) {} + optional(nullopt_t /*unused*/) {} optional(T const &arg) : has_value_(true), value_(arg) {} // NOLINT template optional(optional const &other) : has_value_(other.has_value()), value_(other.value()) {} - optional &operator=(nullopt_t) { + optional &operator=(nullopt_t /*unused*/) { reset(); return *this; } @@ -104,7 +104,6 @@ template class optional { // NOLINT has_value_ = true; } - private: bool has_value_{false}; // NOLINT value_type value_; // NOLINT }; @@ -131,29 +130,29 @@ template inline bool operator>=(optional const &x, op // Comparison with nullopt -template inline bool operator==(optional const &x, nullopt_t) { return (!x); } +template inline bool operator==(optional const &x, nullopt_t /*unused*/) { return (!x); } -template inline bool operator==(nullopt_t, optional const &x) { return (!x); } +template inline bool operator==(nullopt_t /*unused*/, optional const &x) { return (!x); } -template inline bool operator!=(optional const &x, nullopt_t) { return bool(x); } +template inline bool operator!=(optional const &x, nullopt_t /*unused*/) { return bool(x); } -template inline bool operator!=(nullopt_t, optional const &x) { return bool(x); } +template inline bool operator!=(nullopt_t /*unused*/, optional const &x) { return bool(x); } -template inline bool operator<(optional const &, nullopt_t) { return false; } +template inline bool operator<(optional const & /*unused*/, nullopt_t /*unused*/) { return false; } -template inline bool operator<(nullopt_t, optional const &x) { return bool(x); } +template inline bool operator<(nullopt_t /*unused*/, optional const &x) { return bool(x); } -template inline bool operator<=(optional const &x, nullopt_t) { return (!x); } +template inline bool operator<=(optional const &x, nullopt_t /*unused*/) { return (!x); } -template inline bool operator<=(nullopt_t, optional const &) { return true; } +template inline bool operator<=(nullopt_t /*unused*/, optional const & /*unused*/) { return true; } -template inline bool operator>(optional const &x, nullopt_t) { return bool(x); } +template inline bool operator>(optional const &x, nullopt_t /*unused*/) { return bool(x); } -template inline bool operator>(nullopt_t, optional const &) { return false; } +template inline bool operator>(nullopt_t /*unused*/, optional const & /*unused*/) { return false; } -template inline bool operator>=(optional const &, nullopt_t) { return true; } +template inline bool operator>=(optional const & /*unused*/, nullopt_t /*unused*/) { return true; } -template inline bool operator>=(nullopt_t, optional const &x) { return (!x); } +template inline bool operator>=(nullopt_t /*unused*/, optional const &x) { return (!x); } // Comparison with T diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index 9bd3d9d853..d8ca831de0 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -20,13 +20,20 @@ std::unique_ptr RingBuffer::create(size_t len) { return nullptr; } - rb->handle_ = xStreamBufferCreateStatic(len + 1, 0, rb->storage_, &rb->structure_); + rb->handle_ = xStreamBufferCreateStatic(len + 1, 1, rb->storage_, &rb->structure_); ESP_LOGD(TAG, "Created ring buffer with size %u", len); return rb; } size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { - return xStreamBufferReceive(this->handle_, data, len, ticks_to_wait); + if (ticks_to_wait > 0) + xStreamBufferSetTriggerLevel(this->handle_, len); + + size_t bytes_read = xStreamBufferReceive(this->handle_, data, len, ticks_to_wait); + + xStreamBufferSetTriggerLevel(this->handle_, 1); + + return bytes_read; } size_t RingBuffer::write(void *data, size_t len) { @@ -39,6 +46,10 @@ size_t RingBuffer::write(void *data, size_t len) { return xStreamBufferSend(this->handle_, data, len, 0); } +size_t RingBuffer::write_without_replacement(void *data, size_t len, TickType_t ticks_to_wait) { + return xStreamBufferSend(this->handle_, data, len, ticks_to_wait); +} + size_t RingBuffer::available() const { return xStreamBufferBytesAvailable(this->handle_); } size_t RingBuffer::free() const { return xStreamBufferSpacesAvailable(this->handle_); } diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index e602068844..97ffefcefa 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -12,13 +12,69 @@ namespace esphome { class RingBuffer { public: + /** + * @brief Reads from the ring buffer, waiting up to a specified number of ticks if necessary. + * + * Available bytes are read into the provided data pointer. If not enough bytes are available, + * the function will wait up to `ticks_to_wait` FreeRTOS ticks before reading what is available. + * + * @param data Pointer to copy read data into + * @param len Number of bytes to read + * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) + * @return Number of bytes read + */ size_t read(void *data, size_t len, TickType_t ticks_to_wait = 0); + /** + * @brief Writes to the ring buffer, overwriting oldest data if necessary. + * + * The provided data is written to the ring buffer. If not enough space is available, + * the function will overwrite the oldest data in the ring buffer. + * + * @param data Pointer to data for writing + * @param len Number of bytes to write + * @return Number of bytes written + */ size_t write(void *data, size_t len); + /** + * @brief Writes to the ring buffer without overwriting oldest data. + * + * The provided data is written to the ring buffer. If not enough space is available, + * the function will wait up to `ticks_to_wait` FreeRTOS ticks before writing as much as possible. + * + * @param data Pointer to data for writing + * @param len Number of bytes to write + * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) + * @return Number of bytes written + */ + size_t write_without_replacement(void *data, size_t len, TickType_t ticks_to_wait = 0); + + /** + * @brief Returns the number of available bytes in the ring buffer. + * + * This function provides the number of bytes that can be read from the ring buffer + * without blocking the calling FreeRTOS task. + * + * @return Number of available bytes + */ size_t available() const; + + /** + * @brief Returns the number of free bytes in the ring buffer. + * + * This function provides the number of bytes that can be written to the ring buffer + * without overwriting data or blocking the calling FreeRTOS task. + * + * @return Number of free bytes + */ size_t free() const; + /** + * @brief Resets the ring buffer, discarding all stored data. + * + * @return pdPASS if successful, pdFAIL otherwise + */ BaseType_t reset(); static std::unique_ptr create(size_t len); diff --git a/esphome/external_files.py b/esphome/external_files.py index baf62286e4..057ff52f3f 100644 --- a/esphome/external_files.py +++ b/esphome/external_files.py @@ -80,10 +80,10 @@ def compute_local_file_dir(domain: str) -> Path: return base_directory -def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> None: +def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes: if not has_remote_file_changed(url, path): _LOGGER.debug("Remote file has not changed %s", url) - return + return path.read_bytes() _LOGGER.debug( "Remote file has changed, downloading from %s to %s", @@ -102,4 +102,6 @@ def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> None: raise cv.Invalid(f"Could not download from {url}: {e}") path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(req.content) + data = req.content + path.write_bytes(data) + return data diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 5f4701b5a3..c79ba1b0ed 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -7,7 +7,7 @@ dependencies: version: v2.0.9 mdns: git: https://github.com/espressif/esp-protocols.git - version: mdns-v1.2.5 + version: mdns-v1.3.2 path: components/mdns rules: - if: "idf_version >=5.0" diff --git a/esphome/loader.py b/esphome/loader.py index 9399c4cb31..d808805119 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -1,3 +1,4 @@ +from contextlib import AbstractContextManager from dataclasses import dataclass import importlib import importlib.abc @@ -7,7 +8,7 @@ import logging from pathlib import Path import sys from types import ModuleType -from typing import Any, Callable, ContextManager, Optional +from typing import Any, Callable, Optional from esphome.const import SOURCE_FILE_EXTENSIONS from esphome.core import CORE @@ -22,7 +23,7 @@ class FileResource: package: str resource: str - def path(self) -> ContextManager[Path]: + def path(self) -> AbstractContextManager[Path]: return importlib.resources.as_file( importlib.resources.files(self.package) / self.resource ) @@ -176,7 +177,7 @@ def _lookup_module(domain): module = importlib.import_module(f"esphome.components.{domain}") except ImportError as e: if "No module named" in str(e): - _LOGGER.error( + _LOGGER.info( "Unable to import component %s: %s", domain, str(e), exc_info=False ) else: diff --git a/esphome/mqtt.py b/esphome/mqtt.py index d7e14a1d08..c1c45799cc 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -3,7 +3,6 @@ import hashlib import json import logging import ssl -import sys import time import paho.mqtt.client as mqtt @@ -103,10 +102,7 @@ def prepare( if config[CONF_MQTT].get(CONF_SSL_FINGERPRINTS) or config[CONF_MQTT].get( CONF_CERTIFICATE_AUTHORITY ): - if sys.version_info >= (2, 7, 13): - tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member - else: - tls_version = ssl.PROTOCOL_SSLv23 + tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member client.tls_set( ca_certs=None, certfile=None, diff --git a/esphome/storage_json.py b/esphome/storage_json.py index e2e7514904..2d12ee01a0 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -48,6 +48,8 @@ class StorageJSON: firmware_bin_path: str, loaded_integrations: set[str], no_mdns: bool, + framework: str | None = None, + core_platform: str | None = None, ) -> None: # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) @@ -78,6 +80,10 @@ class StorageJSON: self.loaded_integrations = loaded_integrations # Is mDNS disabled self.no_mdns = no_mdns + # The framework used to compile the firmware + self.framework = framework + # The core platform of this firmware. Like "esp32", "rp2040", "host" etc. + self.core_platform = core_platform def as_dict(self): return { @@ -94,6 +100,8 @@ class StorageJSON: "firmware_bin_path": self.firmware_bin_path, "loaded_integrations": sorted(self.loaded_integrations), "no_mdns": self.no_mdns, + "framework": self.framework, + "core_platform": self.core_platform, } def to_json(self): @@ -127,6 +135,8 @@ class StorageJSON: and CONF_DISABLED in esph.config[CONF_MDNS] and esph.config[CONF_MDNS][CONF_DISABLED] is True ), + framework=esph.target_framework, + core_platform=esph.target_platform, ) @staticmethod @@ -147,6 +157,8 @@ class StorageJSON: firmware_bin_path=None, loaded_integrations=set(), no_mdns=False, + framework=None, + core_platform=platform.lower(), ) @staticmethod @@ -168,6 +180,8 @@ class StorageJSON: firmware_bin_path = storage.get("firmware_bin_path") loaded_integrations = set(storage.get("loaded_integrations", [])) no_mdns = storage.get("no_mdns", False) + framework = storage.get("framework") + core_platform = storage.get("core_platform") return StorageJSON( storage_version, name, @@ -182,6 +196,8 @@ class StorageJSON: firmware_bin_path, loaded_integrations, no_mdns, + framework, + core_platform, ) @staticmethod diff --git a/esphome/writer.py b/esphome/writer.py index c6111cbe3f..79ee72996c 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -9,6 +9,7 @@ from esphome.config import iter_component_configs, iter_components from esphome.const import ( ENV_NOGITIGNORE, HEADER_FILE_EXTENSIONS, + PLATFORM_ESP32, SOURCE_FILE_EXTENSIONS, __version__, ) @@ -106,6 +107,11 @@ def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: return True if old.build_path != new.build_path: return True + if old.loaded_integrations != new.loaded_integrations: + if new.core_platform == PLATFORM_ESP32: + from esphome.components.esp32 import FRAMEWORK_ESP_IDF + + return new.framework == FRAMEWORK_ESP_IDF return False @@ -117,7 +123,9 @@ def update_storage_json(): return if storage_should_clean(old, new): - _LOGGER.info("Core config or version changed, cleaning build files...") + _LOGGER.info( + "Core config, version or integrations changed, cleaning build files..." + ) clean_build() new.save(path) diff --git a/platformio.ini b/platformio.ini index baf0a85d73..e3593bf43f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -35,11 +35,12 @@ build_flags = lib_deps = esphome/noise-c@0.1.4 ; api makuna/NeoPixelBus@2.7.3 ; neopixelbus - esphome/Improv@1.2.3 ; improv_serial / esp32_improv + improv/Improv@1.2.4 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier + kikuchan98/pngle@1.0.2 ; online_image ; This is using the repository until a new release is published to PlatformIO https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library lvgl/lvgl@8.4.0 ; lvgl @@ -118,7 +119,7 @@ lib_deps = WiFi ; wifi,web_server_base,ethernet (Arduino built-in) Update ; ota,web_server_base (Arduino built-in) ${common:arduino.lib_deps} - esphome/AsyncTCP-esphome@2.1.3 ; async_tcp + esphome/AsyncTCP-esphome@2.1.4 ; async_tcp WiFiClientSecure ; http_request,nextion (Arduino built-in) HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) @@ -138,13 +139,13 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = platformio/espressif32@5.4.0 platform_packages = - platformio/framework-espidf@~3.40407.0 + platformio/framework-espidf@~3.40408.0 framework = espidf lib_deps = ${common:idf.lib_deps} droscy/esp_wireguard@0.4.2 ; wireguard - kahrendt/ESPMicroSpeechFeatures@1.0.0 ; micro_wake_word + kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word build_flags = ${common:idf.build_flags} -Wno-nonnull-compare @@ -152,6 +153,13 @@ build_flags = -DUSE_ESP32_FRAMEWORK_ESP_IDF extra_scripts = post:esphome/components/esp32/post_build.py.script +; This are common settings for the ESP32 using the latest ESP-IDF version. +[common:esp32-idf-5_3] +extends = common:esp32-idf +platform = platformio/espressif32@6.8.0 +platform_packages = + platformio/framework-espidf@~3.50300.0 + ; These are common settings for the RP2040 using Arduino. [common:rp2040-arduino] extends = common:arduino @@ -160,7 +168,7 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git platform_packages = ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.7.2/rp2040-3.7.2.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.9.4/rp2040-3.9.4.zip framework = arduino lib_deps = @@ -228,6 +236,15 @@ build_flags = ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32 +[env:esp32-idf-5_3] +extends = common:esp32-idf-5_3 +board = esp32dev +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32 + [env:esp32-idf-tidy] extends = common:esp32-idf board = esp32dev @@ -264,6 +281,15 @@ build_flags = ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32C3 +[env:esp32c3-idf-5_3] +extends = common:esp32-idf-5_3 +board = esp32-c3-devkitm-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32C3 + [env:esp32c3-idf-tidy] extends = common:esp32-idf board = esp32-c3-devkitm-1 @@ -300,6 +326,15 @@ build_flags = ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S2 +[env:esp32s2-idf-5_3] +extends = common:esp32-idf-5_3 +board = esp32-s2-kaluga-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32S2 + [env:esp32s2-idf-tidy] extends = common:esp32-idf board = esp32-s2-kaluga-1 @@ -336,6 +371,15 @@ build_flags = ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32S3 +[env:esp32s3-idf-5_3] +extends = common:esp32-idf-5_3 +board = esp32-s3-devkitc-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s3-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32S3 + [env:esp32s3-idf-tidy] extends = common:esp32-idf board = esp32-s3-devkitc-1 diff --git a/requirements_test.txt b/requirements_test.txt index 94abe1cd76..5d94f7f640 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==3.1.0 +pylint==3.2.7 flake8==7.0.0 # also change in .pre-commit-config.yaml when updating black==24.4.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.2 # also change in .pre-commit-config.yaml when updating diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 6bc558d351..db34ad7702 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 -from pathlib import Path -import sys import argparse from collections import defaultdict +from pathlib import Path +import sys -from esphome.helpers import write_file_if_changed from esphome.config import get_component, get_platform -from esphome.core import CORE from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK +from esphome.core import CORE +from esphome.helpers import write_file_if_changed parser = argparse.ArgumentParser() parser.add_argument( diff --git a/script/build_language_schema.py b/script/build_language_schema.py index cb3dc1832d..8b2c28b06b 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -1,9 +1,10 @@ +import argparse +import glob import inspect import json -import argparse import os -import glob import re + import voluptuous as vol # NOTE: Cannot import other esphome components globally as a modification in vol_schema @@ -94,13 +95,12 @@ load_components() # Import esphome after loading components (so schema is tracked) # pylint: disable=wrong-import-position -import esphome.core as esphome_core -import esphome.config_validation as cv -from esphome import automation -from esphome import pins +from esphome import automation, pins from esphome.components import remote_base -from esphome.loader import get_platform, CORE_COMPONENTS_PATH +import esphome.config_validation as cv +import esphome.core as esphome_core from esphome.helpers import write_file_if_changed +from esphome.loader import CORE_COMPONENTS_PATH, get_platform from esphome.util import Registry # pylint: enable=wrong-import-position diff --git a/script/bump-version.py b/script/bump-version.py index a55bb65cd6..8389d482b8 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 import argparse -import re from dataclasses import dataclass +import re import sys diff --git a/script/clang-format b/script/clang-format index b065d80795..d922c5b6f1 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,15 +1,6 @@ #!/usr/bin/env python3 -from helpers import ( - print_error_for_file, - get_output, - git_ls_files, - filter_changed, - get_binary, -) import argparse -import click -import colorama import multiprocessing import os import queue @@ -18,6 +9,9 @@ import subprocess import sys import threading +import click +import colorama +from helpers import filter_changed, get_binary, git_ls_files, print_error_for_file def run_format(executable, args, queue, lock, failed_files): diff --git a/script/clang-tidy b/script/clang-tidy index bd919825fd..5bb93846b2 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,21 +1,6 @@ #!/usr/bin/env python3 -from helpers import ( - print_error_for_file, - get_output, - filter_grep, - build_all_include, - temp_header_file, - git_ls_files, - filter_changed, - load_idedata, - root_path, - basepath, - get_binary, -) import argparse -import click -import colorama import multiprocessing import os import queue @@ -26,6 +11,20 @@ import sys import tempfile import threading +import click +import colorama +from helpers import ( + basepath, + build_all_include, + filter_changed, + filter_grep, + get_binary, + git_ls_files, + load_idedata, + print_error_for_file, + root_path, + temp_header_file, +) def clang_options(idedata): @@ -116,9 +115,10 @@ def clang_options(idedata): pids = set() -def run_tidy(executable, args, options, tmpdir, queue, lock, failed_files): + +def run_tidy(executable, args, options, tmpdir, path_queue, lock, failed_files): while True: - path = queue.get() + path = path_queue.get() invocation = [executable] if tmpdir is not None: @@ -140,17 +140,20 @@ def run_tidy(executable, args, options, tmpdir, queue, lock, failed_files): invocation.append("--") invocation.extend(options) - proc = subprocess.run(invocation, capture_output=True, encoding="utf-8") + proc = subprocess.run( + invocation, capture_output=True, encoding="utf-8", check=False + ) if proc.returncode != 0: with lock: print_error_for_file(path, proc.stdout) failed_files.append(path) - queue.task_done() + path_queue.task_done() def progress_bar_show(value): if value is None: return "" + return None def split_list(a, n): @@ -238,7 +241,15 @@ def main(): for _ in range(args.jobs): t = threading.Thread( target=run_tidy, - args=(executable, args, options, tmpdir, task_queue, lock, failed_files), + args=( + executable, + args, + options, + tmpdir, + task_queue, + lock, + failed_files, + ), ) t.daemon = True t.start() @@ -246,14 +257,14 @@ def main(): # Fill the queue with files. with click.progressbar( files, width=30, file=sys.stderr, item_show_func=progress_bar_show - ) as bar: - for name in bar: + ) as progress_bar: + for name in progress_bar: task_queue.put(name) # Wait for all threads to be done. task_queue.join() - except FileNotFoundError as ex: + except FileNotFoundError: return 1 except KeyboardInterrupt: print() @@ -263,7 +274,7 @@ def main(): # Kill subprocesses (and ourselves!) # No simple, clean alternative appears to be available. os.kill(0, 9) - return 2 # Will not execute. + return 2 # Will not execute. if args.fix and failed_files: print("Applying fixes ...") @@ -273,7 +284,10 @@ def main(): except FileNotFoundError: subprocess.call(["clang-apply-replacements", tmpdir]) except FileNotFoundError: - print("Error please install clang-apply-replacements-14 or clang-apply-replacements.\n", file=sys.stderr) + print( + "Error please install clang-apply-replacements-14 or clang-apply-replacements.\n", + file=sys.stderr, + ) except: print("Error applying fixes.\n", file=sys.stderr) raise diff --git a/script/devcontainer-post-create b/script/devcontainer-post-create index 272d350519..2d376786ac 100755 --- a/script/devcontainer-post-create +++ b/script/devcontainer-post-create @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e # set -x diff --git a/script/helpers.py b/script/helpers.py index 52b0658fb6..6f36faaeb1 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1,8 +1,8 @@ import json import os.path +from pathlib import Path import re import subprocess -from pathlib import Path import colorama @@ -159,20 +159,19 @@ def get_binary(name: str, version: str) -> str: binary_file = f"{name}-{version}" try: result = subprocess.check_output([binary_file, "-version"]) - if result.returncode == 0: - return binary_file - except Exception: + return binary_file + except FileNotFoundError: pass binary_file = name try: result = subprocess.run( - [binary_file, "-version"], text=True, capture_output=True + [binary_file, "-version"], text=True, capture_output=True, check=False ) if result.returncode == 0 and (f"version {version}") in result.stdout: return binary_file raise FileNotFoundError(f"{name} not found") - except FileNotFoundError as ex: + except FileNotFoundError: print( f""" Oops. It looks like {name} is not installed. It should be available under venv/bin diff --git a/script/lint-python b/script/lint-python index 7de1de80b0..01e5e76190 100755 --- a/script/lint-python +++ b/script/lint-python @@ -1,19 +1,20 @@ #!/usr/bin/env python3 -from helpers import ( - styled, - print_error_for_file, - get_output, - get_err, - git_ls_files, - filter_changed, -) import argparse -import colorama import os import re import sys +import colorama +from helpers import ( + filter_changed, + get_err, + get_output, + git_ls_files, + print_error_for_file, + styled, +) + curfile = None diff --git a/script/list-components.py b/script/list-components.py index 559919bb8a..0d4777436b 100755 --- a/script/list-components.py +++ b/script/list-components.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 +import argparse from pathlib import Path import sys -import argparse -from helpers import git_ls_files, changed_files -from esphome.loader import get_component, get_platform -from esphome.core import CORE +from helpers import changed_files, git_ls_files + from esphome.const import ( KEY_CORE, KEY_TARGET_FRAMEWORK, @@ -13,6 +12,8 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, ) +from esphome.core import CORE +from esphome.loader import get_component, get_platform def filter_component_files(str): diff --git a/script/quicklint b/script/quicklint index a4fae98195..84e4c97667 100755 --- a/script/quicklint +++ b/script/quicklint @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e diff --git a/script/setup b/script/setup index aeb1b39bc1..824840c392 100755 --- a/script/setup +++ b/script/setup @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Set up ESPHome dev environment set -e diff --git a/script/setup.bat b/script/setup.bat new file mode 100644 index 0000000000..0b49768139 --- /dev/null +++ b/script/setup.bat @@ -0,0 +1,28 @@ +@echo off + +if defined DEVCONTAINER goto :install +if defined VIRTUAL_ENV goto :install +if defined ESPHOME_NO_VENV goto :install + +echo Starting the Virtual Environment +python -m venv venv +call venv/Scripts/activate +echo Running the Virtual Environment + +:install + +echo Installing required packages... + +python.exe -m pip install --upgrade pip + +pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt -r requirements_dev.txt +pip3 install setuptools wheel +pip3 install -e ".[dev,test,displays]" --config-settings editable_mode=compat + +pre-commit install + +python script/platformio_install_deps.py platformio.ini --libraries --tools --platforms + +echo . +echo . +echo Virtual environment created. Run 'venv/Scripts/activate' to use it. diff --git a/tests/components/animation/test.esp32-ard.yaml b/tests/components/animation/test.esp32-ard.yaml index 5dc132eb2d..af6cd202dd 100644 --- a/tests/components/animation/test.esp32-ard.yaml +++ b/tests/components/animation/test.esp32-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 12 dc_pin: 13 reset_pin: 21 + invert_colors: false # Purposely test that `animation:` does auto-load `image:` # Keep the `image:` undefined. diff --git a/tests/components/animation/test.esp32-c3-ard.yaml b/tests/components/animation/test.esp32-c3-ard.yaml index 9bcfbdb118..10e8ccb47e 100644 --- a/tests/components/animation/test.esp32-c3-ard.yaml +++ b/tests/components/animation/test.esp32-c3-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: false # Purposely test that `animation:` does auto-load `image:` # Keep the `image:` undefined. diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml index 9bcfbdb118..10e8ccb47e 100644 --- a/tests/components/animation/test.esp32-c3-idf.yaml +++ b/tests/components/animation/test.esp32-c3-idf.yaml @@ -11,6 +11,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: false # Purposely test that `animation:` does auto-load `image:` # Keep the `image:` undefined. diff --git a/tests/components/animation/test.esp32-idf.yaml b/tests/components/animation/test.esp32-idf.yaml index 5dc132eb2d..af6cd202dd 100644 --- a/tests/components/animation/test.esp32-idf.yaml +++ b/tests/components/animation/test.esp32-idf.yaml @@ -11,6 +11,7 @@ display: cs_pin: 12 dc_pin: 13 reset_pin: 21 + invert_colors: false # Purposely test that `animation:` does auto-load `image:` # Keep the `image:` undefined. diff --git a/tests/components/animation/test.esp8266-ard.yaml b/tests/components/animation/test.esp8266-ard.yaml index ef0f483a79..ced4996f25 100644 --- a/tests/components/animation/test.esp8266-ard.yaml +++ b/tests/components/animation/test.esp8266-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 5 dc_pin: 15 reset_pin: 16 + invert_colors: false # Purposely test that `animation:` does auto-load `image:` # Keep the `image:` undefined. diff --git a/tests/components/animation/test.rp2040-ard.yaml b/tests/components/animation/test.rp2040-ard.yaml index 6ee29a3347..0e33959cc6 100644 --- a/tests/components/animation/test.rp2040-ard.yaml +++ b/tests/components/animation/test.rp2040-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 20 dc_pin: 21 reset_pin: 22 + invert_colors: false # Purposely test that `animation:` does auto-load `image:` # Keep the `image:` undefined. diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index e0b900f92d..7ac11e4da6 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -5,24 +5,20 @@ esphome: event: esphome.button_pressed data: message: Button was pressed - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: message: Button was pressed - homeassistant.tag_scanned: pulse -wifi: - ssid: MySSID - password: password1 - api: port: 8000 password: pwd reboot_timeout: 0min encryption: key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= - services: - - service: hello_world + actions: + - action: hello_world variables: name: string then: @@ -30,10 +26,10 @@ api: format: Hello World %s! args: - name.c_str() - - service: empty_service + - action: empty_action then: - - logger.log: Service Called - - service: all_types + - logger.log: Action Called + - action: all_types variables: bool_: bool int_: int @@ -41,7 +37,7 @@ api: string_: string then: - logger.log: Something happened - - service: array_types + - action: array_types variables: bool_arr: bool[] int_arr: int[] diff --git a/tests/components/api/test.esp32-ard.yaml b/tests/components/api/test.esp32-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-ard.yaml +++ b/tests/components/api/test.esp32-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-c3-ard.yaml b/tests/components/api/test.esp32-c3-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-c3-ard.yaml +++ b/tests/components/api/test.esp32-c3-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-c3-idf.yaml b/tests/components/api/test.esp32-c3-idf.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-c3-idf.yaml +++ b/tests/components/api/test.esp32-c3-idf.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp32-idf.yaml b/tests/components/api/test.esp32-idf.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp32-idf.yaml +++ b/tests/components/api/test.esp32-idf.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.esp8266-ard.yaml b/tests/components/api/test.esp8266-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.esp8266-ard.yaml +++ b/tests/components/api/test.esp8266-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/api/test.host.yaml b/tests/components/api/test.host.yaml new file mode 100644 index 0000000000..1ecafeab77 --- /dev/null +++ b/tests/components/api/test.host.yaml @@ -0,0 +1,3 @@ +<<: !include common.yaml + +network: diff --git a/tests/components/api/test.rp2040-ard.yaml b/tests/components/api/test.rp2040-ard.yaml index dade44d145..46c01d926f 100644 --- a/tests/components/api/test.rp2040-ard.yaml +++ b/tests/components/api/test.rp2040-ard.yaml @@ -1 +1,5 @@ <<: !include common.yaml + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/atm90e32/test.esp32-ard.yaml b/tests/components/atm90e32/test.esp32-ard.yaml index 131270f8ad..3bdc2bcec6 100644 --- a/tests/components/atm90e32/test.esp32-ard.yaml +++ b/tests/components/atm90e32/test.esp32-ard.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 13 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-c3-ard.yaml b/tests/components/atm90e32/test.esp32-c3-ard.yaml index 263fb6d24e..9ec0037a61 100644 --- a/tests/components/atm90e32/test.esp32-c3-ard.yaml +++ b/tests/components/atm90e32/test.esp32-c3-ard.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 8 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-c3-idf.yaml b/tests/components/atm90e32/test.esp32-c3-idf.yaml index 263fb6d24e..9ec0037a61 100644 --- a/tests/components/atm90e32/test.esp32-c3-idf.yaml +++ b/tests/components/atm90e32/test.esp32-c3-idf.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 8 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp32-idf.yaml b/tests/components/atm90e32/test.esp32-idf.yaml index 131270f8ad..3bdc2bcec6 100644 --- a/tests/components/atm90e32/test.esp32-idf.yaml +++ b/tests/components/atm90e32/test.esp32-idf.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 13 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.esp8266-ard.yaml b/tests/components/atm90e32/test.esp8266-ard.yaml index e8e2abc1a9..fbb3368efa 100644 --- a/tests/components/atm90e32/test.esp8266-ard.yaml +++ b/tests/components/atm90e32/test.esp8266-ard.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 5 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,42 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True + - platform: atm90e32 + cs_pin: 4 + id: chip2 + phase_a: + voltage: + name: EMON Line Voltage A + current: + name: EMON CT1 Current + power: + name: EMON Active Power CT1 + reactive_power: + name: EMON Reactive Power CT1 + power_factor: + name: EMON Power Factor CT1 + gain_voltage: 7305 + gain_ct: 27961 + phase_c: + voltage: + name: EMON Line Voltage C + current: + name: EMON CT2 Current + power: + name: EMON Active Power CT2 + reactive_power: + name: EMON Reactive Power CT2 + power_factor: + name: EMON Power Factor CT2 + gain_voltage: 7305 + gain_ct: 27961 + line_frequency: 60Hz + current_phases: 2 +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/atm90e32/test.rp2040-ard.yaml b/tests/components/atm90e32/test.rp2040-ard.yaml index 525e0b801a..a6b7956da7 100644 --- a/tests/components/atm90e32/test.rp2040-ard.yaml +++ b/tests/components/atm90e32/test.rp2040-ard.yaml @@ -7,6 +7,7 @@ spi: sensor: - platform: atm90e32 cs_pin: 5 + id: chip1 phase_a: voltage: name: EMON Line Voltage A @@ -49,3 +50,11 @@ sensor: line_frequency: 60Hz current_phases: 3 gain_pga: 2X + enable_offset_calibration: True +button: + - platform: atm90e32 + id: chip1 + run_offset_calibration: + name: "Chip1 - Run Offset Calibration" + clear_offset_calibration: + name: "Chip1 - Clear Offset Calibration" diff --git a/tests/components/bl0906/common.yaml b/tests/components/bl0906/common.yaml new file mode 100644 index 0000000000..944791369c --- /dev/null +++ b/tests/components/bl0906/common.yaml @@ -0,0 +1,62 @@ +uart: + - id: uart_bl0906 + tx_pin: + number: ${tx_pin} + rx_pin: + number: ${rx_pin} + baud_rate: 19200 + +sensor: + - platform: bl0906 + frequency: + name: 'Frequency' + temperature: + name: 'Temperature' + voltage: + name: 'Voltage' + channel_1: + current: + name: 'Current_1' + power: + name: 'Power_1' + energy: + name: 'Energy_1' + channel_2: + current: + name: 'Current_2' + power: + name: 'Power_2' + energy: + name: 'Energy_2' + channel_3: + current: + name: 'Current_3' + power: + name: 'Power_3' + energy: + name: 'Energy_3' + channel_4: + current: + name: 'Current_4' + power: + name: 'Power_4' + energy: + name: 'Energy_4' + channel_5: + current: + name: 'Current_5' + power: + name: 'Power_5' + energy: + name: 'Energy_5' + channel_6: + current: + name: 'Current_6' + power: + name: 'Power_6' + energy: + name: 'Energy_6' + total_energy: + name: 'Total_Energy' + total_power: + name: 'Total_Power' diff --git a/tests/components/bl0906/test.esp32-ard.yaml b/tests/components/bl0906/test.esp32-ard.yaml new file mode 100644 index 0000000000..811f6b72a6 --- /dev/null +++ b/tests/components/bl0906/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO12 + rx_pin: GPIO14 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-c3-ard.yaml b/tests/components/bl0906/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..c79d14c740 --- /dev/null +++ b/tests/components/bl0906/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO7 + rx_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-c3-idf.yaml b/tests/components/bl0906/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..c79d14c740 --- /dev/null +++ b/tests/components/bl0906/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO7 + rx_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-idf.yaml b/tests/components/bl0906/test.esp32-idf.yaml new file mode 100644 index 0000000000..811f6b72a6 --- /dev/null +++ b/tests/components/bl0906/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO12 + rx_pin: GPIO14 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp8266-ard.yaml b/tests/components/bl0906/test.esp8266-ard.yaml new file mode 100644 index 0000000000..3b44f9c9c3 --- /dev/null +++ b/tests/components/bl0906/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO1 + rx_pin: GPIO3 + +<<: !include common.yaml diff --git a/tests/components/bl0906/test.rp2040-ard.yaml b/tests/components/bl0906/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b516342f3b --- /dev/null +++ b/tests/components/bl0906/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bl0942/test.bk72xx-ard.yaml b/tests/components/bl0942/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..ea61734441 --- /dev/null +++ b/tests/components/bl0942/test.bk72xx-ard.yaml @@ -0,0 +1,27 @@ +uart: + - id: uart_bl0942 + tx_pin: + number: TX1 + rx_pin: + number: RX1 + baud_rate: 2400 + +sensor: + - platform: bl0942 + address: 0 + line_frequency: 50Hz + reset: false + voltage: + name: BL0942 Voltage + current: + name: BL0942 Current + power: + name: BL0942 Power + energy: + name: BL0942 Energy + frequency: + name: BL0942 Frequency + voltage_reference: 15968 + current_reference: 124180 + power_reference: 309.1 + energy_reference: 2653 diff --git a/tests/components/bl0942/test.esp32-ard.yaml b/tests/components/bl0942/test.esp32-ard.yaml index 45ac85aa2a..4138543967 100644 --- a/tests/components/bl0942/test.esp32-ard.yaml +++ b/tests/components/bl0942/test.esp32-ard.yaml @@ -8,6 +8,7 @@ uart: sensor: - platform: bl0942 + reset: true voltage: name: BL0942 Voltage current: diff --git a/tests/components/bl0942/test.rp2040-ard.yaml b/tests/components/bl0942/test.rp2040-ard.yaml index 8d16efed4f..d07e0c4402 100644 --- a/tests/components/bl0942/test.rp2040-ard.yaml +++ b/tests/components/bl0942/test.rp2040-ard.yaml @@ -18,3 +18,5 @@ sensor: name: BL0942 Energy frequency: name: BL0942 Frequency + voltage_reference: 15968 + current_reference: 124180 diff --git a/tests/components/bme68x_bsec2_i2c/common.yaml b/tests/components/bme68x_bsec2_i2c/common.yaml new file mode 100644 index 0000000000..b8a16ee7bb --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/common.yaml @@ -0,0 +1,34 @@ +i2c: + - id: i2c_bme68x + scl: ${scl_pin} + sda: ${sda_pin} + +bme68x_bsec2_i2c: + address: 0x76 + model: bme688 + algorithm_output: classification + operating_age: 28d + sample_rate: LP + supply_voltage: 3.3V + +sensor: + - platform: bme68x_bsec2 + temperature: + name: BME68X Temperature + pressure: + name: BME68X Pressure + humidity: + name: BME68X Humidity + gas_resistance: + name: BME68X Gas Sensor + iaq: + name: BME68X IAQ + co2_equivalent: + name: BME68X eCO2 + breath_voc_equivalent: + name: BME68X Breath eVOC + +text_sensor: + - platform: bme68x_bsec2 + iaq_accuracy: + name: BME68X Accuracy diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..84a9dd4bb4 --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO6 + sda_pin: GPIO7 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-s2-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml b/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bme68x_bsec2_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280/test.esp32-ard.yaml b/tests/components/bmp280/test.esp32-ard.yaml deleted file mode 100644 index aeb1cb262b..0000000000 --- a/tests/components/bmp280/test.esp32-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -i2c: - - id: i2c_bmp280 - scl: 16 - sda: 17 - -sensor: - - platform: bmp280 - address: 0x77 - temperature: - name: Outside Temperature - oversampling: 16x - pressure: - name: Outside Pressure - iir_filter: 16x - update_interval: 15s diff --git a/tests/components/bmp280/test.esp32-c3-ard.yaml b/tests/components/bmp280/test.esp32-c3-ard.yaml deleted file mode 100644 index 5f7f85d3e2..0000000000 --- a/tests/components/bmp280/test.esp32-c3-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -i2c: - - id: i2c_bmp280 - scl: 5 - sda: 4 - -sensor: - - platform: bmp280 - address: 0x77 - temperature: - name: Outside Temperature - oversampling: 16x - pressure: - name: Outside Pressure - iir_filter: 16x - update_interval: 15s diff --git a/tests/components/bmp280/test.esp32-c3-idf.yaml b/tests/components/bmp280/test.esp32-c3-idf.yaml deleted file mode 100644 index 5f7f85d3e2..0000000000 --- a/tests/components/bmp280/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -i2c: - - id: i2c_bmp280 - scl: 5 - sda: 4 - -sensor: - - platform: bmp280 - address: 0x77 - temperature: - name: Outside Temperature - oversampling: 16x - pressure: - name: Outside Pressure - iir_filter: 16x - update_interval: 15s diff --git a/tests/components/bmp280/test.esp8266-ard.yaml b/tests/components/bmp280/test.esp8266-ard.yaml deleted file mode 100644 index 5f7f85d3e2..0000000000 --- a/tests/components/bmp280/test.esp8266-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -i2c: - - id: i2c_bmp280 - scl: 5 - sda: 4 - -sensor: - - platform: bmp280 - address: 0x77 - temperature: - name: Outside Temperature - oversampling: 16x - pressure: - name: Outside Pressure - iir_filter: 16x - update_interval: 15s diff --git a/tests/components/bmp280/test.rp2040-ard.yaml b/tests/components/bmp280/test.rp2040-ard.yaml deleted file mode 100644 index 5f7f85d3e2..0000000000 --- a/tests/components/bmp280/test.rp2040-ard.yaml +++ /dev/null @@ -1,15 +0,0 @@ -i2c: - - id: i2c_bmp280 - scl: 5 - sda: 4 - -sensor: - - platform: bmp280 - address: 0x77 - temperature: - name: Outside Temperature - oversampling: 16x - pressure: - name: Outside Pressure - iir_filter: 16x - update_interval: 15s diff --git a/tests/components/bmp280/test.esp32-idf.yaml b/tests/components/bmp280_i2c/common.yaml similarity index 56% rename from tests/components/bmp280/test.esp32-idf.yaml rename to tests/components/bmp280_i2c/common.yaml index aeb1cb262b..edf52b2cd4 100644 --- a/tests/components/bmp280/test.esp32-idf.yaml +++ b/tests/components/bmp280_i2c/common.yaml @@ -1,15 +1,17 @@ i2c: - id: i2c_bmp280 - scl: 16 - sda: 17 + scl: ${scl_pin} + sda: ${sda_pin} sensor: - - platform: bmp280 + - platform: bmp280_i2c + i2c_id: i2c_bmp280 address: 0x77 temperature: + id: bmp280_temperature name: Outside Temperature - oversampling: 16x pressure: name: Outside Pressure + id: bmp280_pressure iir_filter: 16x update_interval: 15s diff --git a/tests/components/bmp280_i2c/test.esp32-ard.yaml b/tests/components/bmp280_i2c/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml b/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-idf.yaml b/tests/components/bmp280_i2c/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp8266-ard.yaml b/tests/components/bmp280_i2c/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp280_i2c/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.rp2040-ard.yaml b/tests/components/bmp280_i2c/test.rp2040-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/bmp280_i2c/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/common.yaml b/tests/components/bmp280_spi/common.yaml new file mode 100644 index 0000000000..798804de5b --- /dev/null +++ b/tests/components/bmp280_spi/common.yaml @@ -0,0 +1,18 @@ +spi: + - id: spi_bmp280 + clk_pin: ${clk_pin} + mosi_pin: ${mosi_pin} + miso_pin: ${miso_pin} + +sensor: + - platform: bmp280_spi + spi_id: spi_bmp280 + cs_pin: ${cs_pin} + temperature: + id: bmp280_temperature + name: Outside Temperature + pressure: + name: Outside Pressure + id: bmp280_pressure + iir_filter: 16x + update_interval: 15s diff --git a/tests/components/bmp280_spi/test.esp32-ard.yaml b/tests/components/bmp280_spi/test.esp32-ard.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp32-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-c3-ard.yaml b/tests/components/bmp280_spi/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp32-c3-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-c3-idf.yaml b/tests/components/bmp280_spi/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..2415ba5dc6 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp32-c3-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO6 + mosi_pin: GPIO7 + miso_pin: GPIO5 + cs_pin: GPIO8 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-idf.yaml b/tests/components/bmp280_spi/test.esp32-idf.yaml new file mode 100644 index 0000000000..54e027a614 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO16 + mosi_pin: GPIO17 + miso_pin: GPIO15 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp8266-ard.yaml b/tests/components/bmp280_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dbd158d030 --- /dev/null +++ b/tests/components/bmp280_spi/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO14 + mosi_pin: GPIO13 + miso_pin: GPIO12 + cs_pin: GPIO15 + +<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.rp2040-ard.yaml b/tests/components/bmp280_spi/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f6c3f1eeca --- /dev/null +++ b/tests/components/bmp280_spi/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + clk_pin: GPIO2 + mosi_pin: GPIO3 + miso_pin: GPIO4 + cs_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/ch422g/common.yaml b/tests/components/ch422g/common.yaml new file mode 100644 index 0000000000..02061bda59 --- /dev/null +++ b/tests/components/ch422g/common.yaml @@ -0,0 +1,20 @@ +ch422g: + - id: ch422g_hub + address: 0x24 + +binary_sensor: + - platform: gpio + id: ch422g_input + name: CH422G Binary Sensor + pin: + ch422g: ch422g_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: ch422g_output + pin: + ch422g: ch422g_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/ch422g/test.esp32-ard.yaml b/tests/components/ch422g/test.esp32-ard.yaml new file mode 100644 index 0000000000..cd3f1bbeef --- /dev/null +++ b/tests/components/ch422g/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-c3-ard.yaml b/tests/components/ch422g/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..cd822cb308 --- /dev/null +++ b/tests/components/ch422g/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-c3-idf.yaml b/tests/components/ch422g/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..cd822cb308 --- /dev/null +++ b/tests/components/ch422g/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-idf.yaml b/tests/components/ch422g/test.esp32-idf.yaml new file mode 100644 index 0000000000..cd3f1bbeef --- /dev/null +++ b/tests/components/ch422g/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp8266-ard.yaml b/tests/components/ch422g/test.esp8266-ard.yaml new file mode 100644 index 0000000000..cd822cb308 --- /dev/null +++ b/tests/components/ch422g/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ch422g/test.rp2040-ard.yaml b/tests/components/ch422g/test.rp2040-ard.yaml new file mode 100644 index 0000000000..cd822cb308 --- /dev/null +++ b/tests/components/ch422g/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ch422g + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/cst226/common.yaml b/tests/components/cst226/common.yaml index 4cbf38ef50..7e1c5dde36 100644 --- a/tests/components/cst226/common.yaml +++ b/tests/components/cst226/common.yaml @@ -12,6 +12,7 @@ display: dc_pin: GPIO4 reset_pin: number: GPIO21 + invert_colors: false i2c: scl: GPIO18 diff --git a/tests/components/cst816/common.yaml b/tests/components/cst816/common.yaml index f8deea6e98..91abbfd4f6 100644 --- a/tests/components/cst816/common.yaml +++ b/tests/components/cst816/common.yaml @@ -26,6 +26,7 @@ display: mirror_x: true mirror_y: true auto_clear_enabled: false + invert_colors: false spi: clk_pin: 14 diff --git a/tests/components/display/common.yaml b/tests/components/display/common.yaml index a22aa76780..1df2665067 100644 --- a/tests/components/display/common.yaml +++ b/tests/components/display/common.yaml @@ -11,6 +11,7 @@ display: cs_pin: 12 dc_pin: 13 reset_pin: 21 + invert_colors: false lambda: |- // Draw an analog clock in the center of the screen int centerX = it.get_width() / 2; diff --git a/tests/components/font/test.host.yaml b/tests/components/font/test.host.yaml new file mode 100644 index 0000000000..017328ec83 --- /dev/null +++ b/tests/components/font/test.host.yaml @@ -0,0 +1,23 @@ +font: + - file: "gfonts://Roboto" + id: roboto + size: 20 + glyphs: "0123456789." + extras: + - file: "gfonts://Roboto" + glyphs: ["\u00C4", "\u00C5", "\U000000C7"] + - file: "gfonts://Roboto" + id: roboto_web + size: 20 + - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + id: monocraft + size: 20 + - file: + type: web + url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + id: monocraft2 + size: 24 + - file: $component_dir/Monocraft.ttf + id: monocraft3 + size: 28 + diff --git a/tests/components/ft63x6/test.esp32-ard.yaml b/tests/components/ft63x6/test.esp32-ard.yaml index 32d6634dae..5c43cdff45 100644 --- a/tests/components/ft63x6/test.esp32-ard.yaml +++ b/tests/components/ft63x6/test.esp32-ard.yaml @@ -19,6 +19,7 @@ display: mirror_x: true mirror_y: true auto_clear_enabled: false + invert_colors: false touchscreen: - platform: ft63x6 diff --git a/tests/components/gt911/common.yaml b/tests/components/gt911/common.yaml new file mode 100644 index 0000000000..7bb88108da --- /dev/null +++ b/tests/components/gt911/common.yaml @@ -0,0 +1,25 @@ +i2c: + - id: i2c_gt911 + scl: 5 + sda: 4 + +display: + - platform: ssd1306_i2c + id: ssd1306_display + model: SSD1306_128X64 + reset_pin: 10 + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + +touchscreen: + - platform: gt911 + display: ssd1306_display + interrupt_pin: 20 + reset_pin: 21 + +binary_sensor: + - platform: gt911 + id: touch_key_911 + index: 0 diff --git a/tests/components/gt911/test.esp32-ard.yaml b/tests/components/gt911/test.esp32-ard.yaml index a47f7bf260..dade44d145 100644 --- a/tests/components/gt911/test.esp32-ard.yaml +++ b/tests/components/gt911/test.esp32-ard.yaml @@ -1,24 +1 @@ -i2c: - - id: i2c_gt911 - scl: 16 - sda: 17 - -display: - - platform: ssd1306_i2c - id: ssd1306_display - model: SSD1306_128X64 - reset_pin: 13 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: gt911 - display: ssd1306_display - interrupt_pin: 14 - -binary_sensor: - - platform: gt911 - id: touch_key_911 - index: 0 +<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-c3-ard.yaml b/tests/components/gt911/test.esp32-c3-ard.yaml index 43f7ac5902..dade44d145 100644 --- a/tests/components/gt911/test.esp32-c3-ard.yaml +++ b/tests/components/gt911/test.esp32-c3-ard.yaml @@ -1,24 +1 @@ -i2c: - - id: i2c_gt911 - scl: 5 - sda: 4 - -display: - - platform: ssd1306_i2c - id: ssd1306_display - model: SSD1306_128X64 - reset_pin: 3 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: gt911 - display: ssd1306_display - interrupt_pin: 6 - -binary_sensor: - - platform: gt911 - id: touch_key_911 - index: 0 +<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-c3-idf.yaml b/tests/components/gt911/test.esp32-c3-idf.yaml index 43f7ac5902..dade44d145 100644 --- a/tests/components/gt911/test.esp32-c3-idf.yaml +++ b/tests/components/gt911/test.esp32-c3-idf.yaml @@ -1,24 +1 @@ -i2c: - - id: i2c_gt911 - scl: 5 - sda: 4 - -display: - - platform: ssd1306_i2c - id: ssd1306_display - model: SSD1306_128X64 - reset_pin: 3 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: gt911 - display: ssd1306_display - interrupt_pin: 6 - -binary_sensor: - - platform: gt911 - id: touch_key_911 - index: 0 +<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-idf.yaml b/tests/components/gt911/test.esp32-idf.yaml index a47f7bf260..dade44d145 100644 --- a/tests/components/gt911/test.esp32-idf.yaml +++ b/tests/components/gt911/test.esp32-idf.yaml @@ -1,24 +1 @@ -i2c: - - id: i2c_gt911 - scl: 16 - sda: 17 - -display: - - platform: ssd1306_i2c - id: ssd1306_display - model: SSD1306_128X64 - reset_pin: 13 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: gt911 - display: ssd1306_display - interrupt_pin: 14 - -binary_sensor: - - platform: gt911 - id: touch_key_911 - index: 0 +<<: !include common.yaml diff --git a/tests/components/gt911/test.rp2040-ard.yaml b/tests/components/gt911/test.rp2040-ard.yaml index 43f7ac5902..dade44d145 100644 --- a/tests/components/gt911/test.rp2040-ard.yaml +++ b/tests/components/gt911/test.rp2040-ard.yaml @@ -1,24 +1 @@ -i2c: - - id: i2c_gt911 - scl: 5 - sda: 4 - -display: - - platform: ssd1306_i2c - id: ssd1306_display - model: SSD1306_128X64 - reset_pin: 3 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: gt911 - display: ssd1306_display - interrupt_pin: 6 - -binary_sensor: - - platform: gt911 - id: touch_key_911 - index: 0 +<<: !include common.yaml diff --git a/tests/components/homeassistant/common.yaml b/tests/components/homeassistant/common.yaml index ae016a3bea..9c6cb71b8b 100644 --- a/tests/components/homeassistant/common.yaml +++ b/tests/components/homeassistant/common.yaml @@ -13,12 +13,12 @@ esphome: message: The humidity is {{ my_variable }}%. variables: my_variable: "return id(ha_hello_world_temperature).state;" - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: message: Button was pressed - - homeassistant.service: - service: notify.html5 + - homeassistant.action: + action: notify.html5 data: title: New Humidity data_template: @@ -32,6 +32,32 @@ wifi: api: +switch: + - platform: homeassistant + entity_id: automation.my_cool_automation + id: my_cool_automation + - platform: homeassistant + entity_id: fan.my_cool_fan + id: my_cool_fan + - platform: homeassistant + entity_id: humidifier.my_cool_humidifier + id: my_cool_humidifier + - platform: homeassistant + entity_id: input_boolean.my_cool_input_boolean + id: my_cool_input_boolean + - platform: homeassistant + entity_id: light.my_cool_light + id: my_cool_light + - platform: homeassistant + entity_id: remote.my_cool_remote + id: my_cool_remote + - platform: homeassistant + entity_id: siren.my_cool_siren + id: my_cool_siren + - platform: homeassistant + entity_id: switch.my_cool_switch + id: my_cool_switch + binary_sensor: - platform: homeassistant entity_id: binary_sensor.hello_world @@ -41,6 +67,11 @@ binary_sensor: attribute: world id: ha_hello_world_binary_attribute +number: + - platform: homeassistant + entity_id: number.hello_world + id: ha_hello_world_number + sensor: - platform: homeassistant entity_id: sensor.hello_world diff --git a/tests/components/ili9xxx/test.esp32-ard.yaml b/tests/components/ili9xxx/test.esp32-ard.yaml index ecee21686e..850273230a 100644 --- a/tests/components/ili9xxx/test.esp32-ard.yaml +++ b/tests/components/ili9xxx/test.esp32-ard.yaml @@ -19,6 +19,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + invert_colors: false dimensions: width: 320 height: 240 diff --git a/tests/components/ili9xxx/test.esp32-c3-ard.yaml b/tests/components/ili9xxx/test.esp32-c3-ard.yaml index 9526ae1f6b..fd03bd54b7 100644 --- a/tests/components/ili9xxx/test.esp32-c3-ard.yaml +++ b/tests/components/ili9xxx/test.esp32-c3-ard.yaml @@ -20,6 +20,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + invert_colors: false dimensions: width: 320 height: 240 diff --git a/tests/components/ili9xxx/test.esp32-c3-idf.yaml b/tests/components/ili9xxx/test.esp32-c3-idf.yaml index 9526ae1f6b..fd03bd54b7 100644 --- a/tests/components/ili9xxx/test.esp32-c3-idf.yaml +++ b/tests/components/ili9xxx/test.esp32-c3-idf.yaml @@ -20,6 +20,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + invert_colors: false dimensions: width: 320 height: 240 diff --git a/tests/components/ili9xxx/test.esp8266-ard.yaml b/tests/components/ili9xxx/test.esp8266-ard.yaml index 0791c25aca..b8192e69d1 100644 --- a/tests/components/ili9xxx/test.esp8266-ard.yaml +++ b/tests/components/ili9xxx/test.esp8266-ard.yaml @@ -20,6 +20,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + invert_colors: false dimensions: width: 320 height: 240 diff --git a/tests/components/ili9xxx/test.rp2040-ard.yaml b/tests/components/ili9xxx/test.rp2040-ard.yaml index 54083ebce8..0423f41a1c 100644 --- a/tests/components/ili9xxx/test.rp2040-ard.yaml +++ b/tests/components/ili9xxx/test.rp2040-ard.yaml @@ -20,6 +20,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx + invert_colors: false dimensions: width: 320 height: 240 diff --git a/tests/components/image/test.esp32-ard.yaml b/tests/components/image/test.esp32-ard.yaml index ff9adde6b1..34c7914976 100644 --- a/tests/components/image/test.esp32-ard.yaml +++ b/tests/components/image/test.esp32-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 12 dc_pin: 13 reset_pin: 21 + invert_colors: true image: - id: binary_image diff --git a/tests/components/image/test.esp32-c3-ard.yaml b/tests/components/image/test.esp32-c3-ard.yaml index c083a97c94..91ff0a0579 100644 --- a/tests/components/image/test.esp32-c3-ard.yaml +++ b/tests/components/image/test.esp32-c3-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: true image: - id: binary_image diff --git a/tests/components/image/test.esp32-c3-idf.yaml b/tests/components/image/test.esp32-c3-idf.yaml index c083a97c94..91ff0a0579 100644 --- a/tests/components/image/test.esp32-c3-idf.yaml +++ b/tests/components/image/test.esp32-c3-idf.yaml @@ -11,6 +11,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: true image: - id: binary_image diff --git a/tests/components/image/test.esp32-idf.yaml b/tests/components/image/test.esp32-idf.yaml index ff9adde6b1..34c7914976 100644 --- a/tests/components/image/test.esp32-idf.yaml +++ b/tests/components/image/test.esp32-idf.yaml @@ -11,6 +11,7 @@ display: cs_pin: 12 dc_pin: 13 reset_pin: 21 + invert_colors: true image: - id: binary_image diff --git a/tests/components/image/test.esp8266-ard.yaml b/tests/components/image/test.esp8266-ard.yaml index 3632b95485..5a96ed9497 100644 --- a/tests/components/image/test.esp8266-ard.yaml +++ b/tests/components/image/test.esp8266-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 5 dc_pin: 15 reset_pin: 16 + invert_colors: true image: - id: binary_image diff --git a/tests/components/image/test.rp2040-ard.yaml b/tests/components/image/test.rp2040-ard.yaml index b79c8a9195..4c40ca464f 100644 --- a/tests/components/image/test.rp2040-ard.yaml +++ b/tests/components/image/test.rp2040-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 20 dc_pin: 21 reset_pin: 22 + invert_colors: true image: - id: binary_image diff --git a/tests/components/light/test.esp32-ard.yaml b/tests/components/light/test.esp32-ard.yaml index 7e5718d8d4..1d0b4cd8f0 100644 --- a/tests/components/light/test.esp32-ard.yaml +++ b/tests/components/light/test.esp32-ard.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.esp32-c3-ard.yaml b/tests/components/light/test.esp32-c3-ard.yaml index 8e1709838a..79171805a6 100644 --- a/tests/components/light/test.esp32-c3-ard.yaml +++ b/tests/components/light/test.esp32-c3-ard.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.esp32-c3-idf.yaml b/tests/components/light/test.esp32-c3-idf.yaml index 8e1709838a..79171805a6 100644 --- a/tests/components/light/test.esp32-c3-idf.yaml +++ b/tests/components/light/test.esp32-c3-idf.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.esp32-idf.yaml b/tests/components/light/test.esp32-idf.yaml index 7e5718d8d4..1d0b4cd8f0 100644 --- a/tests/components/light/test.esp32-idf.yaml +++ b/tests/components/light/test.esp32-idf.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.esp8266-ard.yaml b/tests/components/light/test.esp8266-ard.yaml index 4611fb374a..555e1a1b67 100644 --- a/tests/components/light/test.esp8266-ard.yaml +++ b/tests/components/light/test.esp8266-ard.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/light/test.rp2040-ard.yaml b/tests/components/light/test.rp2040-ard.yaml index 0215a17e71..a509bc85c9 100644 --- a/tests/components/light/test.rp2040-ard.yaml +++ b/tests/components/light/test.rp2040-ard.yaml @@ -15,6 +15,8 @@ esphome: - light.dim_relative: id: test_monochromatic_light relative_brightness: 5% + brightness_limits: + max_brightness: 90% output: - platform: gpio diff --git a/tests/components/ltr501/common.yaml b/tests/components/ltr501/common.yaml new file mode 100644 index 0000000000..b7074f52f2 --- /dev/null +++ b/tests/components/ltr501/common.yaml @@ -0,0 +1,9 @@ +sensor: + - platform: ltr501 + address: 0x23 + i2c_id: i2c_ltr501 + type: ALS_PS + gain: 1X + integration_time: 100ms + ambient_light: "Ambient light" + ps_counts: "Proximity counts" diff --git a/tests/components/ltr501/test.esp32-ard.yaml b/tests/components/ltr501/test.esp32-ard.yaml new file mode 100644 index 0000000000..4c710c74fe --- /dev/null +++ b/tests/components/ltr501/test.esp32-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-c3-ard.yaml b/tests/components/ltr501/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..9e7de2768d --- /dev/null +++ b/tests/components/ltr501/test.esp32-c3-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-c3-idf.yaml b/tests/components/ltr501/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..9e7de2768d --- /dev/null +++ b/tests/components/ltr501/test.esp32-c3-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-idf.yaml b/tests/components/ltr501/test.esp32-idf.yaml new file mode 100644 index 0000000000..4c710c74fe --- /dev/null +++ b/tests/components/ltr501/test.esp32-idf.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 16 + sda: 17 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp8266-ard.yaml b/tests/components/ltr501/test.esp8266-ard.yaml new file mode 100644 index 0000000000..9e7de2768d --- /dev/null +++ b/tests/components/ltr501/test.esp8266-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/ltr501/test.rp2040-ard.yaml b/tests/components/ltr501/test.rp2040-ard.yaml new file mode 100644 index 0000000000..9e7de2768d --- /dev/null +++ b/tests/components/ltr501/test.rp2040-ard.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_ltr501 + scl: 5 + sda: 4 + +<<: !include common.yaml diff --git a/tests/components/lvgl/.gitattributes b/tests/components/lvgl/.gitattributes new file mode 100644 index 0000000000..75e7a44254 --- /dev/null +++ b/tests/components/lvgl/.gitattributes @@ -0,0 +1,2 @@ +*.ttf -text + diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 8b92f8caa7..7ef7772ac9 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -8,3 +8,130 @@ touchscreen: x_max: 240 y_max: 320 +font: + - file: "$component_dir/roboto.ttf" + id: roboto20 + size: 20 + extras: + - file: '$component_dir/materialdesignicons-webfont.ttf' + glyphs: [ + "\U000F004B", + "\U0000f0ed", + "\U000F006E", + "\U000F012C", + "\U000F179B", + "\U000F0748", + "\U000F1A1B", + "\U000F02DC", + "\U000F0A02", + "\U000F035F", + "\U000F0156", + "\U000F0C5F", + "\U000f0084", + "\U000f0091", + ] + - file: "$component_dir/helvetica.ttf" + id: helvetica20 + - file: "$component_dir/roboto.ttf" + id: roboto10 + size: 10 + bpp: 4 + extras: + - file: '$component_dir/materialdesignicons-webfont.ttf' + glyphs: [ + "\U000F004B", + "\U0000f0ed", + "\U000F006E", + "\U000F012C", + "\U000F179B", + "\U000F0748", + "\U000F1A1B", + "\U000F02DC", + "\U000F0A02", + "\U000F035F", + "\U000F0156", + "\U000F0C5F", + "\U000f0084", + "\U000f0091", + ] + +sensor: + - platform: lvgl + id: lvgl_sensor_id + name: "LVGL Arc Sensor" + widget: lv_arc + - platform: lvgl + widget: slider_id + name: LVGL Slider + - platform: lvgl + widget: bar_id + id: lvgl_bar_sensor + name: LVGL Bar + - platform: lvgl + widget: spinbox_id + name: LVGL Spinbox + +number: + - platform: lvgl + widget: slider_id + name: LVGL Slider + update_on_release: true + - platform: lvgl + widget: lv_arc + id: lvgl_arc_number + name: LVGL Arc + - platform: lvgl + widget: bar_id + id: lvgl_bar_number + name: LVGL Bar + - platform: lvgl + widget: spinbox_id + id: lvgl_spinbox_number + name: LVGL Spinbox + +light: + - platform: lvgl + name: LVGL LED + id: lv_light + led: lv_led + +binary_sensor: + - platform: lvgl + id: lvgl_pressbutton + name: Pressbutton + widget: spin_up + publish_initial_state: true + - platform: lvgl + name: ButtonMatrix button + widget: button_a + - platform: lvgl + id: switch_d + name: Matrix switch D + widget: button_d + on_click: + then: + - lvgl.page.previous: + animation: move_right + time: 600ms + - platform: lvgl + id: button_checker + name: LVGL button + widget: spin_up + on_state: + then: + - lvgl.checkbox.update: + id: checkbox_id + state: + checked: !lambda return x; + text: Unchecked + - platform: lvgl + name: LVGL checkbox + widget: checkbox_id + +wifi: + ssid: SSID + password: PASSWORD123 + +time: + platform: sntp + id: time_id diff --git a/tests/components/lvgl/helvetica.ttf b/tests/components/lvgl/helvetica.ttf new file mode 100644 index 0000000000..7aec6f3f3c Binary files /dev/null and b/tests/components/lvgl/helvetica.ttf differ diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 696c749876..9d157ea5b0 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -1,114 +1,620 @@ lvgl: log_level: TRACE bg_color: light_blue + disp_bg_color: color_id + disp_bg_image: cat_image + theme: + obj: + border_width: 1 + + gradients: + - id: color_bar + direction: hor + dither: err_diff + stops: + - color: 0xFF0000 + position: 0 + - color: 0xFFFF00 + position: 42 + - color: 0x00FF00 + position: 84 + - color: 0x00FFFF + position: 127 + - color: 0x0000FF + position: 169 + - color: 0xFF00FF + position: 212 + - color: 0xFF0000 + position: 255 + + style_definitions: + - id: style_test + bg_color: 0x2F8CD8 + - id: header_footer + bg_color: 0x20214F + bg_grad_color: 0x005782 + bg_grad_dir: VER + bg_opa: cover + border_width: 0 + radius: 0 + pad_all: 0 + border_color: 0x0077b3 + text_color: 0xFFFFFF + width: 100% + height: 30 + border_side: [left, top] + text_decor: [underline, strikethrough] + - id: style_line + line_color: light_blue + line_width: 8 + line_rounded: true + - id: date_style + text_font: roboto10 + align: center + text_color: color_id2 + bg_opa: cover + radius: 4 + pad_all: 2 + - id: spin_button + height: 40 + width: 40 + - id: spin_label + align: center + text_align: center + text_font: space16 + - id: bdr_style + border_color: 0x808080 + border_width: 2 + pad_all: 4 + align: center + touchscreens: - touchscreen_id: tft_touch long_press_repeat_time: 200ms long_press_time: 500ms - widgets: - - label: - text: Hello world - text_color: 0xFF8000 - align: center - text_font: montserrat_40 - border_post: true - - label: - text: "Hello shiny day" - text_color: 0xFFFFFF - align: bottom_mid - text_font: space16 - - obj: - align: center - arc_opa: COVER - arc_color: 0xFF0000 - arc_rounded: false - arc_width: 3 - anim_time: 1s - bg_color: light_blue - bg_grad_color: light_blue - bg_dither_mode: ordered - bg_grad_dir: hor - bg_grad_stop: 128 - bg_image_opa: transp - bg_image_recolor: light_blue - bg_image_recolor_opa: 50% - bg_main_stop: 0 - bg_opa: 20% - border_color: 0x00FF00 - border_opa: cover - border_post: true - border_side: [bottom, left] + msgboxes: + - id: message_box + close_button: true + title: Messagebox + bg_color: 0xffff + body: + text: This is a sample messagebox + bg_color: 0x808080 + button_style: + bg_color: 0xff00 border_width: 4 - clip_corner: false - height: 50% - image_recolor: light_blue - image_recolor_opa: cover - line_width: 10 - line_dash_width: 10 - line_dash_gap: 10 - line_rounded: false - line_color: light_blue - opa: cover - opa_layered: cover - outline_color: light_blue - outline_opa: cover - outline_pad: 10px - outline_width: 10px - pad_all: 10px - pad_bottom: 10px - pad_column: 10px - pad_left: 10px - pad_right: 10px - pad_row: 10px - pad_top: 10px - shadow_color: light_blue - shadow_ofs_x: 5 - shadow_ofs_y: 5 - shadow_opa: cover - shadow_spread: 5 - shadow_width: 10 - text_align: auto - text_color: light_blue - text_decor: [underline, strikethrough] - text_font: montserrat_18 - text_letter_space: 4 - text_line_space: 4 - text_opa: cover - transform_angle: 180 - transform_height: 100 - transform_pivot_x: 50% - transform_pivot_y: 50% - transform_zoom: 0.5 - translate_x: 10 - translate_y: 10 - max_height: 100 - max_width: 200 - min_height: 20% - min_width: 20% - radius: circle - width: 10px - x: 100 - y: 120 - - button: - width: 20% - height: 10% - pressed: - bg_color: light_blue - widgets: - - label: - text: Button + buttons: + - id: msgbox_button + text: Button + - id: msgbox_apply + text: "Close" + on_click: + then: + - lvgl.widget.hide: message_box + - id: simple_msgbox + title: Simple + pages: + - id: page1 + on_load: + - logger.log: page loaded + - lvgl.widget.focus: + action: restore + on_unload: + - logger.log: page unloaded + - lvgl.widget.focus: mark + on_all_events: + logger.log: + format: "Event %s" + args: ['lv_event_code_name_for(event->code).c_str()'] + skip: true + layout: + type: flex + pad_row: 4 + pad_column: 4px + flex_align_main: center + flex_align_cross: start + flex_align_track: end + widgets: + - animimg: + height: 60 + id: anim_img + src: [cat_image, dog_image] + repeat_count: 10 + duration: 1s + auto_start: true + on_all_events: + logger.log: + format: "Event %s" + args: ['lv_event_code_name_for(event->code).c_str()'] + - label: + id: hello_label + text: Hello world + text_color: 0xFF8000 + align: center + text_font: montserrat_40 + border_post: true + on_click: + then: + - lvgl.animimg.stop: anim_img + - lvgl.update: + disp_bg_color: 0xffff00 + disp_bg_image: cat_image + - lvgl.widget.show: message_box + - label: + text: "Hello shiny day" + text_color: 0xFFFFFF + align: bottom_mid + text_font: space16 + - obj: + align: center + arc_opa: COVER + arc_color: 0xFF0000 + arc_rounded: false + arc_width: 3 + anim_time: 1s + bg_color: light_blue + bg_grad_color: light_blue + bg_dither_mode: ordered + bg_grad_dir: hor + bg_grad_stop: 128 + bg_image_opa: transp + bg_image_recolor: light_blue + bg_image_recolor_opa: 50% + bg_main_stop: 0 + bg_opa: 20% + border_color: 0x00FF00 + border_opa: cover + border_post: true + border_side: [bottom, left] + border_width: 4 + clip_corner: false + color_filter_opa: transp + height: 50% + image_recolor: light_blue + image_recolor_opa: cover + line_width: 10 + line_dash_width: 10 + line_dash_gap: 10 + line_rounded: false + line_color: light_blue + opa: cover + opa_layered: cover + outline_color: light_blue + outline_opa: cover + outline_pad: 10px + outline_width: 10px + pad_all: 10px + pad_bottom: 10px + pad_left: 10px + pad_right: 10px + pad_top: 10px + shadow_color: light_blue + shadow_ofs_x: 5 + shadow_ofs_y: 5 + shadow_opa: cover + shadow_spread: 5 + shadow_width: 10 + text_align: auto + text_color: light_blue + text_decor: [underline, strikethrough] + text_font: montserrat_18 + text_letter_space: 4 + text_line_space: 4 + text_opa: cover + transform_angle: 180 + transform_height: 100 + transform_pivot_x: 50% + transform_pivot_y: 50% + transform_zoom: 0.5 + translate_x: 10 + translate_y: 10 + max_height: 100 + max_width: 200 + min_height: 20% + min_width: 20% + radius: circle + width: 10px + x: 100 + y: 120 + - buttonmatrix: + on_press: + logger.log: + format: "matrix button pressed: %d" + args: ["x"] + on_long_press: + lvgl.matrix.button.update: + id: [button_a, button_e, button_c] + control: + disabled: true + on_click: + logger.log: + format: "matrix button clicked: %d, is button_a = %u" + args: ["x", "id(button_a) == x"] + items: + checked: + bg_color: 0xFFFF00 + id: b_matrix + + rows: + - buttons: + - id: button_a + text: home icon + width: 2 + control: + checkable: true + on_value: + logger.log: + format: "button_a value %d" + args: [x] + - id: button_b + text: B + width: 1 + on_value: + logger.log: + format: "button_b value %d" + args: [x] + on_click: + then: + - lvgl.page.previous: + control: + hidden: false + - buttons: + - id: button_c + text: C + control: + checkable: false + - id: button_d + text: menu left + on_long_press: + then: + logger.log: Long pressed + on_long_press_repeat: + then: + logger.log: Long pressed repeated + - buttons: + - id: button_e + - button: + id: button_button + width: 20% + height: 10% + pressed: + bg_color: light_blue + checkable: true + checked: + bg_color: 0x000000 + widgets: + - label: + text: Button + on_click: + - lvgl.widget.focus: spin_up + - lvgl.widget.focus: next + - lvgl.widget.focus: previous + - lvgl.widget.focus: + action: previous + freeze: true + - lvgl.widget.focus: + id: spin_up + freeze: true + editing: true + - lvgl.label.update: + id: hello_label + bg_color: 0x123456 + text: clicked + - lvgl.label.update: + id: hello_label + text: !lambda return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return "hello world"; + - lvgl.label.update: + id: hello_label + text: !lambda 'return str_sprintf("Hello space");' + - lvgl.label.update: + id: hello_label + text: + format: "sprintf format %s" + args: ['x ? "checked" : "unchecked"'] + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: time_id + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda return id(time_id).now(); + - lvgl.label.update: + id: hello_label + text: + time_format: "%c" + time: !lambda |- + ESP_LOGD("label", "multi-line lambda"); + return id(time_id).now(); + on_value: + logger.log: + format: "state now %d" + args: [x] + on_short_click: + lvgl.widget.hide: hello_label + on_long_press: + lvgl.widget.show: hello_label + on_cancel: + lvgl.widget.enable: hello_label + on_ready: + lvgl.widget.disable: hello_label + on_defocus: + lvgl.widget.hide: hello_label + on_focus: + logger.log: Button clicked + on_scroll: + logger.log: Button clicked + on_scroll_end: + logger.log: Button clicked + on_scroll_begin: + logger.log: Button clicked + on_release: + logger.log: Button clicked + on_long_press_repeat: + logger.log: Button clicked + - led: + id: lv_led + color: 0x00FF00 + brightness: 50% + align: right_mid + - spinner: + arc_length: 120 + spin_time: 2s + align: left_mid + - image: + src: cat_image + align: top_left + y: 50 + - tileview: + id: tileview_id + scrollbar_mode: active + on_value: + then: + - if: + condition: + lambda: return tile == id(tile_1); + then: + - logger.log: "tile 1 is now showing" + tiles: + - id: tile_1 + row: 0 + column: 0 + dir: ALL + widgets: + - obj: + bg_color: 0x000000 + - id: tile_2 + row: 1 + column: 0 + dir: [VER, HOR] + widgets: + - obj: + bg_color: 0x000000 + + - id: page2 + widgets: + - slider: + min_value: 0 + max_value: 255 + bg_opa: cover + bg_grad: color_bar + radius: 0 + indicator: + bg_opa: transp + knob: + radius: 1 + width: 4 + height: 10% + bg_color: 0x000000 + width: 100% + height: 10% + align: top_mid + - button: + styles: spin_button + id: spin_up + on_click: + - lvgl.spinbox.increment: spinbox_id + widgets: + - label: + styles: spin_label + text: "+" + - spinbox: + text_font: space16 + id: spinbox_id + align: center + width: 120 + range_from: -10 + range_to: 1000 + step: 5.0 + rollover: false + digits: 6 + decimal_places: 2 + value: 15 + on_value: + then: + - logger.log: + format: "Spinbox value is %f" + args: [x] + - button: + styles: spin_button + id: spin_down + on_click: + - lvgl.spinbox.decrement: spinbox_id + widgets: + - label: + styles: spin_label + text: "-" + - arc: + align: left_mid + id: lv_arc + adjustable: true + on_value: + then: + - logger.log: + format: "Arc value is %f" + args: [x] + scroll_on_focus: true + value: 75 + min_value: 1 + max_value: 100 + arc_color: 0xFF0000 + indicator: + arc_color: 0xF000FF + pressed: + arc_color: 0xFFFF00 + focused: + arc_color: 0x808080 + - bar: + id: bar_id + align: top_mid + y: 20 + value: 30 + max_value: 100 + min_value: 10 + mode: range + on_click: + then: + - lvgl.bar.update: + id: bar_id + value: !lambda return (int)((float)rand() / RAND_MAX * 100); + - logger.log: + format: "bar value %f" + args: [x] + - line: + id: lv_line_id + align: center + points: + - 5, 5 + - 70, 70 + - 120, 10 + - 180, 60 + - 240, 10 + on_click: + - lvgl.widget.update: + id: lv_line_id + line_color: 0xFFFF + - lvgl.page.next: + - switch: + align: right_mid + - checkbox: + id: checkbox_id + text: Checkbox + align: bottom_right + - slider: + id: slider_id + align: top_mid + y: 40 + value: 30 + max_value: 100 + min_value: 10 + mode: normal + on_value: + then: + - logger.log: + format: "slider value %f" + args: [x] + on_click: + then: + - lvgl.slider.update: + id: slider_id + value: !lambda return (int)((float)rand() / RAND_MAX * 100); + - tabview: + id: tabview_id + width: 100% + height: 80% + position: top + on_value: + then: + - if: + condition: + lambda: return tab == id(tabview_tab_1); + then: + - logger.log: "Dog tab is now showing" + tabs: + - name: Dog + id: tabview_tab_1 + border_width: 2 + border_color: 0xff0000 + width: 100% + pad_all: 8 + layout: + type: grid + grid_row_align: end + grid_rows: [25px, fr(1), content] + grid_columns: [40, fr(1), fr(1)] + pad_row: 6px + pad_column: 0 + widgets: + - image: + grid_cell_row_pos: 0 + grid_cell_column_pos: 0 + src: dog_image + on_click: + then: + - lvgl.tabview.select: + id: tabview_id + index: 1 + animated: true + - label: + styles: bdr_style + grid_cell_x_align: center + grid_cell_y_align: stretch + grid_cell_row_pos: 0 + grid_cell_column_pos: 1 + grid_cell_column_span: 1 + text: "Grid cell 0/1" + - label: + grid_cell_x_align: end + styles: bdr_style + grid_cell_row_pos: 1 + grid_cell_column_pos: 0 + text: "Grid cell 1/0" + - label: + styles: bdr_style + grid_cell_row_pos: 1 + grid_cell_column_pos: 1 + text: "Grid cell 1/1" + - label: + id: cell_1_3 + styles: bdr_style + grid_cell_row_pos: 1 + grid_cell_column_pos: 2 + text: "Grid cell 1/2" + - name: Cat + id: tabview_tab_2 + widgets: + - image: + src: cat_image + on_click: + then: + - logger.log: Cat image clicked + - lvgl.tabview.select: + id: tabview_id + index: 0 + animated: true font: - file: "gfonts://Roboto" id: space16 bpp: 4 image: - - id: cat_img + - id: cat_image resize: 256x48 file: $component_dir/logo-text.svg - - id: dog_img + - id: dog_image file: $component_dir/logo-text.svg resize: 256x48 type: TRANSPARENT_BINARY @@ -116,3 +622,13 @@ image: color: - id: light_blue hex: "3340FF" + - id: color_id + red: 0.5 + green: 0.5 + blue: 0.5 + white: 0.5 + - id: color_id2 + red_int: 0xFF + green_int: 123 + blue_int: 64 + white_int: 255 diff --git a/tests/components/lvgl/materialdesignicons-webfont.ttf b/tests/components/lvgl/materialdesignicons-webfont.ttf new file mode 100644 index 0000000000..eb4f4c45a7 Binary files /dev/null and b/tests/components/lvgl/materialdesignicons-webfont.ttf differ diff --git a/tests/components/lvgl/roboto.ttf b/tests/components/lvgl/roboto.ttf new file mode 100644 index 0000000000..2c97eeadff Binary files /dev/null and b/tests/components/lvgl/roboto.ttf differ diff --git a/tests/components/lvgl/test.esp32-ard.yaml b/tests/components/lvgl/test.esp32-ard.yaml index abfb324ea5..51593e7967 100644 --- a/tests/components/lvgl/test.esp32-ard.yaml +++ b/tests/components/lvgl/test.esp32-ard.yaml @@ -24,6 +24,34 @@ display: invert_colors: false update_interval: never +binary_sensor: + - platform: gpio + internal: true + id: up_button + pin: + number: GPIO38 + inverted: true + - platform: gpio + internal: true + id: down_button + pin: + number: GPIO37 + inverted: true + - platform: gpio + internal: true + id: select_button + pin: + number: GPIO39 + inverted: true +lvgl: + encoders: + group: switches + initial_focus: button_button + enter_button: select_button + sensor: + left_button: up_button + right_button: down_button + packages: lvgl: !include lvgl-package.yaml diff --git a/tests/components/lvgl/test.esp32-idf.yaml b/tests/components/lvgl/test.esp32-idf.yaml index eab75b05f3..927d72d15c 100644 --- a/tests/components/lvgl/test.esp32-idf.yaml +++ b/tests/components/lvgl/test.esp32-idf.yaml @@ -6,6 +6,23 @@ i2c: sda: GPIO18 scl: GPIO19 +sensor: + - platform: rotary_encoder + name: "Rotary Encoder" + id: encoder + pin_a: 2 + pin_b: 1 + internal: true + +binary_sensor: + - platform: gpio + id: pushbutton + name: Pushbutton + pin: + number: 0 + inverted: true + ignore_strapping_warning: true + display: - platform: ili9xxx model: st7789v @@ -50,5 +67,9 @@ lvgl: displays: - tft_display - second_display + encoders: + sensor: encoder + enter_button: pushbutton + group: general <<: !include common.yaml diff --git a/tests/components/max31856/test.esp32-ard.yaml b/tests/components/max31856/test.esp32-ard.yaml index 5561903207..9a4da6b2a2 100644 --- a/tests/components/max31856/test.esp32-ard.yaml +++ b/tests/components/max31856/test.esp32-ard.yaml @@ -10,3 +10,4 @@ sensor: cs_pin: 12 update_interval: 15s mains_filter: 50Hz + thermocouple_type: N diff --git a/tests/components/max31856/test.esp32-c3-ard.yaml b/tests/components/max31856/test.esp32-c3-ard.yaml index 2794866c59..71bbfffb7b 100644 --- a/tests/components/max31856/test.esp32-c3-ard.yaml +++ b/tests/components/max31856/test.esp32-c3-ard.yaml @@ -10,3 +10,5 @@ sensor: cs_pin: 8 update_interval: 15s mains_filter: 50Hz + thermocouple_type: N + diff --git a/tests/components/max31856/test.esp32-c3-idf.yaml b/tests/components/max31856/test.esp32-c3-idf.yaml index 2794866c59..71bbfffb7b 100644 --- a/tests/components/max31856/test.esp32-c3-idf.yaml +++ b/tests/components/max31856/test.esp32-c3-idf.yaml @@ -10,3 +10,5 @@ sensor: cs_pin: 8 update_interval: 15s mains_filter: 50Hz + thermocouple_type: N + diff --git a/tests/components/max31856/test.esp32-idf.yaml b/tests/components/max31856/test.esp32-idf.yaml index 5561903207..9a4da6b2a2 100644 --- a/tests/components/max31856/test.esp32-idf.yaml +++ b/tests/components/max31856/test.esp32-idf.yaml @@ -10,3 +10,4 @@ sensor: cs_pin: 12 update_interval: 15s mains_filter: 50Hz + thermocouple_type: N diff --git a/tests/components/max31856/test.esp8266-ard.yaml b/tests/components/max31856/test.esp8266-ard.yaml index dfd9572ca9..b9c42542fd 100644 --- a/tests/components/max31856/test.esp8266-ard.yaml +++ b/tests/components/max31856/test.esp8266-ard.yaml @@ -10,3 +10,5 @@ sensor: cs_pin: 15 update_interval: 15s mains_filter: 50Hz + thermocouple_type: N + diff --git a/tests/components/max31856/test.rp2040-ard.yaml b/tests/components/max31856/test.rp2040-ard.yaml index 0abc8a081b..8607eb18cf 100644 --- a/tests/components/max31856/test.rp2040-ard.yaml +++ b/tests/components/max31856/test.rp2040-ard.yaml @@ -10,3 +10,5 @@ sensor: cs_pin: 6 update_interval: 15s mains_filter: 50Hz + thermocouple_type: N + diff --git a/tests/components/modbus_controller/test.esp32-ard.yaml b/tests/components/modbus_controller/test.esp32-ard.yaml index 3e022b10ab..cd95d149cb 100644 --- a/tests/components/modbus_controller/test.esp32-ard.yaml +++ b/tests/components/modbus_controller/test.esp32-ard.yaml @@ -20,6 +20,7 @@ modbus_controller: - id: modbus_controller1 address: 0x2 modbus_id: mod_bus1 + allow_duplicate_commands: false - id: modbus_controller2 address: 0x2 modbus_id: mod_bus2 @@ -28,3 +29,4 @@ modbus_controller: value_type: S_DWORD_R read_lambda: |- return 42.3; + max_cmd_retries: 0 diff --git a/tests/components/modbus_controller/test.esp32-idf.yaml b/tests/components/modbus_controller/test.esp32-idf.yaml index c5fe3fd057..ba28e94d73 100644 --- a/tests/components/modbus_controller/test.esp32-idf.yaml +++ b/tests/components/modbus_controller/test.esp32-idf.yaml @@ -12,3 +12,5 @@ modbus_controller: - id: modbus_controller1 address: 0x2 modbus_id: mod_bus1 + allow_duplicate_commands: true + max_cmd_retries: 10 diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index a2a751df63..b7d1655ec9 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -426,3 +426,9 @@ valve: } else { return VALVE_CLOSED; } + +alarm_control_panel: + - platform: template + name: Alarm Control Panel + binary_sensors: + - input: some_binary_sensor diff --git a/tests/components/network/test-ipv6.bk72xx-ard.yaml b/tests/components/network/test-ipv6.bk72xx-ard.yaml new file mode 100644 index 0000000000..d0c4bbfcb9 --- /dev/null +++ b/tests/components/network/test-ipv6.bk72xx-ard.yaml @@ -0,0 +1,8 @@ +substitutions: + network_enable_ipv6: "true" + +bk72xx: + framework: + version: 1.7.0 + +<<: !include common.yaml diff --git a/tests/components/online_image/common-esp32.yaml b/tests/components/online_image/common-esp32.yaml new file mode 100644 index 0000000000..d3a304cdc0 --- /dev/null +++ b/tests/components/online_image/common-esp32.yaml @@ -0,0 +1,19 @@ +<<: !include common.yaml + +spi: + - id: spi_main_lcd + clk_pin: 16 + mosi_pin: 17 + miso_pin: 15 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 12 + dc_pin: 13 + reset_pin: 21 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/common-esp8266.yaml b/tests/components/online_image/common-esp8266.yaml new file mode 100644 index 0000000000..ba15b5025c --- /dev/null +++ b/tests/components/online_image/common-esp8266.yaml @@ -0,0 +1,19 @@ +<<: !include common.yaml + +spi: + - id: spi_main_lcd + clk_pin: 14 + mosi_pin: 13 + miso_pin: 12 + +display: + - platform: ili9xxx + id: main_lcd + model: ili9342 + cs_pin: 15 + dc_pin: 3 + reset_pin: 1 + invert_colors: true + lambda: |- + it.fill(Color(0, 0, 0)); + it.image(0, 0, id(online_rgba_image)); diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml new file mode 100644 index 0000000000..8f7ea6238b --- /dev/null +++ b/tests/components/online_image/common.yaml @@ -0,0 +1,37 @@ +wifi: + ssid: MySSID + password: password1 + +# Purposely test that `online_image:` does auto-load `image:` +# Keep the `image:` undefined. +# image: +online_image: + - id: online_binary_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: BINARY + resize: 50x50 + - id: online_binary_transparent_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + type: TRANSPARENT_BINARY + format: png + - id: online_rgba_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: RGBA + - id: online_rgb24_image + url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png + format: PNG + type: RGB24 + use_transparency: true + +# Check the set_url action +time: + - platform: sntp + on_time: + - at: "13:37:42" + then: + - online_image.set_url: + id: online_rgba_image + url: http://www.example.org/example.png + diff --git a/tests/components/online_image/test.esp32-ard.yaml b/tests/components/online_image/test.esp32-ard.yaml new file mode 100644 index 0000000000..4111cbd0ad --- /dev/null +++ b/tests/components/online_image/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +<<: !include common-esp32.yaml + +http_request: + verify_ssl: false diff --git a/tests/components/online_image/test.esp32-idf.yaml b/tests/components/online_image/test.esp32-idf.yaml new file mode 100644 index 0000000000..3f01009812 --- /dev/null +++ b/tests/components/online_image/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +<<: !include common-esp32.yaml + +http_request: + diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml new file mode 100644 index 0000000000..4148b280d0 --- /dev/null +++ b/tests/components/opentherm/common.yaml @@ -0,0 +1,3 @@ +opentherm: + in_pin: 1 + out_pin: 2 diff --git a/tests/components/opentherm/test.esp32-ard.yaml b/tests/components/opentherm/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp32-c3-ard.yaml b/tests/components/opentherm/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp32-c3-idf.yaml b/tests/components/opentherm/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp32-idf.yaml b/tests/components/opentherm/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/opentherm/test.esp8266-ard.yaml b/tests/components/opentherm/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/opentherm/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp32-ard.yaml b/tests/components/pipsolar/test.esp32-ard.yaml index fcd4575739..b7a7e0cbd9 100644 --- a/tests/components/pipsolar/test.esp32-ard.yaml +++ b/tests/components/pipsolar/test.esp32-ard.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.esp32-c3-ard.yaml b/tests/components/pipsolar/test.esp32-c3-ard.yaml index 12e9266343..83d7070669 100644 --- a/tests/components/pipsolar/test.esp32-c3-ard.yaml +++ b/tests/components/pipsolar/test.esp32-c3-ard.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.esp32-c3-idf.yaml b/tests/components/pipsolar/test.esp32-c3-idf.yaml index 12e9266343..83d7070669 100644 --- a/tests/components/pipsolar/test.esp32-c3-idf.yaml +++ b/tests/components/pipsolar/test.esp32-c3-idf.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.esp32-idf.yaml b/tests/components/pipsolar/test.esp32-idf.yaml index fcd4575739..b7a7e0cbd9 100644 --- a/tests/components/pipsolar/test.esp32-idf.yaml +++ b/tests/components/pipsolar/test.esp32-idf.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.esp8266-ard.yaml b/tests/components/pipsolar/test.esp8266-ard.yaml index 12e9266343..83d7070669 100644 --- a/tests/components/pipsolar/test.esp8266-ard.yaml +++ b/tests/components/pipsolar/test.esp8266-ard.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/pipsolar/test.rp2040-ard.yaml b/tests/components/pipsolar/test.rp2040-ard.yaml index 12e9266343..83d7070669 100644 --- a/tests/components/pipsolar/test.rp2040-ard.yaml +++ b/tests/components/pipsolar/test.rp2040-ard.yaml @@ -220,6 +220,8 @@ switch: name: inverter0_output_source_priority_solar output_source_priority_battery: name: inverter0_output_source_priority_battery + output_source_priority_hybrid: + name: inverter0_output_source_priority_hybrid input_voltage_range: name: inverter0_input_voltage_range pv_ok_condition_for_parallel: diff --git a/tests/components/qr_code/test.esp32-ard.yaml b/tests/components/qr_code/test.esp32-ard.yaml index 3e70d3258f..8689d4d73f 100644 --- a/tests/components/qr_code/test.esp32-ard.yaml +++ b/tests/components/qr_code/test.esp32-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 12 dc_pin: 13 reset_pin: 21 + invert_colors: false lambda: |- // Draw a QR code in the center of the screen auto scale = 2; diff --git a/tests/components/qr_code/test.esp32-c3-ard.yaml b/tests/components/qr_code/test.esp32-c3-ard.yaml index 63973b1aa2..3690d2598c 100644 --- a/tests/components/qr_code/test.esp32-c3-ard.yaml +++ b/tests/components/qr_code/test.esp32-c3-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: false lambda: |- // Draw a QR code in the center of the screen auto scale = 2; diff --git a/tests/components/qr_code/test.esp32-c3-idf.yaml b/tests/components/qr_code/test.esp32-c3-idf.yaml index 63973b1aa2..3690d2598c 100644 --- a/tests/components/qr_code/test.esp32-c3-idf.yaml +++ b/tests/components/qr_code/test.esp32-c3-idf.yaml @@ -11,6 +11,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: false lambda: |- // Draw a QR code in the center of the screen auto scale = 2; diff --git a/tests/components/qr_code/test.esp32-idf.yaml b/tests/components/qr_code/test.esp32-idf.yaml index 3e70d3258f..8689d4d73f 100644 --- a/tests/components/qr_code/test.esp32-idf.yaml +++ b/tests/components/qr_code/test.esp32-idf.yaml @@ -11,6 +11,7 @@ display: cs_pin: 12 dc_pin: 13 reset_pin: 21 + invert_colors: false lambda: |- // Draw a QR code in the center of the screen auto scale = 2; diff --git a/tests/components/qr_code/test.esp8266-ard.yaml b/tests/components/qr_code/test.esp8266-ard.yaml index 3c304d7575..02dc183440 100644 --- a/tests/components/qr_code/test.esp8266-ard.yaml +++ b/tests/components/qr_code/test.esp8266-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 5 dc_pin: 15 reset_pin: 16 + invert_colors: false lambda: |- // Draw a QR code in the center of the screen auto scale = 2; diff --git a/tests/components/qr_code/test.rp2040-ard.yaml b/tests/components/qr_code/test.rp2040-ard.yaml index 94cb772ba3..0d86f8d213 100644 --- a/tests/components/qr_code/test.rp2040-ard.yaml +++ b/tests/components/qr_code/test.rp2040-ard.yaml @@ -11,6 +11,7 @@ display: cs_pin: 20 dc_pin: 21 reset_pin: 22 + invert_colors: false lambda: |- // Draw a QR code in the center of the screen auto scale = 2; diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml index 416e203d7b..ab20f36eb6 100644 --- a/tests/components/speaker/test.esp32-ard.yaml +++ b/tests/components/speaker/test.esp32-ard.yaml @@ -1,8 +1,15 @@ esphome: on_boot: then: - - speaker.play: [0, 1, 2, 3] - - speaker.stop + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: i2s_audio: i2s_lrclk_pin: 16 @@ -14,4 +21,3 @@ speaker: id: speaker_id dac_type: external i2s_dout_pin: 13 - mode: mono diff --git a/tests/components/speaker/test.esp32-c3-ard.yaml b/tests/components/speaker/test.esp32-c3-ard.yaml index c7809baace..c966f9daa7 100644 --- a/tests/components/speaker/test.esp32-c3-ard.yaml +++ b/tests/components/speaker/test.esp32-c3-ard.yaml @@ -1,8 +1,15 @@ esphome: on_boot: then: - - speaker.play: [0, 1, 2, 3] - - speaker.stop + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: i2s_audio: i2s_lrclk_pin: 6 @@ -14,4 +21,3 @@ speaker: id: speaker_id dac_type: external i2s_dout_pin: 3 - mode: mono diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml index c7809baace..c966f9daa7 100644 --- a/tests/components/speaker/test.esp32-c3-idf.yaml +++ b/tests/components/speaker/test.esp32-c3-idf.yaml @@ -1,8 +1,15 @@ esphome: on_boot: then: - - speaker.play: [0, 1, 2, 3] - - speaker.stop + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: i2s_audio: i2s_lrclk_pin: 6 @@ -14,4 +21,3 @@ speaker: id: speaker_id dac_type: external i2s_dout_pin: 3 - mode: mono diff --git a/tests/components/speaker/test.esp32-idf.yaml b/tests/components/speaker/test.esp32-idf.yaml index 416e203d7b..ab20f36eb6 100644 --- a/tests/components/speaker/test.esp32-idf.yaml +++ b/tests/components/speaker/test.esp32-idf.yaml @@ -1,8 +1,15 @@ esphome: on_boot: then: - - speaker.play: [0, 1, 2, 3] - - speaker.stop + - if: + condition: speaker.is_stopped + then: + - speaker.play: [0, 1, 2, 3] + - if: + condition: speaker.is_playing + then: + - speaker.finish: + - speaker.stop: i2s_audio: i2s_lrclk_pin: 16 @@ -14,4 +21,3 @@ speaker: id: speaker_id dac_type: external i2s_dout_pin: 13 - mode: mono diff --git a/tests/components/st7701s/common.yaml b/tests/components/st7701s/common.yaml index 497df8c8ce..6e4fccc9cb 100644 --- a/tests/components/st7701s/common.yaml +++ b/tests/components/st7701s/common.yaml @@ -38,7 +38,12 @@ display: hsync_pin: 16 vsync_pin: 17 pclk_pin: 21 - init_sequence: 1 + init_sequence: + - 1 + - [0x23, 0xA, 0xB] + - delay 20ms + - [0x23, 0xA, 0xB] + - delay 0.2s data_pins: - number: 0 ignore_strapping_warning: true diff --git a/tests/components/statsD/common.yaml b/tests/components/statsD/common.yaml new file mode 100644 index 0000000000..5878101de8 --- /dev/null +++ b/tests/components/statsD/common.yaml @@ -0,0 +1,29 @@ +wifi: + ssid: MySSID + password: password1 + +statsd: + host: "192.168.1.1" + port: 8125 + prefix: esphome + update_interval: 60s + sensors: + id: s + name: sensors + binary_sensors: + id: bs + name: binary_sensors + +sensor: + - platform: template + id: s + name: "42.1" + lambda: |- + return 42.1f; + +binary_sensor: + - platform: template + id: bs + name: "On" + lambda: |- + return true; diff --git a/tests/components/statsD/test.bk72xx-ard.yaml b/tests/components/statsD/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-ard.yaml b/tests/components/statsD/test.esp32-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp32-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-c3-ard.yaml b/tests/components/statsD/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp32-c3-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-c3-idf.yaml b/tests/components/statsD/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp32-c3-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp32-idf.yaml b/tests/components/statsD/test.esp32-idf.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.esp8266-ard.yaml b/tests/components/statsD/test.esp8266-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/statsD/test.rp2040-ard.yaml b/tests/components/statsD/test.rp2040-ard.yaml new file mode 100644 index 0000000000..25cb37a0b4 --- /dev/null +++ b/tests/components/statsD/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + common: !include common.yaml diff --git a/tests/components/tt21100/test.esp32-s2-ard.yaml b/tests/components/tt21100/test.esp32-s2-ard.yaml index 7ebabcb130..86b9e7530d 100644 --- a/tests/components/tt21100/test.esp32-s2-ard.yaml +++ b/tests/components/tt21100/test.esp32-s2-ard.yaml @@ -18,6 +18,7 @@ display: data_rate: 40MHz dimensions: 320x240 update_interval: never + invert_colors: false transform: mirror_y: false mirror_x: false diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml new file mode 100644 index 0000000000..3bdc19ece5 --- /dev/null +++ b/tests/components/udp/common.yaml @@ -0,0 +1,35 @@ +wifi: + ssid: MySSID + password: password1 + +udp: + update_interval: 5s + encryption: "our key goes here" + rolling_code_enable: true + ping_pong_enable: true + binary_sensors: + - binary_sensor_id1 + - id: binary_sensor_id1 + broadcast_id: other_id + sensors: + - sensor_id1 + - id: sensor_id1 + broadcast_id: other_id + providers: + - name: some-device-name + encryption: "their key goes here" + +sensor: + - platform: template + id: sensor_id1 + - platform: udp + provider: some-device-name + id: our_id + remote_id: some_sensor_id + +binary_sensor: + - platform: udp + provider: unencrypted-device + id: other_binary_sensor_id + - platform: template + id: binary_sensor_id1 diff --git a/tests/components/udp/test.bk72xx-ard.yaml b/tests/components/udp/test.bk72xx-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.bk72xx-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-ard.yaml b/tests/components/udp/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-c3-ard.yaml b/tests/components/udp/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-c3-idf.yaml b/tests/components/udp/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp32-idf.yaml b/tests/components/udp/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.esp8266-ard.yaml b/tests/components/udp/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/udp/test.host.yaml b/tests/components/udp/test.host.yaml new file mode 100644 index 0000000000..e735c37e4d --- /dev/null +++ b/tests/components/udp/test.host.yaml @@ -0,0 +1,4 @@ +packages: + common: !include common.yaml + +wifi: !remove diff --git a/tests/components/udp/test.rp2040-ard.yaml b/tests/components/udp/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/udp/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/update/common.yaml b/tests/components/update/common.yaml index 91b8669505..dcb4f42527 100644 --- a/tests/components/update/common.yaml +++ b/tests/components/update/common.yaml @@ -1 +1,28 @@ +substitutions: + verify_ssl: "true" + +esphome: + on_boot: + then: + - if: + condition: + update.is_available: + then: + - logger.log: "Update available" + - update.perform: + force_update: true + +wifi: + ssid: MySSID + password: password1 + +http_request: + verify_ssl: ${verify_ssl} + +ota: + - platform: http_request + update: + - platform: http_request + name: Firmware Update + source: http://example.com/manifest.json diff --git a/tests/components/update/test.esp32-ard.yaml b/tests/components/update/test.esp32-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.esp32-ard.yaml +++ b/tests/components/update/test.esp32-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml diff --git a/tests/components/update/test.esp8266-ard.yaml b/tests/components/update/test.esp8266-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.esp8266-ard.yaml +++ b/tests/components/update/test.esp8266-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml diff --git a/tests/components/update/test.rp2040-ard.yaml b/tests/components/update/test.rp2040-ard.yaml index dade44d145..c1937b5a10 100644 --- a/tests/components/update/test.rp2040-ard.yaml +++ b/tests/components/update/test.rp2040-ard.yaml @@ -1 +1,4 @@ +substitutions: + verify_ssl: "false" + <<: !include common.yaml diff --git a/tests/components/voice_assistant/test.esp32-ard.yaml b/tests/components/voice_assistant/test.esp32-ard.yaml index 2e0209311d..cbf9460087 100644 --- a/tests/components/voice_assistant/test.esp32-ard.yaml +++ b/tests/components/voice_assistant/test.esp32-ard.yaml @@ -28,11 +28,11 @@ speaker: id: speaker_id dac_type: external i2s_dout_pin: 12 - mode: mono voice_assistant: microphone: mic_id_external speaker: speaker_id + conversation_timeout: 60s on_listening: - logger.log: "Voice assistant microphone listening" on_start: diff --git a/tests/components/voice_assistant/test.esp32-c3-ard.yaml b/tests/components/voice_assistant/test.esp32-c3-ard.yaml index 248ae4d0dc..86357fad36 100644 --- a/tests/components/voice_assistant/test.esp32-c3-ard.yaml +++ b/tests/components/voice_assistant/test.esp32-c3-ard.yaml @@ -28,7 +28,6 @@ speaker: id: speaker_id dac_type: external i2s_dout_pin: 2 - mode: mono voice_assistant: microphone: mic_id_external diff --git a/tests/components/voice_assistant/test.esp32-c3-idf.yaml b/tests/components/voice_assistant/test.esp32-c3-idf.yaml index 248ae4d0dc..86357fad36 100644 --- a/tests/components/voice_assistant/test.esp32-c3-idf.yaml +++ b/tests/components/voice_assistant/test.esp32-c3-idf.yaml @@ -28,7 +28,6 @@ speaker: id: speaker_id dac_type: external i2s_dout_pin: 2 - mode: mono voice_assistant: microphone: mic_id_external diff --git a/tests/components/voice_assistant/test.esp32-idf.yaml b/tests/components/voice_assistant/test.esp32-idf.yaml index 2e0209311d..da9b50721f 100644 --- a/tests/components/voice_assistant/test.esp32-idf.yaml +++ b/tests/components/voice_assistant/test.esp32-idf.yaml @@ -28,7 +28,6 @@ speaker: id: speaker_id dac_type: external i2s_dout_pin: 12 - mode: mono voice_assistant: microphone: mic_id_external diff --git a/tests/components/web_server/common_v1.yaml b/tests/components/web_server/common_v1.yaml index bf5aab4ce6..3c51f894b8 100644 --- a/tests/components/web_server/common_v1.yaml +++ b/tests/components/web_server/common_v1.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + device_base: !include common.yaml web_server: port: 8080 diff --git a/tests/components/web_server/common_v2.yaml b/tests/components/web_server/common_v2.yaml index 564c43e553..2af5ceca44 100644 --- a/tests/components/web_server/common_v2.yaml +++ b/tests/components/web_server/common_v2.yaml @@ -1,4 +1,5 @@ -<<: !include common.yaml +packages: + device_base: !include common.yaml web_server: port: 8080 diff --git a/tests/components/xiaomi_lywsd02mmc/common.yaml b/tests/components/xiaomi_lywsd02mmc/common.yaml new file mode 100644 index 0000000000..e63f585830 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/common.yaml @@ -0,0 +1,12 @@ +esp32_ble_tracker: + +sensor: + - platform: xiaomi_lywsd02mmc + mac_address: A4:C1:38:54:5E:18 + bindkey: 2529d8e0d23150a588675cc54ad48400 + temperature: + name: Xiaomi LYWSD02MMC Temperature + humidity: + name: Xiaomi LYWSD02MMC Humidity + battery_level: + name: Xiaomi LYWSD02MMC Battery Level diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/xiaomi_lywsd02mmc/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/xpt2046/test.esp32-ard.yaml b/tests/components/xpt2046/test.esp32-ard.yaml index bb166866f4..f15d1f9b41 100644 --- a/tests/components/xpt2046/test.esp32-ard.yaml +++ b/tests/components/xpt2046/test.esp32-ard.yaml @@ -12,6 +12,7 @@ display: cs_pin: 13 dc_pin: 14 reset_pin: 21 + invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/xpt2046/test.esp32-c3-ard.yaml b/tests/components/xpt2046/test.esp32-c3-ard.yaml index f3a2cf9aae..ef4daa800d 100644 --- a/tests/components/xpt2046/test.esp32-c3-ard.yaml +++ b/tests/components/xpt2046/test.esp32-c3-ard.yaml @@ -12,6 +12,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/xpt2046/test.esp32-c3-idf.yaml b/tests/components/xpt2046/test.esp32-c3-idf.yaml index f3a2cf9aae..ef4daa800d 100644 --- a/tests/components/xpt2046/test.esp32-c3-idf.yaml +++ b/tests/components/xpt2046/test.esp32-c3-idf.yaml @@ -12,6 +12,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/xpt2046/test.esp32-idf.yaml b/tests/components/xpt2046/test.esp32-idf.yaml index bb166866f4..f15d1f9b41 100644 --- a/tests/components/xpt2046/test.esp32-idf.yaml +++ b/tests/components/xpt2046/test.esp32-idf.yaml @@ -12,6 +12,7 @@ display: cs_pin: 13 dc_pin: 14 reset_pin: 21 + invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/xpt2046/test.esp32-s2-ard.yaml b/tests/components/xpt2046/test.esp32-s2-ard.yaml index 6232ca957b..df2a99b4f5 100644 --- a/tests/components/xpt2046/test.esp32-s2-ard.yaml +++ b/tests/components/xpt2046/test.esp32-s2-ard.yaml @@ -14,6 +14,7 @@ display: data_rate: 40MHz dimensions: 320x240 update_interval: never + invert_colors: false transform: mirror_y: false mirror_x: false diff --git a/tests/components/xpt2046/test.esp8266-ard.yaml b/tests/components/xpt2046/test.esp8266-ard.yaml index a917290e8e..0daa25ad60 100644 --- a/tests/components/xpt2046/test.esp8266-ard.yaml +++ b/tests/components/xpt2046/test.esp8266-ard.yaml @@ -12,6 +12,7 @@ display: cs_pin: 15 dc_pin: 4 reset_pin: 5 + invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/xpt2046/test.rp2040-ard.yaml b/tests/components/xpt2046/test.rp2040-ard.yaml index a7a49309ac..8afc45d04d 100644 --- a/tests/components/xpt2046/test.rp2040-ard.yaml +++ b/tests/components/xpt2046/test.rp2040-ard.yaml @@ -12,6 +12,7 @@ display: cs_pin: 8 dc_pin: 9 reset_pin: 10 + invert_colors: false lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height());