diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b3fa6d4932..ab4f8cc960 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -40,6 +40,7 @@ "yaml.customTags": [ "!secret scalar", "!lambda scalar", + "!extend scalar", "!include_dir_named scalar", "!include_dir_list scalar", "!include_dir_merge_list scalar", diff --git a/.gitattributes b/.gitattributes index dad0966222..1b3fd332b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ # Normalize line endings to LF in the repository * text eol=lf +*.png binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index affdf944a7..95e7619a19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,60 +12,266 @@ on: permissions: contents: read +env: + DEFAULT_PYTHON: "3.9" + PYUPGRADE_TARGET: "--py39-plus" + CLANG_FORMAT_VERSION: "13.0.1" + concurrency: # yamllint disable-line rule:line-length group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - ci: - name: ${{ matrix.name }} + common: + name: Create common environment runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v4.6.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python --version + pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt + pip install -e . + + yamllint: + name: yamllint + runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Run yamllint + uses: frenck/action-yamllint@v1.4.1 + + black: + name: Check black + runs-on: ubuntu-latest + needs: + - common + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Run black + run: | + . venv/bin/activate + black --verbose esphome tests + - name: Suggested changes + run: script/ci-suggest-changes + if: always() + + flake8: + name: Check flake8 + runs-on: ubuntu-latest + needs: + - common + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Run flake8 + run: | + . venv/bin/activate + flake8 esphome + - name: Suggested changes + run: script/ci-suggest-changes + if: always() + + pylint: + name: Check pylint + runs-on: ubuntu-latest + needs: + - common + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Run pylint + run: | + . venv/bin/activate + pylint -f parseable --persistent=n esphome + - name: Suggested changes + run: script/ci-suggest-changes + if: always() + + pyupgrade: + name: Check pyupgrade + runs-on: ubuntu-latest + needs: + - common + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Run pyupgrade + run: | + . venv/bin/activate + pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f` + - name: Suggested changes + run: script/ci-suggest-changes + if: always() + + ci-custom: + name: Run script/ci-custom + runs-on: ubuntu-latest + needs: + - common + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Register matcher + run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + - name: Run script/ci-custom + run: | + . venv/bin/activate + script/ci-custom.py + script/build_codeowners.py --check + + pytest: + name: Run pytest + runs-on: ubuntu-latest + needs: + - common + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Register matcher + run: echo "::add-matcher::.github/workflows/matchers/pytest.json" + - name: Run pytest + run: | + . venv/bin/activate + pytest -vv --tb=native tests + + clang-format: + name: Check clang-format + runs-on: ubuntu-latest + needs: + - common + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Install clang-format + run: | + . venv/bin/activate + pip install clang-format==${{ env.CLANG_FORMAT_VERSION }} + - name: Run clang-format + run: | + . venv/bin/activate + script/clang-format -i + git diff-index --quiet HEAD -- + - name: Suggested changes + run: script/ci-suggest-changes + if: always() + + compile-tests: + name: Run YAML test ${{ matrix.file }} + runs-on: ubuntu-latest + needs: + - common + - black + - ci-custom + - clang-format + - flake8 + - pylint + - pytest + - pyupgrade + - yamllint strategy: fail-fast: false - max-parallel: 5 + max-parallel: 2 + matrix: + file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8] + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 + with: + path: venv + # yamllint disable-line rule:line-length + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + - name: Cache platformio + uses: actions/cache@v3.3.1 + with: + path: ~/.platformio + # yamllint disable-line rule:line-length + key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }} + - name: Run esphome compile tests/test${{ matrix.file }}.yaml + run: | + . venv/bin/activate + esphome compile tests/test${{ matrix.file }}.yaml + + clang-tidy: + name: ${{ matrix.name }} + runs-on: ubuntu-latest + needs: + - common + - black + - ci-custom + - clang-format + - flake8 + - pylint + - pytest + - pyupgrade + - yamllint + strategy: + fail-fast: false + max-parallel: 2 matrix: include: - - id: ci-custom - name: Run script/ci-custom - - id: lint-python - name: Run script/lint-python - - id: test - file: tests/test1.yaml - name: Test tests/test1.yaml - pio_cache_key: test1 - - id: test - file: tests/test2.yaml - name: Test tests/test2.yaml - pio_cache_key: test2 - - id: test - file: tests/test3.yaml - name: Test tests/test3.yaml - pio_cache_key: test3 - - id: test - file: tests/test3.1.yaml - name: Test tests/test3.1.yaml - pio_cache_key: test3.1 - - id: test - file: tests/test4.yaml - name: Test tests/test4.yaml - pio_cache_key: test4 - - id: test - file: tests/test5.yaml - name: Test tests/test5.yaml - pio_cache_key: test5 - - id: test - file: tests/test6.yaml - name: Test tests/test6.yaml - pio_cache_key: test6 - - id: test - file: tests/test7.yaml - name: Test tests/test7.yaml - pio_cache_key: test7 - - id: pytest - name: Run pytest - - id: clang-format - name: Run script/clang-format - id: clang-tidy name: Run script/clang-tidy for ESP8266 options: --environment esp8266-arduino-tidy --grep USE_ESP8266 @@ -90,119 +296,65 @@ jobs: name: Run script/clang-tidy for ESP32 IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF pio_cache_key: tidyesp32-idf - - id: yamllint - name: Run yamllint steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - id: python + - name: Check out code from GitHub + uses: actions/checkout@v3.5.2 + - name: Restore Python virtual environment + uses: actions/cache/restore@v3.3.1 with: - python-version: "3.9" - - - name: Cache virtualenv - uses: actions/cache@v3 - with: - path: .venv + path: venv # yamllint disable-line rule:line-length - key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} - restore-keys: | - venv-${{ steps.python.outputs.python-version }}- - - - name: Set up virtualenv - # yamllint disable rule:line-length - run: | - python -m venv .venv - source .venv/bin/activate - pip install -U pip - pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt - pip install -e . - echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH - echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> $GITHUB_ENV - # yamllint enable rule:line-length - - # Use per check platformio cache because checks use different parts + key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} + # Use per check platformio cache because checks use different parts - name: Cache platformio - uses: actions/cache@v3 + uses: actions/cache@v3.3.1 with: path: ~/.platformio # yamllint disable-line rule:line-length key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - if: matrix.id == 'test' || matrix.id == 'clang-tidy' - - name: Install clang tools - run: | - sudo apt-get install \ - clang-format-13 \ - clang-tidy-11 - if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format' + - name: Install clang-tidy + run: sudo apt-get install clang-tidy-11 - name: Register problem matchers run: | - echo "::add-matcher::.github/workflows/matchers/ci-custom.json" - echo "::add-matcher::.github/workflows/matchers/lint-python.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - echo "::add-matcher::.github/workflows/matchers/pytest.json" echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" - - name: Lint Custom - run: | - script/ci-custom.py - script/build_codeowners.py --check - if: matrix.id == 'ci-custom' - - - name: Lint Python - run: script/lint-python -a - if: matrix.id == 'lint-python' - - - run: esphome compile ${{ matrix.file }} - if: matrix.id == 'test' - env: - # Also cache libdeps, store them in a ~/.platformio subfolder - PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps - - - name: Run pytest - run: | - pytest -vv --tb=native tests - if: matrix.id == 'pytest' - - # Also run git-diff-index so that the step is marked as failed on - # formatting errors, since clang-format doesn't do anything but - # change files if -i is passed. - - name: Run clang-format - run: | - script/clang-format -i - git diff-index --quiet HEAD -- - if: matrix.id == 'clang-format' - - name: Run clang-tidy run: | + . venv/bin/activate script/clang-tidy --all-headers --fix ${{ matrix.options }} - if: matrix.id == 'clang-tidy' env: # Also cache libdeps, store them in a ~/.platformio subfolder PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps - - name: Run yamllint - if: matrix.id == 'yamllint' - uses: frenck/action-yamllint@v1.4.0 - - name: Suggested changes run: script/ci-suggest-changes # yamllint disable-line rule:line-length - if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python') + if: always() ci-status: name: CI Status runs-on: ubuntu-latest - needs: [ci] + needs: + - common + - black + - ci-custom + - clang-format + - flake8 + - pylint + - pytest + - pyupgrade + - yamllint + - compile-tests + - clang-tidy if: always() steps: - - name: Successful deploy + - name: Success if: ${{ !(contains(needs.*.result, 'failure')) }} run: exit 0 - - name: Failing deploy + - name: Failure if: ${{ contains(needs.*.result, 'failure') }} run: exit 1 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f5d291b49f..3a3e390eef 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -26,7 +26,7 @@ jobs: days-before-issue-close: -1 remove-stale-when-updated: true stale-pr-label: "stale" - exempt-pr-labels: "no-stale" + exempt-pr-labels: "not-stale" stale-pr-message: > There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 396dd64165..896a0369ac 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -53,8 +53,8 @@ jobs: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot author: esphomebot - branch: sync/device-classes/ - branch-suffix: timestamp + branch: sync/device-classes delete-branch: true title: "Synchronise Device Classes from Home Assistant" body: ${{ steps.pr-template-body.outputs.body }} + token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b858b40e6f..617d6f5d9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.4.0 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 307dd496f0..acf1f29410 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -36,6 +36,24 @@ ] } ] + }, + { + "label": "Generate proto files", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "./script/api_protobuf/api_protobuf.py" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "never", + "close": true, + "panel": "new" + }, + "problemMatcher": [] } ] } diff --git a/CODEOWNERS b/CODEOWNERS index de6488c3d3..c6cbf3c2ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,6 +19,7 @@ esphome/components/addressable_light/* @justfalter esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban +esphome/components/alarm_control_panel/* @grahambrown11 esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/am43/sensor/* @buxtronix @@ -107,6 +108,7 @@ esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal +esphome/components/hm3301/* @freekode esphome/components/homeassistant/* @OttoWinter esphome/components/honeywellabp/* @RubyBailey esphome/components/host/* @esphome/core @@ -220,6 +222,7 @@ esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rp2040/* @jesserockz +esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/safe_mode/* @jsuanet @paulmonigatti @@ -275,13 +278,16 @@ esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet esphome/components/tee501/* @Stock-M esphome/components/teleinfo/* @0hax +esphome/components/template/alarm_control_panel/* @grahambrown11 esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter esphome/components/tlc5947/* @rnauber esphome/components/tm1621/* @Philippe12 esphome/components/tm1637/* @glmnet esphome/components/tm1638/* @skykingjwc +esphome/components/tm1651/* @freekode esphome/components/tmp102/* @timsavage +esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka esphome/components/toshiba/* @kbx81 diff --git a/docker/Dockerfile b/docker/Dockerfile index 720241242f..2d9a8a9ae4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,6 +29,8 @@ RUN \ git=1:2.30.2-1+deb11u2 \ curl=7.74.0-1.3+deb11u7 \ openssh-client=1:8.4p1-5+deb11u1 \ + libcairo2=1.16.0-5 \ + python3-cffi=1.14.5-1 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ @@ -52,7 +54,7 @@ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ wheel==0.37.1 \ - platformio==6.1.6 \ + platformio==6.1.7 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_platformio_interval 1000000 \ diff --git a/esphome/__main__.py b/esphome/__main__.py index 11a363691f..c7c83ad83b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -18,6 +18,9 @@ from esphome.const import ( CONF_LOGGER, CONF_NAME, CONF_OTA, + CONF_MQTT, + CONF_MDNS, + CONF_DISABLED, CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, @@ -42,7 +45,7 @@ from esphome.log import color, setup_log, Fore _LOGGER = logging.getLogger(__name__) -def choose_prompt(options): +def choose_prompt(options, purpose: str = None): if not options: raise EsphomeError( "Found no valid options for upload/logging, please make sure relevant " @@ -53,7 +56,9 @@ def choose_prompt(options): if len(options) == 1: return options[0][1] - safe_print("Found multiple options, please choose one:") + safe_print( + f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:' + ) for i, (desc, _) in enumerate(options): safe_print(f" [{i+1}] {desc}") @@ -72,7 +77,9 @@ def choose_prompt(options): return options[opt - 1][1] -def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): +def choose_upload_log_host( + default, check_default, show_ota, show_mqtt, show_api, purpose: str = None +): options = [] for port in get_serial_ports(): options.append((f"{port.path} ({port.description})", port.path)) @@ -80,7 +87,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api options.append((f"Over The Air ({CORE.address})", CORE.address)) if default == "OTA": return CORE.address - if show_mqtt and "mqtt" in CORE.config: + if show_mqtt and CONF_MQTT in CORE.config: options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) if default == "OTA": return "MQTT" @@ -88,7 +95,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api return default if check_default is not None and check_default in [opt[1] for opt in options]: return check_default - return choose_prompt(options) + return choose_prompt(options, purpose=purpose) def get_port_type(port): @@ -288,19 +295,30 @@ def upload_program(config, args, host): return 1 # Unknown target platform - from esphome import espota2 - if CONF_OTA not in config: raise EsphomeError( "Cannot upload Over the Air as the config does not include the ota: " "component" ) + from esphome import espota2 + ota_conf = config[CONF_OTA] remote_port = ota_conf[CONF_PORT] password = ota_conf.get(CONF_PASSWORD, "") + + if ( + get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED] + ) and CONF_MQTT in config: + from esphome import mqtt + + host = mqtt.get_esphome_device_ip( + config, args.username, args.password, args.client_id + ) + if getattr(args, "file", None) is not None: return espota2.run_ota(host, remote_port, password, args.file) + return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) @@ -310,6 +328,13 @@ def show_logs(config, args, port): if get_port_type(port) == "SERIAL": return run_miniterm(config, port) if get_port_type(port) == "NETWORK" and "api" in config: + if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config: + from esphome import mqtt + + port = mqtt.get_esphome_device_ip( + config, args.username, args.password, args.client_id + ) + from esphome.components.api.client import run_logs return run_logs(config, port) @@ -374,6 +399,7 @@ def command_upload(args, config): show_ota=True, show_mqtt=False, show_api=False, + purpose="uploading", ) exit_code = upload_program(config, args, port) if exit_code != 0: @@ -382,6 +408,15 @@ def command_upload(args, config): return 0 +def command_discover(args, config): + if "mqtt" in config: + from esphome import mqtt + + return mqtt.show_discover(config, args.username, args.password, args.client_id) + + raise EsphomeError("No discover method configured (mqtt)") + + def command_logs(args, config): port = choose_upload_log_host( default=args.device, @@ -389,6 +424,7 @@ def command_logs(args, config): show_ota=False, show_mqtt=True, show_api=True, + purpose="logging", ) return show_logs(config, args, port) @@ -407,6 +443,7 @@ def command_run(args, config): show_ota=True, show_mqtt=False, show_api=True, + purpose="uploading", ) exit_code = upload_program(config, args, port) if exit_code != 0: @@ -420,6 +457,7 @@ def command_run(args, config): show_ota=False, show_mqtt=True, show_api=True, + purpose="logging", ) return show_logs(config, args, port) @@ -623,6 +661,7 @@ POST_CONFIG_ACTIONS = { "clean": command_clean, "idedata": command_idedata, "rename": command_rename, + "discover": command_discover, } @@ -711,6 +750,15 @@ def parse_args(argv): help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", ) + parser_discover = subparsers.add_parser( + "discover", + help="Validate the configuration and show all discovered devices.", + parents=[mqtt_options], + ) + parser_discover.add_argument( + "configuration", help="Your YAML configuration file.", nargs=1 + ) + parser_run = subparsers.add_parser( "run", help="Validate the configuration, create a binary, upload it, and start logs.", diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py new file mode 100644 index 0000000000..963d5ae719 --- /dev/null +++ b/esphome/components/alarm_control_panel/__init__.py @@ -0,0 +1,165 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id +from esphome.core import CORE, coroutine_with_priority +from esphome.const import ( + CONF_ID, + CONF_ON_STATE, + CONF_TRIGGER_ID, + CONF_CODE, +) +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@grahambrown11"] +IS_PLATFORM_COMPONENT = True + +CONF_ON_TRIGGERED = "on_triggered" +CONF_ON_CLEARED = "on_cleared" + +alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") +AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) + +StateTrigger = alarm_control_panel_ns.class_( + "StateTrigger", automation.Trigger.template() +) +TriggeredTrigger = alarm_control_panel_ns.class_( + "TriggeredTrigger", automation.Trigger.template() +) +ClearedTrigger = alarm_control_panel_ns.class_( + "ClearedTrigger", automation.Trigger.template() +) +ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) +ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) +DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action) +PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action) +TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action) +AlarmControlPanelCondition = alarm_control_panel_ns.class_( + "AlarmControlPanelCondition", automation.Condition +) + +ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_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_CLEARED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), + } + ), + } +) + +ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(AlarmControlPanel), + cv.Optional(CONF_CODE): cv.templatable(cv.string), + } +) + +ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(AlarmControlPanel), + } +) + + +async def setup_alarm_control_panel_core_(var, config): + await setup_entity(var, config) + for conf in config.get(CONF_ON_STATE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TRIGGERED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_CLEARED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + +async def register_alarm_control_panel(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_alarm_control_panel(var)) + await setup_alarm_control_panel_core_(var, config) + + +@automation.register_action( + "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_arm_away_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_CODE in config: + templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_arm_home_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_CODE in config: + templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_disarm_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_CODE in config: + templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(templatable_)) + return var + + +@automation.register_action( + "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_pending_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA +) +async def alarm_action_trigger_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_condition( + "alarm_control_panel.is_armed", + AlarmControlPanelCondition, + ALARM_CONTROL_PANEL_CONDITION_SCHEMA, +) +async def alarm_control_panel_is_armed_to_code( + config, condition_id, template_arg, args +): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(alarm_control_panel_ns.using) + cg.add_define("USE_ALARM_CONTROL_PANEL") diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp new file mode 100644 index 0000000000..74c9a502df --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -0,0 +1,111 @@ +#include + +#include "alarm_control_panel.h" + +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +static const char *const TAG = "alarm_control_panel"; + +AlarmControlPanelCall AlarmControlPanel::make_call() { return AlarmControlPanelCall(this); } + +bool AlarmControlPanel::is_state_armed(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_ARMED_AWAY: + case ACP_STATE_ARMED_HOME: + case ACP_STATE_ARMED_NIGHT: + case ACP_STATE_ARMED_VACATION: + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return true; + default: + return false; + } +}; + +void AlarmControlPanel::publish_state(AlarmControlPanelState state) { + this->last_update_ = millis(); + if (state != this->current_state_) { + auto prev_state = this->current_state_; + ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), + LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); + this->current_state_ = state; + this->state_callback_.call(); + if (state == ACP_STATE_TRIGGERED) { + this->triggered_callback_.call(); + } + if (prev_state == ACP_STATE_TRIGGERED) { + this->cleared_callback_.call(); + } + if (state == this->desired_state_) { + // only store when in the desired state + this->pref_.save(&state); + } + } +} + +void AlarmControlPanel::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::add_on_triggered_callback(std::function &&callback) { + this->triggered_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::add_on_cleared_callback(std::function &&callback) { + this->cleared_callback_.add(std::move(callback)); +} + +void AlarmControlPanel::arm_away(optional code) { + auto call = this->make_call(); + call.arm_away(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_home(optional code) { + auto call = this->make_call(); + call.arm_home(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_night(optional code) { + auto call = this->make_call(); + call.arm_night(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_vacation(optional code) { + auto call = this->make_call(); + call.arm_vacation(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::arm_custom_bypass(optional code) { + auto call = this->make_call(); + call.arm_custom_bypass(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +void AlarmControlPanel::disarm(optional code) { + auto call = this->make_call(); + call.disarm(); + if (code.has_value()) + call.set_code(code.value()); + call.perform(); +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h new file mode 100644 index 0000000000..4f15ccb45a --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -0,0 +1,136 @@ +#pragma once + +#include + +#include "alarm_control_panel_call.h" +#include "alarm_control_panel_state.h" + +#include "esphome/core/automation.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +enum AlarmControlPanelFeature : uint8_t { + // Matches Home Assistant values + ACP_FEAT_ARM_HOME = 1 << 0, + ACP_FEAT_ARM_AWAY = 1 << 1, + ACP_FEAT_ARM_NIGHT = 1 << 2, + ACP_FEAT_TRIGGER = 1 << 3, + ACP_FEAT_ARM_CUSTOM_BYPASS = 1 << 4, + ACP_FEAT_ARM_VACATION = 1 << 5, +}; + +class AlarmControlPanel : public EntityBase { + public: + /** Make a AlarmControlPanelCall + * + */ + AlarmControlPanelCall make_call(); + + /** Set the state of the alarm_control_panel. + * + * @param state The AlarmControlPanelState. + */ + void publish_state(AlarmControlPanelState state); + + /** Add a callback for when the state of the alarm_control_panel changes + * + * @param callback The callback function + */ + void add_on_state_callback(std::function &&callback); + + /** Add a callback for when the state of the alarm_control_panel chanes to triggered + * + * @param callback The callback function + */ + void add_on_triggered_callback(std::function &&callback); + + /** Add a callback for when the state of the alarm_control_panel clears from triggered + * + * @param callback The callback function + */ + void add_on_cleared_callback(std::function &&callback); + + /** A numeric representation of the supported features as per HomeAssistant + * + */ + virtual uint32_t get_supported_features() const = 0; + + /** Returns if the alarm_control_panel has a code + * + */ + virtual bool get_requires_code() const = 0; + + /** Returns if the alarm_control_panel requires a code to arm + * + */ + virtual bool get_requires_code_to_arm() const = 0; + + /** arm the alarm in away mode + * + * @param code The code + */ + void arm_away(optional code = nullopt); + + /** arm the alarm in home mode + * + * @param code The code + */ + void arm_home(optional code = nullopt); + + /** arm the alarm in night mode + * + * @param code The code + */ + void arm_night(optional code = nullopt); + + /** arm the alarm in vacation mode + * + * @param code The code + */ + void arm_vacation(optional code = nullopt); + + /** arm the alarm in custom bypass mode + * + * @param code The code + */ + void arm_custom_bypass(optional code = nullopt); + + /** disarm the alarm + * + * @param code The code + */ + void disarm(optional code = nullopt); + + /** Get the state + * + */ + AlarmControlPanelState get_state() const { return this->current_state_; } + + // is the state one of the armed states + bool is_state_armed(AlarmControlPanelState state); + + protected: + friend AlarmControlPanelCall; + // in order to store last panel state in flash + ESPPreferenceObject pref_; + // current state + AlarmControlPanelState current_state_; + // the desired (or previous) state + AlarmControlPanelState desired_state_; + // last time the state was updated + uint32_t last_update_; + // the call control function + virtual void control(const AlarmControlPanelCall &call) = 0; + // state callback + CallbackManager state_callback_{}; + // trigger callback + CallbackManager triggered_callback_{}; + // clear callback + CallbackManager cleared_callback_{}; +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp new file mode 100644 index 0000000000..eb50c4f4b5 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.cpp @@ -0,0 +1,99 @@ +#include "alarm_control_panel_call.h" + +#include "alarm_control_panel.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +static const char *const TAG = "alarm_control_panel"; + +AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {} + +AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) { + this->code_ = code; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_away() { + this->state_ = ACP_STATE_ARMED_AWAY; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_home() { + this->state_ = ACP_STATE_ARMED_HOME; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_night() { + this->state_ = ACP_STATE_ARMED_NIGHT; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_vacation() { + this->state_ = ACP_STATE_ARMED_VACATION; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::arm_custom_bypass() { + this->state_ = ACP_STATE_ARMED_CUSTOM_BYPASS; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::disarm() { + this->state_ = ACP_STATE_DISARMED; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::pending() { + this->state_ = ACP_STATE_PENDING; + return *this; +} + +AlarmControlPanelCall &AlarmControlPanelCall::triggered() { + this->state_ = ACP_STATE_TRIGGERED; + return *this; +} + +const optional &AlarmControlPanelCall::get_state() const { return this->state_; } +const optional &AlarmControlPanelCall::get_code() const { return this->code_; } + +void AlarmControlPanelCall::validate_() { + if (this->state_.has_value()) { + auto state = *this->state_; + if (this->parent_->is_state_armed(state) && this->parent_->get_state() != ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot arm when not disarmed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_PENDING && this->parent_->get_state() == ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot trip alarm when disarmed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_DISARMED && + !(this->parent_->is_state_armed(this->parent_->get_state()) || + this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_ARMING || + this->parent_->get_state() == ACP_STATE_TRIGGERED)) { + ESP_LOGW(TAG, "Cannot disarm when not armed"); + this->state_.reset(); + return; + } + if (state == ACP_STATE_ARMED_HOME && (this->parent_->get_supported_features() & ACP_FEAT_ARM_HOME) == 0) { + ESP_LOGW(TAG, "Cannot arm home when not supported"); + this->state_.reset(); + return; + } + } +} + +void AlarmControlPanelCall::perform() { + this->validate_(); + if (this->state_) { + this->parent_->control(*this); + } +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_call.h b/esphome/components/alarm_control_panel/alarm_control_panel_call.h new file mode 100644 index 0000000000..034e3142da --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_call.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "alarm_control_panel_state.h" + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace alarm_control_panel { + +class AlarmControlPanel; + +class AlarmControlPanelCall { + public: + AlarmControlPanelCall(AlarmControlPanel *parent); + + AlarmControlPanelCall &set_code(const std::string &code); + AlarmControlPanelCall &arm_away(); + AlarmControlPanelCall &arm_home(); + AlarmControlPanelCall &arm_night(); + AlarmControlPanelCall &arm_vacation(); + AlarmControlPanelCall &arm_custom_bypass(); + AlarmControlPanelCall &disarm(); + AlarmControlPanelCall &pending(); + AlarmControlPanelCall &triggered(); + + void perform(); + const optional &get_state() const; + const optional &get_code() const; + + protected: + AlarmControlPanel *parent_; + optional code_{}; + optional state_{}; + void validate_(); +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp new file mode 100644 index 0000000000..231e7228e1 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -0,0 +1,34 @@ +#include "alarm_control_panel_state.h" + +namespace esphome { +namespace alarm_control_panel { + +const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_DISARMED: + return LOG_STR("DISARMED"); + case ACP_STATE_ARMED_HOME: + return LOG_STR("ARMED_HOME"); + case ACP_STATE_ARMED_AWAY: + return LOG_STR("ARMED_AWAY"); + case ACP_STATE_ARMED_NIGHT: + return LOG_STR("NIGHT"); + case ACP_STATE_ARMED_VACATION: + return LOG_STR("ARMED_VACATION"); + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return LOG_STR("ARMED_CUSTOM_BYPASS"); + case ACP_STATE_PENDING: + return LOG_STR("PENDING"); + case ACP_STATE_ARMING: + return LOG_STR("ARMING"); + case ACP_STATE_DISARMING: + return LOG_STR("DISARMING"); + case ACP_STATE_TRIGGERED: + return LOG_STR("TRIGGERED"); + default: + return LOG_STR("UNKNOWN"); + } +} + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.h b/esphome/components/alarm_control_panel/alarm_control_panel_state.h new file mode 100644 index 0000000000..ad16222dc0 --- /dev/null +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace alarm_control_panel { + +enum AlarmControlPanelState : uint8_t { + ACP_STATE_DISARMED = 0, + ACP_STATE_ARMED_HOME = 1, + ACP_STATE_ARMED_AWAY = 2, + ACP_STATE_ARMED_NIGHT = 3, + ACP_STATE_ARMED_VACATION = 4, + ACP_STATE_ARMED_CUSTOM_BYPASS = 5, + ACP_STATE_PENDING = 6, + ACP_STATE_ARMING = 7, + ACP_STATE_DISARMING = 8, + ACP_STATE_TRIGGERED = 9 +}; + +/** Returns a string representation of the state. + * + * @param state The AlarmControlPanelState. + */ +const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state); + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h new file mode 100644 index 0000000000..4368129609 --- /dev/null +++ b/esphome/components/alarm_control_panel/automation.h @@ -0,0 +1,115 @@ + +#pragma once +#include "esphome/core/automation.h" +#include "alarm_control_panel.h" + +namespace esphome { +namespace alarm_control_panel { + +class StateTrigger : public Trigger<> { + public: + explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); + } +}; + +class TriggeredTrigger : public Trigger<> { + public: + explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); }); + } +}; + +class ClearedTrigger : public Trigger<> { + public: + explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { + alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); }); + } +}; + +template class ArmAwayAction : public Action { + public: + explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { + auto call = this->alarm_control_panel_->make_call(); + auto code = this->code_.optional_value(x...); + if (code.has_value()) { + call.set_code(code.value()); + } + call.arm_away(); + call.perform(); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class ArmHomeAction : public Action { + public: + explicit ArmHomeAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { + auto call = this->alarm_control_panel_->make_call(); + auto code = this->code_.optional_value(x...); + if (code.has_value()) { + call.set_code(code.value()); + } + call.arm_home(); + call.perform(); + } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class DisarmAction : public Action { + public: + explicit DisarmAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class PendingAction : public Action { + public: + explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class TriggeredAction : public Action { + public: + explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} + + void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); } + + protected: + AlarmControlPanel *alarm_control_panel_; +}; + +template class AlarmControlPanelCondition : public Condition { + public: + AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} + bool check(Ts... x) override { + return this->parent_->is_state_armed(this->parent_->get_state()) || + this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; + } + + protected: + AlarmControlPanel *parent_; +}; + +} // namespace alarm_control_panel +} // namespace esphome diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 68c3eee132..f51d115d9e 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -3,9 +3,17 @@ import logging from esphome import core from esphome.components import display, font import esphome.components.image as espImage +from esphome.components.image import CONF_USE_TRANSPARENCY import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE +from esphome.const import ( + CONF_FILE, + CONF_ID, + CONF_RAW_DATA_ID, + CONF_REPEAT, + CONF_RESIZE, + CONF_TYPE, +) from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) @@ -13,18 +21,55 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["display"] MULTI_CONF = True +CONF_LOOP = "loop" +CONF_START_FRAME = "start_frame" +CONF_END_FRAME = "end_frame" + Animation_ = display.display_ns.class_("Animation", espImage.Image_) + +def validate_cross_dependencies(config): + """ + Validate fields whose possible values depend on other fields. + For example, validate that explicitly transparent image types + have "use_transparency" set to True. + Also set the default value for those kind of dependent fields. + """ + image_type = config[CONF_TYPE] + is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] + # If the use_transparency option was not specified, set the default depending on the image type + if CONF_USE_TRANSPARENCY not in config: + config[CONF_USE_TRANSPARENCY] = is_transparent_type + + if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: + raise cv.Invalid(f"Image type {image_type} must always be transparent.") + + return config + + ANIMATION_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(Animation_), - cv.Required(CONF_FILE): cv.file_, - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( - espImage.IMAGE_TYPE, upper=True - ), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - } + cv.All( + { + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Required(CONF_FILE): cv.file_, + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( + espImage.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, + cv.Optional(CONF_LOOP): cv.All( + { + cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, + cv.Optional(CONF_END_FRAME): cv.positive_int, + cv.Optional(CONF_REPEAT): cv.positive_int, + } + ), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + }, + validate_cross_dependencies, + ) ) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) @@ -50,16 +95,19 @@ async def to_code(config): else: if width > 500 or height > 500: _LOGGER.warning( - "The image you requested is very big. Please consider using" - " the resize parameter." + 'The image "%s" you requested is very big. Please consider' + " using the resize parameter.", + path, ) + transparent = config[CONF_USE_TRANSPARENCY] + if config[CONF_TYPE] == "GRAYSCALE": data = [0 for _ in range(height * width * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) - frame = image.convert("L", dither=Image.NONE) + frame = image.convert("LA", dither=Image.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) @@ -67,16 +115,22 @@ async def to_code(config): raise core.EsphomeError( f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" ) - for pix in pixels: + for pix, a in pixels: + if transparent: + if pix == 1: + pix = 0 + if a < 0x80: + pix = 1 + data[pos] = pix pos += 1 - elif config[CONF_TYPE] == "RGB24": - data = [0 for _ in range(height * width * 3 * frames)] + elif config[CONF_TYPE] == "RGBA": + data = [0 for _ in range(height * width * 4 * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) - frame = image.convert("RGB") + frame = image.convert("RGBA") if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) @@ -91,13 +145,15 @@ async def to_code(config): pos += 1 data[pos] = pix[2] pos += 1 + data[pos] = pix[3] + pos += 1 - elif config[CONF_TYPE] == "RGB565": - data = [0 for _ in range(height * width * 2 * frames)] + elif config[CONF_TYPE] == "RGB24": + data = [0 for _ in range(height * width * 3 * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) - frame = image.convert("RGB") + frame = image.convert("RGBA") if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) @@ -105,14 +161,50 @@ async def to_code(config): raise core.EsphomeError( f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" ) - for pix in pixels: - R = pix[0] >> 3 - G = pix[1] >> 2 - B = pix[2] >> 3 + for r, g, b, a in pixels: + if transparent: + if r == 0 and g == 0 and b == 1: + b = 0 + if a < 0x80: + r = 0 + g = 0 + b = 1 + + data[pos] = r + pos += 1 + data[pos] = g + pos += 1 + data[pos] = b + pos += 1 + + elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: + data = [0 for _ in range(height * width * 2 * frames)] + pos = 0 + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert("RGBA") + if CONF_RESIZE in config: + frame = frame.resize([width, height]) + pixels = list(frame.getdata()) + if len(pixels) != height * width: + raise core.EsphomeError( + f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" + ) + for r, g, b, a in pixels: + R = r >> 3 + G = g >> 2 + B = b >> 3 rgb = (R << 11) | (G << 5) | B + + if transparent: + if rgb == 0x0020: + rgb = 0 + if a < 0x80: + rgb = 0x0020 + data[pos] = rgb >> 8 pos += 1 - data[pos] = rgb & 255 + data[pos] = rgb & 0xFF pos += 1 elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: @@ -120,19 +212,31 @@ async def to_code(config): data = [0 for _ in range((height * width8 // 8) * frames)] for frameIndex in range(frames): image.seek(frameIndex) + if transparent: + alpha = image.split()[-1] + has_alpha = alpha.getextrema()[0] < 0xFF frame = image.convert("1", dither=Image.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) - for y in range(height): - for x in range(width): - if frame.getpixel((x, y)): + if transparent: + alpha = alpha.resize([width, height]) + for x, y in [(i, j) for i in range(width) for j in range(height)]: + if transparent and has_alpha: + if not alpha.getpixel((x, y)): continue - pos = x + y * width8 + (height * width8 * frameIndex) - data[pos // 8] |= 0x80 >> (pos % 8) + elif frame.getpixel((x, y)): + continue + + pos = x + y * width8 + (height * width8 * frameIndex) + data[pos // 8] |= 0x80 >> (pos % 8) + else: + raise core.EsphomeError( + f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}." + ) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable( + var = cg.new_Pvariable( config[CONF_ID], prog_arr, width, @@ -140,3 +244,9 @@ async def to_code(config): frames, espImage.IMAGE_TYPE[config[CONF_TYPE]], ) + cg.add(var.set_transparency(transparent)) + if CONF_LOOP in config: + start = config[CONF_LOOP][CONF_START_FRAME] + end = config[CONF_LOOP].get(CONF_END_FRAME, frames) + count = config[CONF_LOOP].get(CONF_REPEAT, -1) + cg.add(var.set_loop(start, end, count)) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4cc98c91d9..0d68d9fe55 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -56,6 +56,8 @@ service APIConnection { rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} + + rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {} } @@ -206,7 +208,8 @@ message DeviceInfoResponse { uint32 webserver_port = 10; - uint32 bluetooth_proxy_version = 11; + uint32 legacy_bluetooth_proxy_version = 11; + uint32 bluetooth_proxy_feature_flags = 15; string manufacturer = 12; @@ -1130,6 +1133,8 @@ message SubscribeBluetoothLEAdvertisementsRequest { option (id) = 66; option (source) = SOURCE_CLIENT; option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint32 flags = 1; } message BluetoothServiceData { @@ -1154,6 +1159,23 @@ message BluetoothLEAdvertisementResponse { uint32 address_type = 7; } +message BluetoothLERawAdvertisement { + uint64 address = 1; + sint32 rssi = 2; + uint32 address_type = 3; + + bytes data = 4; +} + +message BluetoothLERawAdvertisementsResponse { + option (id) = 93; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + option (no_delay) = true; + + repeated BluetoothLERawAdvertisement advertisements = 1; +} + enum BluetoothDeviceRequestType { BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; @@ -1397,6 +1419,7 @@ message VoiceAssistantRequest { option (ifdef) = "USE_VOICE_ASSISTANT"; bool start = 1; + string conversation_id = 2; } message VoiceAssistantResponse { @@ -1433,3 +1456,63 @@ message VoiceAssistantEventResponse { VoiceAssistantEvent event_type = 1; repeated VoiceAssistantEventData data = 2; } + +// ==================== ALARM CONTROL PANEL ==================== +enum AlarmControlPanelState { + ALARM_STATE_DISARMED = 0; + ALARM_STATE_ARMED_HOME = 1; + ALARM_STATE_ARMED_AWAY = 2; + ALARM_STATE_ARMED_NIGHT = 3; + ALARM_STATE_ARMED_VACATION = 4; + ALARM_STATE_ARMED_CUSTOM_BYPASS = 5; + ALARM_STATE_PENDING = 6; + ALARM_STATE_ARMING = 7; + ALARM_STATE_DISARMING = 8; + ALARM_STATE_TRIGGERED = 9; +} + +enum AlarmControlPanelStateCommand { + ALARM_CONTROL_PANEL_DISARM = 0; + ALARM_CONTROL_PANEL_ARM_AWAY = 1; + ALARM_CONTROL_PANEL_ARM_HOME = 2; + ALARM_CONTROL_PANEL_ARM_NIGHT = 3; + ALARM_CONTROL_PANEL_ARM_VACATION = 4; + ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5; + ALARM_CONTROL_PANEL_TRIGGER = 6; +} + +message ListEntitiesAlarmControlPanelResponse { + option (id) = 94; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + uint32 supported_features = 8; + bool requires_code = 9; + bool requires_code_to_arm = 10; +} + +message AlarmControlPanelStateResponse { + option (id) = 95; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + option (no_delay) = true; + fixed32 key = 1; + AlarmControlPanelState state = 2; +} + +message AlarmControlPanelCommandRequest { + option (id) = 96; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + option (no_delay) = true; + fixed32 key = 1; + AlarmControlPanelStateCommand command = 2; + string code = 3; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c350197e68..858ff0e525 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -51,6 +51,14 @@ void APIConnection::start() { helper_->set_log_info(client_info_); } +APIConnection::~APIConnection() { +#ifdef USE_BLUETOOTH_PROXY + if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) { + bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); + } +#endif +} + void APIConnection::loop() { if (this->remove_) return; @@ -845,9 +853,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { #endif #ifdef USE_BLUETOOTH_PROXY +void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); +} +void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); +} bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { - if (!this->bluetooth_le_advertisement_subscription_) - return false; if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { BluetoothLEAdvertisementResponse resp = msg; for (auto &service : resp.service_data) { @@ -895,11 +907,12 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_ #endif #ifdef USE_VOICE_ASSISTANT -bool APIConnection::request_voice_assistant(bool start) { +bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id) { if (!this->voice_assistant_subscription_) return false; VoiceAssistantRequest msg; msg.start = start; + msg.conversation_id = conversation_id; return this->send_voice_assistant_request(msg); } void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { @@ -918,6 +931,64 @@ void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventR #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + if (!this->state_subscription_) + return false; + + AlarmControlPanelStateResponse resp{}; + resp.key = a_alarm_control_panel->get_object_id_hash(); + resp.state = static_cast(a_alarm_control_panel->get_state()); + return this->send_alarm_control_panel_state_response(resp); +} +bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + ListEntitiesAlarmControlPanelResponse msg; + msg.key = a_alarm_control_panel->get_object_id_hash(); + msg.object_id = a_alarm_control_panel->get_object_id(); + msg.name = a_alarm_control_panel->get_name(); + msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel); + msg.icon = a_alarm_control_panel->get_icon(); + msg.disabled_by_default = a_alarm_control_panel->is_disabled_by_default(); + msg.entity_category = static_cast(a_alarm_control_panel->get_entity_category()); + msg.supported_features = a_alarm_control_panel->get_supported_features(); + msg.requires_code = a_alarm_control_panel->get_requires_code(); + msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); + return this->send_list_entities_alarm_control_panel_response(msg); +} +void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { + alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); + if (a_alarm_control_panel == nullptr) + return; + + auto call = a_alarm_control_panel->make_call(); + switch (msg.command) { + case enums::ALARM_CONTROL_PANEL_DISARM: + call.disarm(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_AWAY: + call.arm_away(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_HOME: + call.arm_home(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: + call.arm_night(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_VACATION: + call.arm_vacation(); + break; + case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: + call.arm_custom_bypass(); + break; + case enums::ALARM_CONTROL_PANEL_TRIGGER: + call.pending(); + break; + } + call.set_code(msg.code); + call.perform(); +} +#endif + bool APIConnection::send_log_message(int level, const char *tag, const char *line) { if (this->log_subscription_ < level) return false; @@ -942,7 +1013,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 8; + resp.api_version_minor = 9; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; resp.name = App.get_name(); @@ -994,9 +1065,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { resp.webserver_port = USE_WEBSERVER_PORT; #endif #ifdef USE_BLUETOOTH_PROXY - resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() - ? bluetooth_proxy::ACTIVE_CONNECTIONS_VERSION - : bluetooth_proxy::PASSIVE_ONLY_VERSION; + resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version(); + resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); #endif #ifdef USE_VOICE_ASSISTANT resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version(); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 78ecbb98e6..c146adff02 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -16,7 +16,7 @@ namespace api { class APIConnection : public APIServerConnection { public: APIConnection(std::unique_ptr socket, APIServer *parent); - virtual ~APIConnection() = default; + virtual ~APIConnection(); void start(); void loop(); @@ -98,12 +98,8 @@ class APIConnection : public APIServerConnection { this->send_homeassistant_service_response(call); } #ifdef USE_BLUETOOTH_PROXY - void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override { - this->bluetooth_le_advertisement_subscription_ = true; - } - void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override { - this->bluetooth_le_advertisement_subscription_ = false; - } + void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; + void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; @@ -128,11 +124,17 @@ class APIConnection : public APIServerConnection { void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { this->voice_assistant_subscription_ = msg.subscribe; } - bool request_voice_assistant(bool start); + bool request_voice_assistant(bool start, const std::string &conversation_id); void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; #endif +#ifdef USE_ALARM_CONTROL_PANEL + bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); + bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); + void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; +#endif + void on_disconnect_response(const DisconnectResponse &value) override; void on_ping_response(const PingResponse &value) override { // we initiated ping @@ -211,9 +213,6 @@ class APIConnection : public APIServerConnection { uint32_t last_traffic_; bool sent_ping_{false}; bool service_call_subscription_{false}; -#ifdef USE_BLUETOOTH_PROXY - bool bluetooth_le_advertisement_subscription_{false}; -#endif #ifdef USE_VOICE_ASSISTANT bool voice_assistant_subscription_{false}; #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1dd8c82e00..8c7f6d0c4a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -433,6 +433,57 @@ template<> const char *proto_enum_to_string(enums::V } } #endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> const char *proto_enum_to_string(enums::AlarmControlPanelState value) { + switch (value) { + case enums::ALARM_STATE_DISARMED: + return "ALARM_STATE_DISARMED"; + case enums::ALARM_STATE_ARMED_HOME: + return "ALARM_STATE_ARMED_HOME"; + case enums::ALARM_STATE_ARMED_AWAY: + return "ALARM_STATE_ARMED_AWAY"; + case enums::ALARM_STATE_ARMED_NIGHT: + return "ALARM_STATE_ARMED_NIGHT"; + case enums::ALARM_STATE_ARMED_VACATION: + return "ALARM_STATE_ARMED_VACATION"; + case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: + return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; + case enums::ALARM_STATE_PENDING: + return "ALARM_STATE_PENDING"; + case enums::ALARM_STATE_ARMING: + return "ALARM_STATE_ARMING"; + case enums::ALARM_STATE_DISARMING: + return "ALARM_STATE_DISARMING"; + case enums::ALARM_STATE_TRIGGERED: + return "ALARM_STATE_TRIGGERED"; + default: + return "UNKNOWN"; + } +} +#endif +#ifdef HAS_PROTO_MESSAGE_DUMP +template<> +const char *proto_enum_to_string(enums::AlarmControlPanelStateCommand value) { + switch (value) { + case enums::ALARM_CONTROL_PANEL_DISARM: + return "ALARM_CONTROL_PANEL_DISARM"; + case enums::ALARM_CONTROL_PANEL_ARM_AWAY: + return "ALARM_CONTROL_PANEL_ARM_AWAY"; + case enums::ALARM_CONTROL_PANEL_ARM_HOME: + return "ALARM_CONTROL_PANEL_ARM_HOME"; + case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: + return "ALARM_CONTROL_PANEL_ARM_NIGHT"; + case enums::ALARM_CONTROL_PANEL_ARM_VACATION: + return "ALARM_CONTROL_PANEL_ARM_VACATION"; + case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: + return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; + case enums::ALARM_CONTROL_PANEL_TRIGGER: + return "ALARM_CONTROL_PANEL_TRIGGER"; + default: + return "UNKNOWN"; + } +} +#endif bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 2: { @@ -617,7 +668,11 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 11: { - this->bluetooth_proxy_version = value.as_uint32(); + this->legacy_bluetooth_proxy_version = value.as_uint32(); + return true; + } + case 15: { + this->bluetooth_proxy_feature_flags = value.as_uint32(); return true; } case 14: { @@ -681,7 +736,8 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(8, this->project_name); buffer.encode_string(9, this->project_version); buffer.encode_uint32(10, this->webserver_port); - buffer.encode_uint32(11, this->bluetooth_proxy_version); + buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version); + buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); buffer.encode_string(12, this->manufacturer); buffer.encode_string(13, this->friendly_name); buffer.encode_uint32(14, this->voice_assistant_version); @@ -731,8 +787,13 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" bluetooth_proxy_version: "); - sprintf(buffer, "%u", this->bluetooth_proxy_version); + out.append(" legacy_bluetooth_proxy_version: "); + sprintf(buffer, "%u", this->legacy_bluetooth_proxy_version); + out.append(buffer); + out.append("\n"); + + out.append(" bluetooth_proxy_feature_flags: "); + sprintf(buffer, "%u", this->bluetooth_proxy_feature_flags); out.append(buffer); out.append("\n"); @@ -5041,10 +5102,28 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif -void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} +bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->flags = value.as_uint32(); + return true; + } + default: + return false; + } +} +void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint32(1, this->flags); +} #ifdef HAS_PROTO_MESSAGE_DUMP void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { - out.append("SubscribeBluetoothLEAdvertisementsRequest {}"); + __attribute__((unused)) char buffer[64]; + out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); + out.append(" flags: "); + sprintf(buffer, "%u", this->flags); + out.append(buffer); + out.append("\n"); + out.append("}"); } #endif bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -5197,6 +5276,92 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: { + this->address = value.as_uint64(); + return true; + } + case 2: { + this->rssi = value.as_sint32(); + return true; + } + case 3: { + this->address_type = value.as_uint32(); + return true; + } + default: + return false; + } +} +bool BluetoothLERawAdvertisement::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 4: { + this->data = value.as_string(); + return true; + } + default: + return false; + } +} +void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_sint32(2, this->rssi); + buffer.encode_uint32(3, this->address_type); + buffer.encode_string(4, this->data); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothLERawAdvertisement::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLERawAdvertisement {\n"); + out.append(" address: "); + sprintf(buffer, "%llu", this->address); + out.append(buffer); + out.append("\n"); + + out.append(" rssi: "); + sprintf(buffer, "%d", this->rssi); + out.append(buffer); + out.append("\n"); + + out.append(" address_type: "); + sprintf(buffer, "%u", this->address_type); + out.append(buffer); + out.append("\n"); + + out.append(" data: "); + out.append("'").append(this->data).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool BluetoothLERawAdvertisementsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->advertisements.push_back(value.as_message()); + return true; + } + default: + return false; + } +} +void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { + for (auto &it : this->advertisements) { + buffer.encode_message(1, it, true); + } +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("BluetoothLERawAdvertisementsResponse {\n"); + for (const auto &it : this->advertisements) { + out.append(" advertisements: "); + it.dump_to(out); + out.append("\n"); + } + out.append("}"); +} +#endif bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 1: { @@ -6187,7 +6352,20 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) return false; } } -void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); } +bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->conversation_id = value.as_string(); + return true; + } + default: + return false; + } +} +void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_bool(1, this->start); + buffer.encode_string(2, this->conversation_id); +} #ifdef HAS_PROTO_MESSAGE_DUMP void VoiceAssistantRequest::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; @@ -6195,6 +6373,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { out.append(" start: "); out.append(YESNO(this->start)); out.append("\n"); + + out.append(" conversation_id: "); + out.append("'").append(this->conversation_id).append("'"); + out.append("\n"); out.append("}"); } #endif @@ -6305,6 +6487,217 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + case 8: { + this->supported_features = value.as_uint32(); + return true; + } + case 9: { + this->requires_code = value.as_bool(); + return true; + } + case 10: { + this->requires_code_to_arm = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesAlarmControlPanelResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesAlarmControlPanelResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_uint32(8, this->supported_features); + buffer.encode_bool(9, this->requires_code); + buffer.encode_bool(10, this->requires_code_to_arm); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesAlarmControlPanelResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" supported_features: "); + sprintf(buffer, "%u", this->supported_features); + out.append(buffer); + out.append("\n"); + + out.append(" requires_code: "); + out.append(YESNO(this->requires_code)); + out.append("\n"); + + out.append(" requires_code_to_arm: "); + out.append(YESNO(this->requires_code_to_arm)); + out.append("\n"); + out.append("}"); +} +#endif +bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_enum(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void AlarmControlPanelStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AlarmControlPanelStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + out.append("}"); +} +#endif +bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->command = value.as_enum(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 3: { + this->code = value.as_string(); + return true; + } + default: + return false; + } +} +bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->command); + buffer.encode_string(3, this->code); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("AlarmControlPanelCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" code: "); + out.append("'").append(this->code).append("'"); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 0f4b79de19..769f7aaff5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -176,6 +176,27 @@ enum VoiceAssistantEvent : uint32_t { VOICE_ASSISTANT_TTS_START = 7, VOICE_ASSISTANT_TTS_END = 8, }; +enum AlarmControlPanelState : uint32_t { + ALARM_STATE_DISARMED = 0, + ALARM_STATE_ARMED_HOME = 1, + ALARM_STATE_ARMED_AWAY = 2, + ALARM_STATE_ARMED_NIGHT = 3, + ALARM_STATE_ARMED_VACATION = 4, + ALARM_STATE_ARMED_CUSTOM_BYPASS = 5, + ALARM_STATE_PENDING = 6, + ALARM_STATE_ARMING = 7, + ALARM_STATE_DISARMING = 8, + ALARM_STATE_TRIGGERED = 9, +}; +enum AlarmControlPanelStateCommand : uint32_t { + ALARM_CONTROL_PANEL_DISARM = 0, + ALARM_CONTROL_PANEL_ARM_AWAY = 1, + ALARM_CONTROL_PANEL_ARM_HOME = 2, + ALARM_CONTROL_PANEL_ARM_NIGHT = 3, + ALARM_CONTROL_PANEL_ARM_VACATION = 4, + ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5, + ALARM_CONTROL_PANEL_TRIGGER = 6, +}; } // namespace enums @@ -287,7 +308,8 @@ class DeviceInfoResponse : public ProtoMessage { std::string project_name{}; std::string project_version{}; uint32_t webserver_port{0}; - uint32_t bluetooth_proxy_version{0}; + uint32_t legacy_bluetooth_proxy_version{0}; + uint32_t bluetooth_proxy_feature_flags{0}; std::string manufacturer{}; std::string friendly_name{}; uint32_t voice_assistant_version{0}; @@ -1247,12 +1269,14 @@ class MediaPlayerCommandRequest : public ProtoMessage { }; class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { public: + uint32_t flags{0}; 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 BluetoothServiceData : public ProtoMessage { public: @@ -1286,6 +1310,32 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class BluetoothLERawAdvertisement : public ProtoMessage { + public: + uint64_t address{0}; + int32_t rssi{0}; + uint32_t address_type{0}; + std::string data{}; + 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 BluetoothLERawAdvertisementsResponse : public ProtoMessage { + public: + std::vector advertisements{}; + 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 BluetoothDeviceRequest : public ProtoMessage { public: uint64_t address{0}; @@ -1604,12 +1654,14 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage { class VoiceAssistantRequest : public ProtoMessage { public: bool start{false}; + std::string conversation_id{}; 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 VoiceAssistantResponse : public ProtoMessage { @@ -1649,6 +1701,56 @@ class VoiceAssistantEventResponse : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + uint32_t supported_features{0}; + bool requires_code{false}; + bool requires_code_to_arm{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class AlarmControlPanelStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + enums::AlarmControlPanelState state{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class AlarmControlPanelCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + enums::AlarmControlPanelStateCommand command{}; + std::string code{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index df36d0fdea..8752ae6cfd 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -339,6 +339,15 @@ bool APIServerConnectionBase::send_bluetooth_le_advertisement_response(const Blu } #endif #ifdef USE_BLUETOOTH_PROXY +bool APIServerConnectionBase::send_bluetooth_le_raw_advertisements_response( + const BluetoothLERawAdvertisementsResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_bluetooth_le_raw_advertisements_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 93); +} +#endif +#ifdef USE_BLUETOOTH_PROXY #endif #ifdef USE_BLUETOOTH_PROXY bool APIServerConnectionBase::send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg) { @@ -467,6 +476,25 @@ bool APIServerConnectionBase::send_voice_assistant_request(const VoiceAssistantR #endif #ifdef USE_VOICE_ASSISTANT #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response( + const ListEntitiesAlarmControlPanelResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_alarm_control_panel_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 94); +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +bool APIServerConnectionBase::send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_alarm_control_panel_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 95); +} +#endif +#ifdef USE_ALARM_CONTROL_PANEL +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -874,6 +902,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); #endif this->on_voice_assistant_event_response(msg); +#endif + break; + } + case 96: { +#ifdef USE_ALARM_CONTROL_PANEL + AlarmControlPanelCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str()); +#endif + this->on_alarm_control_panel_command_request(msg); #endif break; } @@ -1286,6 +1325,19 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo this->subscribe_voice_assistant(msg); } #endif +#ifdef USE_ALARM_CONTROL_PANEL +void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->alarm_control_panel_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 3808f128a4..2864e303c0 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -161,6 +161,9 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_BLUETOOTH_PROXY bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg); #endif +#ifdef USE_BLUETOOTH_PROXY + bool send_bluetooth_le_raw_advertisements_response(const BluetoothLERawAdvertisementsResponse &msg); +#endif #ifdef USE_BLUETOOTH_PROXY virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; #endif @@ -236,6 +239,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_VOICE_ASSISTANT virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -321,6 +333,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_VOICE_ASSISTANT virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -402,6 +417,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_VOICE_ASSISTANT void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; #endif +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 068f74315c..87b5f9e63f 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -291,112 +291,7 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon client->send_homeassistant_service_call(call); } } -#ifdef USE_BLUETOOTH_PROXY -void APIServer::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_le_advertisement(call); - } -} -void APIServer::send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { - BluetoothDeviceConnectionResponse call; - call.address = address; - call.connected = connected; - call.mtu = mtu; - call.error = error; - for (auto &client : this->clients_) { - client->send_bluetooth_device_connection_response(call); - } -} - -void APIServer::send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error) { - BluetoothDevicePairingResponse call; - call.address = address; - call.paired = paired; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_pairing_response(call); - } -} - -void APIServer::send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error) { - BluetoothDeviceUnpairingResponse call; - call.address = address; - call.success = success; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_unpairing_response(call); - } -} - -void APIServer::send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error) { - BluetoothDeviceClearCacheResponse call; - call.address = address; - call.success = success; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_device_clear_cache_response(call); - } -} - -void APIServer::send_bluetooth_connections_free(uint8_t free, uint8_t limit) { - BluetoothConnectionsFreeResponse call; - call.free = free; - call.limit = limit; - - for (auto &client : this->clients_) { - client->send_bluetooth_connections_free_response(call); - } -} - -void APIServer::send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_read_response(call); - } -} -void APIServer::send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_write_response(call); - } -} -void APIServer::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_notify_data_response(call); - } -} -void APIServer::send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_notify_response(call); - } -} -void APIServer::send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call) { - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_get_services_response(call); - } -} -void APIServer::send_bluetooth_gatt_services_done(uint64_t address) { - BluetoothGATTGetServicesDoneResponse call; - call.address = address; - - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_get_services_done_response(call); - } -} -void APIServer::send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { - BluetoothGATTErrorResponse call; - call.address = address; - call.handle = handle; - call.error = error; - - for (auto &client : this->clients_) { - client->send_bluetooth_gatt_error_response(call); - } -} - -#endif APIServer::APIServer() { global_api_server = this; } void APIServer::subscribe_home_assistant_state(std::string entity_id, optional attribute, std::function f) { @@ -428,20 +323,29 @@ void APIServer::on_shutdown() { } #ifdef USE_VOICE_ASSISTANT -bool APIServer::start_voice_assistant() { +bool APIServer::start_voice_assistant(const std::string &conversation_id) { for (auto &c : this->clients_) { - if (c->request_voice_assistant(true)) + if (c->request_voice_assistant(true, conversation_id)) return true; } return false; } void APIServer::stop_voice_assistant() { for (auto &c : this->clients_) { - if (c->request_voice_assistant(false)) + if (c->request_voice_assistant(false, "")) return; } } #endif +#ifdef USE_ALARM_CONTROL_PANEL +void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_alarm_control_panel_state(obj); +} +#endif + } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index a1bec2802f..be124f42ff 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -75,31 +75,20 @@ class APIServer : public Component, public Controller { void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); -#ifdef USE_BLUETOOTH_PROXY - void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call); - void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); - void send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); - void send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); - void send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); - void send_bluetooth_connections_free(uint8_t free, uint8_t limit); - void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call); - void send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call); - void send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call); - void send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call); - void send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call); - void send_bluetooth_gatt_services_done(uint64_t address); - void send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); -#endif void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } #ifdef USE_HOMEASSISTANT_TIME void request_time(); #endif #ifdef USE_VOICE_ASSISTANT - bool start_voice_assistant(); + bool start_voice_assistant(const std::string &conversation_id); void stop_voice_assistant(); #endif +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; +#endif + bool is_connected() const; struct HomeAssistantStateSubscription { diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 85d4cd61ef..cd73d1ef5d 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -69,6 +69,11 @@ bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_play return this->client_->send_media_player_info(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + return this->client_->send_alarm_control_panel_info(a_alarm_control_panel); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 4fbaa509a2..b40d77e841 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -54,6 +54,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_MEDIA_PLAYER bool on_media_player(media_player::MediaPlayer *media_player) override; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 1d1ba0245e..66b5f40928 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -55,6 +55,11 @@ bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_play return this->client_->send_media_player_state(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); +} +#endif InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 7a7ba697c0..0597b9f384 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -51,6 +51,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_MEDIA_PLAYER bool on_media_player(media_player::MediaPlayer *media_player) override; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index fbd2876dc9..c355953d94 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -442,7 +442,7 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { void BedJetHub::send_local_time() { if (this->time_id_.has_value()) { auto *time_id = *this->time_id_; - time::ESPTime now = time_id->now(); + ESPTime now = time_id->now(); if (now.is_valid()) { this->set_clock(now.hour, now.minute); ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index 5809827cfa..bb1349b2ac 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -13,6 +13,7 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif #include diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index 67f2c3516f..d54b7678e1 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_IBEACON_MAJOR, CONF_IBEACON_MINOR, CONF_IBEACON_UUID, + CONF_MIN_RSSI, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -37,6 +38,9 @@ CONFIG_SCHEMA = cv.All( 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_MIN_RSSI): cv.All( + cv.decibel, cv.int_range(min=-90, max=-30) + ), } ) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) @@ -51,6 +55,9 @@ async def to_code(config): await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) + if CONF_MIN_RSSI in config: + cg.add(var.set_minimum_rssi(config[CONF_MIN_RSSI])) + if CONF_MAC_ADDRESS in config: cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 1689c9ba3f..953ea460a8 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -41,12 +41,19 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, this->check_ibeacon_minor_ = true; this->ibeacon_minor_ = minor; } + void set_minimum_rssi(int rssi) { + this->check_minimum_rssi_ = true; + this->minimum_rssi_ = rssi; + } void on_scan_end() override { if (!this->found_) this->publish_state(false); this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (this->check_minimum_rssi_ && this->minimum_rssi_ <= device.get_rssi()) { + return false; + } switch (this->match_by_) { case MATCH_BY_MAC_ADDRESS: if (device.address_uint64() == this->address_) { @@ -96,17 +103,21 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; MatchType match_by_; - bool found_{false}; - uint64_t address_; esp32_ble_tracker::ESPBTUUID uuid_; esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; - uint16_t ibeacon_major_; - bool check_ibeacon_major_; - uint16_t ibeacon_minor_; - bool check_ibeacon_minor_; + uint16_t ibeacon_major_{0}; + uint16_t ibeacon_minor_{0}; + + int minimum_rssi_{0}; + + bool check_ibeacon_major_{false}; + bool check_ibeacon_minor_{false}; + bool check_minimum_rssi_{false}; + + bool found_{false}; }; } // namespace ble_presence diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 0cb511de3b..79aebce7d3 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -102,8 +102,9 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; uint16_t ibeacon_major_; - bool check_ibeacon_major_; uint16_t ibeacon_minor_; + + bool check_ibeacon_major_; bool check_ibeacon_minor_; }; diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 9354ab36d6..26304325c1 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -1,6 +1,6 @@ #include "bluetooth_connection.h" -#include "esphome/components/api/api_server.h" +#include "esphome/components/api/api_pb2.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -20,24 +20,21 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga switch (event) { case ESP_GATTC_DISCONNECT_EVT: { - api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->disconnect.reason); + this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); this->set_address(0); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_connections_free(); break; } case ESP_GATTC_OPEN_EVT: { if (param->open.conn_id != this->conn_id_) break; if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { - api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->open.status); + this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); this->set_address(0); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_connections_free(); } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_device_connection(this->address_, true, this->mtu_); + this->proxy_->send_connections_free(); } this->seen_mtu_or_services_ = false; break; @@ -52,9 +49,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->seen_mtu_or_services_ = true; break; } - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_device_connection(this->address_, true, this->mtu_); + this->proxy_->send_connections_free(); break; } case ESP_GATTC_SEARCH_CMPL_EVT: { @@ -67,9 +63,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga this->seen_mtu_or_services_ = true; break; } - api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); - api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), - this->proxy_->get_bluetooth_connections_limit()); + this->proxy_->send_device_connection(this->address_, true, this->mtu_); + this->proxy_->send_connections_free(); break; } case ESP_GATTC_READ_DESCR_EVT: @@ -79,7 +74,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga if (param->read.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->read.handle, param->read.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->read.handle, param->read.status); + this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); break; } api::BluetoothGATTReadResponse resp; @@ -89,7 +84,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga for (uint16_t i = 0; i < param->read.value_len; i++) { resp.data.push_back(param->read.value[i]); } - api::global_api_server->send_bluetooth_gatt_read_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp); break; } case ESP_GATTC_WRITE_CHAR_EVT: @@ -99,13 +94,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga if (param->write.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->write.handle, param->write.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->write.handle, param->write.status); + this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); break; } api::BluetoothGATTWriteResponse resp; resp.address = this->address_; resp.handle = param->write.handle; - api::global_api_server->send_bluetooth_gatt_write_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_write_response(resp); break; } case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { @@ -113,28 +108,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle, param->unreg_for_notify.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->unreg_for_notify.handle, - param->unreg_for_notify.status); + this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); break; } api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->unreg_for_notify.handle; - api::global_api_server->send_bluetooth_gatt_notify_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp); break; } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { if (param->reg_for_notify.status != ESP_GATT_OK) { ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_, this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status); - api::global_api_server->send_bluetooth_gatt_error(this->address_, param->reg_for_notify.handle, - param->reg_for_notify.status); + this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); break; } api::BluetoothGATTNotifyResponse resp; resp.address = this->address_; resp.handle = param->reg_for_notify.handle; - api::global_api_server->send_bluetooth_gatt_notify_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp); break; } case ESP_GATTC_NOTIFY_EVT: { @@ -149,7 +142,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga for (uint16_t i = 0; i < param->notify.value_len; i++) { resp.data.push_back(param->notify.value[i]); } - api::global_api_server->send_bluetooth_gatt_notify_data_response(resp); + this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp); break; } default: @@ -166,10 +159,9 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) break; if (param->ble_security.auth_cmpl.success) { - api::global_api_server->send_bluetooth_device_pairing(this->address_, true); + this->proxy_->send_device_pairing(this->address_, true); } else { - api::global_api_server->send_bluetooth_device_pairing(this->address_, false, - param->ble_security.auth_cmpl.fail_reason); + this->proxy_->send_device_pairing(this->address_, false, param->ble_security.auth_cmpl.fail_reason); } break; default: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 76950c944e..b633fe2430 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -1,11 +1,10 @@ #include "bluetooth_proxy.h" #include "esphome/core/log.h" +#include "esphome/core/macros.h" #ifdef USE_ESP32 -#include "esphome/components/api/api_server.h" - namespace esphome { namespace bluetooth_proxy { @@ -27,15 +26,39 @@ std::vector get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - if (!api::global_api_server->is_connected()) + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) return false; + ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), device.get_rssi()); this->send_api_packet_(device); - return true; } +bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) + return false; + + api::BluetoothLERawAdvertisementsResponse resp; + for (size_t i = 0; i < count; i++) { + auto &result = advertisements[i]; + api::BluetoothLERawAdvertisement adv; + adv.address = esp32_ble::ble_addr_to_uint64(result.bda); + adv.rssi = result.rssi; + adv.address_type = result.ble_addr_type; + + uint8_t length = result.adv_data_len + result.scan_rsp_len; + adv.data.reserve(length); + for (uint16_t i = 0; i < length; i++) { + adv.data.push_back(result.ble_adv[i]); + } + + resp.advertisements.push_back(std::move(adv)); + } + ESP_LOGV(TAG, "Proxying %d packets", count); + this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); + return true; +} void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { api::BluetoothLEAdvertisementResponse resp; resp.address = device.address_uint64(); @@ -58,7 +81,7 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi manufacturer_data.data.assign(data.data.begin(), data.data.end()); resp.manufacturer_data.push_back(std::move(manufacturer_data)); } - api::global_api_server->send_bluetooth_le_advertisement(resp); + this->api_connection_->send_bluetooth_le_advertisement(resp); } void BluetoothProxy::dump_config() { @@ -81,7 +104,7 @@ int BluetoothProxy::get_bluetooth_connections_free() { } void BluetoothProxy::loop() { - if (!api::global_api_server->is_connected()) { + if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { for (auto *connection : this->connections_) { if (connection->get_address() != 0) { connection->disconnect(); @@ -92,7 +115,7 @@ void BluetoothProxy::loop() { for (auto *connection : this->connections_) { if (connection->send_service_ == connection->service_count_) { connection->send_service_ = DONE_SENDING_SERVICES; - api::global_api_server->send_bluetooth_gatt_services_done(connection->get_address()); + this->send_gatt_services_done(connection->get_address()); if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { connection->release_services(); @@ -170,7 +193,7 @@ void BluetoothProxy::loop() { service_resp.characteristics.push_back(std::move(characteristic_resp)); } resp.services.push_back(std::move(service_resp)); - api::global_api_server->send_bluetooth_gatt_services(resp); + this->api_connection_->send_bluetooth_gatt_get_services_response(resp); } } } @@ -208,16 +231,15 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest auto *connection = this->get_connection_(msg.address, true); if (connection == nullptr) { ESP_LOGW(TAG, "No free connections available"); - api::global_api_server->send_bluetooth_device_connection(msg.address, false); + this->send_device_connection(msg.address, false); return; } if (connection->state() == espbt::ClientState::CONNECTED || connection->state() == espbt::ClientState::ESTABLISHED) { ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(), connection->address_str().c_str()); - api::global_api_server->send_bluetooth_device_connection(msg.address, true); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, true); + this->send_connections_free(); return; } else if (connection->state() == espbt::ClientState::SEARCHING) { ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device", @@ -263,25 +285,22 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest } else { connection->set_state(espbt::ClientState::SEARCHING); } - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_connections_free(); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { - api::global_api_server->send_bluetooth_device_connection(msg.address, false); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, false); + this->send_connections_free(); return; } if (connection->state() != espbt::ClientState::IDLE) { connection->disconnect(); } else { connection->set_address(0); - api::global_api_server->send_bluetooth_device_connection(msg.address, false); - api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), - this->get_bluetooth_connections_limit()); + this->send_device_connection(msg.address, false); + this->send_connections_free(); } break; } @@ -291,10 +310,10 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest if (!connection->is_paired()) { auto err = connection->pair(); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_device_pairing(msg.address, false, err); + this->send_device_pairing(msg.address, false, err); } } else { - api::global_api_server->send_bluetooth_device_pairing(msg.address, true); + this->send_device_pairing(msg.address, true); } } break; @@ -303,14 +322,20 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest esp_bd_addr_t address; uint64_to_bd_addr(msg.address, address); esp_err_t ret = esp_ble_remove_bond_device(address); - api::global_api_server->send_bluetooth_device_unpairing(msg.address, ret == ESP_OK, ret); + this->send_device_pairing(msg.address, ret == ESP_OK, ret); break; } case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: { esp_bd_addr_t address; uint64_to_bd_addr(msg.address, address); esp_err_t ret = esp_ble_gattc_cache_clean(address); - api::global_api_server->send_bluetooth_device_clear_cache(msg.address, ret == ESP_OK, ret); + api::BluetoothDeviceClearCacheResponse call; + call.address = msg.address; + call.success = ret == ESP_OK; + call.error = ret; + + this->api_connection_->send_bluetooth_device_clear_cache_response(call); + break; } } @@ -320,13 +345,13 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->read_characteristic(msg.handle); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -334,13 +359,13 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest & auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->write_characteristic(msg.handle, msg.data, msg.response); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -348,13 +373,13 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->read_descriptor(msg.handle); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -362,13 +387,13 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->write_descriptor(msg.handle, msg.data, true); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } @@ -376,12 +401,12 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr || !connection->connected()) { ESP_LOGW(TAG, "Cannot get GATT services, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); return; } if (!connection->service_count_) { ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); - api::global_api_server->send_bluetooth_gatt_services_done(msg.address); + this->send_gatt_services_done(msg.address); return; } if (connection->send_service_ == @@ -393,16 +418,89 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest auto *connection = this->get_connection_(msg.address, false); if (connection == nullptr) { ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected"); - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); + this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); return; } auto err = connection->notify_characteristic(msg.handle, msg.enable); if (err != ESP_OK) { - api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); + this->send_gatt_error(msg.address, msg.handle, err); } } +void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { + if (this->api_connection_ != nullptr) { + ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); + return; + } + this->api_connection_ = api_connection; + this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; +} + +void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) { + if (this->api_connection_ != api_connection) { + ESP_LOGV(TAG, "API connection is not subscribed"); + return; + } + this->api_connection_ = nullptr; + this->raw_advertisements_ = false; +} + +void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothDeviceConnectionResponse call; + call.address = address; + call.connected = connected; + call.mtu = mtu; + call.error = error; + this->api_connection_->send_bluetooth_device_connection_response(call); +} +void BluetoothProxy::send_connections_free() { + if (this->api_connection_ == nullptr) + return; + api::BluetoothConnectionsFreeResponse call; + call.free = this->get_bluetooth_connections_free(); + call.limit = this->get_bluetooth_connections_limit(); + this->api_connection_->send_bluetooth_connections_free_response(call); +} + +void BluetoothProxy::send_gatt_services_done(uint64_t address) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothGATTGetServicesDoneResponse call; + call.address = address; + this->api_connection_->send_bluetooth_gatt_get_services_done_response(call); +} + +void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { + if (this->api_connection_ == nullptr) + return; + api::BluetoothGATTErrorResponse call; + call.address = address; + call.handle = handle; + call.error = error; + this->api_connection_->send_bluetooth_gatt_error_response(call); +} + +void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { + api::BluetoothDevicePairingResponse call; + call.address = address; + call.paired = paired; + call.error = error; + + this->api_connection_->send_bluetooth_device_pairing_response(call); +} + +void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { + api::BluetoothDeviceUnpairingResponse call; + call.address = address; + call.success = success; + call.error = error; + + this->api_connection_->send_bluetooth_device_unpairing_response(call); +} + BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace bluetooth_proxy diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index a582abc8a3..97b6396b55 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/components/api/api_connection.h" #include "esphome/components/api/api_pb2.h" #include "esphome/components/esp32_ble_client/ble_client_base.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" @@ -21,10 +22,33 @@ static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; using namespace esp32_ble_client; +// Legacy versions: +// Version 1: Initial version without active connections +// Version 2: Support for active connections +// Version 3: New connection API +// Version 4: Pairing support +// Version 5: Cache clear support +static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; +static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; + +enum BluetoothProxyFeature : uint32_t { + FEATURE_PASSIVE_SCAN = 1 << 0, + FEATURE_ACTIVE_CONNECTIONS = 1 << 1, + FEATURE_REMOTE_CACHING = 1 << 2, + FEATURE_PAIRING = 1 << 3, + FEATURE_CACHE_CLEARING = 1 << 4, + FEATURE_RAW_ADVERTISEMENTS = 1 << 5, +}; + +enum BluetoothProxySubscriptionFlag : uint32_t { + SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, +}; + class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: BluetoothProxy(); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; void dump_config() override; void loop() override; @@ -44,6 +68,18 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com int get_bluetooth_connections_free(); int get_bluetooth_connections_limit() { return this->connections_.size(); } + void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); + void unsubscribe_api_connection(api::APIConnection *api_connection); + api::APIConnection *get_api_connection() { return this->api_connection_; } + + void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); + void send_connections_free(); + void send_gatt_services_done(uint64_t address); + void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); + void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); + void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); + void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); + static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) { bd_addr[0] = (address >> 40) & 0xff; bd_addr[1] = (address >> 32) & 0xff; @@ -56,6 +92,27 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } + uint32_t get_legacy_version() const { + if (this->active_) { + return LEGACY_ACTIVE_CONNECTIONS_VERSION; + } + return LEGACY_PASSIVE_ONLY_VERSION; + } + + uint32_t get_feature_flags() const { + uint32_t flags = 0; + flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN; + flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS; + if (this->active_) { + flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS; + flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; + flags |= BluetoothProxyFeature::FEATURE_PAIRING; + flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; + } + + return flags; + } + protected: void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); @@ -64,18 +121,12 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com bool active_; std::vector connections_{}; + api::APIConnection *api_connection_{nullptr}; + bool raw_advertisements_{false}; }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -// Version 1: Initial version without active connections -// Version 2: Support for active connections -// Version 3: New connection API -// Version 4: Pairing support -// Version 5: Cache clear support -static const uint32_t ACTIVE_CONNECTIONS_VERSION = 5; -static const uint32_t PASSIVE_ONLY_VERSION = 1; - } // namespace bluetooth_proxy } // namespace esphome diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 8dc87cece8..2e54e53c56 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -1,9 +1,9 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/helpers.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #ifdef USE_ESP32 #include @@ -11,6 +11,7 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif namespace esphome { @@ -170,7 +171,7 @@ template class EnterDeepSleepAction : public Action { if (after_time) timestamp += 60 * 60 * 24; - int32_t offset = time::ESPTime::timezone_offset(); + int32_t offset = ESPTime::timezone_offset(); timestamp -= offset; // Change timestamp to utc const uint32_t ms_left = (timestamp - timestamp_now) * 1000; this->deep_sleep_->set_sleep_duration(ms_left); diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index f4e7785b5e..c8dc7b62e2 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -3,18 +3,37 @@ #include #include "esphome/core/application.h" #include "esphome/core/color.h" -#include "esphome/core/log.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace display { static const char *const TAG = "display"; -const Color COLOR_OFF(0, 0, 0, 0); +const Color COLOR_OFF(0, 0, 0, 255); const Color COLOR_ON(255, 255, 255, 255); +static int image_type_to_bpp(ImageType type) { + switch (type) { + case IMAGE_TYPE_BINARY: + return 1; + case IMAGE_TYPE_GRAYSCALE: + return 8; + case IMAGE_TYPE_RGB565: + return 16; + case IMAGE_TYPE_RGB24: + return 24; + case IMAGE_TYPE_RGBA: + return 32; + default: + return 0; + } +} + +static int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; } + void Rect::expand(int16_t horizontal, int16_t vertical) { if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) { this->x = this->x - horizontal; @@ -306,45 +325,8 @@ void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign al this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { - switch (image->get_type()) { - case IMAGE_TYPE_BINARY: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : color_off); - } - } - break; - case IMAGE_TYPE_GRAYSCALE: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); - } - } - break; - case IMAGE_TYPE_RGB24: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); - } - } - break; - case IMAGE_TYPE_TRANSPARENT_BINARY: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - if (image->get_pixel(img_x, img_y)) - this->draw_pixel_at(x + img_x, y + img_y, color_on); - } - } - break; - case IMAGE_TYPE_RGB565: - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y)); - } - } - break; - } +void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { + image->draw(x, y, this, color_on, color_off); } #ifdef USE_GRAPH @@ -472,24 +454,21 @@ void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) this->trigger(from, to); } -#ifdef USE_TIME -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, - time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) { this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); } -void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, align, format, time); } -void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, ESPTime time) { this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); } -#endif void DisplayBuffer::start_clipping(Rect rect) { if (!this->clipping_rectangle_.empty()) { @@ -622,108 +601,170 @@ Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : basel glyphs_.emplace_back(&data[i]); } -bool Image::get_pixel(int x, int y) const { +void Image::draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) { + switch (type_) { + case IMAGE_TYPE_BINARY: { + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + if (this->get_binary_pixel_(img_x, img_y)) { + display->draw_pixel_at(x + img_x, y + img_y, color_on); + } else if (!this->transparent_) { + display->draw_pixel_at(x + img_x, y + img_y, color_off); + } + } + } + break; + } + case IMAGE_TYPE_GRAYSCALE: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_grayscale_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGB565: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgb565_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGB24: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgb24_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + case IMAGE_TYPE_RGBA: + for (int img_x = 0; img_x < width_; img_x++) { + for (int img_y = 0; img_y < height_; img_y++) { + auto color = this->get_rgba_pixel_(img_x, img_y); + if (color.w >= 0x80) { + display->draw_pixel_at(x + img_x, y + img_y, color); + } + } + } + break; + } +} +Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return false; + return color_off; + switch (this->type_) { + case IMAGE_TYPE_BINARY: + return this->get_binary_pixel_(x, y) ? color_on : color_off; + case IMAGE_TYPE_GRAYSCALE: + return this->get_grayscale_pixel_(x, y); + case IMAGE_TYPE_RGB565: + return this->get_rgb565_pixel_(x, y); + case IMAGE_TYPE_RGB24: + return this->get_rgb24_pixel_(x, y); + case IMAGE_TYPE_RGBA: + return this->get_rgba_pixel_(x, y); + default: + return color_off; + } +} +bool Image::get_binary_pixel_(int x, int y) const { const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; const uint32_t pos = x + y * width_8; return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } -Color Image::get_color_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_) * 3; - const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | - (progmem_read_byte(this->data_start_ + pos + 1) << 8) | - (progmem_read_byte(this->data_start_ + pos + 0) << 16); - return Color(color32); +Color Image::get_rgba_pixel_(int x, int y) const { + const uint32_t pos = (x + y * this->width_) * 4; + return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); } -Color Image::get_rgb565_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; +Color Image::get_rgb24_pixel_(int x, int y) const { + const uint32_t pos = (x + y * this->width_) * 3; + Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), + progmem_read_byte(this->data_start_ + pos + 2)); + if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { + // (0, 0, 1) has been defined as transparent color for non-alpha images. + // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) + color.w = 0; + } else { + color.w = 0xFF; + } + return color; +} +Color Image::get_rgb565_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_) * 2; uint16_t rgb565 = progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); auto r = (rgb565 & 0xF800) >> 11; auto g = (rgb565 & 0x07E0) >> 5; auto b = rgb565 & 0x001F; - return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); + Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); + if (rgb565 == 0x0020 && transparent_) { + // darkest green has been defined as transparent color for transparent RGB565 images. + color.w = 0; + } else { + color.w = 0xFF; + } + return color; } -Color Image::get_grayscale_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; +Color Image::get_grayscale_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_); const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - return Color(gray | gray << 8 | gray << 16 | gray << 24); + uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; + return Color(gray, gray, gray, alpha); } int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } ImageType Image::get_type() const { return this->type_; } Image::Image(const uint8_t *data_start, int width, int height, ImageType type) : width_(width), height_(height), type_(type), data_start_(data_start) {} -int Image::get_current_frame() const { return 0; } -bool Animation::get_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return false; - const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t frame_index = this->height_ * width_8 * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return false; - const uint32_t pos = x + y * width_8 + frame_index; - return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); -} -Color Animation::get_color_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 3; - const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | - (progmem_read_byte(this->data_start_ + pos + 1) << 8) | - (progmem_read_byte(this->data_start_ + pos + 0) << 16); - return Color(color32); -} -Color Animation::get_rgb565_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index) * 2; - uint16_t rgb565 = - progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); - auto r = (rgb565 & 0xF800) >> 11; - auto g = (rgb565 & 0x07E0) >> 5; - auto b = rgb565 & 0x001F; - return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); -} -Color Animation::get_grayscale_pixel(int x, int y) const { - if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) - return Color::BLACK; - const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; - if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) - return Color::BLACK; - const uint32_t pos = (x + y * this->width_ + frame_index); - const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - return Color(gray | gray << 8 | gray << 16 | gray << 24); -} Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) - : Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {} -int Animation::get_animation_frame_count() const { return this->animation_frame_count_; } + : Image(data_start, width, height, type), + animation_data_start_(data_start), + current_frame_(0), + animation_frame_count_(animation_frame_count), + loop_start_frame_(0), + loop_end_frame_(animation_frame_count_), + loop_count_(0), + loop_current_iteration_(1) {} +void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) { + loop_start_frame_ = std::min(start_frame, animation_frame_count_); + loop_end_frame_ = std::min(end_frame, animation_frame_count_); + loop_count_ = count; + loop_current_iteration_ = 1; +} + +uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; } int Animation::get_current_frame() const { return this->current_frame_; } void Animation::next_frame() { this->current_frame_++; + if (loop_count_ && this->current_frame_ == loop_end_frame_ && + (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { + this->current_frame_ = loop_start_frame_; + this->loop_current_iteration_++; + } if (this->current_frame_ >= animation_frame_count_) { + this->loop_current_iteration_ = 1; this->current_frame_ = 0; } + + this->update_data_start_(); } void Animation::prev_frame() { this->current_frame_--; if (this->current_frame_ < 0) { this->current_frame_ = this->animation_frame_count_ - 1; } + + this->update_data_start_(); } void Animation::set_frame(int frame) { @@ -736,6 +777,13 @@ void Animation::set_frame(int frame) { this->current_frame_ = this->animation_frame_count_ - abs_frame; } } + + this->update_data_start_(); +} + +void Animation::update_data_start_() { + const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_; + this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; } DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 4477685e1b..0c31ac24d9 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -1,15 +1,12 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/defines.h" -#include "esphome/core/automation.h" -#include "display_color_utils.h" #include #include - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "display_color_utils.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/time.h" #ifdef USE_GRAPH #include "esphome/components/graph/graph.h" @@ -82,8 +79,8 @@ enum ImageType { IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_RGB24 = 2, - IMAGE_TYPE_TRANSPARENT_BINARY = 3, - IMAGE_TYPE_RGB565 = 4, + IMAGE_TYPE_RGB565 = 3, + IMAGE_TYPE_RGBA = 4, }; enum DisplayType { @@ -126,8 +123,8 @@ class Rect { void info(const std::string &prefix = "rect info:"); }; +class BaseImage; class Font; -class Image; class DisplayBuffer; class DisplayPage; class DisplayOnPageChangeTrigger; @@ -263,7 +260,6 @@ class DisplayBuffer { */ void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6))); -#ifdef USE_TIME /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. * * @param x The x coordinate of the text alignment anchor point. @@ -274,7 +270,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) __attribute__((format(strftime, 7, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -286,7 +282,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. @@ -298,7 +294,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -309,9 +305,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, const char *format, time::ESPTime time) - __attribute__((format(strftime, 5, 0))); -#endif + void strftime(int x, int y, Font *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0))); /** Draw the `image` with the top-left corner at [x,y] to the screen. * @@ -321,7 +315,7 @@ class DisplayBuffer { * @param color_on The color to replace in binary images for the on bits. * @param color_off The color to replace in binary images for the off bits. */ - void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); + void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); #ifdef USE_GRAPH /** Draw the `graph` with the top-left corner at [x,y] to the screen. @@ -535,36 +529,46 @@ class Font { int height_; }; -class Image { +class BaseImage { + public: + virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0; + virtual int get_width() const = 0; + virtual int get_height() const = 0; +}; + +class Image : public BaseImage { public: Image(const uint8_t *data_start, int width, int height, ImageType type); - virtual bool get_pixel(int x, int y) const; - virtual Color get_color_pixel(int x, int y) const; - virtual Color get_rgb565_pixel(int x, int y) const; - virtual Color get_grayscale_pixel(int x, int y) const; - int get_width() const; - int get_height() const; + Color get_pixel(int x, int y, Color color_on = COLOR_ON, Color color_off = COLOR_OFF) const; + int get_width() const override; + int get_height() const override; ImageType get_type() const; - virtual int get_current_frame() const; + void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) override; + + void set_transparency(bool transparent) { transparent_ = transparent; } + bool has_transparency() const { return transparent_; } protected: + bool get_binary_pixel_(int x, int y) const; + Color get_rgb24_pixel_(int x, int y) const; + Color get_rgba_pixel_(int x, int y) const; + Color get_rgb565_pixel_(int x, int y) const; + Color get_grayscale_pixel_(int x, int y) const; + int width_; int height_; ImageType type_; const uint8_t *data_start_; + bool transparent_; }; class Animation : public Image { public: Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); - bool get_pixel(int x, int y) const override; - Color get_color_pixel(int x, int y) const override; - Color get_rgb565_pixel(int x, int y) const override; - Color get_grayscale_pixel(int x, int y) const override; - int get_animation_frame_count() const; - int get_current_frame() const override; + uint32_t get_animation_frame_count() const; + int get_current_frame() const; void next_frame(); void prev_frame(); @@ -574,9 +578,18 @@ class Animation : public Image { */ void set_frame(int frame); + void set_loop(uint32_t start_frame, uint32_t end_frame, int count); + protected: + void update_data_start_(); + + const uint8_t *animation_data_start_; int current_frame_; - int animation_frame_count_; + uint32_t animation_frame_count_; + uint32_t loop_start_frame_; + uint32_t loop_end_frame_; + int loop_count_; + int loop_current_iteration_; }; template class DisplayPageShowAction : public Action { diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index d249e9743a..472ccc7a9a 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -37,14 +37,14 @@ void DS1307Component::read_time() { ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); return; } - time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), - .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), - .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), - .day_of_week = uint8_t(ds1307_.reg.weekday), - .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), - .day_of_year = 1, // ignored by recalc_timestamp_utc(false) - .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), - .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; + ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), + .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), + .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), + .day_of_week = uint8_t(ds1307_.reg.weekday), + .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), + .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; rtc_time.recalc_timestamp_utc(false); if (!rtc_time.is_valid()) { ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py index bb662e0989..cec0bdf4fa 100644 --- a/esphome/components/e131/__init__.py +++ b/esphome/components/e131/__init__.py @@ -4,6 +4,7 @@ from esphome.components.light.types import AddressableLightEffect from esphome.components.light.effects import register_addressable_effect from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS +AUTO_LOAD = ["socket"] DEPENDENCIES = ["network"] e131_ns = cg.esphome_ns.namespace("e131") @@ -23,16 +24,11 @@ CHANNELS = { CONF_UNIVERSE = "universe" CONF_E131_ID = "e131_id" -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(E131Component), - cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of( - *METHODS, upper=True - ), - } - ), - cv.only_with_arduino, +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(E131Component), + cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True), + } ) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index 6d584687ce..818006ced7 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -1,18 +1,7 @@ -#ifdef USE_ARDUINO - #include "e131.h" #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" -#ifdef USE_ESP32 -#include -#endif - -#ifdef USE_ESP8266 -#include -#include -#endif - namespace esphome { namespace e131 { @@ -22,17 +11,41 @@ static const int PORT = 5568; E131Component::E131Component() {} E131Component::~E131Component() { - if (udp_) { - udp_->stop(); + if (this->socket_) { + this->socket_->close(); } } void E131Component::setup() { - udp_ = make_unique(); + this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); - if (!udp_->begin(PORT)) { - ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); - mark_failed(); + int enable = 1; + int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); + // we can still continue + } + err = this->socket_->setblocking(false); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); + this->mark_failed(); + return; + } + + struct sockaddr_storage server; + + socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), PORT); + if (sl == 0) { + ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); + this->mark_failed(); + return; + } + server.ss_family = AF_INET; + + err = this->socket_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); return; } @@ -43,22 +56,22 @@ void E131Component::loop() { std::vector payload; E131Packet packet; int universe = 0; + uint8_t buf[1460]; - while (uint16_t packet_size = udp_->parsePacket()) { - payload.resize(packet_size); + ssize_t len = this->socket_->read(buf, sizeof(buf)); + if (len == -1) { + return; + } + payload.resize(len); + memmove(&payload[0], buf, len); - if (!udp_->read(&payload[0], payload.size())) { - continue; - } + if (!this->packet_(payload, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); + return; + } - if (!packet_(payload, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); - continue; - } - - if (!process_(universe, packet)) { - ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); - } + if (!this->process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); } } @@ -106,5 +119,3 @@ bool E131Component::process_(int universe, const E131Packet &packet) { } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index 8bf8999c21..364a05af75 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -1,7 +1,6 @@ #pragma once -#ifdef USE_ARDUINO - +#include "esphome/components/socket/socket.h" #include "esphome/core/component.h" #include @@ -9,8 +8,6 @@ #include #include -class UDP; - namespace esphome { namespace e131 { @@ -47,7 +44,7 @@ class E131Component : public esphome::Component { void leave_(int universe); E131ListenMethod listen_method_{E131_MULTICAST}; - std::unique_ptr udp_; + std::unique_ptr socket_; std::set light_effects_; std::map universe_consumers_; std::map universe_packets_; @@ -55,5 +52,3 @@ class E131Component : public esphome::Component { } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp index 7a3e71808e..42eb0fc56b 100644 --- a/esphome/components/e131/e131_addressable_light_effect.cpp +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -1,7 +1,5 @@ -#ifdef USE_ARDUINO - -#include "e131.h" #include "e131_addressable_light_effect.h" +#include "e131.h" #include "esphome/core/log.h" namespace esphome { @@ -92,5 +90,3 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h index b3e481e43b..56df9cd80f 100644 --- a/esphome/components/e131/e131_addressable_light_effect.h +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -1,7 +1,5 @@ #pragma once -#ifdef USE_ARDUINO - #include "esphome/core/component.h" #include "esphome/components/light/addressable_light_effect.h" @@ -44,5 +42,3 @@ class E131AddressableLightEffect : public light::AddressableLightEffect { } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp index f199d3574b..ac8b72f6e7 100644 --- a/esphome/components/e131/e131_packet.cpp +++ b/esphome/components/e131/e131_packet.cpp @@ -1,15 +1,13 @@ -#ifdef USE_ARDUINO - +#include #include "e131.h" +#include "esphome/components/network/ip_address.h" #include "esphome/core/log.h" #include "esphome/core/util.h" -#include "esphome/components/network/ip_address.h" -#include -#include -#include -#include #include +#include +#include +#include namespace esphome { namespace e131 { @@ -62,7 +60,7 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) bool E131Component::join_igmp_groups_() { if (listen_method_ != E131_MULTICAST) return false; - if (!udp_) + if (this->socket_ == nullptr) return false; for (auto universe : universe_consumers_) { @@ -140,5 +138,3 @@ bool E131Component::packet_(const std::vector &data, int &universe, E13 } // namespace e131 } // namespace esphome - -#endif // USE_ARDUINO diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index e4fdaec0aa..30297654bc 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -42,6 +42,39 @@ ESP32_BASE_PINS = { } ESP32_BOARD_PINS = { + "adafruit_feather_esp32s2_tft": { + "BUTTON": 0, + "A0": 18, + "A1": 17, + "A2": 16, + "A3": 15, + "A4": 14, + "A5": 8, + "SCK": 36, + "MOSI": 35, + "MISO": 37, + "RX": 2, + "TX": 1, + "D13": 13, + "D12": 12, + "D11": 11, + "D10": 10, + "D9": 9, + "D6": 6, + "D5": 5, + "NEOPIXEL": 33, + "PIN_NEOPIXEL": 33, + "NEOPIXEL_POWER": 34, + "SCL": 41, + "SDA": 42, + "TFT_I2C_POWER": 21, + "TFT_CS": 7, + "TFT_DC": 39, + "TFT_RESET": 40, + "TFT_BACKLIGHT": 45, + "LED": 13, + "LED_BUILTIN": 13, + }, "adafruit_qtpy_esp32c3": { "A0": 4, "A1": 3, diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 21ec005e07..6c9124447a 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -244,6 +244,17 @@ void ESP32BLE::dump_config() { } } +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { + uint64_t u = 0; + u |= uint64_t(address[0] & 0xFF) << 40; + u |= uint64_t(address[1] & 0xFF) << 32; + u |= uint64_t(address[2] & 0xFF) << 24; + u |= uint64_t(address[3] & 0xFF) << 16; + u |= uint64_t(address[4] & 0xFF) << 8; + u |= uint64_t(address[5] & 0xFF) << 0; + return u; +} + ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esp32_ble diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 11ae826544..cde17da425 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -18,6 +18,8 @@ namespace esphome { namespace esp32_ble { +uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); + // NOLINTNEXTLINE(modernize-use-using) typedef struct { void *peer_device; diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 23f109b5ac..30589f1a3f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -167,7 +167,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), - cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), } ), cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( @@ -223,7 +223,10 @@ async def to_code(config): for conf in config.get(CONF_ON_BLE_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) if CONF_MAC_ADDRESS in conf: - cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) + addr_list = [] + for it in conf[CONF_MAC_ADDRESS]: + addr_list.append(it.as_hex) + cg.add(trigger.set_addresses(addr_list)) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 6131d6ddf7..6bef9edcb3 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,18 +10,22 @@ namespace esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_address(uint64_t address) { this->address_ = address; } + void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { - if (this->address_ && device.address_uint64() != this->address_) { - return false; + uint64_t u64_addr = device.address_uint64(); + if (!address_vec_.empty()) { + if (std::find(address_vec_.begin(), address_vec_.end(), u64_addr) == address_vec_.end()) { + return false; + } } + this->trigger(device); return true; } protected: - uint64_t address_ = 0; + std::vector address_vec_; }; class BLEServiceDataAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 26687ba9cc..0f6c4117d2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -34,17 +34,6 @@ static const char *const TAG = "esp32_ble_tracker"; ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { - uint64_t u = 0; - u |= uint64_t(address[0] & 0xFF) << 40; - u |= uint64_t(address[1] & 0xFF) << 32; - u |= uint64_t(address[2] & 0xFF) << 24; - u |= uint64_t(address[3] & 0xFF) << 16; - u |= uint64_t(address[4] & 0xFF) << 8; - u |= uint64_t(address[5] & 0xFF) << 0; - return u; -} - float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void ESP32BLETracker::setup() { @@ -114,10 +103,20 @@ void ESP32BLETracker::loop() { if (this->scan_result_index_ && // if it looks like we have a scan result we will take the lock xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { uint32_t index = this->scan_result_index_; - if (index) { - if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { - ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); - } + if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { + ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); + } + + bool bulk_parsed = false; + + for (auto *listener : this->listeners_) { + bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } + for (auto *client : this->clients_) { + bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); + } + + if (!bulk_parsed) { for (size_t i = 0; i < index; i++) { ESPBTDevice device; device.parse_scan_rst(this->scan_result_buffer_[i]); @@ -141,8 +140,8 @@ void ESP32BLETracker::loop() { this->print_bt_device_info(device); } } - this->scan_result_index_ = 0; } + this->scan_result_index_ = 0; xSemaphoreGive(this->scan_result_lock_); } @@ -585,7 +584,7 @@ std::string ESPBTDevice::address_str() const { this->address_[3], this->address_[4], this->address_[5]); return mac; } -uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); } +uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 798f68f2bd..43e88fbf2b 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -113,6 +113,9 @@ class ESPBTDeviceListener { public: virtual void on_scan_end() {} virtual bool parse_device(const ESPBTDevice &device) = 0; + virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { + return false; + }; void set_parent(ESP32BLETracker *parent) { parent_ = parent; } protected: diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index eec1bdc992..df6ee2ce2f 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -1,3 +1,4 @@ +#include #include "led_strip.h" #ifdef USE_ESP32 @@ -195,7 +196,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { break; } ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order); - ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_); + ESP_LOGCONFIG(TAG, " Max refresh rate: %" PRIu32, *this->max_refresh_rate_); ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_); } diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index e46f24ba8e..0f1b989f77 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -16,7 +16,7 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { if (tiny_gps.date.year() < 2019) return; - time::ESPTime val{}; + ESPTime val{}; val.year = tiny_gps.date.year(); val.month = tiny_gps.date.month(); val.day_of_month = tiny_gps.date.day(); diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 8e9ee4c6fb..27af0b5b6b 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -16,6 +16,7 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@freekode"] hm3301_ns = cg.esphome_ns.namespace("hm3301") HM3301Component = hm3301_ns.class_( diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 82ab7bd09a..fdc9fd1ddf 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -7,6 +7,30 @@ namespace i2c { static const char *const TAG = "i2c"; +ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { + ErrorCode err = this->write(&a_register, 1, stop); + if (err != ERROR_OK) + return err; + return bus_->read(address_, data, len); +} + +ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { + WriteBuffer buffers[2]; + buffers[0].data = &a_register; + buffers[0].len = 1; + buffers[1].data = data; + buffers[1].len = len; + return bus_->writev(address_, buffers, 2, stop); +} + +bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { + if (read_register(a_register, reinterpret_cast(data), len * 2) != ERROR_OK) + return false; + for (size_t i = 0; i < len; i++) + data[i] = i2ctohs(data[i]); + return true; +} + bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { // we have to copy in order to be able to change byte order std::unique_ptr temp{new uint16_t[len]}; diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index ffc0dadf81..780528a5c7 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -46,22 +46,10 @@ class I2CDevice { I2CRegister reg(uint8_t a_register) { return {this, a_register}; } ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) { - ErrorCode err = this->write(&a_register, 1, stop); - if (err != ERROR_OK) - return err; - return this->read(data, len); - } + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true) { - WriteBuffer buffers[2]; - buffers[0].data = &a_register; - buffers[0].len = 1; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2, stop); - } + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); // Compat APIs @@ -85,13 +73,7 @@ class I2CDevice { return res; } - bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { - if (read_register(a_register, reinterpret_cast(data), len * 2) != ERROR_OK) - return false; - for (size_t i = 0; i < len; i++) - data[i] = i2ctohs(data[i]); - return true; - } + bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len); bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { return read_register(a_register, data, 1, stop) == ERROR_OK; diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 39d81ef1a1..d72e13630f 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -18,6 +18,7 @@ MULTI_CONF = True CONF_I2S_DOUT_PIN = "i2s_dout_pin" CONF_I2S_DIN_PIN = "i2s_din_pin" +CONF_I2S_MCLK_PIN = "i2s_mclk_pin" CONF_I2S_BCLK_PIN = "i2s_bclk_pin" CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" @@ -44,6 +45,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(I2SAudioComponent), cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, } ) @@ -69,3 +71,5 @@ async def to_code(config): cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) if CONF_I2S_BCLK_PIN in config: cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) + if CONF_I2S_MCLK_PIN in config: + cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN])) diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index f030ed4e75..d8d4a23dde 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -21,7 +21,7 @@ class I2SAudioComponent : public Component { i2s_pin_config_t get_pin_config() const { return { - .mck_io_num = I2S_PIN_NO_CHANGE, + .mck_io_num = this->mclk_pin_, .bck_io_num = this->bclk_pin_, .ws_io_num = this->lrclk_pin_, .data_out_num = I2S_PIN_NO_CHANGE, @@ -29,6 +29,7 @@ class I2SAudioComponent : public Component { }; } + void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } @@ -44,6 +45,7 @@ class I2SAudioComponent : public Component { I2SAudioIn *audio_in_{nullptr}; I2SAudioOut *audio_out_{nullptr}; + int mclk_pin_{I2S_PIN_NO_CHANGE}; int bclk_pin_{I2S_PIN_NO_CHANGE}; int lrclk_pin_; i2s_port_t port_{}; diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index dfc3fb2be2..600a308e6c 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -27,6 +27,7 @@ i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") CONF_MUTE_PIN = "mute_pin" CONF_AUDIO_ID = "audio_id" 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, @@ -38,6 +39,8 @@ EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] +I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] + def validate_esp32_variant(config): if config[CONF_DAC_TYPE] != "internal": @@ -69,6 +72,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MODE, default="mono"): cv.one_of( *EXTERNAL_DAC_OPTIONS, lower=True ), + cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of( + *I2C_COMM_FMT_OPTIONS, lower=True + ), } ).extend(cv.COMPONENT_SCHEMA), }, @@ -94,6 +100,7 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) cg.add(var.set_mute_pin(pin)) cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) + cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp index 6eaa32c23c..9e2e3f136a 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -148,6 +148,7 @@ void I2SAudioMediaPlayer::start_() { pin_config.data_out_num = this->dout_pin_; i2s_set_pin(this->parent_->get_port(), &pin_config); + this->audio_->setI2SCommFMT_LSB(this->i2s_comm_fmt_lsb_); this->audio_->forceMono(this->external_dac_channels_ == 1); if (this->mute_pin_ != nullptr) { this->mute_pin_->setup(); 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 dab9a85d7c..092e6de8e8 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 @@ -39,6 +39,8 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, #endif void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } + void set_i2s_comm_fmt_lsb(bool lsb) { this->i2s_comm_fmt_lsb_ = lsb; } + media_player::MediaPlayerTraits get_traits() override; bool is_muted() const override { return this->muted_; } @@ -71,6 +73,8 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, #endif uint8_t external_dac_channels_; + bool i2s_comm_fmt_lsb_; + HighFrequencyLoopRequester high_freq_; optional current_url_{}; diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 089e796ae0..b917da3045 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -20,6 +20,7 @@ DEPENDENCIES = ["i2s_audio"] CONF_ADC_PIN = "adc_pin" CONF_ADC_TYPE = "adc_type" CONF_PDM = "pdm" +CONF_BITS_PER_SAMPLE = "bits_per_sample" I2SAudioMicrophone = i2s_audio_ns.class_( "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component @@ -30,10 +31,17 @@ 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() @@ -54,6 +62,9 @@ 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_BITS_PER_SAMPLE, default="32bit"): cv.All( + _validate_bits, cv.enum(BITS_PER_SAMPLE) + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -93,6 +104,7 @@ async def to_code(config): cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) cg.add(var.set_pdm(config[CONF_PDM])) - cg.add(var.set_channel(CHANNELS[config[CONF_CHANNEL]])) + cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) 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 9452762e94..9c661c3ac2 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -16,7 +16,13 @@ static const char *const TAG = "i2s_audio.microphone"; void I2SAudioMicrophone::setup() { ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); - this->buffer_.resize(BUFFER_SIZE); + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_ = allocator.allocate(BUFFER_SIZE); + if (this->buffer_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate buffer!"); + this->mark_failed(); + return; + } #if SOC_I2S_SUPPORTS_ADC if (this->adc_) { @@ -48,7 +54,7 @@ void I2SAudioMicrophone::start_() { i2s_driver_config_t config = { .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = 16000, - .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, + .bits_per_sample = this->bits_per_sample_, .channel_format = this->channel_, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, @@ -107,16 +113,35 @@ void I2SAudioMicrophone::stop_() { void I2SAudioMicrophone::read_() { size_t bytes_read = 0; esp_err_t err = - i2s_read(this->parent_->get_port(), this->buffer_.data(), BUFFER_SIZE, &bytes_read, (100 / portTICK_PERIOD_MS)); + i2s_read(this->parent_->get_port(), this->buffer_, BUFFER_SIZE, &bytes_read, (100 / portTICK_PERIOD_MS)); if (err != ESP_OK) { ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); this->status_set_warning(); return; } - this->status_clear_warning(); - this->data_callbacks_.call(this->buffer_); + std::vector samples; + size_t samples_read = 0; + if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { + samples_read = bytes_read / sizeof(int16_t); + } else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) { + samples_read = bytes_read / sizeof(int32_t); + } else { + ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_); + return; + } + samples.resize(samples_read); + if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { + memcpy(samples.data(), this->buffer_, bytes_read); + } else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) { + for (size_t i = 0; i < samples_read; i++) { + int32_t temp = reinterpret_cast(this->buffer_)[i] >> 14; + samples[i] = clamp(temp, INT16_MIN, INT16_MAX); + } + } + + this->data_callbacks_.call(samples); } void I2SAudioMicrophone::loop() { diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index acc7d2b45a..0cb87d42fd 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -29,6 +29,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub #endif void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } + void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } protected: void start_(); @@ -41,8 +42,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool adc_{false}; #endif bool pdm_{false}; - std::vector buffer_; + uint8_t *buffer_; i2s_channel_fmt_t channel_; + i2s_bits_per_sample_t bits_per_sample_; HighFrequencyLoopRequester high_freq_; }; diff --git a/esphome/components/ili9xxx/ili9xxx_init.h b/esphome/components/ili9xxx/ili9xxx_init.h index e8d3614a1d..593b9a79ce 100644 --- a/esphome/components/ili9xxx/ili9xxx_init.h +++ b/esphome/components/ili9xxx/ili9xxx_init.h @@ -116,8 +116,8 @@ static const uint8_t PROGMEM INITCMD_ILI9486[] = { }; static const uint8_t PROGMEM INITCMD_ILI9488[] = { - ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, - ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, + ILI9XXX_GMCTRP1,15, 0x0f, 0x24, 0x1c, 0x0a, 0x0f, 0x08, 0x43, 0x88, 0x32, 0x0f, 0x10, 0x06, 0x0f, 0x07, 0x00, + ILI9XXX_GMCTRN1,15, 0x0F, 0x38, 0x30, 0x09, 0x0f, 0x0f, 0x4e, 0x77, 0x3c, 0x07, 0x10, 0x05, 0x23, 0x1b, 0x00, ILI9XXX_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2 ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 88c625961b..e7cf492c7b 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,5 +1,10 @@ import logging +import io +from pathlib import Path +import re +import requests + from esphome import core from esphome.components import display, font import esphome.config_validation as cv @@ -7,129 +12,347 @@ import esphome.codegen as cg from esphome.const import ( CONF_DITHER, CONF_FILE, + CONF_ICON, CONF_ID, + CONF_PATH, CONF_RAW_DATA_ID, CONF_RESIZE, + CONF_SOURCE, CONF_TYPE, ) from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) +DOMAIN = "image" DEPENDENCIES = ["display"] MULTI_CONF = True ImageType = display.display_ns.enum("ImageType") IMAGE_TYPE = { "BINARY": ImageType.IMAGE_TYPE_BINARY, + "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, - "RGB24": ImageType.IMAGE_TYPE_RGB24, - "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, "RGB565": ImageType.IMAGE_TYPE_RGB565, - "TRANSPARENT_IMAGE": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, + "RGB24": ImageType.IMAGE_TYPE_RGB24, + "RGBA": ImageType.IMAGE_TYPE_RGBA, } +CONF_USE_TRANSPARENCY = "use_transparency" + +# If the MDI file cannot be downloaded within this time, abort. +MDI_DOWNLOAD_TIMEOUT = 30 # seconds + +SOURCE_LOCAL = "local" +SOURCE_MDI = "mdi" + Image_ = display.display_ns.class_("Image") -IMAGE_SCHEMA = cv.Schema( + +def _compute_local_icon_path(value) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN / "mdi" + return base_dir / f"{value[CONF_ICON]}.svg" + + +def download_mdi(value): + mdi_id = value[CONF_ICON] + path = _compute_local_icon_path(value) + if path.is_file(): + return value + url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" + _LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url) + try: + req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(req.content) + return value + + +def validate_cairosvg_installed(value): + """Validate that cairosvg is installed""" + try: + import cairosvg + except ImportError as err: + raise cv.Invalid( + "Please install the cairosvg python package to use this feature. " + "(pip install cairosvg)" + ) from err + + major, minor, _ = cairosvg.__version__.split(".") + if major < "2" or major == "2" and minor < "2": + raise cv.Invalid( + "Please update your cairosvg installation to at least 2.2.0. " + "(pip install -U cairosvg)" + ) + + return value + + +def validate_cross_dependencies(config): + """ + Validate fields whose possible values depend on other fields. + For example, validate that explicitly transparent image types + have "use_transparency" set to True. + Also set the default value for those kind of dependent fields. + """ + is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI + if CONF_TYPE not in config: + if is_mdi: + config[CONF_TYPE] = "TRANSPARENT_BINARY" + else: + config[CONF_TYPE] = "BINARY" + + image_type = config[CONF_TYPE] + is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] + + # If the use_transparency option was not specified, set the default depending on the image type + if CONF_USE_TRANSPARENCY not in config: + config[CONF_USE_TRANSPARENCY] = is_transparent_type + + if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: + raise cv.Invalid(f"Image type {image_type} must always be transparent.") + + if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]: + raise cv.Invalid("MDI images must be binary images.") + + return config + + +def validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("mdi:"): + validate_cairosvg_installed(value) + + match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value) + if match is None: + raise cv.Invalid("Could not parse mdi icon name.") + icon = match.group(1) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_MDI, + CONF_ICON: icon, + } + ) + return FILE_SCHEMA( + { + CONF_SOURCE: SOURCE_LOCAL, + CONF_PATH: value, + } + ) + + +LOCAL_SCHEMA = cv.Schema( { - cv.Required(CONF_ID): cv.declare_id(Image_), - cv.Required(CONF_FILE): cv.file_, - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), - cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( - "NONE", "FLOYDSTEINBERG", upper=True - ), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_PATH): cv.file_, } ) +MDI_SCHEMA = cv.All( + { + cv.Required(CONF_ICON): cv.string, + }, + download_mdi, +) + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + SOURCE_LOCAL: LOCAL_SCHEMA, + SOURCE_MDI: MDI_SCHEMA, + }, + key=CONF_SOURCE, +) + + +def _file_schema(value): + if isinstance(value, str): + return validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +FILE_SCHEMA = cv.Schema(_file_schema) + +IMAGE_SCHEMA = cv.Schema( + cv.All( + { + cv.Required(CONF_ID): cv.declare_id(Image_), + cv.Required(CONF_FILE): FILE_SCHEMA, + cv.Optional(CONF_RESIZE): cv.dimensions, + # Not setting default here on purpose; the default depends on the source type + # (file or mdi), and will be set in the "validate_cross_dependencies" validator. + cv.Optional(CONF_TYPE): 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, + cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( + "NONE", "FLOYDSTEINBERG", upper=True + ), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + }, + validate_cross_dependencies, + ) +) + CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) +def load_svg_image(file: str, resize: tuple[int, int]): + 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 + + if resize: + req_width, req_height = resize + svg_image = svg2png( + url=file, + output_width=req_width, + output_height=req_height, + ) + else: + svg_image = svg2png(url=file) + + return Image.open(io.BytesIO(svg_image)) + + async def to_code(config): from PIL import Image - path = CORE.relative_config_path(config[CONF_FILE]) + conf_file = config[CONF_FILE] + + if conf_file[CONF_SOURCE] == SOURCE_LOCAL: + path = CORE.relative_config_path(conf_file[CONF_PATH]) + + elif conf_file[CONF_SOURCE] == SOURCE_MDI: + path = _compute_local_icon_path(conf_file).as_posix() + try: - image = Image.open(path) + resize = config.get(CONF_RESIZE) + if path.lower().endswith(".svg"): + image = load_svg_image(path, resize) + else: + image = Image.open(path) + if resize: + image.thumbnail(resize) except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") width, height = image.size - if CONF_RESIZE in config: - image.thumbnail(config[CONF_RESIZE]) - width, height = image.size - else: - if width > 500 or height > 500: - _LOGGER.warning( - "The image you requested is very big. Please consider using" - " the resize parameter." - ) + if CONF_RESIZE not in config and (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] dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG if config[CONF_TYPE] == "GRAYSCALE": - image = image.convert("L", dither=dither) + image = image.convert("LA", dither=dither) pixels = list(image.getdata()) data = [0 for _ in range(height * width)] pos = 0 - for pix in pixels: - data[pos] = pix + for g, a in pixels: + if transparent: + if g == 1: + g = 0 + if a < 0x80: + g = 1 + + data[pos] = g + pos += 1 + + elif config[CONF_TYPE] == "RGBA": + image = image.convert("RGBA") + pixels = list(image.getdata()) + data = [0 for _ in range(height * width * 4)] + pos = 0 + for r, g, b, a in pixels: + data[pos] = r + pos += 1 + data[pos] = g + pos += 1 + data[pos] = b + pos += 1 + data[pos] = a pos += 1 elif config[CONF_TYPE] == "RGB24": - image = image.convert("RGB") + image = image.convert("RGBA") pixels = list(image.getdata()) data = [0 for _ in range(height * width * 3)] pos = 0 - for pix in pixels: - data[pos] = pix[0] + for r, g, b, a in pixels: + if transparent: + if r == 0 and g == 0 and b == 1: + b = 0 + if a < 0x80: + r = 0 + g = 0 + b = 1 + + data[pos] = r pos += 1 - data[pos] = pix[1] + data[pos] = g pos += 1 - data[pos] = pix[2] + data[pos] = b pos += 1 - elif config[CONF_TYPE] == "RGB565": - image = image.convert("RGB") + elif config[CONF_TYPE] in ["RGB565"]: + image = image.convert("RGBA") pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 3)] + data = [0 for _ in range(height * width * 2)] pos = 0 - for pix in pixels: - R = pix[0] >> 3 - G = pix[1] >> 2 - B = pix[2] >> 3 + for r, g, b, a in pixels: + R = r >> 3 + G = g >> 2 + B = b >> 3 rgb = (R << 11) | (G << 5) | B + + if transparent: + if rgb == 0x0020: + rgb = 0 + if a < 0x80: + rgb = 0x0020 + data[pos] = rgb >> 8 pos += 1 - data[pos] = rgb & 255 + data[pos] = rgb & 0xFF pos += 1 - elif (config[CONF_TYPE] == "BINARY") or (config[CONF_TYPE] == "TRANSPARENT_BINARY"): + elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: + if transparent: + alpha = image.split()[-1] + has_alpha = alpha.getextrema()[0] < 0xFF + _LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha) image = image.convert("1", dither=dither) width8 = ((width + 7) // 8) * 8 data = [0 for _ in range(height * width8 // 8)] for y in range(height): for x in range(width): - if image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) - - elif config[CONF_TYPE] == "TRANSPARENT_IMAGE": - image = image.convert("RGBA") - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if not image.getpixel((x, y))[3]: + if transparent and has_alpha: + a = alpha.getpixel((x, y)) + if not a: + continue + elif image.getpixel((x, y)): continue pos = x + y * width8 data[pos // 8] |= 0x80 >> (pos % 8) + else: + raise core.EsphomeError( + f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}." + ) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable( + var = cg.new_Pvariable( config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] ) + cg.add(var.set_transparency(transparent)) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index 180d5e93ac..65d7aa508f 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -158,15 +158,13 @@ void LCDDisplay::clear() { for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) this->buffer_[i] = ' '; } -#ifdef USE_TIME -void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) { +void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(column, row, buffer); } -void LCDDisplay::strftime(const char *format, time::ESPTime time) { this->strftime(0, 0, format, time); } -#endif +void LCDDisplay::strftime(const char *format, ESPTime time) { this->strftime(0, 0, format, time); } void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) { location &= 0x7; // we only have 8 locations 0-7 this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (location << 3)); diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index c1dc54a9ed..473acb0bd3 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -1,11 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "esphome/core/time.h" #include #include @@ -44,13 +40,10 @@ class LCDDisplay : public PollingComponent { /// Evaluate the printf-format and print the text at column=0 and row=0. void printf(const char *format, ...) __attribute__((format(printf, 2, 3))); -#ifdef USE_TIME /// Evaluate the strftime-format and print the text at the specified column and row. - void strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) - __attribute__((format(strftime, 4, 0))); + void strftime(uint8_t column, uint8_t row, const char *format, ESPTime time) __attribute__((format(strftime, 4, 0))); /// Evaluate the strftime-format and print the text at column=0 and row=0. - void strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + void strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); /// Load custom char to given location void loadchar(uint8_t location, uint8_t charmap[]); diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index fb4e45b3b6..8dc5d4fbe7 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -1,3 +1,4 @@ +#include #include "light_call.h" #include "light_state.h" #include "esphome/core/log.h" @@ -283,7 +284,7 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' - Invalid effect index %u!", name, *this->effect_); + ESP_LOGW(TAG, "'%s' - Invalid effect index %" PRIu32 "!", name, *this->effect_); this->effect_.reset(); } diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index b50a78eb96..38b4a165cb 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -223,16 +223,14 @@ void MAX7219Component::set_intensity(uint8_t intensity) { } void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } -#ifdef USE_TIME -uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t MAX7219Component::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } } // namespace max7219 } // namespace esphome diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 47b54a4c50..1b724cef69 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -1,11 +1,7 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" - -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif +#include "esphome/core/time.h" #include "esphome/components/spi/spi.h" @@ -46,13 +42,11 @@ class MAX7219Component : public PollingComponent, /// Print `str` at position 0. uint8_t print(const char *str); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); protected: void send_byte_(uint8_t a_register, uint8_t data); diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index c65b8e4823..ec9970d1a0 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -278,7 +278,9 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } } else { - b = pixels[7 - col]; + for (uint8_t i = 0; i < 8; i++) { + b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + } } // send this byte to display at selected chip if (this->invert_) { @@ -325,18 +327,16 @@ uint8_t MAX7219Component::printdigitf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->printdigit(pos, buffer); return 0; } -uint8_t MAX7219Component::strftimedigit(const char *format, time::ESPTime time) { +uint8_t MAX7219Component::strftimedigit(const char *format, ESPTime time) { return this->strftimedigit(0, format, time); } -#endif } // namespace max7219digit } // namespace esphome diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index 17e369a9d9..93d2af21f9 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -1,16 +1,13 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/defines.h" +#include "esphome/core/time.h" + #include "esphome/components/display/display_buffer.h" #include "esphome/components/spi/spi.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace max7219digit { @@ -88,13 +85,11 @@ class MAX7219Component : public PollingComponent, /// Print `str` at position 0. uint8_t printdigit(const char *str); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftimedigit(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftimedigit(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftimedigit(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_BINARY; } diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 66c84da8d8..d9b36c7b09 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_SERVICE, KEY_CORE, KEY_FRAMEWORK_VERSION, + CONF_DISABLED, ) import esphome.codegen as cg import esphome.config_validation as cv @@ -39,7 +40,6 @@ SERVICE_SCHEMA = cv.Schema( } ) -CONF_DISABLED = "disabled" CONFIG_SCHEMA = cv.All( cv.Schema( { diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index ff1f7aa963..d99500bbed 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -41,7 +41,7 @@ async def setup_microphone_core_(var, config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( trigger, - [(cg.std_vector.template(cg.uint8).operator("ref").operator("const"), "x")], + [(cg.std_vector.template(cg.int16).operator("ref").operator("const"), "x")], conf, ) diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h index 5f404b8d74..5313f07f72 100644 --- a/esphome/components/microphone/automation.h +++ b/esphome/components/microphone/automation.h @@ -16,10 +16,10 @@ template class StopCaptureAction : public Action, public void play(Ts... x) override { this->parent_->stop(); } }; -class DataTrigger : public Trigger &> { +class DataTrigger : public Trigger &> { public: explicit DataTrigger(Microphone *mic) { - mic->add_data_callback([this](const std::vector &data) { this->trigger(data); }); + mic->add_data_callback([this](const std::vector &data) { this->trigger(data); }); } }; diff --git a/esphome/components/microphone/microphone.h b/esphome/components/microphone/microphone.h index b725f66ad7..5b16a67c00 100644 --- a/esphome/components/microphone/microphone.h +++ b/esphome/components/microphone/microphone.h @@ -17,7 +17,7 @@ class Microphone { public: virtual void start() = 0; virtual void stop() = 0; - void add_data_callback(std::function &)> &&data_callback) { + void add_data_callback(std::function &)> &&data_callback) { this->data_callbacks_.add(std::move(data_callback)); } @@ -26,7 +26,7 @@ class Microphone { protected: State state_{STATE_STOPPED}; - CallbackManager &)> data_callbacks_{}; + CallbackManager &)> data_callbacks_{}; }; } // namespace microphone diff --git a/esphome/components/mopeka_pro_check/sensor.py b/esphome/components/mopeka_pro_check/sensor.py index 4cd90227ab..51a515ef0c 100644 --- a/esphome/components/mopeka_pro_check/sensor.py +++ b/esphome/components/mopeka_pro_check/sensor.py @@ -44,6 +44,9 @@ CONF_SUPPORTED_TANKS_MAP = { "20LB_V": (38, 254), # empty/full readings for 20lb US tank "30LB_V": (38, 381), "40LB_V": (38, 508), + "EUROPE_6KG": (38, 336), + "EUROPE_11KG": (38, 366), + "EUROPE_14KG": (38, 467), } CODEOWNERS = ["@spbrogan"] diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index af2828ff15..cb5d306976 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -7,6 +7,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/version.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" #endif @@ -14,6 +15,13 @@ #include "lwip/err.h" #include "mqtt_component.h" +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif +#ifdef USE_DASHBOARD_IMPORT +#include "esphome/components/dashboard_import/dashboard_import.h" +#endif + namespace esphome { namespace mqtt { @@ -58,9 +66,63 @@ void MQTTClientComponent::setup() { } #endif + this->subscribe( + "esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, + 2); + + std::string topic = "esphome/ping/"; + topic.append(App.get_name()); + this->subscribe( + topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); + this->last_connected_ = millis(); this->start_dnslookup_(); } + +void MQTTClientComponent::send_device_info_() { + if (!this->is_connected()) { + return; + } + std::string topic = "esphome/discover/"; + topic.append(App.get_name()); + this->publish_json( + topic, + [](JsonObject root) { + auto ip = network::get_ip_address(); + root["ip"] = ip.str(); + root["name"] = App.get_name(); +#ifdef USE_API + root["port"] = api::global_api_server->get_port(); +#endif + root["version"] = ESPHOME_VERSION; + root["mac"] = get_mac_address(); + +#ifdef USE_ESP8266 + root["platform"] = "ESP8266"; +#endif +#ifdef USE_ESP32 + root["platform"] = "ESP32"; +#endif + + root["board"] = ESPHOME_BOARD; +#if defined(USE_WIFI) + root["network"] = "wifi"; +#elif defined(USE_ETHERNET) + root["network"] = "ethernet"; +#endif + +#ifdef ESPHOME_PROJECT_NAME + root["project_name"] = ESPHOME_PROJECT_NAME; + root["project_version"] = ESPHOME_PROJECT_VERSION; +#endif // ESPHOME_PROJECT_NAME + +#ifdef USE_DASHBOARD_IMPORT + root["package_import_url"] = dashboard_import::get_package_import_url(); +#endif + }, + 2, this->discovery_info_.retain); +} + void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT:"); ESP_LOGCONFIG(TAG, " Server Address: %s:%u (%s)", this->credentials_.address.c_str(), this->credentials_.port, @@ -226,6 +288,7 @@ void MQTTClientComponent::check_connected() { delay(100); // NOLINT this->resubscribe_subscriptions_(); + this->send_device_info_(); for (MQTTComponent *component : this->children_) component->schedule_resend_state(); diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 188a027b91..83ed3cc645 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -251,6 +251,8 @@ class MQTTClientComponent : public Component { void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback); protected: + void send_device_info_(); + /// Reconnect to the MQTT broker if not already connected. void start_connect_(); void start_dnslookup_(); diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 86610ef564..28663138d7 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -4,6 +4,8 @@ #include #include "esphome/core/defines.h" +#include "esphome/core/time.h" + #include "esphome/components/uart/uart.h" #include "nextion_base.h" #include "nextion_component.h" @@ -19,10 +21,6 @@ #endif #endif -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace nextion { @@ -318,13 +316,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * Changes the font of the component named `textveiw`. Font IDs are set in the Nextion Editor. */ void set_component_font(const char *component, uint8_t font_id) override; -#ifdef USE_TIME /** * Send the current time to the nextion display. * @param time The time instance to send (get this with id(my_time).now() ). */ - void set_nextion_rtc_time(time::ESPTime time); -#endif + void set_nextion_rtc_time(ESPTime time); /** * Show the page with a given name. diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 308e02bce8..0409e5ea6c 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -219,8 +219,7 @@ void Nextion::filled_circle(int center_x, int center_y, int radius, Color color) display::ColorUtil::color_to_565(color)); } -#ifdef USE_TIME -void Nextion::set_nextion_rtc_time(time::ESPTime time) { +void Nextion::set_nextion_rtc_time(ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year); this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month); this->add_no_result_to_queue_with_printf_("rtc2", "rtc2=%u", time.day_of_month); @@ -228,7 +227,6 @@ void Nextion::set_nextion_rtc_time(time::ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc4", "rtc4=%u", time.minute); this->add_no_result_to_queue_with_printf_("rtc5", "rtc5=%u", time.second); } -#endif } // namespace nextion } // namespace esphome diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 7688629e39..319a1482f1 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -21,25 +21,35 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; } +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // The following function takes longer than the 5 seconds timeout of WDT #if ESP_IDF_VERSION_MAJOR >= 5 esp_task_wdt_config_t wdtc; - wdtc.timeout_ms = 15000; wdtc.idle_core_mask = 0; +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 + wdtc.idle_core_mask |= (1 << 0); +#endif +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 + wdtc.idle_core_mask |= (1 << 1); +#endif + wdtc.timeout_ms = 15000; wdtc.trigger_panic = false; esp_task_wdt_reconfigure(&wdtc); #else esp_task_wdt_init(15, false); +#endif #endif esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 // Set the WDT back to the configured timeout #if ESP_IDF_VERSION_MAJOR >= 5 - wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S; + wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; esp_task_wdt_reconfigure(&wdtc); #else esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); +#endif #endif if (err != ESP_OK) { diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 39ba3dbed4..acf9e923b6 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -99,7 +99,7 @@ void OTAComponent::dump_config() { #endif if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) { - ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %d restarts", + ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %" PRIu32 " restarts", this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_); } } @@ -191,7 +191,7 @@ void OTAComponent::handle_() { this->writeall_(buf, 1); md5::MD5Digest md5{}; md5.init(); - sprintf(sbuf, "%08X", random_uint32()); + sprintf(sbuf, "%08" PRIx32, random_uint32()); md5.add(sbuf, 8); md5.calculate(); md5.get_hex(sbuf); @@ -466,7 +466,7 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_ if (is_manual_safe_mode) { ESP_LOGI(TAG, "Safe mode has been entered manually"); } else { - ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_); + ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_); } if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) { diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index 5073522655..debc007cb8 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -37,7 +37,7 @@ void PCF85063Component::read_time() { ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); return; } - time::ESPTime rtc_time{ + ESPTime rtc_time{ .second = uint8_t(pcf85063_.reg.second + 10 * pcf85063_.reg.second_10), .minute = uint8_t(pcf85063_.reg.minute + 10u * pcf85063_.reg.minute_10), .hour = uint8_t(pcf85063_.reg.hour + 10u * pcf85063_.reg.hour_10), diff --git a/esphome/components/pn532_i2c/__init__.py b/esphome/components/pn532_i2c/__init__.py index 36af2f8aa0..f7b8743967 100644 --- a/esphome/components/pn532_i2c/__init__.py +++ b/esphome/components/pn532_i2c/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ID AUTO_LOAD = ["pn532"] CODEOWNERS = ["@OttoWinter", "@jesserockz"] DEPENDENCIES = ["i2c"] +MULTI_CONF = True pn532_i2c_ns = cg.esphome_ns.namespace("pn532_i2c") PN532I2C = pn532_i2c_ns.class_("PN532I2C", pn532.PN532, i2c.I2CDevice) diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index f416ecf246..0ae2856ce4 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/entity_base.h" #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/controller.h" #include "esphome/core/component.h" diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 3d0d6ec060..ad66ce6d18 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -1,4 +1,7 @@ import logging +import os + +from string import ascii_letters, digits import esphome.codegen as cg import esphome.config_validation as cv @@ -12,9 +15,11 @@ from esphome.const import ( KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, coroutine_with_priority, EsphomeError +from esphome.helpers import mkdir_p, write_file +import esphome.platformio_api as api -from .const import KEY_BOARD, KEY_RP2040, rp2040_ns +from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns # force import gpio to register pin schema from .gpio import rp2040_pin_to_code # noqa @@ -33,6 +38,8 @@ def set_core_data(config): ) CORE.data[KEY_RP2040][KEY_BOARD] = config[CONF_BOARD] + CORE.data[KEY_RP2040][KEY_PIO_FILES] = {} + return config @@ -148,7 +155,9 @@ async def to_code(config): cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option( "platform_packages", - [f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}"], + [ + f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}", + ], ) cg.add_platformio_option("board_build.core", "earlephilhower") @@ -159,3 +168,53 @@ async def to_code(config): "USE_ARDUINO_VERSION_CODE", cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"), ) + + +def add_pio_file(component: str, key: str, data: str): + try: + cv.validate_id_name(key) + except cv.Invalid as e: + raise EsphomeError( + f"[{component}] Invalid PIO key: {key}. Allowed characters: [{ascii_letters}{digits}_]\nPlease report an issue https://github.com/esphome/issues" + ) from e + CORE.data[KEY_RP2040][KEY_PIO_FILES][key] = data + + +def generate_pio_files() -> bool: + import shutil + + shutil.rmtree(CORE.relative_build_path("src/pio"), ignore_errors=True) + + includes: list[str] = [] + files = CORE.data[KEY_RP2040][KEY_PIO_FILES] + if not files: + return False + for key, data in files.items(): + pio_path = CORE.relative_build_path(f"src/pio/{key}.pio") + mkdir_p(os.path.dirname(pio_path)) + write_file(pio_path, data) + _LOGGER.info("Assembling PIO assembly code") + retval = api.run_platformio_cli( + "pkg", + "exec", + "--package", + "earlephilhower/tool-pioasm-rp2040-earlephilhower", + "--", + "pioasm", + pio_path, + pio_path + ".h", + ) + includes.append(f"pio/{key}.pio.h") + if retval != 0: + raise EsphomeError("PIO assembly failed") + + write_file( + CORE.relative_build_path("src/pio_includes.h"), + "#pragma once\n" + "\n".join([f'#include "{include}"' for include in includes]), + ) + return True + + +# Called by writer.py +def copy_files() -> bool: + return generate_pio_files() diff --git a/esphome/components/rp2040/const.py b/esphome/components/rp2040/const.py index e09016ca31..ab5f42d757 100644 --- a/esphome/components/rp2040/const.py +++ b/esphome/components/rp2040/const.py @@ -2,5 +2,6 @@ import esphome.codegen as cg KEY_BOARD = "board" KEY_RP2040 = "rp2040" +KEY_PIO_FILES = "pio_files" rp2040_ns = cg.esphome_ns.namespace("rp2040") diff --git a/esphome/components/rp2040_pio_led_strip/__init__.py b/esphome/components/rp2040_pio_led_strip/__init__.py new file mode 100644 index 0000000000..4c9aa2d155 --- /dev/null +++ b/esphome/components/rp2040_pio_led_strip/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Papa-DMan"] diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.cpp b/esphome/components/rp2040_pio_led_strip/led_strip.cpp new file mode 100644 index 0000000000..ce1836306f --- /dev/null +++ b/esphome/components/rp2040_pio_led_strip/led_strip.cpp @@ -0,0 +1,139 @@ +#include "led_strip.h" + +#ifdef USE_RP2040 + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include +#include +#include + +namespace esphome { +namespace rp2040_pio_led_strip { + +static const char *TAG = "rp2040_pio_led_strip"; + +void RP2040PIOLEDStripLightOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up RP2040 LED Strip..."); + + size_t buffer_size = this->get_buffer_size_(); + + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buf_ = allocator.allocate(buffer_size); + if (this->buf_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate buffer of size %u", buffer_size); + this->mark_failed(); + return; + } + + this->effect_data_ = allocator.allocate(this->num_leds_); + if (this->effect_data_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate effect data of size %u", this->num_leds_); + this->mark_failed(); + return; + } + + // Select PIO instance to use (0 or 1) + this->pio_ = pio0; + if (this->pio_ == nullptr) { + ESP_LOGE(TAG, "Failed to claim PIO instance"); + this->mark_failed(); + return; + } + + // Load the assembled program into the PIO and get its location in the PIO's instruction memory + uint offset = pio_add_program(this->pio_, this->program_); + + // Configure the state machine's PIO, and start it + this->sm_ = pio_claim_unused_sm(this->pio_, true); + if (this->sm_ < 0) { + ESP_LOGE(TAG, "Failed to claim PIO state machine"); + this->mark_failed(); + return; + } + this->init_(this->pio_, this->sm_, offset, this->pin_, this->max_refresh_rate_); +} + +void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) { + ESP_LOGVV(TAG, "Writing state..."); + + if (this->is_failed()) { + ESP_LOGW(TAG, "Light is in failed state, not writing state."); + return; + } + + if (this->buf_ == nullptr) { + ESP_LOGW(TAG, "Buffer is null, not writing state."); + return; + } + + // assemble bits in buffer to 32 bit words with ex for GBR: 0bGGGGGGGGRRRRRRRRBBBBBBBB00000000 + for (int i = 0; i < this->num_leds_; i++) { + uint8_t c1 = this->buf_[(i * 3) + 0]; + uint8_t c2 = this->buf_[(i * 3) + 1]; + uint8_t c3 = this->buf_[(i * 3) + 2]; + uint8_t w = this->is_rgbw_ ? this->buf_[(i * 4) + 3] : 0; + uint32_t color = encode_uint32(c1, c2, c3, w); + pio_sm_put_blocking(this->pio_, this->sm_, color); + } +} + +light::ESPColorView RP2040PIOLEDStripLightOutput::get_view_internal(int32_t index) const { + int32_t r = 0, g = 0, b = 0, w = 0; + switch (this->rgb_order_) { + case ORDER_RGB: + r = 0; + g = 1; + b = 2; + break; + case ORDER_RBG: + r = 0; + g = 2; + b = 1; + break; + case ORDER_GRB: + r = 1; + g = 0; + b = 2; + break; + case ORDER_GBR: + r = 2; + g = 0; + b = 1; + break; + case ORDER_BGR: + r = 2; + g = 1; + b = 0; + break; + case ORDER_BRG: + r = 1; + g = 2; + b = 0; + break; + } + uint8_t multiplier = this->is_rgbw_ ? 4 : 3; + return {this->buf_ + (index * multiplier) + r, + this->buf_ + (index * multiplier) + g, + this->buf_ + (index * multiplier) + b, + this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, + &this->effect_data_[index], + &this->correction_}; +} + +void RP2040PIOLEDStripLightOutput::dump_config() { + ESP_LOGCONFIG(TAG, "RP2040 PIO LED Strip Light Output:"); + ESP_LOGCONFIG(TAG, " Pin: GPIO%d", this->pin_); + ESP_LOGCONFIG(TAG, " Number of LEDs: %d", this->num_leds_); + ESP_LOGCONFIG(TAG, " RGBW: %s", YESNO(this->is_rgbw_)); + ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order_to_string(this->rgb_order_)); + ESP_LOGCONFIG(TAG, " Max Refresh Rate: %f Hz", this->max_refresh_rate_); +} + +float RP2040PIOLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } + +} // namespace rp2040_pio_led_strip +} // namespace esphome + +#endif diff --git a/esphome/components/rp2040_pio_led_strip/led_strip.h b/esphome/components/rp2040_pio_led_strip/led_strip.h new file mode 100644 index 0000000000..25ef9ca55f --- /dev/null +++ b/esphome/components/rp2040_pio_led_strip/led_strip.h @@ -0,0 +1,108 @@ +#pragma once + +#ifdef USE_RP2040 + +#include "esphome/core/color.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include "esphome/components/light/addressable_light.h" +#include "esphome/components/light/light_output.h" + +#include +#include +#include + +namespace esphome { +namespace rp2040_pio_led_strip { + +enum RGBOrder : uint8_t { + ORDER_RGB, + ORDER_RBG, + ORDER_GRB, + ORDER_GBR, + ORDER_BGR, + ORDER_BRG, +}; + +inline const char *rgb_order_to_string(RGBOrder order) { + switch (order) { + case ORDER_RGB: + return "RGB"; + case ORDER_RBG: + return "RBG"; + case ORDER_GRB: + return "GRB"; + case ORDER_GBR: + return "GBR"; + case ORDER_BGR: + return "BGR"; + case ORDER_BRG: + return "BRG"; + default: + return "UNKNOWN"; + } +} + +using init_fn = void (*)(PIO pio, uint sm, uint offset, uint pin, float freq); + +class RP2040PIOLEDStripLightOutput : public light::AddressableLight { + public: + void setup() override; + void write_state(light::LightState *state) override; + float get_setup_priority() const override; + + int32_t size() const override { return this->num_leds_; } + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + this->is_rgbw_ ? traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE}) + : traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; + } + void set_pin(uint8_t pin) { this->pin_ = pin; } + void set_num_leds(uint32_t num_leds) { this->num_leds_ = num_leds; } + void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } + + void set_max_refresh_rate(float interval_us) { this->max_refresh_rate_ = interval_us; } + + void set_pio(int pio_num) { pio_num ? this->pio_ = pio1 : this->pio_ = pio0; } + void set_program(const pio_program_t *program) { this->program_ = program; } + void set_init_function(init_fn init) { this->init_ = init; } + + void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } + void clear_effect_data() override { + for (int i = 0; i < this->size(); i++) { + this->effect_data_[i] = 0; + } + } + + void dump_config() override; + + protected: + light::ESPColorView get_view_internal(int32_t index) const override; + + size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } + + uint8_t *buf_{nullptr}; + uint8_t *effect_data_{nullptr}; + + uint8_t pin_; + uint32_t num_leds_; + bool is_rgbw_; + + pio_hw_t *pio_; + uint sm_; + + RGBOrder rgb_order_{ORDER_RGB}; + + uint32_t last_refresh_{0}; + float max_refresh_rate_; + + const pio_program_t *program_; + init_fn init_; +}; + +} // namespace rp2040_pio_led_strip +} // namespace esphome + +#endif // USE_RP2040 diff --git a/esphome/components/rp2040_pio_led_strip/light.py b/esphome/components/rp2040_pio_led_strip/light.py new file mode 100644 index 0000000000..a2ba72318f --- /dev/null +++ b/esphome/components/rp2040_pio_led_strip/light.py @@ -0,0 +1,273 @@ +from dataclasses import dataclass + +from esphome import pins +from esphome.components import light, rp2040 +from esphome.const import ( + CONF_CHIPSET, + CONF_ID, + CONF_NUM_LEDS, + CONF_OUTPUT_ID, + CONF_PIN, + CONF_RGB_ORDER, +) + +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome.util import _LOGGER + + +def get_nops(timing): + """ + Calculate the number of NOP instructions required to wait for a given amount of time. + """ + time_remaining = timing + nops = [] + if time_remaining < 32: + nops.append(time_remaining - 1) + return nops + nops.append(31) + time_remaining -= 32 + while time_remaining > 0: + if time_remaining >= 32: + nops.append("nop [31]") + time_remaining -= 32 + else: + nops.append("nop [" + str(time_remaining) + " - 1 ]") + time_remaining = 0 + return nops + + +def generate_assembly_code(id, rgbw, t0h, t0l, t1h, t1l): + """ + Generate assembly code with the given timing values. + """ + nops_t0h = get_nops(t0h) + nops_t0l = get_nops(t0l) + nops_t1h = get_nops(t1h) + nops_t1l = get_nops(t1l) + + t0h = nops_t0h.pop(0) + t0l = nops_t0l.pop(0) + t1h = nops_t1h.pop(0) + t1l = nops_t1l.pop(0) + + nops_t0h = "\n".join(" " * 4 + nop for nop in nops_t0h) + nops_t0l = "\n".join(" " * 4 + nop for nop in nops_t0l) + nops_t1h = "\n".join(" " * 4 + nop for nop in nops_t1h) + nops_t1l = "\n".join(" " * 4 + nop for nop in nops_t1l) + + const_csdk_code = f""" +% c-sdk {{ +#include "hardware/clocks.h" + +static inline void rp2040_pio_led_strip_driver_{id}_init(PIO pio, uint sm, uint offset, uint pin, float freq) {{ + pio_gpio_init(pio, pin); + pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); + + pio_sm_config c = rp2040_pio_led_strip_{id}_program_get_default_config(offset); + sm_config_set_set_pins(&c, pin, 1); + sm_config_set_out_shift(&c, false, true, {32 if rgbw else 24}); + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX); + + int cycles_per_bit = 69; + float div = 2.409; + sm_config_set_clkdiv(&c, div); + + + pio_sm_init(pio, sm, offset, &c); + pio_sm_set_enabled(pio, sm, true); +}} +%}}""" + + assembly_template = f""".program rp2040_pio_led_strip_{id} + +.wrap_target +awaiting_data: + ; Wait for data in FIFO queue + pull block ; this will block until there is data in the FIFO queue and then it will pull it into the shift register + set y, {31 if rgbw else 23} ; set y to the number of bits to write counting 0, (23 if RGB, 31 if RGBW) + +mainloop: + ; go through each bit in the shift register and jump to the appropriate label + ; depending on the value of the bit + + out x, 1 + jmp !x, writezero + jmp writeone + +writezero: + ; Write T0H and T0L bits to the output pin + set pins, 1 [{t0h}] +{nops_t0h} + set pins, 0 [{t0l}] +{nops_t0l} + jmp y--, mainloop + jmp awaiting_data + +writeone: + ; Write T1H and T1L bits to the output pin + set pins, 1 [{t1h}] +{nops_t1h} + set pins, 0 [{t1l}] +{nops_t1l} + jmp y--, mainloop + jmp awaiting_data + +.wrap""" + + return assembly_template + const_csdk_code + + +def time_to_cycles(time_us): + cycles_per_us = 57.5 + cycles = round(float(time_us) * cycles_per_us) + return cycles + + +CONF_PIO = "pio" + +CODEOWNERS = ["@Papa-DMan"] +DEPENDENCIES = ["rp2040"] + +rp2040_pio_led_strip_ns = cg.esphome_ns.namespace("rp2040_pio_led_strip") +RP2040PIOLEDStripLightOutput = rp2040_pio_led_strip_ns.class_( + "RP2040PIOLEDStripLightOutput", light.AddressableLight +) + +RGBOrder = rp2040_pio_led_strip_ns.enum("RGBOrder") + +Chipsets = rp2040_pio_led_strip_ns.enum("Chipset") + + +@dataclass +class LEDStripTimings: + T0H: int + T0L: int + T1H: int + T1L: int + + +RGB_ORDERS = { + "RGB": RGBOrder.ORDER_RGB, + "RBG": RGBOrder.ORDER_RBG, + "GRB": RGBOrder.ORDER_GRB, + "GBR": RGBOrder.ORDER_GBR, + "BGR": RGBOrder.ORDER_BGR, + "BRG": RGBOrder.ORDER_BRG, +} + +CHIPSETS = { + "WS2812": LEDStripTimings(20, 43, 41, 31), + "WS2812B": LEDStripTimings(23, 46, 46, 23), + "SK6812": LEDStripTimings(17, 52, 31, 31), + "SM16703": LEDStripTimings(17, 52, 52, 17), +} + +CONF_IS_RGBW = "is_rgbw" +CONF_BIT0_HIGH = "bit0_high" +CONF_BIT0_LOW = "bit0_low" +CONF_BIT1_HIGH = "bit1_high" +CONF_BIT1_LOW = "bit1_low" + + +def _validate_timing(value): + # if doesn't end with us, raise error + if not value.endswith("us"): + raise cv.Invalid("Timing must be in microseconds (us)") + value = float(value[:-2]) + nops = get_nops(value) + nops.pop(0) + if len(nops) > 3: + raise cv.Invalid("Timing is too long, please try again.") + return value + + +CONFIG_SCHEMA = cv.All( + light.ADDRESSABLE_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RP2040PIOLEDStripLightOutput), + cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, + cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), + cv.Required(CONF_PIO): cv.one_of(0, 1, int=True), + cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), + cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, + cv.Inclusive( + CONF_BIT0_HIGH, + "custom", + ): _validate_timing, + cv.Inclusive( + CONF_BIT0_LOW, + "custom", + ): _validate_timing, + cv.Inclusive( + CONF_BIT1_HIGH, + "custom", + ): _validate_timing, + cv.Inclusive( + CONF_BIT1_LOW, + "custom", + ): _validate_timing, + } + ), + cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + id = config[CONF_ID].id + await light.register_light(var, config) + await cg.register_component(var, config) + + cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) + cg.add(var.set_pin(config[CONF_PIN])) + + cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) + cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) + + cg.add(var.set_pio(config[CONF_PIO])) + cg.add(var.set_program(cg.RawExpression(f"&rp2040_pio_led_strip_{id}_program"))) + cg.add( + var.set_init_function( + cg.RawExpression(f"rp2040_pio_led_strip_driver_{id}_init") + ) + ) + + key = f"led_strip_{id}" + + if CONF_CHIPSET in config: + _LOGGER.info("Generating PIO assembly code") + rp2040.add_pio_file( + __name__, + key, + generate_assembly_code( + id, + config[CONF_IS_RGBW], + CHIPSETS[config[CONF_CHIPSET]].T0H, + CHIPSETS[config[CONF_CHIPSET]].T0L, + CHIPSETS[config[CONF_CHIPSET]].T1H, + CHIPSETS[config[CONF_CHIPSET]].T1L, + ), + ) + else: + _LOGGER.info("Generating custom PIO assembly code") + rp2040.add_pio_file( + __name__, + key, + generate_assembly_code( + id, + config[CONF_IS_RGBW], + time_to_cycles(config[CONF_BIT0_HIGH]), + time_to_cycles(config[CONF_BIT0_LOW]), + time_to_cycles(config[CONF_BIT1_HIGH]), + time_to_cycles(config[CONF_BIT1_LOW]), + ), + ) + cg.add_platformio_option( + "platform_packages", + [ + "earlephilhower/tool-pioasm-rp2040-earlephilhower", + ], + ) diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 23258d5785..8ca9a69d1c 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -17,6 +17,13 @@ namespace select { } \ } +#define SUB_SELECT(name) \ + protected: \ + select::Select *name##_select_{nullptr}; \ +\ + public: \ + void set_##name##_select(select::Select *select) { this->name##_select_ = select; } + /** Base-class for all selects. * * A select can use publish_state to send out a new value. diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index 4c70a6b09f..987dd23a19 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -22,7 +22,7 @@ class SNTPComponent : public time::RealTimeClock { this->server_2_ = server_2; this->server_3_ = server_3; } - float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } void update() override; void loop() override; diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index d18e305cc2..a81101f2d1 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.components import display, spi +from esphome.components import display, spi, power_supply from esphome.const import ( CONF_BACKLIGHT_PIN, CONF_DC_PIN, @@ -11,6 +11,7 @@ from esphome.const import ( CONF_MODEL, CONF_RESET_PIN, CONF_WIDTH, + CONF_POWER_SUPPLY, ) from . import st7789v_ns @@ -32,6 +33,7 @@ MODELS = { "TTGO_TDISPLAY_135X240": ST7789VModel.ST7789V_MODEL_TTGO_TDISPLAY_135_240, "ADAFRUIT_FUNHOUSE_240X240": ST7789VModel.ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240, "ADAFRUIT_RR_280X240": ST7789VModel.ST7789V_MODEL_ADAFRUIT_RR_280_240, + "ADAFRUIT_S2_TFT_FEATHER_240X135": ST7789VModel.ST7789V_MODEL_ADAFRUIT_S2_TFT_FEATHER_240_135, "CUSTOM": ST7789VModel.ST7789V_MODEL_CUSTOM, } @@ -58,6 +60,14 @@ def validate_st7789v(config): raise cv.Invalid( f'Do not specify {CONF_HEIGHT}, {CONF_WIDTH}, {CONF_OFFSET_HEIGHT} or {CONF_OFFSET_WIDTH} when using {CONF_MODEL} that is not "CUSTOM"' ) + + if ( + config[CONF_MODEL].upper() == "ADAFRUIT_S2_TFT_FEATHER_240X135" + and CONF_POWER_SUPPLY not in config + ): + raise cv.Invalid( + f'{CONF_POWER_SUPPLY} must be specified when {CONF_MODEL} is "ADAFRUIT_S2_TFT_FEATHER_240X135"' + ) return config @@ -69,6 +79,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, cv.Optional(CONF_HEIGHT): cv.int_, cv.Optional(CONF_WIDTH): cv.int_, @@ -113,3 +124,7 @@ async def to_code(config): config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) + + if CONF_POWER_SUPPLY in config: + ps = await cg.get_variable(config[CONF_POWER_SUPPLY]) + cg.add(var.set_power_supply(ps)) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index 8a4fcfb179..0e7c9b9123 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -8,6 +8,10 @@ static const char *const TAG = "st7789v"; void ST7789V::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI ST7789V..."); +#ifdef USE_POWER_SUPPLY + this->power_.request(); + // the PowerSupply component takes care of post turn-on delay +#endif this->spi_setup(); this->dc_pin_->setup(); // OUTPUT @@ -128,6 +132,9 @@ void ST7789V::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" B/L Pin: ", this->backlight_pin_); LOG_UPDATE_INTERVAL(this); +#ifdef USE_POWER_SUPPLY + ESP_LOGCONFIG(TAG, " Power Supply Configured: yes"); +#endif } float ST7789V::get_setup_priority() const { return setup_priority::PROCESSOR; } @@ -162,6 +169,13 @@ void ST7789V::set_model(ST7789VModel model) { this->offset_width_ = 20; break; + case ST7789V_MODEL_ADAFRUIT_S2_TFT_FEATHER_240_135: + this->height_ = 240; + this->width_ = 135; + this->offset_height_ = 52; + this->offset_width_ = 40; + break; + default: break; } @@ -323,6 +337,8 @@ const char *ST7789V::model_str_() { return "Adafruit Funhouse 240x240"; case ST7789V_MODEL_ADAFRUIT_RR_280_240: return "Adafruit Round-Rectangular 280x240"; + case ST7789V_MODEL_ADAFRUIT_S2_TFT_FEATHER_240_135: + return "Adafruit ESP32-S2 TFT Feather"; default: return "Custom"; } diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 96e97c9d78..ccbe50cf85 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -3,6 +3,9 @@ #include "esphome/core/component.h" #include "esphome/components/spi/spi.h" #include "esphome/components/display/display_buffer.h" +#ifdef USE_POWER_SUPPLY +#include "esphome/components/power_supply/power_supply.h" +#endif namespace esphome { namespace st7789v { @@ -11,6 +14,7 @@ enum ST7789VModel { ST7789V_MODEL_TTGO_TDISPLAY_135_240, ST7789V_MODEL_ADAFRUIT_FUNHOUSE_240_240, ST7789V_MODEL_ADAFRUIT_RR_280_240, + ST7789V_MODEL_ADAFRUIT_S2_TFT_FEATHER_240_135, ST7789V_MODEL_CUSTOM }; @@ -120,6 +124,9 @@ class ST7789V : public PollingComponent, void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } +#ifdef USE_POWER_SUPPLY + void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } +#endif void set_eightbitcolor(bool eightbitcolor) { this->eightbitcolor_ = eightbitcolor; } void set_height(uint32_t height) { this->height_ = height; } @@ -143,6 +150,9 @@ class ST7789V : public PollingComponent, GPIOPin *dc_pin_{nullptr}; GPIOPin *reset_pin_{nullptr}; GPIOPin *backlight_pin_{nullptr}; +#ifdef USE_POWER_SUPPLY + power_supply::PowerSupplyRequester power_; +#endif bool eightbitcolor_{false}; uint16_t height_{0}; diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index b65410cbed..ef368015b1 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -1,19 +1,14 @@ import logging -import re import esphome.config_validation as cv from esphome import core -from esphome.const import CONF_SUBSTITUTIONS +from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS from esphome.yaml_util import ESPHomeDataBase, make_data_base from esphome.config_helpers import merge_config CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) -VALID_SUBSTITUTIONS_CHARACTERS = ( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" -) - def validate_substitution_key(value): value = cv.string(value) @@ -42,12 +37,6 @@ async def to_code(config): pass -# pylint: disable=consider-using-f-string -VARIABLE_PROG = re.compile( - "\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS) -) - - def _expand_substitutions(substitutions, value, path, ignore_missing): if "$" not in value: return value @@ -56,7 +45,7 @@ def _expand_substitutions(substitutions, value, path, ignore_missing): i = 0 while True: - m = VARIABLE_PROG.search(value, i) + m = cv.VARIABLE_PROG.search(value, i) if not m: # Nothing more to match. Done break diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 5f9179682a..2fd9394a5e 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -37,7 +37,7 @@ num_t EquatorialCoordinate::declination_rad() const { return radians(declination num_t HorizontalCoordinate::elevation_rad() const { return radians(elevation); } num_t HorizontalCoordinate::azimuth_rad() const { return radians(azimuth); } -num_t julian_day(time::ESPTime moment) { +num_t julian_day(ESPTime moment) { // p. 59 // UT -> JD, TT -> JDE int y = moment.year; @@ -54,7 +54,7 @@ num_t julian_day(time::ESPTime moment) { int b = 2 - a + a / 4; return ((int) (365.25 * (y + 4716))) + ((int) (30.6001 * (m + 1))) + d + b - 1524.5; } -num_t delta_t(time::ESPTime moment) { +num_t delta_t(ESPTime moment) { // approximation for 2005-2050 from NASA (https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html) int t = moment.year - 2000; return 62.92 + t * (0.32217 + t * 0.005589); @@ -199,7 +199,7 @@ struct SunAtLocation { // see chapter 12, p. 87 num_t jd = moment.jd(); // eq 12.1, p.87; jd for 0h UT of this date - time::ESPTime moment_0h = moment.dt; + ESPTime moment_0h = moment.dt; moment_0h.hour = moment_0h.minute = moment_0h.second = 0; num_t jd0 = Moment{moment_0h}.jd(); num_t t = (jd0 - 2451545) / 36525; @@ -227,9 +227,9 @@ struct SunAtLocation { return HorizontalCoordinate{degrees(elevation_rad), degrees(azimuth_rad) + 180}; } - optional sunrise(time::ESPTime date, num_t zenith) const { return event(true, date, zenith); } - optional sunset(time::ESPTime date, num_t zenith) const { return event(false, date, zenith); } - optional event(bool rise, time::ESPTime date, num_t zenith) const { + optional sunrise(ESPTime date, num_t zenith) const { return event(true, date, zenith); } + optional sunset(ESPTime date, num_t zenith) const { return event(false, date, zenith); } + optional event(bool rise, ESPTime date, num_t zenith) const { // couldn't get the method described in chapter 15 to work, // so instead this is based on the algorithm in time4j // https://github.com/MenoData/Time4J/blob/master/base/src/main/java/net/time4j/calendar/astro/StdSolarCalculator.java @@ -244,7 +244,7 @@ struct SunAtLocation { new_h = *x; } while (std::abs(new_h - old_h) >= 15); time_t new_timestamp = m.timestamp + (time_t) new_h; - return time::ESPTime::from_epoch_local(new_timestamp); + return ESPTime::from_epoch_local(new_timestamp); } protected: @@ -263,14 +263,14 @@ struct SunAtLocation { return hour_angle; } - time::ESPTime local_event_(time::ESPTime date, int hour) const { + ESPTime local_event_(ESPTime date, int hour) const { // input date should be in UTC, and hour/minute/second fields 0 num_t added_d = hour / 24.0 - location.longitude / 360; num_t jd = julian_day(date) + added_d; num_t eot = SunAtTime(jd).equation_of_time() * 240; time_t new_timestamp = (time_t) (date.timestamp + added_d * 86400 - eot); - return time::ESPTime::from_epoch_utc(new_timestamp); + return ESPTime::from_epoch_utc(new_timestamp); } }; @@ -287,7 +287,7 @@ HorizontalCoordinate Sun::calc_coords_() { */ return sun.true_coordinate(m); } -optional Sun::calc_event_(time::ESPTime date, bool rising, double zenith) { +optional Sun::calc_event_(ESPTime date, bool rising, double zenith) { SunAtLocation sun{location_}; if (!date.is_valid()) return {}; @@ -301,24 +301,20 @@ optional Sun::calc_event_(time::ESPTime date, bool rising, double // We're calculating *next* sunrise/sunset, but calculated event // is today, so try again tomorrow time_t new_timestamp = today.timestamp + 24 * 60 * 60; - today = time::ESPTime::from_epoch_utc(new_timestamp); + today = ESPTime::from_epoch_utc(new_timestamp); it = sun.event(rising, today, zenith); } return it; } -optional Sun::calc_event_(bool rising, double zenith) { +optional Sun::calc_event_(bool rising, double zenith) { auto it = Sun::calc_event_(this->time_->utcnow(), rising, zenith); return it; } -optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } -optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } -optional Sun::sunrise(time::ESPTime date, double elevation) { - return this->calc_event_(date, true, 90 - elevation); -} -optional Sun::sunset(time::ESPTime date, double elevation) { - return this->calc_event_(date, false, 90 - elevation); -} +optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } +optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } +optional Sun::sunrise(ESPTime date, double elevation) { return this->calc_event_(date, true, 90 - elevation); } +optional Sun::sunset(ESPTime date, double elevation) { return this->calc_event_(date, false, 90 - elevation); } double Sun::elevation() { return this->calc_coords_().elevation; } double Sun::azimuth() { return this->calc_coords_().azimuth; } diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 9547b2f280..de4801a655 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -1,8 +1,10 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include "esphome/core/automation.h" +#include "esphome/core/time.h" + #include "esphome/components/time/real_time_clock.h" namespace esphome { @@ -26,7 +28,7 @@ struct GeoLocation { }; struct Moment { - time::ESPTime dt; + ESPTime dt; num_t jd() const; num_t jde() const; @@ -57,18 +59,18 @@ class Sun { void set_latitude(double latitude) { location_.latitude = latitude; } void set_longitude(double longitude) { location_.longitude = longitude; } - optional sunrise(double elevation); - optional sunset(double elevation); - optional sunrise(time::ESPTime date, double elevation); - optional sunset(time::ESPTime date, double elevation); + optional sunrise(double elevation); + optional sunset(double elevation); + optional sunrise(ESPTime date, double elevation); + optional sunset(ESPTime date, double elevation); double elevation(); double azimuth(); protected: internal::HorizontalCoordinate calc_coords_(); - optional calc_event_(bool rising, double zenith); - optional calc_event_(time::ESPTime date, bool rising, double zenith); + optional calc_event_(bool rising, double zenith); + optional calc_event_(ESPTime date, bool rising, double zenith); time::RealTimeClock *time_; internal::GeoLocation location_; diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.h b/esphome/components/sun/text_sensor/sun_text_sensor.h index ad01d64ff1..ce7d21fb86 100644 --- a/esphome/components/sun/text_sensor/sun_text_sensor.h +++ b/esphome/components/sun/text_sensor/sun_text_sensor.h @@ -1,6 +1,8 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/time.h" + #include "esphome/components/sun/sun.h" #include "esphome/components/text_sensor/text_sensor.h" @@ -15,7 +17,7 @@ class SunTextSensor : public text_sensor::TextSensor, public PollingComponent { void set_format(const std::string &format) { format_ = format; } void update() override { - optional res; + optional res; if (this->sunrise_) { res = this->parent_->sunrise(this->elevation_); } else { diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 9daac4ee23..b5395a2c83 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -8,6 +8,13 @@ namespace esphome { namespace switch_ { +#define SUB_SWITCH(name) \ + protected: \ + switch_::Switch *name##_switch_{nullptr}; \ +\ + public: \ + void set_##name##_switch(switch_::Switch *s) { this->name##_switch_ = s; } + // bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled const int RESTORE_MODE_ON_MASK = 0x01; const int RESTORE_MODE_PERSISTENT_MASK = 0x02; diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py new file mode 100644 index 0000000000..5156a0832a --- /dev/null +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -0,0 +1,123 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ( + binary_sensor, + alarm_control_panel, +) +from esphome.const import ( + CONF_ID, + CONF_BINARY_SENSORS, + CONF_INPUT, + CONF_RESTORE_MODE, +) +from .. import template_ns + +CODEOWNERS = ["@grahambrown11"] + +CONF_CODES = "codes" +CONF_BYPASS_ARMED_HOME = "bypass_armed_home" +CONF_REQUIRES_CODE_TO_ARM = "requires_code_to_arm" +CONF_ARMING_HOME_TIME = "arming_home_time" +CONF_ARMING_AWAY_TIME = "arming_away_time" +CONF_PENDING_TIME = "pending_time" +CONF_TRIGGER_TIME = "trigger_time" + +FLAG_NORMAL = "normal" +FLAG_BYPASS_ARMED_HOME = "bypass_armed_home" + +BinarySensorFlags = { + FLAG_NORMAL: 1 << 0, + FLAG_BYPASS_ARMED_HOME: 1 << 1, +} + +TemplateAlarmControlPanel = template_ns.class_( + "TemplateAlarmControlPanel", alarm_control_panel.AlarmControlPanel, cg.Component +) + +TemplateAlarmControlPanelRestoreMode = template_ns.enum( + "TemplateAlarmControlPanelRestoreMode" +) +RESTORE_MODES = { + "ALWAYS_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_ALWAYS_DISARMED, + "RESTORE_DEFAULT_DISARMED": TemplateAlarmControlPanelRestoreMode.ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, +} + + +def validate_config(config): + if config.get(CONF_REQUIRES_CODE_TO_ARM, False) and not config.get(CONF_CODES, []): + raise cv.Invalid( + f"{CONF_REQUIRES_CODE_TO_ARM} cannot be True when there are no codes." + ) + return config + + +TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA = cv.maybe_simple_value( + { + cv.Required(CONF_INPUT): cv.use_id(binary_sensor.BinarySensor), + cv.Optional(CONF_BYPASS_ARMED_HOME, default=False): cv.boolean, + }, + key=CONF_INPUT, +) + +TEMPLATE_ALARM_CONTROL_PANEL_SCHEMA = ( + alarm_control_panel.ALARM_CONTROL_PANEL_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateAlarmControlPanel), + cv.Optional(CONF_CODES): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_REQUIRES_CODE_TO_ARM): cv.boolean, + cv.Optional(CONF_ARMING_HOME_TIME): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_ARMING_AWAY_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_PENDING_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_TRIGGER_TIME, default="0s" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list( + TEMPLATE_ALARM_CONTROL_PANEL_BINARY_SENSOR_SCHEMA + ), + cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_DISARMED"): cv.enum( + RESTORE_MODES, upper=True + ), + } + ).extend(cv.COMPONENT_SCHEMA) +) + +CONFIG_SCHEMA = cv.All( + TEMPLATE_ALARM_CONTROL_PANEL_SCHEMA, + validate_config, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await alarm_control_panel.register_alarm_control_panel(var, config) + if CONF_CODES in config: + for acode in config[CONF_CODES]: + cg.add(var.add_code(acode)) + if CONF_REQUIRES_CODE_TO_ARM in config: + cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM])) + + cg.add(var.set_arming_away_time(config[CONF_ARMING_AWAY_TIME])) + cg.add(var.set_pending_time(config[CONF_PENDING_TIME])) + cg.add(var.set_trigger_time(config[CONF_TRIGGER_TIME])) + + supports_arm_home = False + if CONF_ARMING_HOME_TIME in config: + cg.add(var.set_arming_home_time(config[CONF_ARMING_HOME_TIME])) + supports_arm_home = True + + for sensor in config.get(CONF_BINARY_SENSORS, []): + bs = await cg.get_variable(sensor[CONF_INPUT]) + flags = BinarySensorFlags[FLAG_NORMAL] + if sensor[CONF_BYPASS_ARMED_HOME]: + flags |= BinarySensorFlags[FLAG_BYPASS_ARMED_HOME] + supports_arm_home = True + cg.add(var.add_sensor(bs, flags)) + + cg.add(var.set_supports_arm_home(supports_arm_home)) + + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp new file mode 100644 index 0000000000..1c54998e42 --- /dev/null +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -0,0 +1,180 @@ +#include "template_alarm_control_panel.h" +#include +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +using namespace esphome::alarm_control_panel; + +static const char *const TAG = "template.alarm_control_panel"; + +TemplateAlarmControlPanel::TemplateAlarmControlPanel(){}; + +#ifdef USE_BINARY_SENSOR +void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags) { + this->sensor_map_[sensor] = flags; +}; +#endif + +void TemplateAlarmControlPanel::dump_config() { + ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:"); + ESP_LOGCONFIG(TAG, " Current State: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_))); + ESP_LOGCONFIG(TAG, " Number of Codes: %u", this->codes_.size()); + if (!this->codes_.empty()) + ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->requires_code_to_arm_)); + ESP_LOGCONFIG(TAG, " Arming Away Time: %us", (this->arming_away_time_ / 1000)); + if (this->arming_home_time_ != 0) + ESP_LOGCONFIG(TAG, " Arming Home Time: %us", (this->arming_home_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Pending Time: %us", (this->pending_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Trigger Time: %us", (this->trigger_time_ / 1000)); + ESP_LOGCONFIG(TAG, " Supported Features: %u", this->get_supported_features()); +#ifdef USE_BINARY_SENSOR + for (auto sensor_pair : this->sensor_map_) { + ESP_LOGCONFIG(TAG, " Binary Sesnsor:"); + ESP_LOGCONFIG(TAG, " Name: %s", sensor_pair.first->get_name().c_str()); + ESP_LOGCONFIG(TAG, " Armed home bypass: %s", + TRUEFALSE(sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)); + } +#endif +} + +void TemplateAlarmControlPanel::setup() { + ESP_LOGCONFIG(TAG, "Setting up Template AlarmControlPanel '%s'...", this->name_.c_str()); + switch (this->restore_mode_) { + case ALARM_CONTROL_PANEL_ALWAYS_DISARMED: + this->current_state_ = ACP_STATE_DISARMED; + break; + case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: { + uint8_t value; + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (this->pref_.load(&value)) { + this->current_state_ = static_cast(value); + } else { + this->current_state_ = ACP_STATE_DISARMED; + } + break; + } + } + this->desired_state_ = this->current_state_; +} + +void TemplateAlarmControlPanel::loop() { + // change from ARMING to ARMED_x after the arming_time_ has passed + if (this->current_state_ == ACP_STATE_ARMING) { + auto delay = this->arming_away_time_; + if (this->desired_state_ == ACP_STATE_ARMED_HOME) { + delay = this->arming_home_time_; + } + if ((millis() - this->last_update_) > delay) { + this->publish_state(this->desired_state_); + } + return; + } + // change from PENDING to TRIGGERED after the delay_time_ has passed + if (this->current_state_ == ACP_STATE_PENDING && (millis() - this->last_update_) > this->pending_time_) { + this->publish_state(ACP_STATE_TRIGGERED); + return; + } + auto future_state = this->current_state_; + // reset triggered if all clear + if (this->current_state_ == ACP_STATE_TRIGGERED && this->trigger_time_ > 0 && + (millis() - this->last_update_) > this->trigger_time_) { + future_state = this->desired_state_; + } + bool trigger = false; +#ifdef USE_BINARY_SENSOR + if (this->is_state_armed(future_state)) { + // TODO might be better to register change for each sensor in setup... + for (auto sensor_pair : this->sensor_map_) { + if (sensor_pair.first->state) { + if (this->current_state_ == ACP_STATE_ARMED_HOME && + (sensor_pair.second & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { + continue; + } + trigger = true; + break; + } + } + } +#endif + if (trigger) { + if (this->pending_time_ > 0 && this->current_state_ != ACP_STATE_TRIGGERED) { + this->publish_state(ACP_STATE_PENDING); + } else { + this->publish_state(ACP_STATE_TRIGGERED); + } + } else if (future_state != this->current_state_) { + this->publish_state(future_state); + } +} + +bool TemplateAlarmControlPanel::is_code_valid_(optional code) { + if (!this->codes_.empty()) { + if (code.has_value()) { + ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str()); + return (std::count(this->codes_.begin(), this->codes_.end(), code.value()) == 1); + } + ESP_LOGD(TAG, "No code provided"); + return false; + } + return true; +} + +uint32_t TemplateAlarmControlPanel::get_supported_features() const { + uint32_t features = ACP_FEAT_ARM_AWAY | ACP_FEAT_TRIGGER; + if (this->supports_arm_home_) { + features |= ACP_FEAT_ARM_HOME; + } + return features; +} + +bool TemplateAlarmControlPanel::get_requires_code() const { return !this->codes_.empty(); } + +void TemplateAlarmControlPanel::arm_(optional code, alarm_control_panel::AlarmControlPanelState state, + uint32_t delay) { + if (this->current_state_ != ACP_STATE_DISARMED) { + ESP_LOGW(TAG, "Cannot arm when not disarmed"); + return; + } + if (this->requires_code_to_arm_ && !this->is_code_valid_(std::move(code))) { + ESP_LOGW(TAG, "Not arming code doesn't match"); + return; + } + this->desired_state_ = state; + if (delay > 0) { + this->publish_state(ACP_STATE_ARMING); + } else { + this->publish_state(state); + } +} + +void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { + if (call.get_state()) { + if (call.get_state() == ACP_STATE_ARMED_AWAY) { + this->arm_(call.get_code(), ACP_STATE_ARMED_AWAY, this->arming_away_time_); + } else if (call.get_state() == ACP_STATE_ARMED_HOME) { + this->arm_(call.get_code(), ACP_STATE_ARMED_HOME, this->arming_home_time_); + } else if (call.get_state() == ACP_STATE_DISARMED) { + if (!this->is_code_valid_(call.get_code())) { + ESP_LOGW(TAG, "Not disarming code doesn't match"); + return; + } + this->desired_state_ = ACP_STATE_DISARMED; + this->publish_state(ACP_STATE_DISARMED); + } else if (call.get_state() == ACP_STATE_TRIGGERED) { + this->publish_state(ACP_STATE_TRIGGERED); + } else if (call.get_state() == ACP_STATE_PENDING) { + this->publish_state(ACP_STATE_PENDING); + } else { + ESP_LOGE(TAG, "State not yet implemented: %s", + LOG_STR_ARG(alarm_control_panel_state_to_string(*call.get_state()))); + } + } +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h new file mode 100644 index 0000000000..4065356ba8 --- /dev/null +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -0,0 +1,116 @@ +#pragma once + +#include + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" + +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" + +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +namespace esphome { +namespace template_ { + +#ifdef USE_BINARY_SENSOR +enum BinarySensorFlags : uint16_t { + BINARY_SENSOR_MODE_NORMAL = 1 << 0, + BINARY_SENSOR_MODE_BYPASS_ARMED_HOME = 1 << 1, +}; +#endif + +enum TemplateAlarmControlPanelRestoreMode { + ALARM_CONTROL_PANEL_ALWAYS_DISARMED, + ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED, +}; + +class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { + public: + TemplateAlarmControlPanel(); + void dump_config() override; + void setup() override; + void loop() override; + uint32_t get_supported_features() const override; + bool get_requires_code() const override; + bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } + void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + +#ifdef USE_BINARY_SENSOR + /** Add a binary_sensor to the alarm_panel. + * + * @param sensor The BinarySensor instance. + * @param ignore_when_home if this should be ignored when armed_home mode + */ + void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0); +#endif + + /** add a code + * + * @param code The code + */ + void add_code(const std::string &code) { this->codes_.push_back(code); } + + /** set requires a code to arm + * + * @param code_to_arm The requires code to arm + */ + void set_requires_code_to_arm(bool code_to_arm) { this->requires_code_to_arm_ = code_to_arm; } + + /** set the delay before arming away + * + * @param time The milliseconds + */ + void set_arming_away_time(uint32_t time) { this->arming_away_time_ = time; } + + /** set the delay before arming home + * + * @param time The milliseconds + */ + void set_arming_home_time(uint32_t time) { this->arming_home_time_ = time; } + + /** set the delay before triggering + * + * @param time The milliseconds + */ + void set_pending_time(uint32_t time) { this->pending_time_ = time; } + + /** set the delay before resetting after triggered + * + * @param time The milliseconds + */ + void set_trigger_time(uint32_t time) { this->trigger_time_ = time; } + + void set_supports_arm_home(bool supports_arm_home) { supports_arm_home_ = supports_arm_home; } + + protected: + void control(const alarm_control_panel::AlarmControlPanelCall &call) override; +#ifdef USE_BINARY_SENSOR + // the map of binary sensors that the alarm_panel monitors with their modes + std::map sensor_map_; +#endif + TemplateAlarmControlPanelRestoreMode restore_mode_{}; + + // the arming away delay + uint32_t arming_away_time_; + // the arming home delay + uint32_t arming_home_time_{0}; + // the trigger delay + uint32_t pending_time_; + // the time in trigger + uint32_t trigger_time_; + // a list of codes + std::vector codes_; + // requires a code to arm + bool requires_code_to_arm_ = false; + bool supports_arm_home_ = false; + // check if the code is valid + bool is_code_valid_(optional code); + + void arm_(optional code, alarm_control_panel::AlarmControlPanelState state, uint32_t delay); +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index af2b6c720c..f7c1916ffe 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -1,5 +1,7 @@ #include "automation.h" + #include "esphome/core/log.h" + #include namespace esphome { diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index e97413e420..b5c8291533 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -1,7 +1,9 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/time.h" + #include "real_time_clock.h" #include diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index de76676a4d..10fa9597b9 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -52,171 +52,5 @@ void RealTimeClock::apply_timezone_() { tzset(); } -size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { - struct tm c_tm = this->to_c_tm(); - return ::strftime(buffer, buffer_len, format, &c_tm); -} -ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { - ESPTime res{}; - res.second = uint8_t(c_tm->tm_sec); - res.minute = uint8_t(c_tm->tm_min); - res.hour = uint8_t(c_tm->tm_hour); - res.day_of_week = uint8_t(c_tm->tm_wday + 1); - res.day_of_month = uint8_t(c_tm->tm_mday); - res.day_of_year = uint16_t(c_tm->tm_yday + 1); - res.month = uint8_t(c_tm->tm_mon + 1); - res.year = uint16_t(c_tm->tm_year + 1900); - res.is_dst = bool(c_tm->tm_isdst); - res.timestamp = c_time; - return res; -} -struct tm ESPTime::to_c_tm() { - struct tm c_tm {}; - c_tm.tm_sec = this->second; - c_tm.tm_min = this->minute; - c_tm.tm_hour = this->hour; - c_tm.tm_mday = this->day_of_month; - c_tm.tm_mon = this->month - 1; - c_tm.tm_year = this->year - 1900; - c_tm.tm_wday = this->day_of_week - 1; - c_tm.tm_yday = this->day_of_year - 1; - c_tm.tm_isdst = this->is_dst; - return c_tm; -} -std::string ESPTime::strftime(const std::string &format) { - std::string timestr; - timestr.resize(format.size() * 4); - struct tm c_tm = this->to_c_tm(); - size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - while (len == 0) { - timestr.resize(timestr.size() * 2); - len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); - } - timestr.resize(len); - return timestr; -} - -template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) { - current++; - if (current >= end) { - current = begin; - return true; - } - return false; -} - -static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } - -static uint8_t days_in_month(uint8_t month, uint16_t year) { - static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - uint8_t days = DAYS_IN_MONTH[month]; - if (month == 2 && is_leap_year(year)) - return 29; - return days; -} - -void ESPTime::increment_second() { - this->timestamp++; - if (!increment_time_value(this->second, 0, 60)) - return; - - // second roll-over, increment minute - if (!increment_time_value(this->minute, 0, 60)) - return; - - // minute roll-over, increment hour - if (!increment_time_value(this->hour, 0, 24)) - return; - - // hour roll-over, increment day - increment_time_value(this->day_of_week, 1, 8); - - if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { - // day of month roll-over, increment month - increment_time_value(this->month, 1, 13); - } - - uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; - if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { - // day of year roll-over, increment year - this->year++; - } -} -void ESPTime::increment_day() { - this->timestamp += 86400; - - // increment day - increment_time_value(this->day_of_week, 1, 8); - - if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { - // day of month roll-over, increment month - increment_time_value(this->month, 1, 13); - } - - uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; - if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { - // day of year roll-over, increment year - this->year++; - } -} -void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { - time_t res = 0; - - if (!this->fields_in_range()) { - this->timestamp = -1; - return; - } - - for (int i = 1970; i < this->year; i++) - res += is_leap_year(i) ? 366 : 365; - - if (use_day_of_year) { - res += this->day_of_year - 1; - } else { - for (int i = 1; i < this->month; i++) - res += days_in_month(i, this->year); - - res += this->day_of_month - 1; - } - - res *= 24; - res += this->hour; - res *= 60; - res += this->minute; - res *= 60; - res += this->second; - this->timestamp = res; -} - -int32_t ESPTime::timezone_offset() { - int32_t offset = 0; - time_t now = ::time(nullptr); - auto local = ESPTime::from_epoch_local(now); - auto utc = ESPTime::from_epoch_utc(now); - bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; - - if (utc.minute > local.minute) { - local.minute += 60; - local.hour -= 1; - } - offset += (local.minute - utc.minute) * 60; - - if (negative) { - offset -= (utc.hour - local.hour) * 3600; - } else { - if (utc.hour > local.hour) { - local.hour += 24; - } - offset += (local.hour - utc.hour) * 3600; - } - return offset; -} - -bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } -bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } -bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } -bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; } -bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; } - } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 7f4afee306..a17168ae6f 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -1,106 +1,15 @@ #pragma once +#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -#include -#include -#include +#include "esphome/core/time.h" namespace esphome { namespace time { -/// A more user-friendly version of struct tm from time.h -struct ESPTime { - /** seconds after the minute [0-60] - * @note second is generally 0-59; the extra range is to accommodate leap seconds. - */ - uint8_t second; - /// minutes after the hour [0-59] - uint8_t minute; - /// hours since midnight [0-23] - uint8_t hour; - /// day of the week; sunday=1 [1-7] - uint8_t day_of_week; - /// day of the month [1-31] - uint8_t day_of_month; - /// day of the year [1-366] - uint16_t day_of_year; - /// month; january=1 [1-12] - uint8_t month; - /// year - uint16_t year; - /// daylight saving time flag - bool is_dst; - /// unix epoch time (seconds since UTC Midnight January 1, 1970) - time_t timestamp; - - /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. - * Up to buffer_len bytes are written. - * - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime - */ - size_t strftime(char *buffer, size_t buffer_len, const char *format); - - /** Convert this ESPTime struct to a string as specified by the format argument. - * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime - * - * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some - * microcontrollers. - */ - std::string strftime(const std::string &format); - - /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) - bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } - - /// Check if all time fields of this ESPTime are in range. - bool fields_in_range() const { - return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && - this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && - this->day_of_year < 367 && this->month > 0 && this->month < 13; - } - - /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance. - static ESPTime from_c_tm(struct tm *c_tm, time_t c_time); - - /** Convert an UTC epoch timestamp to a local time ESPTime instance. - * - * @param epoch Seconds since 1st January 1970. In UTC. - * @return The generated ESPTime - */ - static ESPTime from_epoch_local(time_t epoch) { - struct tm *c_tm = ::localtime(&epoch); - return ESPTime::from_c_tm(c_tm, epoch); - } - /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. - * - * @param epoch Seconds since 1st January 1970. In UTC. - * @return The generated ESPTime - */ - static ESPTime from_epoch_utc(time_t epoch) { - struct tm *c_tm = ::gmtime(&epoch); - return ESPTime::from_c_tm(c_tm, epoch); - } - - /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). - void recalc_timestamp_utc(bool use_day_of_year = true); - - /// Convert this ESPTime instance back to a tm struct. - struct tm to_c_tm(); - - static int32_t timezone_offset(); - - /// Increment this clock instance by one second. - void increment_second(); - /// Increment this clock instance by one day. - void increment_day(); - bool operator<(ESPTime other); - bool operator<=(ESPTime other); - bool operator==(ESPTime other); - bool operator>=(ESPTime other); - bool operator>(ESPTime other); -}; - /// The RealTimeClock class exposes common timekeeping functions via the device's local real-time clock. /// /// \note diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 5b8cbc6004..434c6e65f3 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -368,16 +368,14 @@ uint8_t TM1637Display::printf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t TM1637Display::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t TM1637Display::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t TM1637Display::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t TM1637Display::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } } // namespace tm1637 } // namespace esphome diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 2fb572bc55..aba0071b12 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -3,13 +3,10 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/time.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif @@ -61,12 +58,10 @@ class TM1637Display : public PollingComponent { void add_tm1637_key(TM1637Key *tm1637_key) { this->tm1637_keys_.push_back(tm1637_key); } #endif -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); protected: void bit_delay_(); diff --git a/esphome/components/tm1638/tm1638.cpp b/esphome/components/tm1638/tm1638.cpp index 24cb4122bf..a15b006046 100644 --- a/esphome/components/tm1638/tm1638.cpp +++ b/esphome/components/tm1638/tm1638.cpp @@ -211,16 +211,14 @@ uint8_t TM1638Component::printf(const char *format, ...) { return 0; } -#ifdef USE_TIME -uint8_t TM1638Component::strftime(uint8_t pos, const char *format, time::ESPTime time) { +uint8_t TM1638Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) return this->print(pos, buffer); return 0; } -uint8_t TM1638Component::strftime(const char *format, time::ESPTime time) { return this->strftime(0, format, time); } -#endif +uint8_t TM1638Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } //////////////// SPI //////////////// diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h index 2e1ac6fad3..add72cfdf3 100644 --- a/esphome/components/tm1638/tm1638.h +++ b/esphome/components/tm1638/tm1638.h @@ -1,16 +1,13 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" -#include "esphome/core/automation.h" #include "esphome/core/hal.h" +#include "esphome/core/time.h" #include -#ifdef USE_TIME -#include "esphome/components/time/real_time_clock.h" -#endif - namespace esphome { namespace tm1638 { @@ -52,12 +49,10 @@ class TM1638Component : public PollingComponent { void loop() override; uint8_t get_keys(); -#ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. - uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + uint8_t strftime(uint8_t pos, const char *format, ESPTime time) __attribute__((format(strftime, 3, 0))); /// Evaluate the strftime-format and print the result at position 0. - uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); -#endif + uint8_t strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); void set_led(int led_pos, bool led_on_off); diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py index 9d2b17afdc..a6b2189eb6 100644 --- a/esphome/components/tm1651/__init__.py +++ b/esphome/components/tm1651/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CONF_BRIGHTNESS, ) +CODEOWNERS = ["@freekode"] + tm1651_ns = cg.esphome_ns.namespace("tm1651") TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component) diff --git a/esphome/components/tmp1075/__init__.py b/esphome/components/tmp1075/__init__.py new file mode 100644 index 0000000000..ddd04ad11a --- /dev/null +++ b/esphome/components/tmp1075/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@sybrenstuvel"] diff --git a/esphome/components/tmp1075/sensor.py b/esphome/components/tmp1075/sensor.py new file mode 100644 index 0000000000..25ec350b7a --- /dev/null +++ b/esphome/components/tmp1075/sensor.py @@ -0,0 +1,92 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + ICON_THERMOMETER, +) + +DEPENDENCIES = ["i2c"] + +tmp1075_ns = cg.esphome_ns.namespace("tmp1075") + +TMP1075Sensor = tmp1075_ns.class_( + "TMP1075Sensor", cg.PollingComponent, sensor.Sensor, i2c.I2CDevice +) + +EConversionRate = tmp1075_ns.enum("EConversionRate") +CONVERSION_RATES = { + "27.5ms": EConversionRate.CONV_RATE_27_5_MS, + "55ms": EConversionRate.CONV_RATE_55_MS, + "110ms": EConversionRate.CONV_RATE_110_MS, + "220ms": EConversionRate.CONV_RATE_220_MS, +} + +POLARITY = { + "ACTIVE_LOW": 0, + "ACTIVE_HIGH": 1, +} + +EAlertFunction = tmp1075_ns.enum("EAlertFunction") +ALERT_FUNCTION = { + "COMPARATOR": EAlertFunction.ALERT_COMPARATOR, + "INTERRUPT": EAlertFunction.ALERT_INTERRUPT, +} + +CONF_ALERT = "alert" +CONF_LIMIT_LOW = "limit_low" +CONF_LIMIT_HIGH = "limit_high" +CONF_FAULT_COUNT = "fault_count" +CONF_POLARITY = "polarity" +CONF_CONVERSION_RATE = "conversion_rate" +CONF_FUNCTION = "function" + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + TMP1075Sensor, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.Optional(CONF_CONVERSION_RATE): cv.enum(CONVERSION_RATES, lower=True), + cv.Optional(CONF_ALERT, default={}): cv.Schema( + { + cv.Optional(CONF_LIMIT_LOW): cv.temperature, + cv.Optional(CONF_LIMIT_HIGH): cv.temperature, + cv.Optional(CONF_FAULT_COUNT): cv.int_range(min=1, max=4), + cv.Optional(CONF_POLARITY): cv.enum(POLARITY, upper=True), + cv.Optional(CONF_FUNCTION): cv.enum(ALERT_FUNCTION, upper=True), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_CONVERSION_RATE in config: + cg.add(var.set_conversion_rate(config[CONF_CONVERSION_RATE])) + + alert = config[CONF_ALERT] + if CONF_LIMIT_LOW in alert: + cg.add(var.set_alert_limit_low(alert[CONF_LIMIT_LOW])) + if CONF_LIMIT_HIGH in alert: + cg.add(var.set_alert_limit_high(alert[CONF_LIMIT_HIGH])) + if CONF_FAULT_COUNT in alert: + cg.add(var.set_fault_count(alert[CONF_FAULT_COUNT])) + if CONF_POLARITY in alert: + cg.add(var.set_alert_polarity(alert[CONF_POLARITY])) + if CONF_FUNCTION in alert: + cg.add(var.set_alert_function(alert[CONF_FUNCTION])) diff --git a/esphome/components/tmp1075/tmp1075.cpp b/esphome/components/tmp1075/tmp1075.cpp new file mode 100644 index 0000000000..38ed2bea31 --- /dev/null +++ b/esphome/components/tmp1075/tmp1075.cpp @@ -0,0 +1,129 @@ +#include "esphome/core/log.h" +#include "tmp1075.h" + +namespace esphome { +namespace tmp1075 { + +static const char *const TAG = "tmp1075"; + +constexpr uint8_t REG_TEMP = 0x0; // Temperature result +constexpr uint8_t REG_CFGR = 0x1; // Configuration +constexpr uint8_t REG_LLIM = 0x2; // Low limit +constexpr uint8_t REG_HLIM = 0x3; // High limit +constexpr uint8_t REG_DIEID = 0xF; // Device ID + +constexpr uint16_t EXPECT_DIEID = 0x0075; // Expected Device ID. + +static uint16_t temp2regvalue(float temp); +static float regvalue2temp(uint16_t regvalue); + +void TMP1075Sensor::setup() { + uint8_t die_id; + if (!this->read_byte(REG_DIEID, &die_id)) { + ESP_LOGW(TAG, "'%s' - unable to read ID", this->name_.c_str()); + this->mark_failed(); + return; + } + if (die_id != EXPECT_DIEID) { + ESP_LOGW(TAG, "'%s' - unexpected ID 0x%x found, expected 0x%x", this->name_.c_str(), die_id, EXPECT_DIEID); + this->mark_failed(); + return; + } + + this->write_config(); +} + +void TMP1075Sensor::update() { + uint16_t regvalue; + if (!read_byte_16(REG_TEMP, ®value)) { + ESP_LOGW(TAG, "'%s' - unable to read temperature register", this->name_.c_str()); + this->status_set_warning(); + return; + } + + const float temp = regvalue2temp(regvalue); + this->publish_state(temp); +} + +void TMP1075Sensor::dump_config() { + LOG_SENSOR("", "TMP1075 Sensor", this); + if (this->is_failed()) { + ESP_LOGE(TAG, " Communication with TMP1075 failed!"); + return; + } + ESP_LOGCONFIG(TAG, " limit low : %.4f °C", alert_limit_low_); + ESP_LOGCONFIG(TAG, " limit high : %.4f °C", alert_limit_high_); + ESP_LOGCONFIG(TAG, " oneshot : %d", config_.fields.oneshot); + ESP_LOGCONFIG(TAG, " rate : %d", config_.fields.rate); + ESP_LOGCONFIG(TAG, " fault_count: %d", config_.fields.faults); + ESP_LOGCONFIG(TAG, " polarity : %d", config_.fields.polarity); + ESP_LOGCONFIG(TAG, " alert_mode : %d", config_.fields.alert_mode); + ESP_LOGCONFIG(TAG, " shutdown : %d", config_.fields.shutdown); +} + +void TMP1075Sensor::set_fault_count(const int faults) { + if (faults < 1) { + ESP_LOGE(TAG, "'%s' - fault_count too low: %d", this->name_.c_str(), faults); + return; + } + if (faults > 4) { + ESP_LOGE(TAG, "'%s' - fault_count too high: %d", this->name_.c_str(), faults); + return; + } + config_.fields.faults = faults - 1; +} + +void TMP1075Sensor::log_config_() { + ESP_LOGV(TAG, " oneshot : %d", config_.fields.oneshot); + ESP_LOGV(TAG, " rate : %d", config_.fields.rate); + ESP_LOGV(TAG, " faults : %d", config_.fields.faults); + ESP_LOGV(TAG, " polarity : %d", config_.fields.polarity); + ESP_LOGV(TAG, " alert_mode: %d", config_.fields.alert_mode); + ESP_LOGV(TAG, " shutdown : %d", config_.fields.shutdown); +} + +void TMP1075Sensor::write_config() { + send_alert_limit_low_(); + send_alert_limit_high_(); + send_config_(); +} + +void TMP1075Sensor::send_config_() { + ESP_LOGV(TAG, "'%s' - sending configuration %04x", this->name_.c_str(), config_.regvalue); + log_config_(); + if (!this->write_byte_16(REG_CFGR, config_.regvalue)) { + ESP_LOGW(TAG, "'%s' - unable to write configuration register", this->name_.c_str()); + return; + } +} + +void TMP1075Sensor::send_alert_limit_low_() { + ESP_LOGV(TAG, "'%s' - sending alert limit low %.3f °C", this->name_.c_str(), alert_limit_low_); + const uint16_t regvalue = temp2regvalue(alert_limit_low_); + if (!this->write_byte_16(REG_LLIM, regvalue)) { + ESP_LOGW(TAG, "'%s' - unable to write low limit register", this->name_.c_str()); + return; + } +} + +void TMP1075Sensor::send_alert_limit_high_() { + ESP_LOGV(TAG, "'%s' - sending alert limit high %.3f °C", this->name_.c_str(), alert_limit_high_); + const uint16_t regvalue = temp2regvalue(alert_limit_high_); + if (!this->write_byte_16(REG_HLIM, regvalue)) { + ESP_LOGW(TAG, "'%s' - unable to write high limit register", this->name_.c_str()); + return; + } +} + +static uint16_t temp2regvalue(const float temp) { + const uint16_t regvalue = temp / 0.0625f; + return regvalue << 4; +} + +static float regvalue2temp(const uint16_t regvalue) { + const int16_t signed_value = regvalue; + return (signed_value >> 4) * 0.0625f; +} + +} // namespace tmp1075 +} // namespace esphome diff --git a/esphome/components/tmp1075/tmp1075.h b/esphome/components/tmp1075/tmp1075.h new file mode 100644 index 0000000000..db2bac517a --- /dev/null +++ b/esphome/components/tmp1075/tmp1075.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace tmp1075 { + +struct TMP1075Config { + union { + struct { + uint8_t oneshot : 1; // One-shot conversion mode. Writing 1, starts a single temperature + // conversion. Read returns 0. + + uint8_t rate : 2; // Conversion rate setting when device is in continuous conversion mode. + // 00: 27.5 ms conversion rate + // 01: 55 ms conversion rate + // 10: 110 ms conversion rate + // 11: 220 ms conversion rate (35 ms TMP1075N) + + uint8_t faults : 2; // Consecutive fault measurements to trigger the alert function. + // 00: 1 fault + // 01: 2 faults + // 10: 3 faults (4 faults TMP1075N) + // 11: 4 faults (6 faults TMP1075N) + + uint8_t polarity : 1; // Polarity of the output pin. + // 0: Active low ALERT pin + // 1: Active high ALERT pin + + uint8_t alert_mode : 1; // Selects the function of the ALERT pin. + // 0: ALERT pin functions in comparator mode + // 1: ALERT pin functions in interrupt mode + + uint8_t shutdown : 1; // Sets the device in shutdown mode to conserve power. + // 0: Device is in continuous conversion + // 1: Device is in shutdown mode + uint8_t unused : 8; + } fields; + uint16_t regvalue; + }; +}; + +enum EConversionRate { + CONV_RATE_27_5_MS, + CONV_RATE_55_MS, + CONV_RATE_110_MS, + CONV_RATE_220_MS, +}; + +enum EAlertFunction { + ALERT_COMPARATOR = 0, + ALERT_INTERRUPT = 1, +}; + +class TMP1075Sensor : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + void dump_config() override; + + // Call write_config() after calling any of these to send the new config to + // the IC. The setup() function also does this. + void set_alert_limit_low(const float temp) { this->alert_limit_low_ = temp; } + void set_alert_limit_high(const float temp) { this->alert_limit_high_ = temp; } + void set_oneshot(const bool oneshot) { config_.fields.oneshot = oneshot; } + void set_conversion_rate(const enum EConversionRate rate) { config_.fields.rate = rate; } + void set_alert_polarity(const bool polarity) { config_.fields.polarity = polarity; } + void set_alert_function(const enum EAlertFunction function) { config_.fields.alert_mode = function; } + void set_fault_count(int faults); + + void write_config(); + + protected: + TMP1075Config config_ = {}; + + // Disable the alert pin by default. + float alert_limit_low_ = -128.0f; + float alert_limit_high_ = 127.9375f; + + void send_alert_limit_low_(); + void send_alert_limit_high_(); + void send_config_(); + void log_config_(); +}; + +} // namespace tmp1075 +} // namespace esphome diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 040b9b7ed5..7e6b1d53fe 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -508,7 +508,7 @@ void Tuya::send_wifi_status_() { void Tuya::send_local_time_() { std::vector payload; auto *time_id = *this->time_id_; - time::ESPTime now = time_id->now(); + ESPTime now = time_id->now(); if (now.is_valid()) { uint8_t year = now.year - 2000; uint8_t month = now.month; diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 8d6153482f..b9901dd5e7 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -7,6 +7,7 @@ #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" +#include "esphome/core/time.h" #endif #include diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 624fcdf52c..55d995be88 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -1,16 +1,23 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_MICROPHONE, CONF_SPEAKER +from esphome.const import ( + CONF_ID, + CONF_MICROPHONE, + CONF_SPEAKER, + CONF_MEDIA_PLAYER, +) from esphome import automation -from esphome.automation import register_action -from esphome.components import microphone, speaker +from esphome.automation import register_action, register_condition +from esphome.components import microphone, speaker, media_player AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] CODEOWNERS = ["@jesserockz"] +CONF_SILENCE_DETECTION = "silence_detection" +CONF_ON_LISTENING = "on_listening" CONF_ON_START = "on_start" CONF_ON_STT_END = "on_stt_end" CONF_ON_TTS_START = "on_tts_start" @@ -25,16 +32,25 @@ VoiceAssistant = voice_assistant_ns.class_("VoiceAssistant", cg.Component) StartAction = voice_assistant_ns.class_( "StartAction", automation.Action, cg.Parented.template(VoiceAssistant) ) +StartContinuousAction = voice_assistant_ns.class_( + "StartContinuousAction", automation.Action, cg.Parented.template(VoiceAssistant) +) StopAction = voice_assistant_ns.class_( "StopAction", automation.Action, cg.Parented.template(VoiceAssistant) ) +IsRunningCondition = voice_assistant_ns.class_( + "IsRunningCondition", automation.Condition, cg.Parented.template(VoiceAssistant) +) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(VoiceAssistant), cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), - cv.Optional(CONF_SPEAKER): cv.use_id(speaker.Speaker), + cv.Exclusive(CONF_SPEAKER, "output"): cv.use_id(speaker.Speaker), + cv.Exclusive(CONF_MEDIA_PLAYER, "output"): cv.use_id(media_player.MediaPlayer), + cv.Optional(CONF_SILENCE_DETECTION, default=True): cv.boolean, + cv.Optional(CONF_ON_LISTENING): automation.validate_automation(single=True), cv.Optional(CONF_ON_START): automation.validate_automation(single=True), cv.Optional(CONF_ON_STT_END): automation.validate_automation(single=True), cv.Optional(CONF_ON_TTS_START): automation.validate_automation(single=True), @@ -56,6 +72,17 @@ async def to_code(config): spkr = await cg.get_variable(config[CONF_SPEAKER]) cg.add(var.set_speaker(spkr)) + if CONF_MEDIA_PLAYER in config: + mp = await cg.get_variable(config[CONF_MEDIA_PLAYER]) + cg.add(var.set_media_player(mp)) + + cg.add(var.set_silence_detection(config[CONF_SILENCE_DETECTION])) + + if CONF_ON_LISTENING in config: + await automation.build_automation( + var.get_listening_trigger(), [], config[CONF_ON_LISTENING] + ) + if CONF_ON_START in config: await automation.build_automation( var.get_start_trigger(), [], config[CONF_ON_START] @@ -96,6 +123,11 @@ async def to_code(config): VOICE_ASSISTANT_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(VoiceAssistant)}) +@register_action( + "voice_assistant.start_continuous", + StartContinuousAction, + VOICE_ASSISTANT_ACTION_SCHEMA, +) @register_action("voice_assistant.start", StartAction, VOICE_ASSISTANT_ACTION_SCHEMA) async def voice_assistant_listen_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -108,3 +140,12 @@ async def voice_assistant_stop_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var + + +@register_condition( + "voice_assistant.is_running", IsRunningCondition, VOICE_ASSISTANT_ACTION_SCHEMA +) +async def voice_assistant_is_running_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index fb96d484d4..44d640ff39 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -58,27 +58,53 @@ void VoiceAssistant::setup() { } #endif - this->mic_->add_data_callback([this](const std::vector &data) { + this->mic_->add_data_callback([this](const std::vector &data) { if (!this->running_) { return; } - this->socket_->sendto(data.data(), data.size(), 0, (struct sockaddr *) &this->dest_addr_, sizeof(this->dest_addr_)); + this->socket_->sendto(data.data(), data.size() * sizeof(int16_t), 0, (struct sockaddr *) &this->dest_addr_, + sizeof(this->dest_addr_)); }); } void VoiceAssistant::loop() { #ifdef USE_SPEAKER - if (this->speaker_ == nullptr) { + if (this->speaker_ != nullptr) { + uint8_t buf[1024]; + auto len = this->socket_->read(buf, sizeof(buf)); + if (len == -1) { + return; + } + this->speaker_->play(buf, len); + this->set_timeout("data-incoming", 200, [this]() { + if (this->continuous_) { + this->request_start(true); + } + }); return; } - - uint8_t buf[1024]; - auto len = this->socket_->read(buf, sizeof(buf)); - if (len == -1) { - return; - } - this->speaker_->play(buf, len); #endif +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + if (!this->playing_tts_ || + this->media_player_->state == media_player::MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING) { + return; + } + this->set_timeout("playing-media", 1000, [this]() { + this->playing_tts_ = false; + if (this->continuous_) { + this->request_start(true); + } + }); + return; + } +#endif + // Set a 1 second timeout to start the voice assistant again. + this->set_timeout("continuous-no-sound", 1000, [this]() { + if (this->continuous_) { + this->request_start(true); + } + }); } void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { @@ -99,14 +125,19 @@ void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { } this->running_ = true; this->mic_->start(); + this->listening_trigger_->trigger(); } -void VoiceAssistant::request_start() { +void VoiceAssistant::request_start(bool continuous) { ESP_LOGD(TAG, "Requesting start..."); - if (!api::global_api_server->start_voice_assistant()) { + if (!api::global_api_server->start_voice_assistant(this->conversation_id_)) { ESP_LOGW(TAG, "Could not request start."); this->error_trigger_->trigger("not-connected", "Could not request start."); + this->continuous_ = false; + return; } + this->continuous_ = continuous; + this->set_timeout("reset-conversation_id", 5 * 60 * 1000, [this]() { this->conversation_id_ = ""; }); } void VoiceAssistant::signal_stop() { @@ -135,9 +166,18 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); + this->signal_stop(); this->stt_end_trigger_->trigger(text); break; } + case api::enums::VOICE_ASSISTANT_INTENT_END: { + for (auto arg : msg.data) { + if (arg.name == "conversation_id") { + this->conversation_id_ = std::move(arg.value); + } + } + break; + } case api::enums::VOICE_ASSISTANT_TTS_START: { std::string text; for (auto arg : msg.data) { @@ -165,6 +205,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); +#ifdef USE_MEDIA_PLAYER + if (this->media_player_ != nullptr) { + this->playing_tts_ = true; + this->media_player_->make_call().set_media_url(url).perform(); + } +#endif this->tts_end_trigger_->trigger(url); break; } @@ -183,6 +229,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str()); + this->continuous_ = false; + this->signal_stop(); this->error_trigger_->trigger(code, message); } default: diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index c1a6e8883b..b103584509 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -15,6 +15,9 @@ #ifdef USE_SPEAKER #include "esphome/components/speaker/speaker.h" #endif +#ifdef USE_MEDIA_PLAYER +#include "esphome/components/media_player/media_player.h" +#endif #include "esphome/components/socket/socket.h" namespace esphome { @@ -22,8 +25,10 @@ namespace voice_assistant { // Version 1: Initial version // Version 2: Adds raw speaker support +// Version 3: Adds continuous support static const uint32_t INITIAL_VERSION = 1; static const uint32_t SPEAKER_SUPPORT = 2; +static const uint32_t SILENCE_DETECTION_SUPPORT = 3; class VoiceAssistant : public Component { public: @@ -36,20 +41,34 @@ class VoiceAssistant : public Component { #ifdef USE_SPEAKER void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } #endif +#ifdef USE_MEDIA_PLAYER + void set_media_player(media_player::MediaPlayer *media_player) { this->media_player_ = media_player; } +#endif uint32_t get_version() const { #ifdef USE_SPEAKER - if (this->speaker_ != nullptr) + if (this->speaker_ != nullptr) { + if (this->silence_detection_) { + return SILENCE_DETECTION_SUPPORT; + } return SPEAKER_SUPPORT; + } #endif return INITIAL_VERSION; } - void request_start(); + void request_start(bool continuous = false); void signal_stop(); void on_event(const api::VoiceAssistantEventResponse &msg); + bool is_running() const { return this->running_; } + void set_continuous(bool continuous) { this->continuous_ = continuous; } + bool is_continuous() const { return this->continuous_; } + + void set_silence_detection(bool silence_detection) { this->silence_detection_ = silence_detection; } + + Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } Trigger<> *get_start_trigger() const { return this->start_trigger_; } Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } @@ -61,6 +80,7 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; + Trigger<> *listening_trigger_ = new Trigger<>(); Trigger<> *start_trigger_ = new Trigger<>(); Trigger *stt_end_trigger_ = new Trigger(); Trigger *tts_start_trigger_ = new Trigger(); @@ -72,8 +92,16 @@ class VoiceAssistant : public Component { #ifdef USE_SPEAKER speaker::Speaker *speaker_{nullptr}; #endif +#ifdef USE_MEDIA_PLAYER + media_player::MediaPlayer *media_player_{nullptr}; + bool playing_tts_{false}; +#endif + + std::string conversation_id_{""}; bool running_{false}; + bool continuous_{false}; + bool silence_detection_; }; template class StartAction : public Action, public Parented { @@ -81,9 +109,22 @@ template class StartAction : public Action, public Parent void play(Ts... x) override { this->parent_->request_start(); } }; +template class StartContinuousAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->request_start(true); } +}; + template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->signal_stop(); } + void play(Ts... x) override { + this->parent_->set_continuous(false); + this->parent_->signal_stop(); + } +}; + +template class IsRunningCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 6f833a5c83..ce7b4be7f3 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -91,6 +91,16 @@ bool ListEntitiesIterator::on_select(select::Select *select) { } #endif +#ifdef USE_ALARM_CONTROL_PANEL +bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + this->web_server_->events_.send( + this->web_server_->alarm_control_panel_json(a_alarm_control_panel, a_alarm_control_panel->get_state(), DETAIL_ALL) + .c_str(), + "state"); + return true; +} +#endif + } // namespace web_server } // namespace esphome diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 85868caff8..8ddca15edf 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -49,6 +49,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; #endif +#ifdef USE_ALARM_CONTROL_PANEL + bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; +#endif protected: WebServer *web_server_; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 00b2e20015..1ac94375c2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -813,6 +813,9 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value } #endif +// Longest: HORIZONTAL +#define PSTR_LOCAL(mode_s) strncpy_P(buf, (PGM_P) ((mode_s)), 15) + #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { this->events_.send(this->climate_json(obj, DETAIL_STATE).c_str(), "state"); @@ -869,16 +872,13 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url request->send(404); } -// Longest: HORIZONTAL -#define PSTR_LOCAL(mode_s) strncpy_P(__buf, (PGM_P) ((mode_s)), 15) - std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { return json::build_json([obj, start_config](JsonObject root) { set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); const auto traits = obj->get_traits(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); - char __buf[16]; + char buf[16]; if (start_config == DETAIL_ALL) { JsonArray opt = root.createNestedArray("modes"); @@ -996,6 +996,34 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } #endif +#ifdef USE_ALARM_CONTROL_PANEL +void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + this->events_.send(this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE).c_str(), "state"); +} +std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, + JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { + char buf[16]; + set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), + PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); + }); +} +void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->alarm_control_panel_json(obj, obj->get_state(), DETAIL_STATE); + request->send(200, "application/json", data.c_str()); + return; + } + } + request->send(404); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -1073,6 +1101,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_ALARM_CONTROL_PANEL + if (request->method() == HTTP_GET && match.domain == "alarm_control_panel") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -1180,6 +1213,14 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_ALARM_CONTROL_PANEL + if (match.domain == "alarm_control_panel") { + this->handle_alarm_control_panel_request(request, match); + + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index f4122ef754..45d0bc03a4 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -216,6 +216,17 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif +#ifdef USE_ALARM_CONTROL_PANEL + void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; + + /// Handle a alarm_control_panel request under '/alarm_control_panel/'. + void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the alarm_control_panel state with its value as a JSON string. + std::string alarm_control_panel_json(alarm_control_panel::AlarmControlPanel *obj, + alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c9da07795c..068d015732 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -53,6 +53,9 @@ WIFI_POWER_SAVE_MODES = { "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) +WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) +WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) +WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) def validate_password(value): @@ -253,6 +256,7 @@ def _validate(config): CONF_OUTPUT_POWER = "output_power" CONF_PASSIVE_SCAN = "passive_scan" +CONF_ENABLE_ON_BOOT = "enable_on_boot" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -286,6 +290,7 @@ CONFIG_SCHEMA = cv.All( "This option has been removed. Please use the [disabled] option under the " "new mdns component instead." ), + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, } ), _validate, @@ -385,6 +390,8 @@ async def to_code(config): if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) + if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) elif CORE.is_esp32 and CORE.using_arduino: @@ -410,3 +417,18 @@ async def to_code(config): @automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({})) async def wifi_connected_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg) + + +@automation.register_condition("wifi.enabled", WiFiEnabledCondition, cv.Schema({})) +async def wifi_enabled_to_code(config, condition_id, template_arg, args): + return cg.new_Pvariable(condition_id, template_arg) + + +@automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({})) +async def wifi_enable_to_code(config, action_id, template_arg, args): + return cg.new_Pvariable(action_id, template_arg) + + +@automation.register_action("wifi.disable", WiFiDisableAction, cv.Schema({})) +async def wifi_disable_to_code(config, action_id, template_arg, args): + return cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 9f047dd5ed..ff621291f0 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -36,9 +36,18 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } void WiFiComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up WiFi..."); + this->wifi_pre_setup_(); + if (this->enable_on_boot_) { + this->start(); + } else { + this->state_ = WIFI_COMPONENT_STATE_DISABLED; + } +} + +void WiFiComponent::start() { + ESP_LOGCONFIG(TAG, "Starting WiFi..."); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); this->last_connected_ = millis(); - this->wifi_pre_setup_(); uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; @@ -135,6 +144,8 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_OFF: case WIFI_COMPONENT_STATE_AP: break; + case WIFI_COMPONENT_STATE_DISABLED: + return; } if (this->has_ap() && !this->ap_setup_) { @@ -183,6 +194,11 @@ network::IPAddress WiFiComponent::get_ip_address() { return this->wifi_soft_ap_ip(); return {}; } +network::IPAddress WiFiComponent::get_dns_address(int num) { + if (this->has_sta()) + return this->wifi_dns_ip_(num); + return {}; +} std::string WiFiComponent::get_use_address() const { if (this->use_address_.empty()) { return App.get_name() + ".local"; @@ -382,6 +398,28 @@ void WiFiComponent::print_connect_params_() { #endif } +void WiFiComponent::enable() { + if (this->state_ != WIFI_COMPONENT_STATE_DISABLED) + return; + + ESP_LOGD(TAG, "Enabling WIFI..."); + this->error_from_callback_ = false; + this->state_ = WIFI_COMPONENT_STATE_OFF; + this->start(); +} + +void WiFiComponent::disable() { + if (this->state_ == WIFI_COMPONENT_STATE_DISABLED) + return; + + ESP_LOGD(TAG, "Disabling WIFI..."); + this->state_ = WIFI_COMPONENT_STATE_DISABLED; + this->wifi_disconnect_(); + this->wifi_mode_(false, false); +} + +bool WiFiComponent::is_disabled() { return this->state_ == WIFI_COMPONENT_STATE_DISABLED; } + void WiFiComponent::start_scanning() { this->action_started_ = millis(); ESP_LOGD(TAG, "Starting scan..."); @@ -603,7 +641,7 @@ void WiFiComponent::retry_connect() { } bool WiFiComponent::can_proceed() { - if (!this->has_sta()) { + if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED) { return true; } return this->is_connected(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 3f81b94cce..d39b062990 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -47,6 +47,8 @@ struct SavedWifiSettings { enum WiFiComponentState { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, + /** WiFi is disabled. */ + WIFI_COMPONENT_STATE_DISABLED, /** WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of time. */ WIFI_COMPONENT_STATE_COOLDOWN, /** WiFi is in STA-only mode and currently scanning for APs. */ @@ -198,6 +200,9 @@ class WiFiComponent : public Component { void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } + void enable(); + void disable(); + bool is_disabled(); void start_scanning(); void check_scanning_finished(); void start_connecting(const WiFiAP &ap, bool two); @@ -224,6 +229,7 @@ class WiFiComponent : public Component { // (In most use cases you won't need these) /// Setup WiFi interface. void setup() override; + void start(); void dump_config() override; /// WIFI setup_priority. float get_setup_priority() const override; @@ -240,6 +246,7 @@ class WiFiComponent : public Component { void set_rrm(bool rrm); #endif + network::IPAddress get_dns_address(int num); network::IPAddress get_ip_address(); std::string get_use_address() const; void set_use_address(const std::string &use_address); @@ -281,6 +288,8 @@ class WiFiComponent : public Component { int8_t wifi_rssi(); + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + protected: static std::string format_mac_addr(const uint8_t mac[6]); void setup_ap_config_(); @@ -358,18 +367,30 @@ class WiFiComponent : public Component { bool btm_{false}; bool rrm_{false}; #endif + bool enable_on_boot_; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class WiFiConnectedCondition : public Condition { public: - bool check(Ts... x) override; + bool check(Ts... x) override { return global_wifi_component->is_connected(); } }; -template bool WiFiConnectedCondition::check(Ts... x) { - return global_wifi_component->is_connected(); -} +template class WiFiEnabledCondition : public Condition { + public: + bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } +}; + +template class WiFiEnableAction : public Action { + public: + void play(Ts... x) override { global_wifi_component->enable(); } +}; + +template class WiFiDisableAction : public Action { + public: + void play(Ts... x) override { global_wifi_component->disable(); } +}; } // namespace wifi } // namespace esphome diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 54993d48ee..659fd08db1 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_SCAN_RESULTS, CONF_SSID, CONF_MAC_ADDRESS, + CONF_DNS_ADDRESS, ENTITY_CATEGORY_DIAGNOSTIC, ) @@ -28,6 +29,9 @@ BSSIDWiFiInfo = wifi_info_ns.class_( MacAddressWifiInfo = wifi_info_ns.class_( "MacAddressWifiInfo", text_sensor.TextSensor, cg.Component ) +DNSAddressWifiInfo = wifi_info_ns.class_( + "DNSAddressWifiInfo", text_sensor.TextSensor, cg.PollingComponent +) CONFIG_SCHEMA = cv.Schema( { @@ -46,6 +50,9 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), + cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema( + DNSAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ).extend(cv.polling_component_schema("1s")), } ) @@ -63,3 +70,4 @@ async def to_code(config): await setup_conf(config, CONF_BSSID) await setup_conf(config, CONF_MAC_ADDRESS) await setup_conf(config, CONF_SCAN_RESULTS) + await setup_conf(config, CONF_DNS_ADDRESS) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 0b73de68de..eeb4985398 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -11,6 +11,7 @@ void ScanResultsWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Scan Res void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); } void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); } +void DNSAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo DNS Address", this); } } // namespace wifi_info } // namespace esphome diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index e5b0fa3223..35ce108c86 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -24,6 +24,32 @@ class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSenso network::IPAddress last_ip_; }; +class DNSAddressWifiInfo : public PollingComponent, public text_sensor::TextSensor { + public: + void update() override { + std::string dns_results; + + auto dns_one = wifi::global_wifi_component->get_dns_address(0); + auto dns_two = wifi::global_wifi_component->get_dns_address(1); + + dns_results += "DNS1: "; + dns_results += dns_one.str(); + dns_results += " DNS2: "; + dns_results += dns_two.str(); + + if (dns_results != this->last_results_) { + this->last_results_ = dns_results; + this->publish_state(dns_results); + } + } + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + std::string unique_id() override { return get_mac_address() + "-wifiinfo-dns"; } + void dump_config() override; + + protected: + std::string last_results_; +}; + class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 2482e5471c..0a6b2dfbb0 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -53,6 +53,7 @@ from esphome.const import ( KEY_TARGET_PLATFORM, TYPE_GIT, TYPE_LOCAL, + VALID_SUBSTITUTIONS_CHARACTERS, ) from esphome.core import ( CORE, @@ -79,6 +80,11 @@ from esphome.yaml_util import make_data_base _LOGGER = logging.getLogger(__name__) +# pylint: disable=consider-using-f-string +VARIABLE_PROG = re.compile( + "\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS) +) + # pylint: disable=invalid-name Schema = _Schema @@ -265,6 +271,14 @@ def alphanumeric(value): def valid_name(value): value = string_strict(value) + + if CORE.vscode: + # If the value is a substitution, it can't be validated until the substitution + # is actually made. + sub_match = VARIABLE_PROG.search(value) + if sub_match: + return value + for c in value: if c not in ALLOWED_NAME_CHARS: raise Invalid( @@ -447,6 +461,14 @@ def validate_id_name(value): raise Invalid( "Dashes are not supported in IDs, please use underscores instead." ) + + if CORE.vscode: + # If the value is a substitution, it can't be validated until the substitution + # is actually made + sub_match = VARIABLE_PROG.match(value) + if sub_match: + return value + valid_chars = f"{ascii_letters + digits}_" for char in value: if char not in valid_chars: diff --git a/esphome/const.py b/esphome/const.py index fce8ab9f1c..470f8a46e5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,8 +1,11 @@ """Constants used by esphome.""" -__version__ = "2023.5.5" +__version__ = "2023.6.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" +VALID_SUBSTITUTIONS_CHARACTERS = ( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" +) PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" @@ -180,6 +183,7 @@ CONF_DIR_PIN = "dir_pin" CONF_DIRECTION = "direction" CONF_DIRECTION_OUTPUT = "direction_output" CONF_DISABLE_CRC = "disable_crc" +CONF_DISABLED = "disabled" CONF_DISABLED_BY_DEFAULT = "disabled_by_default" CONF_DISCONNECT_DELAY = "disconnect_delay" CONF_DISCOVERY = "discovery" @@ -190,6 +194,7 @@ CONF_DISCOVERY_UNIQUE_ID_GENERATOR = "discovery_unique_id_generator" CONF_DISTANCE = "distance" CONF_DITHER = "dither" CONF_DIV_RATIO = "div_ratio" +CONF_DNS_ADDRESS = "dns_address" CONF_DNS1 = "dns1" CONF_DNS2 = "dns2" CONF_DOMAIN = "domain" @@ -391,8 +396,10 @@ CONF_MAX_SPEED = "max_speed" CONF_MAX_TEMPERATURE = "max_temperature" CONF_MAX_VALUE = "max_value" CONF_MAX_VOLTAGE = "max_voltage" +CONF_MDNS = "mdns" CONF_MEASUREMENT_DURATION = "measurement_duration" CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" +CONF_MEDIA_PLAYER = "media_player" CONF_MEDIUM = "medium" CONF_MEMORY_BLOCKS = "memory_blocks" CONF_METHOD = "method" @@ -410,6 +417,7 @@ CONF_MIN_LENGTH = "min_length" CONF_MIN_LEVEL = "min_level" CONF_MIN_POWER = "min_power" CONF_MIN_RANGE = "min_range" +CONF_MIN_RSSI = "min_rssi" CONF_MIN_TEMPERATURE = "min_temperature" CONF_MIN_VALUE = "min_value" CONF_MIN_VERSION = "min_version" diff --git a/esphome/core/application.h b/esphome/core/application.h index 0992a4df39..0501d1a56a 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -48,6 +48,9 @@ #ifdef USE_MEDIA_PLAYER #include "esphome/components/media_player/media_player.h" #endif +#ifdef USE_ALARM_CONTROL_PANEL +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#endif namespace esphome { @@ -126,6 +129,12 @@ class Application { void register_media_player(media_player::MediaPlayer *media_player) { this->media_players_.push_back(media_player); } #endif +#ifdef USE_ALARM_CONTROL_PANEL + void register_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { + this->alarm_control_panels_.push_back(a_alarm_control_panel); + } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -296,6 +305,18 @@ class Application { } #endif +#ifdef USE_ALARM_CONTROL_PANEL + const std::vector &get_alarm_control_panels() { + 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_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif + Scheduler scheduler; protected: @@ -349,6 +370,9 @@ class Application { #ifdef USE_MEDIA_PLAYER std::vector media_players_{}; #endif +#ifdef USE_ALARM_CONTROL_PANEL + std::vector alarm_control_panels_{}; +#endif std::string name_; std::string friendly_name_; diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 16c56a90c5..871f6d6c0e 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -246,6 +246,21 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_ALARM_CONTROL_PANEL + case IteratorState::ALARM_CONTROL_PANEL: + if (this->at_ >= App.get_alarm_control_panels().size()) { + advance_platform = true; + } else { + auto *a_alarm_control_panel = App.get_alarm_control_panels()[this->at_]; + if (a_alarm_control_panel->is_internal() && !this->include_internal_) { + success = true; + break; + } else { + success = this->on_alarm_control_panel(a_alarm_control_panel); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index e8e048bd73..8b2da6218c 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -65,6 +65,9 @@ class ComponentIterator { #endif #ifdef USE_MEDIA_PLAYER virtual bool on_media_player(media_player::MediaPlayer *media_player); +#endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) = 0; #endif virtual bool on_end(); @@ -116,6 +119,9 @@ class ComponentIterator { #endif #ifdef USE_MEDIA_PLAYER MEDIA_PLAYER, +#endif +#ifdef USE_ALARM_CONTROL_PANEL + ALARM_CONTROL_PANEL, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 857ee6398f..2ce471ead0 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -79,6 +79,12 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); }); } #endif +#ifdef USE_ALARM_CONTROL_PANEL + for (auto *obj : App.get_alarm_control_panels()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 0966504c43..25a4acb36e 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -40,6 +40,9 @@ #ifdef USE_MEDIA_PLAYER #include "esphome/components/media_player/media_player.h" #endif +#ifdef USE_ALARM_CONTROL_PANEL +#include "esphome/components/alarm_control_panel/alarm_control_panel.h" +#endif namespace esphome { @@ -82,6 +85,9 @@ class Controller { #ifdef USE_MEDIA_PLAYER virtual void on_media_player_update(media_player::MediaPlayer *obj){}; #endif +#ifdef USE_ALARM_CONTROL_PANEL + virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 64edabc878..638dd39364 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -17,6 +17,7 @@ #define USE_API #define USE_API_NOISE #define USE_API_PLAINTEXT +#define USE_ALARM_CONTROL_PANEL #define USE_BINARY_SENSOR #define USE_BUTTON #define USE_CLIMATE diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp new file mode 100644 index 0000000000..c03506fd2a --- /dev/null +++ b/esphome/core/time.cpp @@ -0,0 +1,171 @@ +#include "time.h" // NOLINT + +namespace esphome { + +size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { + struct tm c_tm = this->to_c_tm(); + return ::strftime(buffer, buffer_len, format, &c_tm); +} +ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { + ESPTime res{}; + res.second = uint8_t(c_tm->tm_sec); + res.minute = uint8_t(c_tm->tm_min); + res.hour = uint8_t(c_tm->tm_hour); + res.day_of_week = uint8_t(c_tm->tm_wday + 1); + res.day_of_month = uint8_t(c_tm->tm_mday); + res.day_of_year = uint16_t(c_tm->tm_yday + 1); + res.month = uint8_t(c_tm->tm_mon + 1); + res.year = uint16_t(c_tm->tm_year + 1900); + res.is_dst = bool(c_tm->tm_isdst); + res.timestamp = c_time; + return res; +} +struct tm ESPTime::to_c_tm() { + struct tm c_tm {}; + c_tm.tm_sec = this->second; + c_tm.tm_min = this->minute; + c_tm.tm_hour = this->hour; + c_tm.tm_mday = this->day_of_month; + c_tm.tm_mon = this->month - 1; + c_tm.tm_year = this->year - 1900; + c_tm.tm_wday = this->day_of_week - 1; + c_tm.tm_yday = this->day_of_year - 1; + c_tm.tm_isdst = this->is_dst; + return c_tm; +} +std::string ESPTime::strftime(const std::string &format) { + std::string timestr; + timestr.resize(format.size() * 4); + struct tm c_tm = this->to_c_tm(); + size_t len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + while (len == 0) { + timestr.resize(timestr.size() * 2); + len = ::strftime(×tr[0], timestr.size(), format.c_str(), &c_tm); + } + timestr.resize(len); + return timestr; +} + +void ESPTime::increment_second() { + this->timestamp++; + if (!increment_time_value(this->second, 0, 60)) + return; + + // second roll-over, increment minute + if (!increment_time_value(this->minute, 0, 60)) + return; + + // minute roll-over, increment hour + if (!increment_time_value(this->hour, 0, 24)) + return; + + // hour roll-over, increment day + increment_time_value(this->day_of_week, 1, 8); + + if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { + // day of month roll-over, increment month + increment_time_value(this->month, 1, 13); + } + + uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; + if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { + // day of year roll-over, increment year + this->year++; + } +} +void ESPTime::increment_day() { + this->timestamp += 86400; + + // increment day + increment_time_value(this->day_of_week, 1, 8); + + if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { + // day of month roll-over, increment month + increment_time_value(this->month, 1, 13); + } + + uint16_t days_in_year = (this->year % 4 == 0) ? 366 : 365; + if (increment_time_value(this->day_of_year, 1, days_in_year + 1)) { + // day of year roll-over, increment year + this->year++; + } +} +void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { + time_t res = 0; + + if (!this->fields_in_range()) { + this->timestamp = -1; + return; + } + + for (int i = 1970; i < this->year; i++) + res += is_leap_year(i) ? 366 : 365; + + if (use_day_of_year) { + res += this->day_of_year - 1; + } else { + for (int i = 1; i < this->month; i++) + res += days_in_month(i, this->year); + + res += this->day_of_month - 1; + } + + res *= 24; + res += this->hour; + res *= 60; + res += this->minute; + res *= 60; + res += this->second; + this->timestamp = res; +} + +int32_t ESPTime::timezone_offset() { + int32_t offset = 0; + time_t now = ::time(nullptr); + auto local = ESPTime::from_epoch_local(now); + auto utc = ESPTime::from_epoch_utc(now); + bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; + + if (utc.minute > local.minute) { + local.minute += 60; + local.hour -= 1; + } + offset += (local.minute - utc.minute) * 60; + + if (negative) { + offset -= (utc.hour - local.hour) * 3600; + } else { + if (utc.hour > local.hour) { + local.hour += 24; + } + offset += (local.hour - utc.hour) * 3600; + } + return offset; +} + +bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } +bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } +bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } +bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; } +bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; } + +template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) { + current++; + if (current >= end) { + current = begin; + return true; + } + return false; +} + +static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } + +static uint8_t days_in_month(uint8_t month, uint16_t year) { + static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + uint8_t days = DAYS_IN_MONTH[month]; + if (month == 2 && is_leap_year(year)) + return 29; + return days; +} + +} // namespace esphome diff --git a/esphome/core/time.h b/esphome/core/time.h new file mode 100644 index 0000000000..e1bdc8c839 --- /dev/null +++ b/esphome/core/time.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include + +namespace esphome { + +template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end); + +static bool is_leap_year(uint32_t year); + +static uint8_t days_in_month(uint8_t month, uint16_t year); + +/// A more user-friendly version of struct tm from time.h +struct ESPTime { + /** seconds after the minute [0-60] + * @note second is generally 0-59; the extra range is to accommodate leap seconds. + */ + uint8_t second; + /// minutes after the hour [0-59] + uint8_t minute; + /// hours since midnight [0-23] + uint8_t hour; + /// day of the week; sunday=1 [1-7] + uint8_t day_of_week; + /// day of the month [1-31] + uint8_t day_of_month; + /// day of the year [1-366] + uint16_t day_of_year; + /// month; january=1 [1-12] + uint8_t month; + /// year + uint16_t year; + /// daylight saving time flag + bool is_dst; + /// unix epoch time (seconds since UTC Midnight January 1, 1970) + time_t timestamp; + + /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. + * Up to buffer_len bytes are written. + * + * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + */ + size_t strftime(char *buffer, size_t buffer_len, const char *format); + + /** Convert this ESPTime struct to a string as specified by the format argument. + * @see https://www.gnu.org/software/libc/manual/html_node/Formatting-Calendar-Time.html#index-strftime + * + * @warning This method uses dynamically allocated strings which can cause heap fragmentation with some + * microcontrollers. + */ + std::string strftime(const std::string &format); + + /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) + bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } + + /// Check if all time fields of this ESPTime are in range. + bool fields_in_range() const { + return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && + this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && + this->day_of_year < 367 && this->month > 0 && this->month < 13; + } + + /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance. + static ESPTime from_c_tm(struct tm *c_tm, time_t c_time); + + /** Convert an UTC epoch timestamp to a local time ESPTime instance. + * + * @param epoch Seconds since 1st January 1970. In UTC. + * @return The generated ESPTime + */ + static ESPTime from_epoch_local(time_t epoch) { + struct tm *c_tm = ::localtime(&epoch); + return ESPTime::from_c_tm(c_tm, epoch); + } + /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. + * + * @param epoch Seconds since 1st January 1970. In UTC. + * @return The generated ESPTime + */ + static ESPTime from_epoch_utc(time_t epoch) { + struct tm *c_tm = ::gmtime(&epoch); + return ESPTime::from_c_tm(c_tm, epoch); + } + + /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). + void recalc_timestamp_utc(bool use_day_of_year = true); + + /// Convert this ESPTime instance back to a tm struct. + struct tm to_c_tm(); + + static int32_t timezone_offset(); + + /// Increment this clock instance by one second. + void increment_second(); + /// Increment this clock instance by one day. + void increment_day(); + bool operator<(ESPTime other); + bool operator<=(ESPTime other); + bool operator==(ESPTime other); + bool operator>=(ESPTime other); + bool operator>(ESPTime other); +}; +} // namespace esphome diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 1a50592a2d..8d8eb74b4b 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,4 +1,5 @@ import base64 +import binascii import codecs import collections import functools @@ -76,6 +77,10 @@ class DashboardSettings: def status_use_ping(self): return get_bool_env("ESPHOME_DASHBOARD_USE_PING") + @property + def status_use_mqtt(self): + return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT") + @property def using_ha_addon_auth(self): if not self.on_ha_addon: @@ -583,6 +588,12 @@ class DashboardEntry: return None return self.storage.address + @property + def no_mdns(self): + if self.storage is None: + return None + return self.storage.no_mdns + @property def web_port(self): if self.storage is None: @@ -775,9 +786,12 @@ class MDNSStatusThread(threading.Thread): stat.start() while not STOP_EVENT.is_set(): entries = _list_dashboard_entries() - stat.request_query( - {entry.filename: f"{entry.name}.local." for entry in entries} - ) + hosts = {} + for entry in entries: + if entry.no_mdns is not True: + hosts[entry.filename] = f"{entry.name}.local." + + stat.request_query(hosts) IMPORT_RESULT = imports.import_state PING_REQUEST.wait() @@ -801,6 +815,9 @@ class PingStatusThread(threading.Thread): entries = _list_dashboard_entries() queue = collections.deque() for entry in entries: + if entry.no_mdns is True: + continue + if entry.address is None: PING_RESULT[entry.filename] = None continue @@ -832,10 +849,67 @@ class PingStatusThread(threading.Thread): PING_REQUEST.clear() +class MqttStatusThread(threading.Thread): + def run(self): + from esphome import mqtt + + entries = _list_dashboard_entries() + + config = mqtt.config_from_env() + topic = "esphome/discover/#" + + def on_message(client, userdata, msg): + nonlocal entries + + payload = msg.payload.decode(errors="backslashreplace") + if len(payload) > 0: + data = json.loads(payload) + if "name" not in data: + return + for entry in entries: + if entry.name == data["name"]: + PING_RESULT[entry.filename] = True + return + + def on_connect(client, userdata, flags, return_code): + client.publish("esphome/discover", None, retain=False) + + mqttid = str(binascii.hexlify(os.urandom(6)).decode()) + + client = mqtt.prepare( + config, + [topic], + on_message, + on_connect, + None, + None, + f"esphome-dashboard-{mqttid}", + ) + client.loop_start() + + while not STOP_EVENT.wait(2): + # update entries + entries = _list_dashboard_entries() + + # will be set to true on on_message + for entry in entries: + if entry.no_mdns: + PING_RESULT[entry.filename] = False + + client.publish("esphome/discover", None, retain=False) + MQTT_PING_REQUEST.wait() + MQTT_PING_REQUEST.clear() + + client.disconnect() + client.loop_stop() + + class PingRequestHandler(BaseHandler): @authenticated def get(self): PING_REQUEST.set() + if settings.status_use_mqtt: + MQTT_PING_REQUEST.set() self.set_header("content-type", "application/json") self.write(json.dumps(PING_RESULT)) @@ -910,6 +984,7 @@ PING_RESULT: dict = {} IMPORT_RESULT = {} STOP_EVENT = threading.Event() PING_REQUEST = threading.Event() +MQTT_PING_REQUEST = threading.Event() class LoginHandler(BaseHandler): @@ -1197,6 +1272,11 @@ def start_web_server(args): else: status_thread = MDNSStatusThread() status_thread.start() + + if settings.status_use_mqtt: + status_thread_mqtt = MqttStatusThread() + status_thread_mqtt.start() + try: tornado.ioloop.IOLoop.current().start() except KeyboardInterrupt: @@ -1204,5 +1284,8 @@ def start_web_server(args): STOP_EVENT.set() PING_REQUEST.set() status_thread.join() + if settings.status_use_mqtt: + status_thread_mqtt.join() + MQTT_PING_REQUEST.set() if args.socket is not None: os.remove(args.socket) diff --git a/esphome/helpers.py b/esphome/helpers.py index 884f640d7b..fd8893ad99 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -147,6 +147,14 @@ def get_bool_env(var, default=False): return bool(os.getenv(var, default)) +def get_str_env(var, default=None): + return str(os.getenv(var, default)) + + +def get_int_env(var, default=0): + return int(os.getenv(var, default)) + + def is_ha_addon(): return get_bool_env("ESPHOME_IS_HA_ADDON") diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 0ddd976072..166301005d 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -4,6 +4,7 @@ import logging import ssl import sys import time +import json import paho.mqtt.client as mqtt @@ -24,15 +25,45 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.log import color, Fore from esphome.util import safe_print +from esphome.helpers import get_str_env, get_int_env _LOGGER = logging.getLogger(__name__) -def initialize(config, subscriptions, on_message, username, password, client_id): - def on_connect(client, userdata, flags, return_code): +def config_from_env(): + config = { + CONF_MQTT: { + CONF_USERNAME: get_str_env("ESPHOME_DASHBOARD_MQTT_USERNAME"), + CONF_PASSWORD: get_str_env("ESPHOME_DASHBOARD_MQTT_PASSWORD"), + CONF_BROKER: get_str_env("ESPHOME_DASHBOARD_MQTT_BROKER"), + CONF_PORT: get_int_env("ESPHOME_DASHBOARD_MQTT_PORT", 1883), + }, + } + return config + + +def initialize( + config, subscriptions, on_message, on_connect, username, password, client_id +): + client = prepare( + config, subscriptions, on_message, on_connect, username, password, client_id + ) + try: + client.loop_forever() + except KeyboardInterrupt: + pass + return 0 + + +def prepare( + config, subscriptions, on_message, on_connect, username, password, client_id +): + def on_connect_(client, userdata, flags, return_code): _LOGGER.info("Connected to MQTT broker!") for topic in subscriptions: client.subscribe(topic) + if on_connect is not None: + on_connect(client, userdata, flags, return_code) def on_disconnect(client, userdata, result_code): if result_code == 0: @@ -57,7 +88,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id) tries += 1 client = mqtt.Client(client_id or "") - client.on_connect = on_connect + client.on_connect = on_connect_ client.on_message = on_message client.on_disconnect = on_disconnect if username is None: @@ -89,11 +120,88 @@ def initialize(config, subscriptions, on_message, username, password, client_id) except OSError as err: raise EsphomeError(f"Cannot connect to MQTT broker: {err}") from err - try: - client.loop_forever() - except KeyboardInterrupt: - pass - return 0 + return client + + +def show_discover(config, username=None, password=None, client_id=None): + topic = "esphome/discover/#" + _LOGGER.info("Starting log output from %s", topic) + + def on_message(client, userdata, msg): + time_ = datetime.now().time().strftime("[%H:%M:%S]") + payload = msg.payload.decode(errors="backslashreplace") + if len(payload) > 0: + message = time_ + " " + payload + safe_print(message) + + def on_connect(client, userdata, flags, return_code): + _LOGGER.info("Send discover via MQTT broker") + client.publish("esphome/discover", None, retain=False) + + return initialize( + config, [topic], on_message, on_connect, username, password, client_id + ) + + +def get_esphome_device_ip( + config, username=None, password=None, client_id=None, timeout=25 +): + if CONF_MQTT not in config: + raise EsphomeError( + "Cannot discover IP via MQTT as the config does not include the mqtt: " + "component" + ) + if CONF_ESPHOME not in config or CONF_NAME not in config[CONF_ESPHOME]: + raise EsphomeError( + "Cannot discover IP via MQTT as the config does not include the device name: " + "component" + ) + + dev_name = config[CONF_ESPHOME][CONF_NAME] + dev_ip = None + + topic = "esphome/discover/" + dev_name + _LOGGER.info("Starting looking for IP in topic %s", topic) + + def on_message(client, userdata, msg): + nonlocal dev_ip + time_ = datetime.now().time().strftime("[%H:%M:%S]") + payload = msg.payload.decode(errors="backslashreplace") + if len(payload) > 0: + message = time_ + " " + payload + _LOGGER.debug(message) + + data = json.loads(payload) + if "name" not in data or data["name"] != dev_name: + _LOGGER.Warn("Wrong device answer") + return + + if "ip" in data: + dev_ip = data["ip"] + client.disconnect() + + def on_connect(client, userdata, flags, return_code): + topic = "esphome/ping/" + dev_name + _LOGGER.info("Send discover via MQTT broker topic: %s", topic) + client.publish(topic, None, retain=False) + + mqtt_client = prepare( + config, [topic], on_message, on_connect, username, password, client_id + ) + + mqtt_client.loop_start() + while timeout > 0: + if dev_ip is not None: + break + timeout -= 0.250 + time.sleep(0.250) + mqtt_client.loop_stop() + + if dev_ip is None: + raise EsphomeError("Failed to find IP via MQTT") + + _LOGGER.info("Found IP: %s", dev_ip) + return dev_ip def show_logs(config, topic=None, username=None, password=None, client_id=None): @@ -118,7 +226,7 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): message = time_ + payload safe_print(message) - return initialize(config, [topic], on_message, username, password, client_id) + return initialize(config, [topic], on_message, None, username, password, client_id) def clear_topic(config, topic, username=None, password=None, client_id=None): @@ -142,7 +250,7 @@ def clear_topic(config, topic, username=None, password=None, client_id=None): return client.publish(msg.topic, None, retain=True) - return initialize(config, [topic], on_message, username, password, client_id) + return initialize(config, [topic], on_message, None, username, password, client_id) # From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py diff --git a/esphome/storage_json.py b/esphome/storage_json.py index bbdfbbc8a2..acf525203d 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -10,6 +10,12 @@ from esphome import const from esphome.core import CORE from esphome.helpers import write_file_if_changed + +from esphome.const import ( + CONF_MDNS, + CONF_DISABLED, +) + from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) @@ -46,6 +52,7 @@ class StorageJSON: build_path, firmware_bin_path, loaded_integrations, + no_mdns, ): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) @@ -75,6 +82,8 @@ class StorageJSON: # A list of strings of names of loaded integrations self.loaded_integrations: list[str] = loaded_integrations self.loaded_integrations.sort() + # Is mDNS disabled + self.no_mdns = no_mdns def as_dict(self): return { @@ -90,6 +99,7 @@ class StorageJSON: "build_path": self.build_path, "firmware_bin_path": self.firmware_bin_path, "loaded_integrations": self.loaded_integrations, + "no_mdns": self.no_mdns, } def to_json(self): @@ -120,6 +130,11 @@ class StorageJSON: build_path=esph.build_path, firmware_bin_path=esph.firmware_bin, loaded_integrations=list(esph.loaded_integrations), + no_mdns=( + CONF_MDNS in esph.config + and CONF_DISABLED in esph.config[CONF_MDNS] + and esph.config[CONF_MDNS][CONF_DISABLED] is True + ), ) @staticmethod @@ -139,6 +154,7 @@ class StorageJSON: build_path=None, firmware_bin_path=None, loaded_integrations=[], + no_mdns=False, ) @staticmethod @@ -159,6 +175,7 @@ class StorageJSON: build_path = storage.get("build_path") firmware_bin_path = storage.get("firmware_bin_path") loaded_integrations = storage.get("loaded_integrations", []) + no_mdns = storage.get("no_mdns", False) return StorageJSON( storage_version, name, @@ -172,6 +189,7 @@ class StorageJSON: build_path, firmware_bin_path, loaded_integrations, + no_mdns, ) @staticmethod diff --git a/esphome/writer.py b/esphome/writer.py index 2bf665c2b2..ad506b6ae6 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -299,6 +299,16 @@ def copy_src_tree(): copy_files() + elif CORE.is_rp2040: + from esphome.components.rp2040 import copy_files + + (pio) = copy_files() + if pio: + write_file_if_changed( + CORE.relative_src_path("esphome.h"), + ESPHOME_H_FORMAT.format(include_s + '\n#include "pio_includes.h"'), + ) + def generate_defines_h(): define_content_l = [x.as_macro for x in CORE.defines] diff --git a/requirements.txt b/requirements.txt index 0da1d8a812..b994de1932 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,16 +2,16 @@ voluptuous==0.13.1 PyYAML==6.0 paho-mqtt==1.6.1 colorama==0.4.6 -tornado==6.3.1 +tornado==6.3.2 tzlocal==5.0.1 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.6 # When updating platformio, also update Dockerfile -esptool==4.5.1 +platformio==6.1.7 # When updating platformio, also update Dockerfile +esptool==4.6 click==8.1.3 esphome-dashboard==20230516.0 -aioesphomeapi==13.7.5 -zeroconf==0.60.0 +aioesphomeapi==14.0.0 +zeroconf==0.63.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/requirements_optional.txt b/requirements_optional.txt index 2c73430109..df6b3b387e 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,2 +1,3 @@ pillow>4.0.0 +cairosvg>=2.2.0 cryptography>=2.0.0,<4 diff --git a/requirements_test.txt b/requirements_test.txt index 55f8da245e..d5235d733b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,13 +1,15 @@ -pylint==2.17.3 +pylint==2.17.4 flake8==6.0.0 # also change in .pre-commit-config.yaml when updating black==23.3.0 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.3.2 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.4.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests pytest==7.3.1 -pytest-cov==4.0.0 +pytest-cov==4.1.0 pytest-mock==3.10.0 pytest-asyncio==0.21.0 asyncmock==0.4.2 hypothesis==5.49.0 + +clang-format==13.0.1 ; platform_machine != 'armv7l' diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index dba6f47d43..5a0c92350d 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -18,6 +18,7 @@ will be generated, they still need to be formatted """ import re +import os from pathlib import Path from textwrap import dedent from subprocess import call @@ -944,3 +945,19 @@ with open(root / "api_pb2_service.cpp", "w") as f: f.write(cpp) prot.unlink() + +try: + import clang_format + + def exec_clang_format(path): + clang_format_path = os.path.join( + os.path.dirname(clang_format.__file__), "data", "bin", "clang-format" + ) + call([clang_format_path, "-i", path]) + + exec_clang_format(root / "api_pb2_service.h") + exec_clang_format(root / "api_pb2_service.cpp") + exec_clang_format(root / "api_pb2.h") + exec_clang_format(root / "api_pb2.cpp") +except ImportError: + pass diff --git a/script/ci-custom.py b/script/ci-custom.py index 20f607f987..44ed83f392 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -66,6 +66,7 @@ file_types = ( ".txt", ".ico", ".svg", + ".png", ".py", ".html", ".js", @@ -80,7 +81,7 @@ file_types = ( "", ) cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") -ignore_types = (".ico", ".woff", ".woff2", "") +ignore_types = (".ico", ".png", ".woff", ".woff2", "") LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index c84715dcd8..512ef42b44 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -37,9 +37,7 @@ def test_button_config_value_internal_set(generate_main): # Given # When - main_cpp = generate_main( - "tests/component_tests/button/test_button.yaml" - ) + main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then assert "wol_1->set_internal(true);" in main_cpp diff --git a/tests/component_tests/deep_sleep/test_deep_sleep.py b/tests/component_tests/deep_sleep/test_deep_sleep.py index 690d323a50..11f1bcb58e 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep.py +++ b/tests/component_tests/deep_sleep/test_deep_sleep.py @@ -5,9 +5,7 @@ def test_deep_sleep_setup(generate_main): """ When the deep sleep is set in the yaml file, it should be registered in main """ - main_cpp = generate_main( - "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" - ) + main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") assert "deepsleep = new deep_sleep::DeepSleepComponent();" in main_cpp assert "App.register_component(deepsleep);" in main_cpp @@ -17,9 +15,7 @@ def test_deep_sleep_sleep_duration(generate_main): """ When deep sleep is configured with sleep duration, it should be set. """ - main_cpp = generate_main( - "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" - ) + main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") assert "deepsleep->set_sleep_duration(60000);" in main_cpp @@ -28,9 +24,7 @@ def test_deep_sleep_run_duration_simple(generate_main): """ When deep sleep is configured with run duration, it should be set. """ - main_cpp = generate_main( - "tests/component_tests/deep_sleep/test_deep_sleep1.yaml" - ) + main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml") assert "deepsleep->set_run_duration(10000);" in main_cpp @@ -39,9 +33,7 @@ def test_deep_sleep_run_duration_dictionary(generate_main): """ When deep sleep is configured with dictionary run duration, it should be set. """ - main_cpp = generate_main( - "tests/component_tests/deep_sleep/test_deep_sleep2.yaml" - ) + main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep2.yaml") assert ( "deepsleep->set_run_duration(deep_sleep::WakeupCauseToRunDuration{\n" diff --git a/tests/pnglogo.png b/tests/pnglogo.png new file mode 100644 index 0000000000..bd2fd54783 Binary files /dev/null and b/tests/pnglogo.png differ diff --git a/tests/test1.yaml b/tests/test1.yaml index 46c6bb80c6..f8928430f4 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1299,6 +1299,17 @@ sensor: id: temp_etuve humidity: name: "Humidity hyt271" + - platform: tmp1075 + name: "Temperature TMP1075" + update_interval: 10s + i2c_id: i2c_bus + conversion_rate: 27.5ms + alert: + limit_low: 50 + limit_high: 75 + fault_count: 1 + polarity: active_high + function: comparator esp32_touch: setup_mode: false @@ -3077,6 +3088,8 @@ text_sensor: name: BSSID mac_address: name: Mac Address + dns_address: + name: DNS ADdress - platform: version name: ESPHome Version No Timestamp hide_timestamp: true @@ -3358,3 +3371,21 @@ lcd_menu: on_prev: then: lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());' + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - binary_sensor1 + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())); diff --git a/tests/test2.yaml b/tests/test2.yaml index 51ec69ef34..2873d0e8e0 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -488,6 +488,14 @@ binary_sensor: esp32_ble_tracker: on_ble_advertise: + - mac_address: + - AA:BB:CC:DD:EE:FF + - FF:EE:DD:CC:BB:AA + then: + # yamllint disable rule:line-length + - lambda: !lambda |- + ESP_LOGD("main", "The device address (%s) exists in list", x.address_str().c_str()); + # yamllint enable rule:line-length - mac_address: AC:37:43:77:5F:4C then: # yamllint disable rule:line-length @@ -659,6 +667,34 @@ interval: display: +image: + - id: binary_image + file: pnglogo.png + type: BINARY + dither: FloydSteinberg + - id: transparent_transparent_image + file: pnglogo.png + type: TRANSPARENT_BINARY + - id: rgba_image + file: pnglogo.png + type: RGBA + resize: 50x50 + - id: rgb24_image + file: pnglogo.png + type: RGB24 + use_transparency: yes + - id: rgb565_image + file: pnglogo.png + type: RGB565 + use_transparency: no + + - id: mdi_alert + file: mdi:alert-circle-outline + resize: 50x50 + - id: another_alert_icon + file: mdi:alert-outline + type: BINARY + cap1188: id: cap1188_component address: 0x29 diff --git a/tests/test3.yaml b/tests/test3.yaml index c4847725e8..8307ac2984 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -822,7 +822,6 @@ switch: name: R0 Switch component_name: page0.r0 - climate: - platform: bang_bang name: Bang Bang Climate @@ -1106,7 +1105,6 @@ rf_bridge: - rf_bridge.send_raw: raw: "AAA5070008001000ABC12355" - display: - platform: nextion uart_id: uart_1 @@ -1171,3 +1169,22 @@ daly_bms: qr_code: - id: homepage_qr value: https://esphome.io/index.html + +alarm_control_panel: + - platform: template + id: alarmcontrolpanel1 + name: Alarm Panel + codes: + - "1234" + requires_code_to_arm: true + arming_home_time: 1s + arming_away_time: 15s + pending_time: 15s + trigger_time: 30s + binary_sensors: + - input: bin1 + bypass_armed_home: true + on_state: + then: + - lambda: !lambda |- + ESP_LOGD("TEST", "State change %s", alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())); diff --git a/tests/test4.yaml b/tests/test4.yaml index c1d49a4349..8e76a5fd66 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -666,6 +666,7 @@ touchscreen: i2s_audio: i2s_lrclk_pin: GPIO26 i2s_bclk_pin: GPIO27 + i2s_mclk_pin: GPIO25 media_player: - platform: i2s_audio diff --git a/tests/test6.yaml b/tests/test6.yaml index 7d4bd7bb19..6224563a77 100644 --- a/tests/test6.yaml +++ b/tests/test6.yaml @@ -37,6 +37,26 @@ switch: output: pin_4 id: pin_4_switch +#light: +# - platform: rp2040_pio_led_strip +# id: led_strip +# pin: GPIO13 +# num_leds: 60 +# pio: 0 +# rgb_order: GRB +# chipset: WS2812 +# - platform: rp2040_pio_led_strip +# id: led_strip_custom_timings +# pin: GPIO13 +# num_leds: 60 +# pio: 1 +# rgb_order: GRB +# bit0_high: .1us +# bit0_low: 1.2us +# bit1_high: .69us +# bit1_low: .4us + + sensor: - platform: internal_temperature name: Internal Temperature diff --git a/tests/test8.yaml b/tests/test8.yaml index 1d3c8a31f4..2430a0d1e6 100644 --- a/tests/test8.yaml +++ b/tests/test8.yaml @@ -4,7 +4,7 @@ wifi: ssid: "ssid" esp32: - board: esp32-c3-devkitm-1 + board: esp32s3box variant: ESP32S3 framework: type: arduino diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 9e9af52d00..34f70be2fb 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -6,7 +6,7 @@ from hypothesis.strategies import one_of, text, integers, builds from esphome import config_validation from esphome.config_validation import Invalid -from esphome.core import Lambda, HexInt +from esphome.core import CORE, Lambda, HexInt def test_check_not_templatable__invalid(): @@ -40,6 +40,47 @@ def test_valid_name__invalid(value): config_validation.valid_name(value) +@pytest.mark.parametrize("value", ("${name}", "${NAME}", "$NAME", "${name}_name")) +def test_valid_name__substitution_valid(value): + CORE.vscode = True + actual = config_validation.valid_name(value) + assert actual == value + + CORE.vscode = False + with pytest.raises(Invalid): + actual = config_validation.valid_name(value) + + +@pytest.mark.parametrize("value", ("{NAME}", "${A NAME}")) +def test_valid_name__substitution_like_invalid(value): + with pytest.raises(Invalid): + config_validation.valid_name(value) + + +@pytest.mark.parametrize("value", ("myid", "anID", "SOME_ID_test", "MYID_99")) +def test_validate_id_name__valid(value): + actual = config_validation.validate_id_name(value) + + assert actual == value + + +@pytest.mark.parametrize("value", ("id of mine", "id-4", "{name_id}", "id::name")) +def test_validate_id_name__invalid(value): + with pytest.raises(Invalid): + config_validation.validate_id_name(value) + + +@pytest.mark.parametrize("value", ("${id}", "${ID}", "${ID}_test_1", "$MYID")) +def test_validate_id_name__substitution_valid(value): + CORE.vscode = True + actual = config_validation.validate_id_name(value) + assert actual == value + + CORE.vscode = False + with pytest.raises(Invalid): + config_validation.validate_id_name(value) + + @given(one_of(integers(), text())) def test_string__valid(value): actual = config_validation.string(value) @@ -66,7 +107,16 @@ def test_string_string__invalid(value): config_validation.string_strict(value) -@given(builds(lambda v: "mdi:" + v, text(alphabet=string.ascii_letters + string.digits + "-_", min_size=1, max_size=20))) +@given( + builds( + lambda v: "mdi:" + v, + text( + alphabet=string.ascii_letters + string.digits + "-_", + min_size=1, + max_size=20, + ), + ) +) @example("") def test_icon__valid(value): actual = config_validation.icon(value)