Merge commit 'bb044a789c636ac787bdb6e53c5b20d851dd117c' into optolink

This commit is contained in:
j0ta29 2023-05-24 23:08:06 +00:00
commit 9f4e10cf55
71 changed files with 2054 additions and 495 deletions

1
.gitattributes vendored
View file

@ -1,2 +1,3 @@
# Normalize line endings to LF in the repository # Normalize line endings to LF in the repository
* text eol=lf * text eol=lf
*.png binary

View file

@ -12,60 +12,266 @@ on:
permissions: permissions:
contents: read contents: read
env:
DEFAULT_PYTHON: "3.9"
PYUPGRADE_TARGET: "--py39-plus"
CLANG_FORMAT_VERSION: "13.0.1"
concurrency: concurrency:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
ci: common:
name: ${{ matrix.name }} name: Create common environment
runs-on: ubuntu-latest 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.0
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: strategy:
fail-fast: false 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: matrix:
include: 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 - id: clang-tidy
name: Run script/clang-tidy for ESP8266 name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 options: --environment esp8266-arduino-tidy --grep USE_ESP8266
@ -90,119 +296,65 @@ jobs:
name: Run script/clang-tidy for ESP32 IDF name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF
pio_cache_key: tidyesp32-idf pio_cache_key: tidyesp32-idf
- id: yamllint
name: Run yamllint
steps: steps:
- uses: actions/checkout@v3 - name: Check out code from GitHub
- name: Set up Python uses: actions/checkout@v3.5.2
uses: actions/setup-python@v4 - name: Restore Python virtual environment
id: python uses: actions/cache/restore@v3.3.1
with: with:
python-version: "3.9" path: venv
- name: Cache virtualenv
uses: actions/cache@v3
with:
path: .venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
restore-keys: | # Use per check platformio cache because checks use different parts
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
- name: Cache platformio - name: Cache platformio
uses: actions/cache@v3 uses: actions/cache@v3.3.1
with: with:
path: ~/.platformio path: ~/.platformio
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
if: matrix.id == 'test' || matrix.id == 'clang-tidy'
- name: Install clang tools - name: Install clang-tidy
run: | run: sudo apt-get install clang-tidy-11
sudo apt-get install \
clang-format-13 \
clang-tidy-11
if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format'
- name: Register problem matchers - name: Register problem matchers
run: | 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/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.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 - name: Run clang-tidy
run: | run: |
. venv/bin/activate
script/clang-tidy --all-headers --fix ${{ matrix.options }} script/clang-tidy --all-headers --fix ${{ matrix.options }}
if: matrix.id == 'clang-tidy'
env: env:
# Also cache libdeps, store them in a ~/.platformio subfolder # Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Run yamllint
if: matrix.id == 'yamllint'
uses: frenck/action-yamllint@v1.4.0
- name: Suggested changes - name: Suggested changes
run: script/ci-suggest-changes run: script/ci-suggest-changes
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python') if: always()
ci-status: ci-status:
name: CI Status name: CI Status
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ci] needs:
- common
- black
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
- yamllint
- compile-tests
- clang-tidy
if: always() if: always()
steps: steps:
- name: Successful deploy - name: Success
if: ${{ !(contains(needs.*.result, 'failure')) }} if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0 run: exit 0
- name: Failing deploy - name: Failure
if: ${{ contains(needs.*.result, 'failure') }} if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1 run: exit 1

View file

@ -27,7 +27,7 @@ repos:
- --branch=release - --branch=release
- --branch=beta - --branch=beta
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.3.2 rev: v3.4.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py39-plus]

View file

@ -107,6 +107,7 @@ esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/light/* @DotNetDann
esphome/components/heatpumpir/* @rob-deutsch esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode
esphome/components/homeassistant/* @OttoWinter esphome/components/homeassistant/* @OttoWinter
esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp/* @RubyBailey
esphome/components/host/* @esphome/core esphome/components/host/* @esphome/core
@ -221,6 +222,7 @@ esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti esphome/components/safe_mode/* @jsuanet @paulmonigatti
@ -282,6 +284,7 @@ esphome/components/tlc5947/* @rnauber
esphome/components/tm1621/* @Philippe12 esphome/components/tm1621/* @Philippe12
esphome/components/tm1637/* @glmnet esphome/components/tm1637/* @glmnet
esphome/components/tm1638/* @skykingjwc esphome/components/tm1638/* @skykingjwc
esphome/components/tm1651/* @freekode
esphome/components/tmp102/* @timsavage esphome/components/tmp102/* @timsavage
esphome/components/tmp117/* @Azimath esphome/components/tmp117/* @Azimath
esphome/components/tof10120/* @wstrzalka esphome/components/tof10120/* @wstrzalka

View file

@ -52,7 +52,7 @@ RUN \
# Ubuntu python3-pip is missing wheel # Ubuntu python3-pip is missing wheel
pip3 install --no-cache-dir \ pip3 install --no-cache-dir \
wheel==0.37.1 \ wheel==0.37.1 \
platformio==6.1.6 \ platformio==6.1.7 \
# Change some platformio settings # Change some platformio settings
&& platformio settings set enable_telemetry No \ && platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \ && platformio settings set check_platformio_interval 1000000 \

View file

@ -18,6 +18,9 @@ from esphome.const import (
CONF_LOGGER, CONF_LOGGER,
CONF_NAME, CONF_NAME,
CONF_OTA, CONF_OTA,
CONF_MQTT,
CONF_MDNS,
CONF_DISABLED,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PORT, CONF_PORT,
CONF_ESPHOME, CONF_ESPHOME,
@ -42,7 +45,7 @@ from esphome.log import color, setup_log, Fore
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def choose_prompt(options): def choose_prompt(options, purpose: str = None):
if not options: if not options:
raise EsphomeError( raise EsphomeError(
"Found no valid options for upload/logging, please make sure relevant " "Found no valid options for upload/logging, please make sure relevant "
@ -53,7 +56,9 @@ def choose_prompt(options):
if len(options) == 1: if len(options) == 1:
return options[0][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): for i, (desc, _) in enumerate(options):
safe_print(f" [{i+1}] {desc}") safe_print(f" [{i+1}] {desc}")
@ -72,7 +77,9 @@ def choose_prompt(options):
return options[opt - 1][1] 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 = [] options = []
for port in get_serial_ports(): for port in get_serial_ports():
options.append((f"{port.path} ({port.description})", port.path)) 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)) options.append((f"Over The Air ({CORE.address})", CORE.address))
if default == "OTA": if default == "OTA":
return CORE.address 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")) options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
if default == "OTA": if default == "OTA":
return "MQTT" return "MQTT"
@ -88,7 +95,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api
return default return default
if check_default is not None and check_default in [opt[1] for opt in options]: if check_default is not None and check_default in [opt[1] for opt in options]:
return check_default return check_default
return choose_prompt(options) return choose_prompt(options, purpose=purpose)
def get_port_type(port): def get_port_type(port):
@ -288,19 +295,30 @@ def upload_program(config, args, host):
return 1 # Unknown target platform return 1 # Unknown target platform
from esphome import espota2
if CONF_OTA not in config: if CONF_OTA not in config:
raise EsphomeError( raise EsphomeError(
"Cannot upload Over the Air as the config does not include the ota: " "Cannot upload Over the Air as the config does not include the ota: "
"component" "component"
) )
from esphome import espota2
ota_conf = config[CONF_OTA] ota_conf = config[CONF_OTA]
remote_port = ota_conf[CONF_PORT] remote_port = ota_conf[CONF_PORT]
password = ota_conf.get(CONF_PASSWORD, "") 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: 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, args.file)
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) 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": if get_port_type(port) == "SERIAL":
return run_miniterm(config, port) return run_miniterm(config, port)
if get_port_type(port) == "NETWORK" and "api" in config: 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 from esphome.components.api.client import run_logs
return run_logs(config, port) return run_logs(config, port)
@ -374,6 +399,7 @@ def command_upload(args, config):
show_ota=True, show_ota=True,
show_mqtt=False, show_mqtt=False,
show_api=False, show_api=False,
purpose="uploading",
) )
exit_code = upload_program(config, args, port) exit_code = upload_program(config, args, port)
if exit_code != 0: if exit_code != 0:
@ -382,6 +408,15 @@ def command_upload(args, config):
return 0 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): def command_logs(args, config):
port = choose_upload_log_host( port = choose_upload_log_host(
default=args.device, default=args.device,
@ -389,6 +424,7 @@ def command_logs(args, config):
show_ota=False, show_ota=False,
show_mqtt=True, show_mqtt=True,
show_api=True, show_api=True,
purpose="logging",
) )
return show_logs(config, args, port) return show_logs(config, args, port)
@ -407,6 +443,7 @@ def command_run(args, config):
show_ota=True, show_ota=True,
show_mqtt=False, show_mqtt=False,
show_api=True, show_api=True,
purpose="uploading",
) )
exit_code = upload_program(config, args, port) exit_code = upload_program(config, args, port)
if exit_code != 0: if exit_code != 0:
@ -420,6 +457,7 @@ def command_run(args, config):
show_ota=False, show_ota=False,
show_mqtt=True, show_mqtt=True,
show_api=True, show_api=True,
purpose="logging",
) )
return show_logs(config, args, port) return show_logs(config, args, port)
@ -623,6 +661,7 @@ POST_CONFIG_ACTIONS = {
"clean": command_clean, "clean": command_clean,
"idedata": command_idedata, "idedata": command_idedata,
"rename": command_rename, "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.", 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( parser_run = subparsers.add_parser(
"run", "run",
help="Validate the configuration, create a binary, upload it, and start logs.", help="Validate the configuration, create a binary, upload it, and start logs.",
@ -932,6 +980,8 @@ def run_esphome(argv):
_LOGGER.error(e, exc_info=args.verbose) _LOGGER.error(e, exc_info=args.verbose)
return 1 return 1
safe_print(f"ESPHome {const.__version__}")
for conf_path in args.configuration: for conf_path in args.configuration:
if any(os.path.basename(conf_path) == x for x in SECRETS_FILES): if any(os.path.basename(conf_path) == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path) _LOGGER.warning("Skipping secrets file %s", conf_path)

View file

@ -3,6 +3,7 @@ import logging
from esphome import core from esphome import core
from esphome.components import display, font from esphome.components import display, font
import esphome.components.image as espImage import esphome.components.image as espImage
from esphome.components.image import CONF_USE_TRANSPARENCY
import esphome.config_validation as cv import esphome.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome.const import 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_RESIZE, CONF_TYPE
@ -15,16 +16,42 @@ MULTI_CONF = True
Animation_ = display.display_ns.class_("Animation", espImage.Image_) 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( ANIMATION_SCHEMA = cv.Schema(
{ cv.All(
cv.Required(CONF_ID): cv.declare_id(Animation_), {
cv.Required(CONF_FILE): cv.file_, cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Optional(CONF_RESIZE): cv.dimensions, cv.Required(CONF_FILE): cv.file_,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( cv.Optional(CONF_RESIZE): cv.dimensions,
espImage.IMAGE_TYPE, upper=True cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
), espImage.IMAGE_TYPE, upper=True
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), ),
} # 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.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
},
validate_cross_dependencies,
)
) )
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
@ -50,16 +77,19 @@ async def to_code(config):
else: else:
if width > 500 or height > 500: if width > 500 or height > 500:
_LOGGER.warning( _LOGGER.warning(
"The image you requested is very big. Please consider using" 'The image "%s" you requested is very big. Please consider'
" the resize parameter." " using the resize parameter.",
path,
) )
transparent = config[CONF_USE_TRANSPARENCY]
if config[CONF_TYPE] == "GRAYSCALE": if config[CONF_TYPE] == "GRAYSCALE":
data = [0 for _ in range(height * width * frames)] data = [0 for _ in range(height * width * frames)]
pos = 0 pos = 0
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
frame = image.convert("L", dither=Image.NONE) frame = image.convert("LA", dither=Image.NONE)
if CONF_RESIZE in config: if CONF_RESIZE in config:
frame = frame.resize([width, height]) frame = frame.resize([width, height])
pixels = list(frame.getdata()) pixels = list(frame.getdata())
@ -67,16 +97,22 @@ async def to_code(config):
raise core.EsphomeError( raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" 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 data[pos] = pix
pos += 1 pos += 1
elif config[CONF_TYPE] == "RGB24": elif config[CONF_TYPE] == "RGBA":
data = [0 for _ in range(height * width * 3 * frames)] data = [0 for _ in range(height * width * 4 * frames)]
pos = 0 pos = 0
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
frame = image.convert("RGB") frame = image.convert("RGBA")
if CONF_RESIZE in config: if CONF_RESIZE in config:
frame = frame.resize([width, height]) frame = frame.resize([width, height])
pixels = list(frame.getdata()) pixels = list(frame.getdata())
@ -91,13 +127,15 @@ async def to_code(config):
pos += 1 pos += 1
data[pos] = pix[2] data[pos] = pix[2]
pos += 1 pos += 1
data[pos] = pix[3]
pos += 1
elif config[CONF_TYPE] == "RGB565": elif config[CONF_TYPE] == "RGB24":
data = [0 for _ in range(height * width * 2 * frames)] data = [0 for _ in range(height * width * 3 * frames)]
pos = 0 pos = 0
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
frame = image.convert("RGB") frame = image.convert("RGBA")
if CONF_RESIZE in config: if CONF_RESIZE in config:
frame = frame.resize([width, height]) frame = frame.resize([width, height])
pixels = list(frame.getdata()) pixels = list(frame.getdata())
@ -105,14 +143,50 @@ async def to_code(config):
raise core.EsphomeError( raise core.EsphomeError(
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
) )
for pix in pixels: for r, g, b, a in pixels:
R = pix[0] >> 3 if transparent:
G = pix[1] >> 2 if r == 0 and g == 0 and b == 1:
B = pix[2] >> 3 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 rgb = (R << 11) | (G << 5) | B
if transparent:
if rgb == 0x0020:
rgb = 0
if a < 0x80:
rgb = 0x0020
data[pos] = rgb >> 8 data[pos] = rgb >> 8
pos += 1 pos += 1
data[pos] = rgb & 255 data[pos] = rgb & 0xFF
pos += 1 pos += 1
elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
@ -120,19 +194,31 @@ async def to_code(config):
data = [0 for _ in range((height * width8 // 8) * frames)] data = [0 for _ in range((height * width8 // 8) * frames)]
for frameIndex in range(frames): for frameIndex in range(frames):
image.seek(frameIndex) image.seek(frameIndex)
if transparent:
alpha = image.split()[-1]
has_alpha = alpha.getextrema()[0] < 0xFF
frame = image.convert("1", dither=Image.NONE) frame = image.convert("1", dither=Image.NONE)
if CONF_RESIZE in config: if CONF_RESIZE in config:
frame = frame.resize([width, height]) frame = frame.resize([width, height])
for y in range(height): if transparent:
for x in range(width): alpha = alpha.resize([width, height])
if frame.getpixel((x, y)): 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 continue
pos = x + y * width8 + (height * width8 * frameIndex) elif frame.getpixel((x, y)):
data[pos // 8] |= 0x80 >> (pos % 8) 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] rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.new_Pvariable( var = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
prog_arr, prog_arr,
width, width,
@ -140,3 +226,4 @@ async def to_code(config):
frames, frames,
espImage.IMAGE_TYPE[config[CONF_TYPE]], espImage.IMAGE_TYPE[config[CONF_TYPE]],
) )
cg.add(var.set_transparency(transparent))

View file

@ -140,8 +140,9 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
/** Stop the cover. /** Stop the cover.
* *
* This is a legacy method and may be removed later, please use `.make_call()` instead. * This is a legacy method and may be removed later, please use `.make_call()` instead.
* As per solution from issue #2885 the call should include perform()
*/ */
ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9") ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
void stop(); void stop();
void add_on_state_callback(std::function<void()> &&f); void add_on_state_callback(std::function<void()> &&f);

View file

@ -12,7 +12,7 @@ namespace display {
static const char *const TAG = "display"; static const char *const TAG = "display";
const Color COLOR_OFF(0, 0, 0, 0); const Color COLOR_OFF(0, 0, 0, 255);
const Color COLOR_ON(255, 255, 255, 255); const Color COLOR_ON(255, 255, 255, 255);
void Rect::expand(int16_t horizontal, int16_t vertical) { void Rect::expand(int16_t horizontal, int16_t vertical) {
@ -307,40 +307,58 @@ void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign al
} }
void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) {
bool transparent = image->has_transparency();
switch (image->get_type()) { switch (image->get_type()) {
case IMAGE_TYPE_BINARY: case IMAGE_TYPE_BINARY: {
for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) { 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); if (image->get_pixel(img_x, img_y)) {
this->draw_pixel_at(x + img_x, y + img_y, color_on);
} else if (!transparent) {
this->draw_pixel_at(x + img_x, y + img_y, color_off);
}
} }
} }
break; break;
}
case IMAGE_TYPE_GRAYSCALE: case IMAGE_TYPE_GRAYSCALE:
for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) { 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)); auto color = image->get_grayscale_pixel(img_x, img_y);
} if (color.w >= 0x80) {
} this->draw_pixel_at(x + img_x, y + img_y, color);
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; break;
case IMAGE_TYPE_RGB565: case IMAGE_TYPE_RGB565:
for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) { 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)); auto color = image->get_rgb565_pixel(img_x, img_y);
if (color.w >= 0x80) {
this->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
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++) {
auto color = image->get_color_pixel(img_x, img_y);
if (color.w >= 0x80) {
this->draw_pixel_at(x + img_x, y + img_y, color);
}
}
}
break;
case IMAGE_TYPE_RGBA:
for (int img_x = 0; img_x < image->get_width(); img_x++) {
for (int img_y = 0; img_y < image->get_height(); img_y++) {
auto color = image->get_rgba_pixel(img_x, img_y);
if (color.w >= 0x80) {
this->draw_pixel_at(x + img_x, y + img_y, color);
}
} }
} }
break; break;
@ -629,14 +647,27 @@ bool Image::get_pixel(int x, int y) const {
const uint32_t pos = x + y * width_8; const uint32_t pos = x + y * width_8;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
} }
Color Image::get_rgba_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_) * 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_color_pixel(int x, int y) const { Color Image::get_color_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK; return Color::BLACK;
const uint32_t pos = (x + y * this->width_) * 3; const uint32_t pos = (x + y * this->width_) * 3;
const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | 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 + 1) << 8) | progmem_read_byte(this->data_start_ + pos + 2));
(progmem_read_byte(this->data_start_ + pos + 0) << 16); if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
return Color(color32); // (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 { Color Image::get_rgb565_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
@ -647,14 +678,22 @@ Color Image::get_rgb565_pixel(int x, int y) const {
auto r = (rgb565 & 0xF800) >> 11; auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5; auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F; 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 { Color Image::get_grayscale_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK; return Color::BLACK;
const uint32_t pos = (x + y * this->width_); const uint32_t pos = (x + y * this->width_);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos); 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_width() const { return this->width_; }
int Image::get_height() const { return this->height_; } int Image::get_height() const { return this->height_; }
@ -673,6 +712,16 @@ bool Animation::get_pixel(int x, int y) const {
const uint32_t pos = x + y * width_8 + frame_index; const uint32_t pos = x + y * width_8 + frame_index;
return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
} }
Color Animation::get_rgba_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) * 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 Animation::get_color_pixel(int x, int y) const { Color Animation::get_color_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return Color::BLACK; return Color::BLACK;
@ -680,10 +729,16 @@ Color Animation::get_color_pixel(int x, int y) const {
if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_))
return Color::BLACK; return Color::BLACK;
const uint32_t pos = (x + y * this->width_ + frame_index) * 3; const uint32_t pos = (x + y * this->width_ + frame_index) * 3;
const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | 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 + 1) << 8) | progmem_read_byte(this->data_start_ + pos + 2));
(progmem_read_byte(this->data_start_ + pos + 0) << 16); if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) {
return Color(color32); // (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 Animation::get_rgb565_pixel(int x, int y) const { Color Animation::get_rgb565_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
@ -697,7 +752,14 @@ Color Animation::get_rgb565_pixel(int x, int y) const {
auto r = (rgb565 & 0xF800) >> 11; auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5; auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F; 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 Animation::get_grayscale_pixel(int x, int y) const { Color Animation::get_grayscale_pixel(int x, int y) const {
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
@ -707,7 +769,8 @@ Color Animation::get_grayscale_pixel(int x, int y) const {
return Color::BLACK; return Color::BLACK;
const uint32_t pos = (x + y * this->width_ + frame_index); const uint32_t pos = (x + y * this->width_ + frame_index);
const uint8_t gray = progmem_read_byte(this->data_start_ + pos); 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);
} }
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
: Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {} : Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {}

View file

@ -82,8 +82,8 @@ enum ImageType {
IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_BINARY = 0,
IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_GRAYSCALE = 1,
IMAGE_TYPE_RGB24 = 2, IMAGE_TYPE_RGB24 = 2,
IMAGE_TYPE_TRANSPARENT_BINARY = 3, IMAGE_TYPE_RGB565 = 3,
IMAGE_TYPE_RGB565 = 4, IMAGE_TYPE_RGBA = 4,
}; };
enum DisplayType { enum DisplayType {
@ -540,6 +540,7 @@ class Image {
Image(const uint8_t *data_start, int width, int height, ImageType type); Image(const uint8_t *data_start, int width, int height, ImageType type);
virtual bool get_pixel(int x, int y) const; virtual bool get_pixel(int x, int y) const;
virtual Color get_color_pixel(int x, int y) const; virtual Color get_color_pixel(int x, int y) const;
virtual Color get_rgba_pixel(int x, int y) const;
virtual Color get_rgb565_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; virtual Color get_grayscale_pixel(int x, int y) const;
int get_width() const; int get_width() const;
@ -548,11 +549,15 @@ class Image {
virtual int get_current_frame() const; virtual int get_current_frame() const;
void set_transparency(bool transparent) { transparent_ = transparent; }
bool has_transparency() const { return transparent_; }
protected: protected:
int width_; int width_;
int height_; int height_;
ImageType type_; ImageType type_;
const uint8_t *data_start_; const uint8_t *data_start_;
bool transparent_;
}; };
class Animation : public Image { class Animation : public Image {
@ -560,6 +565,7 @@ class Animation : public Image {
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); 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; bool get_pixel(int x, int y) const override;
Color get_color_pixel(int x, int y) const override; Color get_color_pixel(int x, int y) const override;
Color get_rgba_pixel(int x, int y) const override;
Color get_rgb565_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; Color get_grayscale_pixel(int x, int y) const override;

View file

@ -4,6 +4,7 @@ from esphome.components.light.types import AddressableLightEffect
from esphome.components.light.effects import register_addressable_effect from esphome.components.light.effects import register_addressable_effect
from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS
AUTO_LOAD = ["socket"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
e131_ns = cg.esphome_ns.namespace("e131") e131_ns = cg.esphome_ns.namespace("e131")
@ -23,16 +24,11 @@ CHANNELS = {
CONF_UNIVERSE = "universe" CONF_UNIVERSE = "universe"
CONF_E131_ID = "e131_id" CONF_E131_ID = "e131_id"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.Schema(
cv.Schema( {
{ cv.GenerateID(): cv.declare_id(E131Component),
cv.GenerateID(): cv.declare_id(E131Component), cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True),
cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of( }
*METHODS, upper=True
),
}
),
cv.only_with_arduino,
) )

View file

@ -1,18 +1,7 @@
#ifdef USE_ARDUINO
#include "e131.h" #include "e131.h"
#include "e131_addressable_light_effect.h" #include "e131_addressable_light_effect.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP32
#include <WiFi.h>
#endif
#ifdef USE_ESP8266
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#endif
namespace esphome { namespace esphome {
namespace e131 { namespace e131 {
@ -22,17 +11,41 @@ static const int PORT = 5568;
E131Component::E131Component() {} E131Component::E131Component() {}
E131Component::~E131Component() { E131Component::~E131Component() {
if (udp_) { if (this->socket_) {
udp_->stop(); this->socket_->close();
} }
} }
void E131Component::setup() { void E131Component::setup() {
udp_ = make_unique<WiFiUDP>(); this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
if (!udp_->begin(PORT)) { int enable = 1;
ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
mark_failed(); 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; return;
} }
@ -43,22 +56,22 @@ void E131Component::loop() {
std::vector<uint8_t> payload; std::vector<uint8_t> payload;
E131Packet packet; E131Packet packet;
int universe = 0; int universe = 0;
uint8_t buf[1460];
while (uint16_t packet_size = udp_->parsePacket()) { ssize_t len = this->socket_->read(buf, sizeof(buf));
payload.resize(packet_size); if (len == -1) {
return;
}
payload.resize(len);
memmove(&payload[0], buf, len);
if (!udp_->read(&payload[0], payload.size())) { if (!this->packet_(payload, universe, packet)) {
continue; ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
} return;
}
if (!packet_(payload, universe, packet)) { if (!this->process_(universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
continue;
}
if (!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 e131
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View file

@ -1,7 +1,6 @@
#pragma once #pragma once
#ifdef USE_ARDUINO #include "esphome/components/socket/socket.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <map> #include <map>
@ -9,8 +8,6 @@
#include <set> #include <set>
#include <vector> #include <vector>
class UDP;
namespace esphome { namespace esphome {
namespace e131 { namespace e131 {
@ -47,7 +44,7 @@ class E131Component : public esphome::Component {
void leave_(int universe); void leave_(int universe);
E131ListenMethod listen_method_{E131_MULTICAST}; E131ListenMethod listen_method_{E131_MULTICAST};
std::unique_ptr<UDP> udp_; std::unique_ptr<socket::Socket> socket_;
std::set<E131AddressableLightEffect *> light_effects_; std::set<E131AddressableLightEffect *> light_effects_;
std::map<int, int> universe_consumers_; std::map<int, int> universe_consumers_;
std::map<int, E131Packet> universe_packets_; std::map<int, E131Packet> universe_packets_;
@ -55,5 +52,3 @@ class E131Component : public esphome::Component {
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View file

@ -1,7 +1,5 @@
#ifdef USE_ARDUINO
#include "e131.h"
#include "e131_addressable_light_effect.h" #include "e131_addressable_light_effect.h"
#include "e131.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@ -92,5 +90,3 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View file

@ -1,7 +1,5 @@
#pragma once #pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/light/addressable_light_effect.h" #include "esphome/components/light/addressable_light_effect.h"
@ -44,5 +42,3 @@ class E131AddressableLightEffect : public light::AddressableLightEffect {
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View file

@ -1,15 +1,13 @@
#ifdef USE_ARDUINO #include <cstring>
#include "e131.h" #include "e131.h"
#include "esphome/components/network/ip_address.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
#include "esphome/components/network/ip_address.h"
#include <cstring>
#include <lwip/init.h>
#include <lwip/ip_addr.h>
#include <lwip/ip4_addr.h>
#include <lwip/igmp.h> #include <lwip/igmp.h>
#include <lwip/init.h>
#include <lwip/ip4_addr.h>
#include <lwip/ip_addr.h>
namespace esphome { namespace esphome {
namespace e131 { namespace e131 {
@ -62,7 +60,7 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *)
bool E131Component::join_igmp_groups_() { bool E131Component::join_igmp_groups_() {
if (listen_method_ != E131_MULTICAST) if (listen_method_ != E131_MULTICAST)
return false; return false;
if (!udp_) if (this->socket_ == nullptr)
return false; return false;
for (auto universe : universe_consumers_) { for (auto universe : universe_consumers_) {
@ -140,5 +138,3 @@ bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E13
} // namespace e131 } // namespace e131
} // namespace esphome } // namespace esphome
#endif // USE_ARDUINO

View file

@ -34,7 +34,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
light::LightTraits get_traits() override { light::LightTraits get_traits() override {
auto traits = light::LightTraits(); auto traits = light::LightTraits();
if (this->is_rgbw_) { if (this->is_rgbw_) {
traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::RGB_WHITE}); traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else { } else {
traits.set_supported_color_modes({light::ColorMode::RGB}); traits.set_supported_color_modes({light::ColorMode::RGB});
} }

View file

@ -16,6 +16,7 @@ from esphome.const import (
) )
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@freekode"]
hm3301_ns = cg.esphome_ns.namespace("hm3301") hm3301_ns = cg.esphome_ns.namespace("hm3301")
HM3301Component = hm3301_ns.class_( HM3301Component = hm3301_ns.class_(

View file

@ -7,6 +7,30 @@ namespace i2c {
static const char *const TAG = "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<uint8_t *>(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) { 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 // we have to copy in order to be able to change byte order
std::unique_ptr<uint16_t[]> temp{new uint16_t[len]}; std::unique_ptr<uint16_t[]> temp{new uint16_t[len]};

View file

@ -46,22 +46,10 @@ class I2CDevice {
I2CRegister reg(uint8_t a_register) { return {this, a_register}; } 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(uint8_t *data, size_t len) { return bus_->read(address_, data, len); }
ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) { ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true);
ErrorCode err = this->write(&a_register, 1, stop);
if (err != ERROR_OK)
return err;
return this->read(data, len);
}
ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); }
ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true) { ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true);
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);
}
// Compat APIs // Compat APIs
@ -85,13 +73,7 @@ class I2CDevice {
return res; return res;
} }
bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len);
if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK)
return false;
for (size_t i = 0; i < len; i++)
data[i] = i2ctohs(data[i]);
return true;
}
bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) {
return read_register(a_register, data, 1, stop) == ERROR_OK; return read_register(a_register, data, 1, stop) == ERROR_OK;

View file

@ -18,6 +18,7 @@ MULTI_CONF = True
CONF_I2S_DOUT_PIN = "i2s_dout_pin" CONF_I2S_DOUT_PIN = "i2s_dout_pin"
CONF_I2S_DIN_PIN = "i2s_din_pin" CONF_I2S_DIN_PIN = "i2s_din_pin"
CONF_I2S_MCLK_PIN = "i2s_mclk_pin"
CONF_I2S_BCLK_PIN = "i2s_bclk_pin" CONF_I2S_BCLK_PIN = "i2s_bclk_pin"
CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
@ -44,6 +45,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(): cv.declare_id(I2SAudioComponent), cv.GenerateID(): cv.declare_id(I2SAudioComponent),
cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, 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_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])) cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
if CONF_I2S_BCLK_PIN in config: if CONF_I2S_BCLK_PIN in config:
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) 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]))

View file

@ -21,7 +21,7 @@ class I2SAudioComponent : public Component {
i2s_pin_config_t get_pin_config() const { i2s_pin_config_t get_pin_config() const {
return { return {
.mck_io_num = I2S_PIN_NO_CHANGE, .mck_io_num = this->mclk_pin_,
.bck_io_num = this->bclk_pin_, .bck_io_num = this->bclk_pin_,
.ws_io_num = this->lrclk_pin_, .ws_io_num = this->lrclk_pin_,
.data_out_num = I2S_PIN_NO_CHANGE, .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_bclk_pin(int pin) { this->bclk_pin_ = pin; }
void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; }
@ -44,6 +45,7 @@ class I2SAudioComponent : public Component {
I2SAudioIn *audio_in_{nullptr}; I2SAudioIn *audio_in_{nullptr};
I2SAudioOut *audio_out_{nullptr}; I2SAudioOut *audio_out_{nullptr};
int mclk_pin_{I2S_PIN_NO_CHANGE};
int bclk_pin_{I2S_PIN_NO_CHANGE}; int bclk_pin_{I2S_PIN_NO_CHANGE};
int lrclk_pin_; int lrclk_pin_;
i2s_port_t port_{}; i2s_port_t port_{};

View file

@ -133,7 +133,7 @@ void I2SAudioMediaPlayer::play_() {
void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; } void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; }
void I2SAudioMediaPlayer::start_() { void I2SAudioMediaPlayer::start_() {
if (this->parent_->try_lock()) { if (!this->parent_->try_lock()) {
return; // Waiting for another i2s to return lock return; // Waiting for another i2s to return lock
} }
@ -156,6 +156,7 @@ void I2SAudioMediaPlayer::start_() {
#if SOC_I2S_SUPPORTS_DAC #if SOC_I2S_SUPPORTS_DAC
} }
#endif #endif
this->i2s_state_ = I2S_STATE_RUNNING; this->i2s_state_ = I2S_STATE_RUNNING;
this->high_freq_.start(); this->high_freq_.start();
this->audio_->setVolume(remap<uint8_t, float>(this->volume, 0.0f, 1.0f, 0, 21)); this->audio_->setVolume(remap<uint8_t, float>(this->volume, 0.0f, 1.0f, 0, 21));
@ -218,6 +219,12 @@ void I2SAudioMediaPlayer::dump_config() {
default: default:
break; break;
} }
} else {
#endif
ESP_LOGCONFIG(TAG, " External DAC channels: %d", this->external_dac_channels_);
ESP_LOGCONFIG(TAG, " I2S DOUT Pin: %d", this->dout_pin_);
LOG_PIN(" Mute Pin: ", this->mute_pin_);
#if SOC_I2S_SUPPORTS_DAC
} }
#endif #endif
} }

View file

@ -2,7 +2,7 @@ import esphome.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome import pins from esphome import pins
from esphome.const import CONF_ID, CONF_NUMBER from esphome.const import CONF_CHANNEL, CONF_ID, CONF_NUMBER
from esphome.components import microphone, esp32 from esphome.components import microphone, esp32
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
@ -25,6 +25,12 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
) )
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
CHANNELS = {
"left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT,
"right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT,
}
INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32]
PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3]
@ -47,6 +53,7 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), cv.GenerateID(): cv.declare_id(I2SAudioMicrophone),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@ -86,4 +93,6 @@ async def to_code(config):
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
cg.add(var.set_pdm(config[CONF_PDM])) cg.add(var.set_pdm(config[CONF_PDM]))
cg.add(var.set_channel(CHANNELS[config[CONF_CHANNEL]]))
await microphone.register_microphone(var, config) await microphone.register_microphone(var, config)

View file

@ -49,7 +49,7 @@ void I2SAudioMicrophone::start_() {
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = 16000, .sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, .channel_format = this->channel_,
.communication_format = I2S_COMM_FORMAT_STAND_I2S, .communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4, .dma_buf_count = 4,

View file

@ -28,6 +28,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
} }
#endif #endif
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
protected: protected:
void start_(); void start_();
void stop_(); void stop_();
@ -40,6 +42,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
#endif #endif
bool pdm_{false}; bool pdm_{false};
std::vector<uint8_t> buffer_; std::vector<uint8_t> buffer_;
i2s_channel_fmt_t channel_;
HighFrequencyLoopRequester high_freq_; HighFrequencyLoopRequester high_freq_;
}; };

View file

@ -116,8 +116,8 @@ static const uint8_t PROGMEM INITCMD_ILI9486[] = {
}; };
static const uint8_t PROGMEM INITCMD_ILI9488[] = { 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_GMCTRP1,15, 0x0f, 0x24, 0x1c, 0x0a, 0x0f, 0x08, 0x43, 0x88, 0x32, 0x0f, 0x10, 0x06, 0x0f, 0x07, 0x00,
ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, 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_PWCTR1, 2, 0x17, 0x15, // VRH1 VRH2
ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL ILI9XXX_PWCTR2, 1, 0x41, // VGH, VGL

View file

@ -22,26 +22,55 @@ MULTI_CONF = True
ImageType = display.display_ns.enum("ImageType") ImageType = display.display_ns.enum("ImageType")
IMAGE_TYPE = { IMAGE_TYPE = {
"BINARY": ImageType.IMAGE_TYPE_BINARY, "BINARY": ImageType.IMAGE_TYPE_BINARY,
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY,
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
"RGB24": ImageType.IMAGE_TYPE_RGB24,
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
"RGB565": ImageType.IMAGE_TYPE_RGB565, "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"
Image_ = display.display_ns.class_("Image") Image_ = display.display_ns.class_("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
IMAGE_SCHEMA = cv.Schema( IMAGE_SCHEMA = cv.Schema(
{ cv.All(
cv.Required(CONF_ID): cv.declare_id(Image_), {
cv.Required(CONF_FILE): cv.file_, cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Optional(CONF_RESIZE): cv.dimensions, cv.Required(CONF_FILE): cv.file_,
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
"NONE", "FLOYDSTEINBERG", 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.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), 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) CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA)
@ -64,72 +93,113 @@ async def to_code(config):
else: else:
if width > 500 or height > 500: if width > 500 or height > 500:
_LOGGER.warning( _LOGGER.warning(
"The image you requested is very big. Please consider using" 'The image "%s" you requested is very big. Please consider'
" the resize parameter." " using the resize parameter.",
path,
) )
transparent = config[CONF_USE_TRANSPARENCY]
dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG
if config[CONF_TYPE] == "GRAYSCALE": if config[CONF_TYPE] == "GRAYSCALE":
image = image.convert("L", dither=dither) image = image.convert("LA", dither=dither)
pixels = list(image.getdata()) pixels = list(image.getdata())
data = [0 for _ in range(height * width)] data = [0 for _ in range(height * width)]
pos = 0 pos = 0
for pix in pixels: for g, a in pixels:
data[pos] = pix 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 pos += 1
elif config[CONF_TYPE] == "RGB24": elif config[CONF_TYPE] == "RGB24":
image = image.convert("RGB") image = image.convert("RGBA")
pixels = list(image.getdata()) pixels = list(image.getdata())
data = [0 for _ in range(height * width * 3)] data = [0 for _ in range(height * width * 3)]
pos = 0 pos = 0
for pix in pixels: for r, g, b, a in pixels:
data[pos] = pix[0] 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 pos += 1
data[pos] = pix[1] data[pos] = g
pos += 1 pos += 1
data[pos] = pix[2] data[pos] = b
pos += 1 pos += 1
elif config[CONF_TYPE] == "RGB565": elif config[CONF_TYPE] in ["RGB565"]:
image = image.convert("RGB") image = image.convert("RGBA")
pixels = list(image.getdata()) pixels = list(image.getdata())
data = [0 for _ in range(height * width * 3)] data = [0 for _ in range(height * width * 2)]
pos = 0 pos = 0
for pix in pixels: for r, g, b, a in pixels:
R = pix[0] >> 3 R = r >> 3
G = pix[1] >> 2 G = g >> 2
B = pix[2] >> 3 B = b >> 3
rgb = (R << 11) | (G << 5) | B rgb = (R << 11) | (G << 5) | B
if transparent:
if rgb == 0x0020:
rgb = 0
if a < 0x80:
rgb = 0x0020
data[pos] = rgb >> 8 data[pos] = rgb >> 8
pos += 1 pos += 1
data[pos] = rgb & 255 data[pos] = rgb & 0xFF
pos += 1 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) image = image.convert("1", dither=dither)
width8 = ((width + 7) // 8) * 8 width8 = ((width + 7) // 8) * 8
data = [0 for _ in range(height * width8 // 8)] data = [0 for _ in range(height * width8 // 8)]
for y in range(height): for y in range(height):
for x in range(width): for x in range(width):
if image.getpixel((x, y)): if transparent and has_alpha:
continue a = alpha.getpixel((x, y))
pos = x + y * width8 if not a:
data[pos // 8] |= 0x80 >> (pos % 8) continue
elif image.getpixel((x, y)):
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]:
continue continue
pos = x + y * width8 pos = x + y * width8
data[pos // 8] |= 0x80 >> (pos % 8) 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] rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) 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]] config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]]
) )
cg.add(var.set_transparency(transparent))

View file

@ -33,6 +33,10 @@ void InternalTemperatureSensor::update() {
temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT();
temp_sensor_set_config(tsens); temp_sensor_set_config(tsens);
temp_sensor_start(); temp_sensor_start();
#if defined(USE_ESP32_VARIANT_ESP32S3) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 3))
#error \
"ESP32-S3 internal temperature sensor requires ESP IDF V4.4.3 or higher. See https://github.com/esphome/issues/issues/4271"
#endif
esp_err_t result = temp_sensor_read_celsius(&temperature); esp_err_t result = temp_sensor_read_celsius(&temperature);
temp_sensor_stop(); temp_sensor_stop();
success = (result == ESP_OK); success = (result == ESP_OK);

View file

@ -1,18 +1,45 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import sensor from esphome.components import sensor
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32S3,
)
from esphome.const import ( from esphome.const import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS, UNIT_CELSIUS,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_DIAGNOSTIC,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
) )
from esphome.core import CORE
internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature") internal_temperature_ns = cg.esphome_ns.namespace("internal_temperature")
InternalTemperatureSensor = internal_temperature_ns.class_( InternalTemperatureSensor = internal_temperature_ns.class_(
"InternalTemperatureSensor", sensor.Sensor, cg.PollingComponent "InternalTemperatureSensor", sensor.Sensor, cg.PollingComponent
) )
def validate_config(config):
if CORE.is_esp32:
variant = get_esp32_variant()
if variant == VARIANT_ESP32S3:
if CORE.using_arduino and CORE.data[KEY_CORE][
KEY_FRAMEWORK_VERSION
] < cv.Version(2, 0, 6):
raise cv.Invalid(
"ESP32-S3 Internal Temperature Sensor requires framework version 2.0.6 or higher. See <https://github.com/esphome/issues/issues/4271>."
)
if CORE.using_esp_idf and CORE.data[KEY_CORE][
KEY_FRAMEWORK_VERSION
] < cv.Version(4, 4, 3):
raise cv.Invalid(
"ESP32-S3 Internal Temperature Sensor requires framework version 4.4.3 or higher. See <https://github.com/esphome/issues/issues/4271>."
)
return config
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
sensor.sensor_schema( sensor.sensor_schema(
InternalTemperatureSensor, InternalTemperatureSensor,
@ -23,6 +50,7 @@ CONFIG_SCHEMA = cv.All(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
).extend(cv.polling_component_schema("60s")), ).extend(cv.polling_component_schema("60s")),
cv.only_on(["esp32", "rp2040"]), cv.only_on(["esp32", "rp2040"]),
validate_config,
) )

View file

@ -6,6 +6,7 @@ from esphome.const import (
CONF_SERVICE, CONF_SERVICE,
KEY_CORE, KEY_CORE,
KEY_FRAMEWORK_VERSION, KEY_FRAMEWORK_VERSION,
CONF_DISABLED,
) )
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
@ -39,7 +40,6 @@ SERVICE_SCHEMA = cv.Schema(
} }
) )
CONF_DISABLED = "disabled"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {

View file

@ -506,12 +506,12 @@ void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueTy
case SensorValueType::U_DWORD: case SensorValueType::U_DWORD:
case SensorValueType::S_DWORD: case SensorValueType::S_DWORD:
case SensorValueType::FP32: case SensorValueType::FP32:
case SensorValueType::FP32_R:
data.push_back((value & 0xFFFF0000) >> 16); data.push_back((value & 0xFFFF0000) >> 16);
data.push_back(value & 0xFFFF); data.push_back(value & 0xFFFF);
break; break;
case SensorValueType::U_DWORD_R: case SensorValueType::U_DWORD_R:
case SensorValueType::S_DWORD_R: case SensorValueType::S_DWORD_R:
case SensorValueType::FP32_R:
data.push_back(value & 0xFFFF); data.push_back(value & 0xFFFF);
data.push_back((value & 0xFFFF0000) >> 16); data.push_back((value & 0xFFFF0000) >> 16);
break; break;

View file

@ -44,6 +44,9 @@ CONF_SUPPORTED_TANKS_MAP = {
"20LB_V": (38, 254), # empty/full readings for 20lb US tank "20LB_V": (38, 254), # empty/full readings for 20lb US tank
"30LB_V": (38, 381), "30LB_V": (38, 381),
"40LB_V": (38, 508), "40LB_V": (38, 508),
"EUROPE_6KG": (38, 336),
"EUROPE_11KG": (38, 366),
"EUROPE_14KG": (38, 467),
} }
CODEOWNERS = ["@spbrogan"] CODEOWNERS = ["@spbrogan"]

View file

@ -7,6 +7,7 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/version.h"
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
#endif #endif
@ -14,6 +15,13 @@
#include "lwip/err.h" #include "lwip/err.h"
#include "mqtt_component.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 esphome {
namespace mqtt { namespace mqtt {
@ -58,9 +66,63 @@ void MQTTClientComponent::setup() {
} }
#endif #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->last_connected_ = millis();
this->start_dnslookup_(); 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() { void MQTTClientComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT:"); ESP_LOGCONFIG(TAG, "MQTT:");
ESP_LOGCONFIG(TAG, " Server Address: %s:%u (%s)", this->credentials_.address.c_str(), this->credentials_.port, 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 delay(100); // NOLINT
this->resubscribe_subscriptions_(); this->resubscribe_subscriptions_();
this->send_device_info_();
for (MQTTComponent *component : this->children_) for (MQTTComponent *component : this->children_)
component->schedule_resend_state(); component->schedule_resend_state();

View file

@ -251,6 +251,8 @@ class MQTTClientComponent : public Component {
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback); void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
protected: protected:
void send_device_info_();
/// Reconnect to the MQTT broker if not already connected. /// Reconnect to the MQTT broker if not already connected.
void start_connect_(); void start_connect_();
void start_dnslookup_(); void start_dnslookup_();

View file

@ -21,7 +21,7 @@ void PsramComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available));
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0)
if (available) { if (available) {
ESP_LOGCONFIG(TAG, " Size: %d MB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024 / 1024); ESP_LOGCONFIG(TAG, " Size: %d KB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024);
} }
#endif #endif
} }

View file

@ -1,4 +1,7 @@
import logging import logging
import os
from string import ascii_letters, digits
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
@ -12,9 +15,11 @@ from esphome.const import (
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, 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 # force import gpio to register pin schema
from .gpio import rp2040_pin_to_code # noqa 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_BOARD] = config[CONF_BOARD]
CORE.data[KEY_RP2040][KEY_PIO_FILES] = {}
return config return config
@ -148,7 +155,10 @@ async def to_code(config):
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option( cg.add_platformio_option(
"platform_packages", "platform_packages",
[f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}"], [
f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}",
"earlephilhower/tool-pioasm-rp2040-earlephilhower",
],
) )
cg.add_platformio_option("board_build.core", "earlephilhower") cg.add_platformio_option("board_build.core", "earlephilhower")
@ -159,3 +169,53 @@ async def to_code(config):
"USE_ARDUINO_VERSION_CODE", "USE_ARDUINO_VERSION_CODE",
cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"), 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()

View file

@ -2,5 +2,6 @@ import esphome.codegen as cg
KEY_BOARD = "board" KEY_BOARD = "board"
KEY_RP2040 = "rp2040" KEY_RP2040 = "rp2040"
KEY_PIO_FILES = "pio_files"
rp2040_ns = cg.esphome_ns.namespace("rp2040") rp2040_ns = cg.esphome_ns.namespace("rp2040")

View file

@ -0,0 +1 @@
CODEOWNERS = ["@Papa-DMan"]

View file

@ -0,0 +1,139 @@
#include "led_strip.h"
#ifdef USE_RP2040
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <hardware/clocks.h>
#include <hardware/pio.h>
#include <pico/stdlib.h>
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<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::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

View file

@ -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 <hardware/pio.h>
#include <hardware/structs/pio.h>
#include <pico/stdio.h>
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

View file

@ -0,0 +1,267 @@
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]),
),
)

View file

@ -599,15 +599,6 @@ async def to_code(config):
) )
cg.add(var.set_controller_auto_adv_switch(sw_aa_var)) cg.add(var.set_controller_auto_adv_switch(sw_aa_var))
if CONF_QUEUE_ENABLE_SWITCH in sprinkler_controller:
sw_qen_var = await switch.new_switch(
sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH]
)
await cg.register_component(
sw_qen_var, sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH]
)
cg.add(var.set_controller_queue_enable_switch(sw_qen_var))
if CONF_REVERSE_SWITCH in sprinkler_controller: if CONF_REVERSE_SWITCH in sprinkler_controller:
sw_rev_var = await switch.new_switch( sw_rev_var = await switch.new_switch(
sprinkler_controller[CONF_REVERSE_SWITCH] sprinkler_controller[CONF_REVERSE_SWITCH]
@ -617,78 +608,83 @@ async def to_code(config):
) )
cg.add(var.set_controller_reverse_switch(sw_rev_var)) cg.add(var.set_controller_reverse_switch(sw_rev_var))
if CONF_STANDBY_SWITCH in sprinkler_controller: if CONF_STANDBY_SWITCH in sprinkler_controller:
sw_stb_var = await switch.new_switch( sw_stb_var = await switch.new_switch(
sprinkler_controller[CONF_STANDBY_SWITCH] sprinkler_controller[CONF_STANDBY_SWITCH]
) )
await cg.register_component( await cg.register_component(
sw_stb_var, sprinkler_controller[CONF_STANDBY_SWITCH] sw_stb_var, sprinkler_controller[CONF_STANDBY_SWITCH]
) )
cg.add(var.set_controller_standby_switch(sw_stb_var)) cg.add(var.set_controller_standby_switch(sw_stb_var))
if CONF_MULTIPLIER_NUMBER in sprinkler_controller: if CONF_QUEUE_ENABLE_SWITCH in sprinkler_controller:
num_mult_var = await number.new_number( sw_qen_var = await switch.new_switch(
sprinkler_controller[CONF_MULTIPLIER_NUMBER], sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH]
min_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][ )
CONF_MIN_VALUE await cg.register_component(
], sw_qen_var, sprinkler_controller[CONF_QUEUE_ENABLE_SWITCH]
max_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][ )
CONF_MAX_VALUE cg.add(var.set_controller_queue_enable_switch(sw_qen_var))
],
step=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_STEP], if CONF_MULTIPLIER_NUMBER in sprinkler_controller:
num_mult_var = await number.new_number(
sprinkler_controller[CONF_MULTIPLIER_NUMBER],
min_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_MIN_VALUE],
max_value=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_MAX_VALUE],
step=sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_STEP],
)
await cg.register_component(
num_mult_var, sprinkler_controller[CONF_MULTIPLIER_NUMBER]
)
cg.add(
num_mult_var.set_initial_value(
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_INITIAL_VALUE]
) )
await cg.register_component( )
num_mult_var, sprinkler_controller[CONF_MULTIPLIER_NUMBER] cg.add(
num_mult_var.set_restore_value(
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_RESTORE_VALUE]
) )
cg.add( )
num_mult_var.set_initial_value(
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_INITIAL_VALUE] if CONF_SET_ACTION in sprinkler_controller[CONF_MULTIPLIER_NUMBER]:
) await automation.build_automation(
) num_mult_var.get_set_trigger(),
cg.add( [(float, "x")],
num_mult_var.set_restore_value( sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_SET_ACTION],
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_RESTORE_VALUE]
)
) )
if CONF_SET_ACTION in sprinkler_controller[CONF_MULTIPLIER_NUMBER]: cg.add(var.set_controller_multiplier_number(num_mult_var))
await automation.build_automation(
num_mult_var.get_set_trigger(),
[(float, "x")],
sprinkler_controller[CONF_MULTIPLIER_NUMBER][CONF_SET_ACTION],
)
cg.add(var.set_controller_multiplier_number(num_mult_var)) if CONF_REPEAT_NUMBER in sprinkler_controller:
num_repeat_var = await number.new_number(
sprinkler_controller[CONF_REPEAT_NUMBER],
min_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MIN_VALUE],
max_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MAX_VALUE],
step=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_STEP],
)
await cg.register_component(
num_repeat_var, sprinkler_controller[CONF_REPEAT_NUMBER]
)
cg.add(
num_repeat_var.set_initial_value(
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_INITIAL_VALUE]
)
)
cg.add(
num_repeat_var.set_restore_value(
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_RESTORE_VALUE]
)
)
if CONF_REPEAT_NUMBER in sprinkler_controller: if CONF_SET_ACTION in sprinkler_controller[CONF_REPEAT_NUMBER]:
num_repeat_var = await number.new_number( await automation.build_automation(
sprinkler_controller[CONF_REPEAT_NUMBER], num_repeat_var.get_set_trigger(),
min_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MIN_VALUE], [(float, "x")],
max_value=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_MAX_VALUE], sprinkler_controller[CONF_REPEAT_NUMBER][CONF_SET_ACTION],
step=sprinkler_controller[CONF_REPEAT_NUMBER][CONF_STEP],
)
await cg.register_component(
num_repeat_var, sprinkler_controller[CONF_REPEAT_NUMBER]
)
cg.add(
num_repeat_var.set_initial_value(
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_INITIAL_VALUE]
)
)
cg.add(
num_repeat_var.set_restore_value(
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_RESTORE_VALUE]
)
) )
if CONF_SET_ACTION in sprinkler_controller[CONF_REPEAT_NUMBER]: cg.add(var.set_controller_repeat_number(num_repeat_var))
await automation.build_automation(
num_repeat_var.get_set_trigger(),
[(float, "x")],
sprinkler_controller[CONF_REPEAT_NUMBER][CONF_SET_ACTION],
)
cg.add(var.set_controller_repeat_number(num_repeat_var))
for valve in sprinkler_controller[CONF_VALVES]: for valve in sprinkler_controller[CONF_VALVES]:
sw_valve_var = await switch.new_switch(valve[CONF_VALVE_SWITCH]) sw_valve_var = await switch.new_switch(valve[CONF_VALVE_SWITCH])

View file

@ -147,22 +147,22 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler
: controller_(controller), valve_(valve) {} : controller_(controller), valve_(valve) {}
void SprinklerValveOperator::loop() { void SprinklerValveOperator::loop() {
if (millis() >= this->pinned_millis_) { // dummy check if (millis() >= this->start_millis_) { // dummy check
switch (this->state_) { switch (this->state_) {
case STARTING: case STARTING:
if (millis() > (this->pinned_millis_ + this->start_delay_)) { if (millis() > (this->start_millis_ + this->start_delay_)) {
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
} }
break; break;
case ACTIVE: case ACTIVE:
if (millis() > (this->pinned_millis_ + this->start_delay_ + this->run_duration_)) { if (millis() > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
} }
break; break;
case STOPPING: case STOPPING:
if (millis() > (this->pinned_millis_ + this->stop_delay_)) { if (millis() > (this->stop_millis_ + this->stop_delay_)) {
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
} }
break; break;
@ -183,11 +183,12 @@ void SprinklerValveOperator::set_controller(Sprinkler *controller) {
void SprinklerValveOperator::set_valve(SprinklerValve *valve) { void SprinklerValveOperator::set_valve(SprinklerValve *valve) {
if (valve != nullptr) { if (valve != nullptr) {
this->state_ = IDLE; // reset state this->state_ = IDLE; // reset state
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
this->pinned_millis_ = 0; // reset because (new) valve has not been started yet this->start_millis_ = 0; // reset because (new) valve has not been started yet
this->kill_(); // ensure everything is off before we let go! this->stop_millis_ = 0; // reset because (new) valve has not been started yet
this->valve_ = valve; // finally, set the pointer to the new valve this->kill_(); // ensure everything is off before we let go!
this->valve_ = valve; // finally, set the pointer to the new valve
} }
} }
@ -221,7 +222,8 @@ void SprinklerValveOperator::start() {
} else { } else {
this->run_(); // there is no start_delay_, so just start the pump and valve this->run_(); // there is no start_delay_, so just start the pump and valve
} }
this->pinned_millis_ = millis(); // save the time the start request was made this->stop_millis_ = 0;
this->start_millis_ = millis(); // save the time the start request was made
} }
void SprinklerValveOperator::stop() { void SprinklerValveOperator::stop() {
@ -238,19 +240,33 @@ void SprinklerValveOperator::stop() {
if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use... if (this->pump_switch()->state()) { // if the pump is still on at this point, it may be in use...
this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time this->valve_off_(); // ...so just switch the valve off now to ensure consistent run time
} }
this->pinned_millis_ = millis(); // save the time the stop request was made
} else { } else {
this->kill_(); // there is no stop_delay_, so just stop the pump and valve this->kill_(); // there is no stop_delay_, so just stop the pump and valve
} }
this->stop_millis_ = millis(); // save the time the stop request was made
} }
uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_; } uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; }
uint32_t SprinklerValveOperator::time_remaining() { uint32_t SprinklerValveOperator::time_remaining() {
if ((this->state_ == STARTING) || (this->state_ == ACTIVE)) { if (this->start_millis_ == 0) {
return (this->pinned_millis_ + this->start_delay_ + this->run_duration_ - millis()) / 1000; return this->run_duration(); // hasn't been started yet
} }
return 0;
if (this->stop_millis_) {
if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) {
return 0; // valve was active for more than its configured duration, so we are done
} else {
// we're stopped; return time remaining
return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000;
}
}
auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_;
if (completed_millis > millis()) {
return (completed_millis - millis()) / 1000; // running now
}
return 0; // run completed
} }
SprinklerState SprinklerValveOperator::state() { return this->state_; } SprinklerState SprinklerValveOperator::state() { return this->state_; }
@ -386,6 +402,9 @@ void Sprinkler::loop() {
for (auto &vo : this->valve_op_) { for (auto &vo : this->valve_op_) {
vo.loop(); vo.loop();
} }
if (this->prev_req_.has_request() && this->prev_req_.valve_operator()->state() == IDLE) {
this->prev_req_.reset();
}
} }
void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw) { void Sprinkler::add_valve(SprinklerControllerSwitch *valve_sw, SprinklerControllerSwitch *enable_sw) {
@ -732,7 +751,7 @@ bool Sprinkler::auto_advance() {
if (this->auto_adv_sw_ != nullptr) { if (this->auto_adv_sw_ != nullptr) {
return this->auto_adv_sw_->state; return this->auto_adv_sw_->state;
} }
return false; return true;
} }
float Sprinkler::multiplier() { float Sprinkler::multiplier() {
@ -972,7 +991,14 @@ optional<SprinklerValveRunRequestOrigin> Sprinkler::active_valve_request_is_from
return nullopt; return nullopt;
} }
optional<size_t> Sprinkler::active_valve() { return this->active_req_.valve_as_opt(); } optional<size_t> Sprinkler::active_valve() {
if (!this->valve_overlap_ && this->prev_req_.has_request() &&
(this->prev_req_.valve_operator()->state() == STARTING || this->prev_req_.valve_operator()->state() == ACTIVE)) {
return this->prev_req_.valve_as_opt();
}
return this->active_req_.valve_as_opt();
}
optional<size_t> Sprinkler::paused_valve() { return this->paused_valve_; } optional<size_t> Sprinkler::paused_valve() { return this->paused_valve_; }
optional<size_t> Sprinkler::queued_valve() { optional<size_t> Sprinkler::queued_valve() {
@ -1097,22 +1123,35 @@ uint32_t Sprinkler::total_cycle_time_enabled_valves() {
uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() { uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() {
uint32_t total_time_remaining = 0; uint32_t total_time_remaining = 0;
uint32_t valve_count = 0; uint32_t enabled_valve_count = 0;
uint32_t incomplete_valve_count = 0;
for (size_t valve = 0; valve < this->number_of_valves(); valve++) { for (size_t valve = 0; valve < this->number_of_valves(); valve++) {
if (this->valve_is_enabled_(valve) && !this->valve_cycle_complete_(valve)) { if (this->valve_is_enabled_(valve)) {
if (!this->active_valve().has_value() || (valve != this->active_valve().value())) { enabled_valve_count++;
total_time_remaining += this->valve_run_duration_adjusted(valve); if (!this->valve_cycle_complete_(valve)) {
valve_count++; if (!this->active_valve().has_value() || (valve != this->active_valve().value())) {
total_time_remaining += this->valve_run_duration_adjusted(valve);
incomplete_valve_count++;
} else {
// to get here, there must be an active valve and this valve must be equal to 'valve'
if (this->active_req_.valve_operator() == nullptr) { // no SVO has been assigned yet so it hasn't started
total_time_remaining += this->valve_run_duration_adjusted(valve);
incomplete_valve_count++;
}
}
} }
} }
} }
if (valve_count) { if (incomplete_valve_count >= enabled_valve_count) {
incomplete_valve_count--;
}
if (incomplete_valve_count) {
if (this->valve_overlap_) { if (this->valve_overlap_) {
total_time_remaining -= this->switching_delay_.value_or(0) * (valve_count - 1); total_time_remaining -= this->switching_delay_.value_or(0) * incomplete_valve_count;
} else { } else {
total_time_remaining += this->switching_delay_.value_or(0) * (valve_count - 1); total_time_remaining += this->switching_delay_.value_or(0) * incomplete_valve_count;
} }
} }
@ -1149,31 +1188,32 @@ optional<uint32_t> Sprinkler::time_remaining_active_valve() {
return this->active_req_.valve_operator()->time_remaining(); return this->active_req_.valve_operator()->time_remaining();
} }
} }
for (auto &vo : this->valve_op_) { // ...else return the value from the first non-IDLE SprinklerValveOperator if (this->prev_req_.has_request()) { // try to return the value based on prev_req_...
if (vo.state() != IDLE) { if (this->prev_req_.valve_operator() != nullptr) {
return vo.time_remaining(); return this->prev_req_.valve_operator()->time_remaining();
} }
} }
return nullopt; return nullopt;
} }
optional<uint32_t> Sprinkler::time_remaining_current_operation() { optional<uint32_t> Sprinkler::time_remaining_current_operation() {
auto total_time_remaining = this->time_remaining_active_valve(); if (!this->time_remaining_active_valve().has_value() && this->state_ == IDLE) {
return nullopt;
}
if (total_time_remaining.has_value()) { auto total_time_remaining = this->time_remaining_active_valve().value_or(0);
if (this->auto_advance()) { if (this->auto_advance()) {
total_time_remaining = total_time_remaining.value() + this->total_cycle_time_enabled_incomplete_valves(); total_time_remaining += this->total_cycle_time_enabled_incomplete_valves();
total_time_remaining = if (this->repeat().value_or(0) > 0) {
total_time_remaining.value() + total_time_remaining +=
(this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0))); (this->total_cycle_time_enabled_valves() * (this->repeat().value_or(0) - this->repeat_count().value_or(0)));
} }
if (this->queue_enabled()) {
total_time_remaining = total_time_remaining.value() + this->total_queue_time();
}
return total_time_remaining;
} }
return nullopt;
if (this->queue_enabled()) {
total_time_remaining += this->total_queue_time();
}
return total_time_remaining;
} }
bool Sprinkler::any_controller_is_active() { bool Sprinkler::any_controller_is_active() {
@ -1305,6 +1345,12 @@ optional<size_t> Sprinkler::next_valve_number_in_cycle_(const optional<size_t> f
} }
void Sprinkler::load_next_valve_run_request_(const optional<size_t> first_valve) { void Sprinkler::load_next_valve_run_request_(const optional<size_t> first_valve) {
if (this->active_req_.has_request()) {
this->prev_req_ = this->active_req_;
} else {
this->prev_req_.reset();
}
if (this->next_req_.has_request()) { if (this->next_req_.has_request()) {
if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on if (!this->next_req_.run_duration()) { // ensure the run duration is set correctly for consumption later on
this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve())); this->next_req_.set_run_duration(this->valve_run_duration_adjusted(this->next_req_.valve()));

View file

@ -170,7 +170,8 @@ class SprinklerValveOperator {
uint32_t start_delay_{0}; uint32_t start_delay_{0};
uint32_t stop_delay_{0}; uint32_t stop_delay_{0};
uint32_t run_duration_{0}; uint32_t run_duration_{0};
uint64_t pinned_millis_{0}; uint64_t start_millis_{0};
uint64_t stop_millis_{0};
Sprinkler *controller_{nullptr}; Sprinkler *controller_{nullptr};
SprinklerValve *valve_{nullptr}; SprinklerValve *valve_{nullptr};
SprinklerState state_{IDLE}; SprinklerState state_{IDLE};
@ -538,15 +539,18 @@ class Sprinkler : public Component {
/// The valve run request that is currently active /// The valve run request that is currently active
SprinklerValveRunRequest active_req_; SprinklerValveRunRequest active_req_;
/// The next run request for the controller to consume after active_req_ is complete
SprinklerValveRunRequest next_req_;
/// The previous run request the controller processed
SprinklerValveRunRequest prev_req_;
/// The number of the manually selected valve currently selected /// The number of the manually selected valve currently selected
optional<size_t> manual_valve_; optional<size_t> manual_valve_;
/// The number of the valve to resume from (if paused) /// The number of the valve to resume from (if paused)
optional<size_t> paused_valve_; optional<size_t> paused_valve_;
/// The next run request for the controller to consume after active_req_ is complete
SprinklerValveRunRequest next_req_;
/// Set the number of times to repeat a full cycle /// Set the number of times to repeat a full cycle
optional<uint32_t> target_repeats_; optional<uint32_t> target_repeats_;

View file

@ -1,19 +1,14 @@
import logging import logging
import re
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import core 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.yaml_util import ESPHomeDataBase, make_data_base
from esphome.config_helpers import merge_config from esphome.config_helpers import merge_config
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
VALID_SUBSTITUTIONS_CHARACTERS = (
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
)
def validate_substitution_key(value): def validate_substitution_key(value):
value = cv.string(value) value = cv.string(value)
@ -42,12 +37,6 @@ async def to_code(config):
pass 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): def _expand_substitutions(substitutions, value, path, ignore_missing):
if "$" not in value: if "$" not in value:
return value return value
@ -56,7 +45,7 @@ def _expand_substitutions(substitutions, value, path, ignore_missing):
i = 0 i = 0
while True: while True:
m = VARIABLE_PROG.search(value, i) m = cv.VARIABLE_PROG.search(value, i)
if not m: if not m:
# Nothing more to match. Done # Nothing more to match. Done
break break

View file

@ -10,6 +10,8 @@ from esphome.const import (
CONF_BRIGHTNESS, CONF_BRIGHTNESS,
) )
CODEOWNERS = ["@freekode"]
tm1651_ns = cg.esphome_ns.namespace("tm1651") tm1651_ns = cg.esphome_ns.namespace("tm1651")
TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component) TM1651Display = tm1651_ns.class_("TM1651Display", cg.Component)

View file

@ -3,7 +3,6 @@ import esphome.config_validation as cv
from esphome import pins from esphome import pins
from esphome.const import CONF_ID, CONF_SDO_PIN, CONF_SCL_PIN from esphome.const import CONF_ID, CONF_SDO_PIN, CONF_SCL_PIN
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["binary_sensor"] AUTO_LOAD = ["binary_sensor"]
CONF_TTP229_ID = "ttp229_id" CONF_TTP229_ID = "ttp229_id"

View file

@ -102,6 +102,16 @@ void Wiegand::loop() {
uint8_t key = KEYS[value]; uint8_t key = KEYS[value];
this->send_key_(key); this->send_key_(key);
} }
} else if (count == 8) {
if ((value ^ 0xf0) >> 4 == (value & 0xf)) {
value &= 0xf;
for (auto *trigger : this->key_triggers_)
trigger->trigger(value);
if (value < 12) {
uint8_t key = KEYS[value];
this->send_key_(key);
}
}
} else { } else {
ESP_LOGD(TAG, "received unknown %d-bit value: %llx", count, value); ESP_LOGD(TAG, "received unknown %d-bit value: %llx", count, value);
} }

View file

@ -183,6 +183,11 @@ network::IPAddress WiFiComponent::get_ip_address() {
return this->wifi_soft_ap_ip(); return this->wifi_soft_ap_ip();
return {}; 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 { std::string WiFiComponent::get_use_address() const {
if (this->use_address_.empty()) { if (this->use_address_.empty()) {
return App.get_name() + ".local"; return App.get_name() + ".local";

View file

@ -240,6 +240,7 @@ class WiFiComponent : public Component {
void set_rrm(bool rrm); void set_rrm(bool rrm);
#endif #endif
network::IPAddress get_dns_address(int num);
network::IPAddress get_ip_address(); network::IPAddress get_ip_address();
std::string get_use_address() const; std::string get_use_address() const;
void set_use_address(const std::string &use_address); void set_use_address(const std::string &use_address);

View file

@ -7,6 +7,7 @@ from esphome.const import (
CONF_SCAN_RESULTS, CONF_SCAN_RESULTS,
CONF_SSID, CONF_SSID,
CONF_MAC_ADDRESS, CONF_MAC_ADDRESS,
CONF_DNS_ADDRESS,
ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_DIAGNOSTIC,
) )
@ -28,6 +29,9 @@ BSSIDWiFiInfo = wifi_info_ns.class_(
MacAddressWifiInfo = wifi_info_ns.class_( MacAddressWifiInfo = wifi_info_ns.class_(
"MacAddressWifiInfo", text_sensor.TextSensor, cg.Component "MacAddressWifiInfo", text_sensor.TextSensor, cg.Component
) )
DNSAddressWifiInfo = wifi_info_ns.class_(
"DNSAddressWifiInfo", text_sensor.TextSensor, cg.PollingComponent
)
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
@ -46,6 +50,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema(
MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC 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_BSSID)
await setup_conf(config, CONF_MAC_ADDRESS) await setup_conf(config, CONF_MAC_ADDRESS)
await setup_conf(config, CONF_SCAN_RESULTS) await setup_conf(config, CONF_SCAN_RESULTS)
await setup_conf(config, CONF_DNS_ADDRESS)

View file

@ -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 SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", 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 wifi_info
} // namespace esphome } // namespace esphome

View file

@ -24,6 +24,32 @@ class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSenso
network::IPAddress last_ip_; 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 { class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
public: public:
void update() override { void update() override {

View file

@ -53,6 +53,7 @@ from esphome.const import (
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
TYPE_GIT, TYPE_GIT,
TYPE_LOCAL, TYPE_LOCAL,
VALID_SUBSTITUTIONS_CHARACTERS,
) )
from esphome.core import ( from esphome.core import (
CORE, CORE,
@ -79,6 +80,11 @@ from esphome.yaml_util import make_data_base
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# pylint: disable=consider-using-f-string
VARIABLE_PROG = re.compile(
"\\$([{0}]+|\\{{[{0}]*\\}})".format(VALID_SUBSTITUTIONS_CHARACTERS)
)
# pylint: disable=invalid-name # pylint: disable=invalid-name
Schema = _Schema Schema = _Schema
@ -265,6 +271,14 @@ def alphanumeric(value):
def valid_name(value): def valid_name(value):
value = string_strict(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: for c in value:
if c not in ALLOWED_NAME_CHARS: if c not in ALLOWED_NAME_CHARS:
raise Invalid( raise Invalid(
@ -447,6 +461,14 @@ def validate_id_name(value):
raise Invalid( raise Invalid(
"Dashes are not supported in IDs, please use underscores instead." "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}_" valid_chars = f"{ascii_letters + digits}_"
for char in value: for char in value:
if char not in valid_chars: if char not in valid_chars:

View file

@ -3,6 +3,9 @@
__version__ = "2023.6.0-dev" __version__ = "2023.6.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
)
PLATFORM_ESP32 = "esp32" PLATFORM_ESP32 = "esp32"
PLATFORM_ESP8266 = "esp8266" PLATFORM_ESP8266 = "esp8266"
@ -180,6 +183,7 @@ CONF_DIR_PIN = "dir_pin"
CONF_DIRECTION = "direction" CONF_DIRECTION = "direction"
CONF_DIRECTION_OUTPUT = "direction_output" CONF_DIRECTION_OUTPUT = "direction_output"
CONF_DISABLE_CRC = "disable_crc" CONF_DISABLE_CRC = "disable_crc"
CONF_DISABLED = "disabled"
CONF_DISABLED_BY_DEFAULT = "disabled_by_default" CONF_DISABLED_BY_DEFAULT = "disabled_by_default"
CONF_DISCONNECT_DELAY = "disconnect_delay" CONF_DISCONNECT_DELAY = "disconnect_delay"
CONF_DISCOVERY = "discovery" CONF_DISCOVERY = "discovery"
@ -190,6 +194,7 @@ CONF_DISCOVERY_UNIQUE_ID_GENERATOR = "discovery_unique_id_generator"
CONF_DISTANCE = "distance" CONF_DISTANCE = "distance"
CONF_DITHER = "dither" CONF_DITHER = "dither"
CONF_DIV_RATIO = "div_ratio" CONF_DIV_RATIO = "div_ratio"
CONF_DNS_ADDRESS = "dns_address"
CONF_DNS1 = "dns1" CONF_DNS1 = "dns1"
CONF_DNS2 = "dns2" CONF_DNS2 = "dns2"
CONF_DOMAIN = "domain" CONF_DOMAIN = "domain"
@ -391,6 +396,7 @@ CONF_MAX_SPEED = "max_speed"
CONF_MAX_TEMPERATURE = "max_temperature" CONF_MAX_TEMPERATURE = "max_temperature"
CONF_MAX_VALUE = "max_value" CONF_MAX_VALUE = "max_value"
CONF_MAX_VOLTAGE = "max_voltage" CONF_MAX_VOLTAGE = "max_voltage"
CONF_MDNS = "mdns"
CONF_MEASUREMENT_DURATION = "measurement_duration" CONF_MEASUREMENT_DURATION = "measurement_duration"
CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number"
CONF_MEDIUM = "medium" CONF_MEDIUM = "medium"

View file

@ -1,4 +1,5 @@
import base64 import base64
import binascii
import codecs import codecs
import collections import collections
import functools import functools
@ -76,6 +77,10 @@ class DashboardSettings:
def status_use_ping(self): def status_use_ping(self):
return get_bool_env("ESPHOME_DASHBOARD_USE_PING") return get_bool_env("ESPHOME_DASHBOARD_USE_PING")
@property
def status_use_mqtt(self):
return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT")
@property @property
def using_ha_addon_auth(self): def using_ha_addon_auth(self):
if not self.on_ha_addon: if not self.on_ha_addon:
@ -583,6 +588,12 @@ class DashboardEntry:
return None return None
return self.storage.address return self.storage.address
@property
def no_mdns(self):
if self.storage is None:
return None
return self.storage.no_mdns
@property @property
def web_port(self): def web_port(self):
if self.storage is None: if self.storage is None:
@ -775,9 +786,12 @@ class MDNSStatusThread(threading.Thread):
stat.start() stat.start()
while not STOP_EVENT.is_set(): while not STOP_EVENT.is_set():
entries = _list_dashboard_entries() entries = _list_dashboard_entries()
stat.request_query( hosts = {}
{entry.filename: f"{entry.name}.local." for entry in entries} 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 IMPORT_RESULT = imports.import_state
PING_REQUEST.wait() PING_REQUEST.wait()
@ -801,6 +815,9 @@ class PingStatusThread(threading.Thread):
entries = _list_dashboard_entries() entries = _list_dashboard_entries()
queue = collections.deque() queue = collections.deque()
for entry in entries: for entry in entries:
if entry.no_mdns is True:
continue
if entry.address is None: if entry.address is None:
PING_RESULT[entry.filename] = None PING_RESULT[entry.filename] = None
continue continue
@ -832,10 +849,67 @@ class PingStatusThread(threading.Thread):
PING_REQUEST.clear() 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): class PingRequestHandler(BaseHandler):
@authenticated @authenticated
def get(self): def get(self):
PING_REQUEST.set() PING_REQUEST.set()
if settings.status_use_mqtt:
MQTT_PING_REQUEST.set()
self.set_header("content-type", "application/json") self.set_header("content-type", "application/json")
self.write(json.dumps(PING_RESULT)) self.write(json.dumps(PING_RESULT))
@ -910,6 +984,7 @@ PING_RESULT: dict = {}
IMPORT_RESULT = {} IMPORT_RESULT = {}
STOP_EVENT = threading.Event() STOP_EVENT = threading.Event()
PING_REQUEST = threading.Event() PING_REQUEST = threading.Event()
MQTT_PING_REQUEST = threading.Event()
class LoginHandler(BaseHandler): class LoginHandler(BaseHandler):
@ -1197,6 +1272,11 @@ def start_web_server(args):
else: else:
status_thread = MDNSStatusThread() status_thread = MDNSStatusThread()
status_thread.start() status_thread.start()
if settings.status_use_mqtt:
status_thread_mqtt = MqttStatusThread()
status_thread_mqtt.start()
try: try:
tornado.ioloop.IOLoop.current().start() tornado.ioloop.IOLoop.current().start()
except KeyboardInterrupt: except KeyboardInterrupt:
@ -1204,5 +1284,8 @@ def start_web_server(args):
STOP_EVENT.set() STOP_EVENT.set()
PING_REQUEST.set() PING_REQUEST.set()
status_thread.join() status_thread.join()
if settings.status_use_mqtt:
status_thread_mqtt.join()
MQTT_PING_REQUEST.set()
if args.socket is not None: if args.socket is not None:
os.remove(args.socket) os.remove(args.socket)

View file

@ -147,6 +147,14 @@ def get_bool_env(var, default=False):
return bool(os.getenv(var, default)) 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(): def is_ha_addon():
return get_bool_env("ESPHOME_IS_HA_ADDON") return get_bool_env("ESPHOME_IS_HA_ADDON")

View file

@ -4,6 +4,7 @@ import logging
import ssl import ssl
import sys import sys
import time import time
import json
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
@ -24,15 +25,45 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError from esphome.core import CORE, EsphomeError
from esphome.log import color, Fore from esphome.log import color, Fore
from esphome.util import safe_print from esphome.util import safe_print
from esphome.helpers import get_str_env, get_int_env
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def initialize(config, subscriptions, on_message, username, password, client_id): def config_from_env():
def on_connect(client, userdata, flags, return_code): 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!") _LOGGER.info("Connected to MQTT broker!")
for topic in subscriptions: for topic in subscriptions:
client.subscribe(topic) client.subscribe(topic)
if on_connect is not None:
on_connect(client, userdata, flags, return_code)
def on_disconnect(client, userdata, result_code): def on_disconnect(client, userdata, result_code):
if result_code == 0: if result_code == 0:
@ -57,7 +88,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id)
tries += 1 tries += 1
client = mqtt.Client(client_id or "") client = mqtt.Client(client_id or "")
client.on_connect = on_connect client.on_connect = on_connect_
client.on_message = on_message client.on_message = on_message
client.on_disconnect = on_disconnect client.on_disconnect = on_disconnect
if username is None: if username is None:
@ -89,11 +120,88 @@ def initialize(config, subscriptions, on_message, username, password, client_id)
except OSError as err: except OSError as err:
raise EsphomeError(f"Cannot connect to MQTT broker: {err}") from err raise EsphomeError(f"Cannot connect to MQTT broker: {err}") from err
try: return client
client.loop_forever()
except KeyboardInterrupt:
pass def show_discover(config, username=None, password=None, client_id=None):
return 0 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): 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 message = time_ + payload
safe_print(message) 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): 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 return
client.publish(msg.topic, None, retain=True) 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 # From marvinroger/async-mqtt-client -> scripts/get-fingerprint/get-fingerprint.py

View file

@ -10,6 +10,12 @@ from esphome import const
from esphome.core import CORE from esphome.core import CORE
from esphome.helpers import write_file_if_changed from esphome.helpers import write_file_if_changed
from esphome.const import (
CONF_MDNS,
CONF_DISABLED,
)
from esphome.types import CoreType from esphome.types import CoreType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -46,6 +52,7 @@ class StorageJSON:
build_path, build_path,
firmware_bin_path, firmware_bin_path,
loaded_integrations, loaded_integrations,
no_mdns,
): ):
# Version of the storage JSON schema # Version of the storage JSON schema
assert storage_version is None or isinstance(storage_version, int) 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 # A list of strings of names of loaded integrations
self.loaded_integrations: list[str] = loaded_integrations self.loaded_integrations: list[str] = loaded_integrations
self.loaded_integrations.sort() self.loaded_integrations.sort()
# Is mDNS disabled
self.no_mdns = no_mdns
def as_dict(self): def as_dict(self):
return { return {
@ -90,6 +99,7 @@ class StorageJSON:
"build_path": self.build_path, "build_path": self.build_path,
"firmware_bin_path": self.firmware_bin_path, "firmware_bin_path": self.firmware_bin_path,
"loaded_integrations": self.loaded_integrations, "loaded_integrations": self.loaded_integrations,
"no_mdns": self.no_mdns,
} }
def to_json(self): def to_json(self):
@ -120,6 +130,11 @@ class StorageJSON:
build_path=esph.build_path, build_path=esph.build_path,
firmware_bin_path=esph.firmware_bin, firmware_bin_path=esph.firmware_bin,
loaded_integrations=list(esph.loaded_integrations), 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 @staticmethod
@ -139,6 +154,7 @@ class StorageJSON:
build_path=None, build_path=None,
firmware_bin_path=None, firmware_bin_path=None,
loaded_integrations=[], loaded_integrations=[],
no_mdns=False,
) )
@staticmethod @staticmethod
@ -159,6 +175,7 @@ class StorageJSON:
build_path = storage.get("build_path") build_path = storage.get("build_path")
firmware_bin_path = storage.get("firmware_bin_path") firmware_bin_path = storage.get("firmware_bin_path")
loaded_integrations = storage.get("loaded_integrations", []) loaded_integrations = storage.get("loaded_integrations", [])
no_mdns = storage.get("no_mdns", False)
return StorageJSON( return StorageJSON(
storage_version, storage_version,
name, name,
@ -172,6 +189,7 @@ class StorageJSON:
build_path, build_path,
firmware_bin_path, firmware_bin_path,
loaded_integrations, loaded_integrations,
no_mdns,
) )
@staticmethod @staticmethod

View file

@ -299,6 +299,16 @@ def copy_src_tree():
copy_files() 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(): def generate_defines_h():
define_content_l = [x.as_macro for x in CORE.defines] define_content_l = [x.as_macro for x in CORE.defines]

View file

@ -2,16 +2,16 @@ voluptuous==0.13.1
PyYAML==6.0 PyYAML==6.0
paho-mqtt==1.6.1 paho-mqtt==1.6.1
colorama==0.4.6 colorama==0.4.6
tornado==6.3.1 tornado==6.3.2
tzlocal==5.0.1 # from time tzlocal==5.0.1 # from time
tzdata>=2021.1 # from time tzdata>=2021.1 # from time
pyserial==3.5 pyserial==3.5
platformio==6.1.6 # When updating platformio, also update Dockerfile platformio==6.1.7 # When updating platformio, also update Dockerfile
esptool==4.5.1 esptool==4.5.1
click==8.1.3 click==8.1.3
esphome-dashboard==20230516.0 esphome-dashboard==20230516.0
aioesphomeapi==13.7.5 aioesphomeapi==13.7.5
zeroconf==0.60.0 zeroconf==0.62.0
# esp-idf requires this, but doesn't bundle it by default # esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24

View file

@ -1,7 +1,7 @@
pylint==2.17.3 pylint==2.17.4
flake8==6.0.0 # also change in .pre-commit-config.yaml when updating 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 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 pre-commit
# Unit tests # Unit tests

View file

@ -66,6 +66,7 @@ file_types = (
".txt", ".txt",
".ico", ".ico",
".svg", ".svg",
".png",
".py", ".py",
".html", ".html",
".js", ".js",
@ -80,7 +81,7 @@ file_types = (
"", "",
) )
cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc")
ignore_types = (".ico", ".woff", ".woff2", "") ignore_types = (".ico", ".png", ".woff", ".woff2", "")
LINT_FILE_CHECKS = [] LINT_FILE_CHECKS = []
LINT_CONTENT_CHECKS = [] LINT_CONTENT_CHECKS = []

BIN
tests/pnglogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

View file

@ -3099,6 +3099,8 @@ text_sensor:
name: BSSID name: BSSID
mac_address: mac_address:
name: Mac Address name: Mac Address
dns_address:
name: DNS ADdress
- platform: version - platform: version
name: ESPHome Version No Timestamp name: ESPHome Version No Timestamp
hide_timestamp: true hide_timestamp: true

View file

@ -659,6 +659,27 @@ interval:
display: 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
cap1188: cap1188:
id: cap1188_component id: cap1188_component
address: 0x29 address: 0x29

View file

@ -666,6 +666,7 @@ touchscreen:
i2s_audio: i2s_audio:
i2s_lrclk_pin: GPIO26 i2s_lrclk_pin: GPIO26
i2s_bclk_pin: GPIO27 i2s_bclk_pin: GPIO27
i2s_mclk_pin: GPIO25
media_player: media_player:
- platform: i2s_audio - platform: i2s_audio

View file

@ -37,6 +37,26 @@ switch:
output: pin_4 output: pin_4
id: pin_4_switch 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: sensor:
- platform: internal_temperature - platform: internal_temperature
name: Internal Temperature name: Internal Temperature

View file

@ -4,7 +4,7 @@ wifi:
ssid: "ssid" ssid: "ssid"
esp32: esp32:
board: esp32-c3-devkitm-1 board: esp32s3box
variant: ESP32S3 variant: ESP32S3
framework: framework:
type: arduino type: arduino

View file

@ -6,7 +6,7 @@ from hypothesis.strategies import one_of, text, integers, builds
from esphome import config_validation from esphome import config_validation
from esphome.config_validation import Invalid 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(): def test_check_not_templatable__invalid():
@ -40,6 +40,47 @@ def test_valid_name__invalid(value):
config_validation.valid_name(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())) @given(one_of(integers(), text()))
def test_string__valid(value): def test_string__valid(value):
actual = config_validation.string(value) actual = config_validation.string(value)