mirror of
https://github.com/esphome/esphome.git
synced 2026-07-09 08:25:36 +00:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bfbaaebf3 | |||
| 98b79b132a | |||
| f6221f0007 | |||
| 263b375088 | |||
| a1f819e9b8 | |||
| ca7f50f37f | |||
| 7b92fe95af | |||
| 42ddf0870c | |||
| 0512dd2339 | |||
| 1fc73731a8 | |||
| e47feace11 | |||
| 4472d3b61b | |||
| 06c5bcbc66 | |||
| 6c44775bf5 | |||
| b127363fa0 | |||
| 782b58bbeb | |||
| 5de508ad8c | |||
| 054c8ba485 | |||
| 3b2be021b2 | |||
| 0cbbd64577 | |||
| a618ee11b4 | |||
| 6251c26cc6 | |||
| 4fbe0d87ec | |||
| 24d8e99c50 | |||
| 14b6a0ede1 | |||
| 1793ca5eac | |||
| 62e19bcb27 | |||
| 84d1c34c28 | |||
| f78cbf9200 | |||
| eb711381d3 | |||
| 9a1daa5247 | |||
| f3d61ca3e1 | |||
| 29dfd820c6 | |||
| 8bc5b97298 | |||
| 7a64163c4f | |||
| dfe14f9c3a | |||
| 26cf373ae7 | |||
| 94ccddf176 | |||
| 2ec24505d0 | |||
| 4f7faa7712 | |||
| b3dcaac262 | |||
| ee118d384a | |||
| 8d36167e11 | |||
| 6d559a32df | |||
| bf0d31b3ab | |||
| d8ffb732b7 | |||
| 9ab2a573ab | |||
| 99d1c4eb69 | |||
| b079be756f | |||
| 039a1f063e | |||
| 2354165e41 | |||
| f5697b0ae5 | |||
| fe794a26e8 | |||
| 8d77051b9a | |||
| 9534ab2a19 | |||
| 1b1c8d767d | |||
| e3d68deef9 | |||
| 20cd6a1771 | |||
| d27229a1c7 | |||
| 129aebe8f4 | |||
| a84ad7b1f8 | |||
| 86096b96f5 | |||
| ac5a28301a | |||
| e2157a3d26 | |||
| d934fb3910 | |||
| c4076ec8a9 | |||
| 9ac22f9244 | |||
| 9e7b3e0330 | |||
| 2abe272867 | |||
| db6b9166f4 | |||
| 7ab95ddcb1 | |||
| cdd2bfbc60 | |||
| 41f7f8cccb | |||
| 045de436ba | |||
| 24e276c3f9 | |||
| 9e768bb510 | |||
| 53fd99578a | |||
| 310baab524 | |||
| 0422b581cb | |||
| 0ce89c17ab | |||
| 66be793cd8 | |||
| 1d38498ca7 | |||
| aef9b5b72f | |||
| 9bf35ab8fb | |||
| 33ace9d698 | |||
| 32ab3abd7c | |||
| 94b248527d | |||
| a46aa594b3 | |||
| 99425e3a97 | |||
| f83e3ad6a6 | |||
| c768e2eabc | |||
| 9ffd350095 | |||
| 26ccaf70db | |||
| 20925b3220 | |||
| 83504d2de2 | |||
| acbb662316 | |||
| abf6212a5a | |||
| 4dbc5ce920 | |||
| 92c82f3d25 | |||
| 77009cfafe | |||
| cd7e54dbf2 | |||
| 29a79b1373 | |||
| dafc3560dd | |||
| a25ac28ae5 | |||
| 6809af3de0 | |||
| e16a877745 | |||
| 4963ddcb95 | |||
| 4f62bb7171 | |||
| eb6d6eac7d | |||
| 7533835e04 | |||
| 2310b9e3fe | |||
| 8206df6e4e | |||
| 5faed9d5f5 | |||
| 25d656d468 | |||
| cdc63f0fed | |||
| ddd21ba442 | |||
| a32817207c | |||
| 6e01f3fccd | |||
| e0072ef4c5 | |||
| b21a69f07a | |||
| 36e043debb | |||
| 54c73bf1bc | |||
| cbc3770b11 |
+1
-1
@@ -1 +1 @@
|
||||
58a760f5fd174bd438bcc3a7018292c158530c1a1d15181941c832d4c032511c
|
||||
72f02816e288b68ff4ef4b3d6fb66432c893b187a80ad3ebaa29afa443ff9ea6
|
||||
|
||||
@@ -15,11 +15,6 @@ inputs:
|
||||
description: "Version to build"
|
||||
required: true
|
||||
example: "2023.12.0"
|
||||
base_os:
|
||||
description: "Base OS to use"
|
||||
required: false
|
||||
default: "debian"
|
||||
example: "debian"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
@@ -60,7 +55,6 @@ runs:
|
||||
build-args: |
|
||||
BUILD_TYPE=${{ inputs.build_type }}
|
||||
BUILD_VERSION=${{ inputs.version }}
|
||||
BUILD_OS=${{ inputs.base_os }}
|
||||
outputs: |
|
||||
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
@@ -86,7 +80,6 @@ runs:
|
||||
build-args: |
|
||||
BUILD_TYPE=${{ inputs.build_type }}
|
||||
BUILD_VERSION=${{ inputs.version }}
|
||||
BUILD_OS=${{ inputs.base_os }}
|
||||
outputs: |
|
||||
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ on:
|
||||
- "script/platformio_install_deps.py"
|
||||
|
||||
permissions:
|
||||
contents: read # actions/checkout only; the build does not push images
|
||||
contents: read # actions/checkout only
|
||||
|
||||
concurrency:
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -33,6 +33,9 @@ jobs:
|
||||
check-docker:
|
||||
name: Build docker containers
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read # actions/checkout to load Dockerfile and build context
|
||||
packages: write # push branch-tagged images to ghcr.io for local testing
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -41,6 +44,9 @@ jobs:
|
||||
- "ha-addon"
|
||||
- "docker"
|
||||
# - "lint"
|
||||
outputs:
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
push: ${{ steps.tag.outputs.push }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up Python
|
||||
@@ -50,14 +56,82 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Set TAG
|
||||
- name: Determine tag and whether to push
|
||||
id: tag
|
||||
run: |
|
||||
echo "TAG=check" >> $GITHUB_ENV
|
||||
# Sanitize the branch name into a valid docker tag: replace invalid
|
||||
# characters, ensure the first character is valid (tags must start
|
||||
# with [A-Za-z0-9_]), and cap the length at 128 characters.
|
||||
branch="${{ github.head_ref || github.ref_name }}"
|
||||
tag="${branch//[^a-zA-Z0-9_.-]/-}"
|
||||
case "$tag" in
|
||||
[a-zA-Z0-9_]*) ;;
|
||||
*) tag="pr-${tag}" ;;
|
||||
esac
|
||||
tag="${tag:0:128}"
|
||||
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||
# Only push branch images for same-repo pull requests. Push events
|
||||
# only fire for dev/beta/release, whose images are owned by the
|
||||
# release pipeline -- never overwrite those from here.
|
||||
if [ "${{ github.event_name }}" = "pull_request" ] \
|
||||
&& [ "${{ github.repository }}" = "esphome/esphome" ] \
|
||||
&& [ "${{ github.event.pull_request.head.repo.full_name }}" = "esphome/esphome" ]; then
|
||||
echo "push=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "push=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Log in to the GitHub container registry
|
||||
if: steps.tag.outputs.push == 'true'
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run build
|
||||
run: |
|
||||
docker/build.py \
|
||||
--tag "${TAG}" \
|
||||
--tag "${{ steps.tag.outputs.tag }}" \
|
||||
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
|
||||
--build-type "${{ matrix.build_type }}" \
|
||||
build
|
||||
--registry ghcr \
|
||||
build ${{ steps.tag.outputs.push == 'true' && '--push --no-cache-to' || '' }}
|
||||
|
||||
manifest:
|
||||
name: Push ${{ matrix.build_type }} manifest to ghcr.io
|
||||
needs: [check-docker]
|
||||
if: needs.check-docker.outputs.push == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read # actions/checkout to run docker/build.py
|
||||
packages: write # buildx imagetools writes the multi-arch tag to ghcr.io
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build_type:
|
||||
- "ha-addon"
|
||||
- "docker"
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifest
|
||||
run: |
|
||||
docker/build.py \
|
||||
--tag "${{ needs.check-docker.outputs.tag }}" \
|
||||
--build-type "${{ matrix.build_type }}" \
|
||||
--registry ghcr \
|
||||
manifest
|
||||
|
||||
@@ -113,6 +113,7 @@ jobs:
|
||||
script/build_language_schema.py --check
|
||||
script/generate-esp32-boards.py --check
|
||||
script/generate-rp2040-boards.py --check
|
||||
script/ci_check_duplicate_test_ids.py
|
||||
|
||||
import-time:
|
||||
name: Check import esphome.__main__ time
|
||||
@@ -244,7 +245,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
|
||||
+6
-3
@@ -19,7 +19,6 @@ esphome/components/ac_dimmer/* @glmnet
|
||||
esphome/components/adc/* @esphome/core
|
||||
esphome/components/adc128s102/* @DeerMaximum
|
||||
esphome/components/addressable_light/* @justfalter
|
||||
esphome/components/ade7880/* @kpfleming
|
||||
esphome/components/ade7953/* @angelnu
|
||||
esphome/components/ade7953_base/* @angelnu
|
||||
esphome/components/ade7953_i2c/* @angelnu
|
||||
@@ -28,7 +27,7 @@ esphome/components/ads1118/* @solomondg1
|
||||
esphome/components/ags10/* @mak-42
|
||||
esphome/components/aic3204/* @kbx81
|
||||
esphome/components/airthings_ble/* @jeromelaban
|
||||
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
|
||||
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
|
||||
esphome/components/airthings_wave_mini/* @ncareau
|
||||
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
||||
@@ -84,6 +83,7 @@ esphome/components/bme680_bsec/* @trvrnrth
|
||||
esphome/components/bme68x_bsec2/* @kbx81 @neffs
|
||||
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
|
||||
esphome/components/bmi160/* @flaviut
|
||||
esphome/components/bmi270/* @clydebarrow
|
||||
esphome/components/bmp280_base/* @ademuri
|
||||
esphome/components/bmp280_i2c/* @ademuri
|
||||
esphome/components/bmp280_spi/* @ademuri
|
||||
@@ -139,7 +139,7 @@ esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
esphome/components/dht/* @OttoWinter
|
||||
esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dlms_meter/* @SimonFischer04
|
||||
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
@@ -291,6 +291,7 @@ esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
esphome/components/logger/select/* @clydebarrow
|
||||
esphome/components/lps22/* @nagisa
|
||||
esphome/components/lsm6ds/* @clydebarrow
|
||||
esphome/components/ltr390/* @latonita @sjtrny
|
||||
esphome/components/ltr501/* @latonita
|
||||
esphome/components/ltr_als_ps/* @latonita
|
||||
@@ -380,6 +381,7 @@ esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
esphome/components/pcm5122/* @remcom
|
||||
esphome/components/pi4ioe5v6408/* @jesserockz
|
||||
esphome/components/pid/* @OttoWinter
|
||||
esphome/components/pipsolar/* @andreashergert1984
|
||||
@@ -596,6 +598,7 @@ esphome/components/wk2212_spi/* @DrCoolZic
|
||||
esphome/components/wl_134/* @hobbypunk90
|
||||
esphome/components/wts01/* @alepee
|
||||
esphome/components/x9c/* @EtienneMD
|
||||
esphome/components/xdb401/* @RT530
|
||||
esphome/components/xgzp68xx/* @gcormier
|
||||
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
||||
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.6.0-dev
|
||||
PROJECT_NUMBER = 2026.6.5
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
+9
-11
@@ -1,10 +1,9 @@
|
||||
ARG BUILD_VERSION=dev
|
||||
ARG BUILD_OS=alpine
|
||||
ARG BUILD_BASE_VERSION=2025.04.0
|
||||
ARG BUILD_BASE_VERSION=2026.06.0
|
||||
ARG BUILD_TYPE=docker
|
||||
|
||||
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-docker
|
||||
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
|
||||
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base-source-docker
|
||||
FROM ghcr.io/esphome/docker-base:debian-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
|
||||
|
||||
ARG BUILD_TYPE
|
||||
FROM base-source-${BUILD_TYPE} AS base
|
||||
@@ -18,13 +17,9 @@ RUN git config --system --add safe.directory "*" \
|
||||
# validate openocd-esp32 (it dynamically links libusb-1.0.so.0); without
|
||||
# it idf_tools.py rejects the openocd install with exit 127 and aborts
|
||||
# the whole framework setup.
|
||||
RUN if command -v apk > /dev/null; then \
|
||||
apk add --no-cache build-base libusb; \
|
||||
else \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
@@ -36,6 +31,9 @@ RUN \
|
||||
uv pip install --no-cache-dir \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.1.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
&& platformio settings set check_platformio_interval 1000000 \
|
||||
|
||||
+42
-13
@@ -20,6 +20,10 @@ TYPE_HA_ADDON = "ha-addon"
|
||||
TYPE_LINT = "lint"
|
||||
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
|
||||
|
||||
REGISTRY_GHCR = "ghcr"
|
||||
REGISTRY_DOCKERHUB = "dockerhub"
|
||||
REGISTRIES = [REGISTRY_GHCR, REGISTRY_DOCKERHUB]
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
@@ -34,6 +38,12 @@ parser.add_argument(
|
||||
parser.add_argument(
|
||||
"--build-type", choices=TYPES, required=True, help="The type of build to run"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registry",
|
||||
choices=REGISTRIES,
|
||||
action="append",
|
||||
help="Restrict to specific registries (default: all). May be passed multiple times.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Don't run any commands, just print them"
|
||||
)
|
||||
@@ -45,6 +55,11 @@ build_parser.add_argument("--push", help="Also push the images", action="store_t
|
||||
build_parser.add_argument(
|
||||
"--load", help="Load the docker image locally", action="store_true"
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--no-cache-to",
|
||||
help="Don't write the build cache (avoids polluting the shared cache)",
|
||||
action="store_true",
|
||||
)
|
||||
manifest_parser = subparsers.add_parser(
|
||||
"manifest", help="Create a manifest from already pushed images"
|
||||
)
|
||||
@@ -95,11 +110,14 @@ def main():
|
||||
print("Command failed")
|
||||
sys.exit(1)
|
||||
|
||||
registries = args.registry or REGISTRIES
|
||||
|
||||
# detect channel from tag
|
||||
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
|
||||
major_minor_version = None
|
||||
if match is None:
|
||||
channel = CHANNEL_DEV
|
||||
# Custom tag (e.g. a branch name) -- push only the tag itself
|
||||
channel = None
|
||||
elif match.group(2) is None:
|
||||
major_minor_version = match.group(1)
|
||||
channel = CHANNEL_RELEASE
|
||||
@@ -128,11 +146,18 @@ def main():
|
||||
CHANNEL_DEV: "cache-dev",
|
||||
CHANNEL_BETA: "cache-beta",
|
||||
CHANNEL_RELEASE: "cache-latest",
|
||||
}[channel]
|
||||
cache_img = f"ghcr.io/{params.build_to}:{cache_tag}"
|
||||
}.get(channel, "cache-dev")
|
||||
# Cache images live alongside the pushed images; prefer GHCR when it is
|
||||
# one of the selected registries, otherwise fall back to Docker Hub so a
|
||||
# registry-restricted build doesn't need GHCR auth.
|
||||
cache_prefix = "ghcr.io/" if REGISTRY_GHCR in registries else ""
|
||||
cache_img = f"{cache_prefix}{params.build_to}:{cache_tag}"
|
||||
|
||||
imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push]
|
||||
imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
|
||||
imgs = []
|
||||
if REGISTRY_DOCKERHUB in registries:
|
||||
imgs += [f"{params.build_to}:{tag}" for tag in tags_to_push]
|
||||
if REGISTRY_GHCR in registries:
|
||||
imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
|
||||
|
||||
# 3. build
|
||||
cmd = [
|
||||
@@ -155,7 +180,9 @@ def main():
|
||||
for img in imgs:
|
||||
cmd += ["--tag", img]
|
||||
if args.push:
|
||||
cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"]
|
||||
cmd += ["--push"]
|
||||
if not args.no_cache_to:
|
||||
cmd += ["--cache-to", f"type=registry,ref={cache_img},mode=max"]
|
||||
if args.load:
|
||||
cmd += ["--load"]
|
||||
|
||||
@@ -163,20 +190,22 @@ def main():
|
||||
elif args.command == "manifest":
|
||||
manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
|
||||
|
||||
targets = [f"{manifest}:{tag}" for tag in tags_to_push]
|
||||
targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
|
||||
# 1. Create manifests
|
||||
targets = []
|
||||
if REGISTRY_DOCKERHUB in registries:
|
||||
targets += [f"{manifest}:{tag}" for tag in tags_to_push]
|
||||
if REGISTRY_GHCR in registries:
|
||||
targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
|
||||
# Use buildx imagetools (not `docker manifest`) so the per-arch sources,
|
||||
# which buildx pushes as single-platform manifest lists, are combined
|
||||
# and pushed correctly in one step.
|
||||
for target in targets:
|
||||
cmd = ["docker", "manifest", "create", target]
|
||||
cmd = ["docker", "buildx", "imagetools", "create", "--tag", target]
|
||||
for arch in ARCHS:
|
||||
src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}"
|
||||
if target.startswith("ghcr.io"):
|
||||
src = f"ghcr.io/{src}"
|
||||
cmd.append(src)
|
||||
run_command(*cmd)
|
||||
# 2. Push manifests
|
||||
for target in targets:
|
||||
run_command("docker", "manifest", "push", target)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -27,4 +27,12 @@ if [[ -d /build ]]; then
|
||||
export ESPHOME_BUILD_PATH=/build
|
||||
fi
|
||||
|
||||
# The default CMD is "dashboard /config". Route the dashboard to the new
|
||||
# Device Builder, but pass every other subcommand (compile, run, config,
|
||||
# logs, ...) straight through to the esphome CLI so direct CLI use keeps working.
|
||||
if [[ "$1" == "dashboard" ]]; then
|
||||
shift
|
||||
exec esphome-device-builder "$@"
|
||||
fi
|
||||
|
||||
exec esphome "$@"
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# ==============================================================================
|
||||
# Installs the latest prerelease of esphome-device-builder when the
|
||||
# `use_new_device_builder` config option is enabled.
|
||||
# This is a temporary install-on-boot step until esphome-device-builder
|
||||
# becomes a direct dependency of esphome.
|
||||
# ==============================================================================
|
||||
|
||||
if ! bashio::config.true 'use_new_device_builder'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bashio::log.info "Installing latest prerelease of esphome-device-builder..."
|
||||
if command -v uv > /dev/null; then
|
||||
uv pip install --system --no-cache-dir --prerelease=allow --upgrade \
|
||||
esphome-device-builder ||
|
||||
bashio::exit.nok "Failed installing esphome-device-builder."
|
||||
else
|
||||
pip install --no-cache-dir --pre --upgrade esphome-device-builder ||
|
||||
bashio::exit.nok "Failed installing esphome-device-builder."
|
||||
fi
|
||||
bashio::log.info "Installed esphome-device-builder."
|
||||
@@ -1,96 +0,0 @@
|
||||
types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
text/xml xml;
|
||||
image/gif gif;
|
||||
image/jpeg jpeg jpg;
|
||||
application/javascript js;
|
||||
application/atom+xml atom;
|
||||
application/rss+xml rss;
|
||||
|
||||
text/mathml mml;
|
||||
text/plain txt;
|
||||
text/vnd.sun.j2me.app-descriptor jad;
|
||||
text/vnd.wap.wml wml;
|
||||
text/x-component htc;
|
||||
|
||||
image/png png;
|
||||
image/svg+xml svg svgz;
|
||||
image/tiff tif tiff;
|
||||
image/vnd.wap.wbmp wbmp;
|
||||
image/webp webp;
|
||||
image/x-icon ico;
|
||||
image/x-jng jng;
|
||||
image/x-ms-bmp bmp;
|
||||
|
||||
font/woff woff;
|
||||
font/woff2 woff2;
|
||||
|
||||
application/java-archive jar war ear;
|
||||
application/json json;
|
||||
application/mac-binhex40 hqx;
|
||||
application/msword doc;
|
||||
application/pdf pdf;
|
||||
application/postscript ps eps ai;
|
||||
application/rtf rtf;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
application/vnd.google-earth.kml+xml kml;
|
||||
application/vnd.google-earth.kmz kmz;
|
||||
application/vnd.ms-excel xls;
|
||||
application/vnd.ms-fontobject eot;
|
||||
application/vnd.ms-powerpoint ppt;
|
||||
application/vnd.oasis.opendocument.graphics odg;
|
||||
application/vnd.oasis.opendocument.presentation odp;
|
||||
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||
application/vnd.oasis.opendocument.text odt;
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||
pptx;
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
xlsx;
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
docx;
|
||||
application/vnd.wap.wmlc wmlc;
|
||||
application/x-7z-compressed 7z;
|
||||
application/x-cocoa cco;
|
||||
application/x-java-archive-diff jardiff;
|
||||
application/x-java-jnlp-file jnlp;
|
||||
application/x-makeself run;
|
||||
application/x-perl pl pm;
|
||||
application/x-pilot prc pdb;
|
||||
application/x-rar-compressed rar;
|
||||
application/x-redhat-package-manager rpm;
|
||||
application/x-sea sea;
|
||||
application/x-shockwave-flash swf;
|
||||
application/x-stuffit sit;
|
||||
application/x-tcl tcl tk;
|
||||
application/x-x509-ca-cert der pem crt;
|
||||
application/x-xpinstall xpi;
|
||||
application/xhtml+xml xhtml;
|
||||
application/xspf+xml xspf;
|
||||
application/zip zip;
|
||||
|
||||
application/octet-stream bin exe dll;
|
||||
application/octet-stream deb;
|
||||
application/octet-stream dmg;
|
||||
application/octet-stream iso img;
|
||||
application/octet-stream msi msp msm;
|
||||
|
||||
audio/midi mid midi kar;
|
||||
audio/mpeg mp3;
|
||||
audio/ogg ogg;
|
||||
audio/x-m4a m4a;
|
||||
audio/x-realaudio ra;
|
||||
|
||||
video/3gpp 3gpp 3gp;
|
||||
video/mp2t ts;
|
||||
video/mp4 mp4;
|
||||
video/mpeg mpeg mpg;
|
||||
video/quicktime mov;
|
||||
video/webm webm;
|
||||
video/x-flv flv;
|
||||
video/x-m4v m4v;
|
||||
video/x-mng mng;
|
||||
video/x-ms-asf asx asf;
|
||||
video/x-ms-wmv wmv;
|
||||
video/x-msvideo avi;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
proxy_http_version 1.1;
|
||||
proxy_ignore_client_abort off;
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_redirect off;
|
||||
proxy_send_timeout 86400s;
|
||||
proxy_max_temp_file_size 0;
|
||||
|
||||
proxy_set_header Accept-Encoding "";
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Authorization "";
|
||||
@@ -1,8 +0,0 @@
|
||||
root /dev/null;
|
||||
server_name $hostname;
|
||||
|
||||
client_max_body_size 512m;
|
||||
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Robots-Tag none;
|
||||
@@ -1,8 +0,0 @@
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
@@ -1,3 +0,0 @@
|
||||
upstream esphome {
|
||||
server unix:/var/run/esphome.sock;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
daemon off;
|
||||
user root;
|
||||
pid /var/run/nginx.pid;
|
||||
worker_processes 1;
|
||||
error_log /proc/1/fd/1 error;
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/includes/mime.types;
|
||||
|
||||
access_log off;
|
||||
default_type application/octet-stream;
|
||||
gzip on;
|
||||
keepalive_timeout 65;
|
||||
sendfile on;
|
||||
server_tokens off;
|
||||
|
||||
tcp_nodelay on;
|
||||
tcp_nopush on;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
include /etc/nginx/includes/upstream.conf;
|
||||
include /etc/nginx/servers/*.conf;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley)
|
||||
@@ -1,28 +0,0 @@
|
||||
server {
|
||||
{{ if not .ssl }}
|
||||
listen 6052 default_server;
|
||||
{{ else }}
|
||||
listen 6052 default_server ssl http2;
|
||||
{{ end }}
|
||||
|
||||
include /etc/nginx/includes/server_params.conf;
|
||||
include /etc/nginx/includes/proxy_params.conf;
|
||||
|
||||
{{ if .ssl }}
|
||||
include /etc/nginx/includes/ssl_params.conf;
|
||||
|
||||
ssl_certificate /ssl/{{ .certfile }};
|
||||
ssl_certificate_key /ssl/{{ .keyfile }};
|
||||
|
||||
# Redirect http requests to https on the same port.
|
||||
# https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/
|
||||
error_page 497 https://$http_host$request_uri;
|
||||
{{ end }}
|
||||
|
||||
# Clear Home Assistant Ingress header
|
||||
proxy_set_header X-HA-Ingress "";
|
||||
|
||||
location / {
|
||||
proxy_pass http://esphome;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
server {
|
||||
listen 127.0.0.1:{{ .port }} default_server;
|
||||
listen {{ .interface }}:{{ .port }} default_server;
|
||||
|
||||
include /etc/nginx/includes/server_params.conf;
|
||||
include /etc/nginx/includes/proxy_params.conf;
|
||||
|
||||
# Set Home Assistant Ingress header
|
||||
proxy_set_header X-HA-Ingress "YES";
|
||||
|
||||
location / {
|
||||
allow 172.30.32.2;
|
||||
allow 127.0.0.1;
|
||||
deny all;
|
||||
|
||||
proxy_pass http://esphome;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ fi
|
||||
|
||||
port=$(bashio::addon.ingress_port)
|
||||
|
||||
# Wait for NGINX to become available
|
||||
# Wait for the ESPHome Device Builder to become available
|
||||
bashio::net.wait_for "${port}" "127.0.0.1" 300
|
||||
|
||||
config=$(\
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# shellcheck shell=bash
|
||||
# ==============================================================================
|
||||
# Home Assistant Community Add-on: ESPHome
|
||||
# Take down the S6 supervision tree when ESPHome dashboard fails
|
||||
# Take down the S6 supervision tree when ESPHome Device Builder fails
|
||||
# ==============================================================================
|
||||
declare exit_code
|
||||
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
|
||||
@@ -10,7 +10,7 @@ readonly exit_code_service="${1}"
|
||||
readonly exit_code_signal="${2}"
|
||||
|
||||
bashio::log.info \
|
||||
"Service ESPHome dashboard exited with code ${exit_code_service}" \
|
||||
"Service ESPHome Device Builder exited with code ${exit_code_service}" \
|
||||
"(by signal ${exit_code_signal})"
|
||||
|
||||
if [[ "${exit_code_service}" -eq 256 ]]; then
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# shellcheck shell=bash
|
||||
# ==============================================================================
|
||||
# Community Hass.io Add-ons: ESPHome
|
||||
# Runs the ESPHome dashboard
|
||||
# Runs the ESPHome Device Builder
|
||||
# ==============================================================================
|
||||
readonly pio_cache_base=/data/cache/platformio
|
||||
|
||||
@@ -49,12 +49,21 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then
|
||||
rm -rf /config/esphome/.esphome
|
||||
fi
|
||||
|
||||
if bashio::config.true 'use_new_device_builder'; then
|
||||
bashio::log.info "Starting ESPHome Device Builder..."
|
||||
exec esphome-device-builder /config/esphome \
|
||||
--ha-addon \
|
||||
--ingress-port "$(bashio::addon.ingress_port)"
|
||||
# Only signal device-builder to expose the public LAN port when the operator
|
||||
# mapped port 6052, matching the legacy dashboard where nginx listened on the
|
||||
# fixed port 6052 only when it was configured. We use the mapping purely as a
|
||||
# presence check and don't forward the published value; device-builder binds
|
||||
# its default port 6052 (the fixed container port, as the legacy
|
||||
# "listen 6052" did). --ha-addon-allow-public is inert on its own: the no-auth
|
||||
# gate is the DISABLE_HA_AUTHENTICATION env var set above, so both opt-ins are
|
||||
# required to bind 6052 unauthenticated; either alone stays ingress-only.
|
||||
set --
|
||||
if bashio::var.has_value "$(bashio::addon.port 6052)"; then
|
||||
set -- --ha-addon-allow-public
|
||||
fi
|
||||
|
||||
bashio::log.info "Starting ESPHome dashboard..."
|
||||
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon
|
||||
bashio::log.info "Starting ESPHome Device Builder..."
|
||||
exec esphome-device-builder /config/esphome \
|
||||
--ha-addon \
|
||||
--ingress-port "$(bashio::addon.ingress_port)" \
|
||||
"$@"
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/command/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
# ==============================================================================
|
||||
# Community Hass.io Add-ons: ESPHome
|
||||
# Configures NGINX for use with ESPHome
|
||||
# ==============================================================================
|
||||
|
||||
# When the new device builder is enabled it serves HA ingress directly,
|
||||
# so nginx is not used at all -- skip configuration.
|
||||
if bashio::config.true 'use_new_device_builder'; then
|
||||
bashio::log.info "Skipping NGINX setup: new device builder serves ingress directly."
|
||||
bashio::exit.ok
|
||||
fi
|
||||
|
||||
mkdir -p /var/log/nginx
|
||||
|
||||
# Generate Ingress configuration
|
||||
bashio::var.json \
|
||||
interface "$(bashio::addon.ip_address)" \
|
||||
port "^$(bashio::addon.ingress_port)" \
|
||||
| tempio \
|
||||
-template /etc/nginx/templates/ingress.gtpl \
|
||||
-out /etc/nginx/servers/ingress.conf
|
||||
|
||||
# Generate direct access configuration, if enabled.
|
||||
if bashio::var.has_value "$(bashio::addon.port 6052)"; then
|
||||
bashio::config.require.ssl
|
||||
bashio::var.json \
|
||||
certfile "$(bashio::config 'certfile')" \
|
||||
keyfile "$(bashio::config 'keyfile')" \
|
||||
ssl "^$(bashio::config 'ssl')" \
|
||||
| tempio \
|
||||
-template /etc/nginx/templates/direct.gtpl \
|
||||
-out /etc/nginx/servers/direct.conf
|
||||
fi
|
||||
@@ -1 +0,0 @@
|
||||
oneshot
|
||||
@@ -1 +0,0 @@
|
||||
/etc/s6-overlay/s6-rc.d/init-nginx/run
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/command/with-contenv bashio
|
||||
# ==============================================================================
|
||||
# Community Hass.io Add-ons: ESPHome
|
||||
# Take down the S6 supervision tree when NGINX fails
|
||||
# ==============================================================================
|
||||
declare exit_code
|
||||
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
|
||||
readonly exit_code_service="${1}"
|
||||
readonly exit_code_signal="${2}"
|
||||
|
||||
bashio::log.info \
|
||||
"Service NGINX exited with code ${exit_code_service}" \
|
||||
"(by signal ${exit_code_signal})"
|
||||
|
||||
if [[ "${exit_code_service}" -eq 256 ]]; then
|
||||
if [[ "${exit_code_container}" -eq 0 ]]; then
|
||||
echo $((128 + $exit_code_signal)) > /run/s6-linux-init-container-results/exitcode
|
||||
fi
|
||||
[[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt
|
||||
elif [[ "${exit_code_service}" -ne 0 ]]; then
|
||||
if [[ "${exit_code_container}" -eq 0 ]]; then
|
||||
echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode
|
||||
fi
|
||||
exec /run/s6/basedir/bin/halt
|
||||
fi
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/command/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
# ==============================================================================
|
||||
# Community Hass.io Add-ons: ESPHome
|
||||
# Runs the NGINX proxy
|
||||
# ==============================================================================
|
||||
|
||||
# The new device builder handles HA ingress itself, so nginx is bypassed.
|
||||
# Block the longrun so s6 keeps the dependency satisfied, but exit 0 on
|
||||
# SIGTERM instead of being signal-killed; a 256/15 exit makes nginx/finish
|
||||
# stamp the container exit 143, which trips the Supervisor's SIGTERM check.
|
||||
if bashio::config.true 'use_new_device_builder'; then
|
||||
bashio::log.info "NGINX bypassed: new device builder serves ingress directly."
|
||||
trap 'exit 0' TERM
|
||||
sleep infinity &
|
||||
wait
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bashio::log.info "Waiting for ESPHome dashboard to come up..."
|
||||
|
||||
while [[ ! -S /var/run/esphome.sock ]]; do
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
bashio::log.info "Starting NGINX..."
|
||||
exec nginx
|
||||
@@ -1 +0,0 @@
|
||||
longrun
|
||||
+43
-5
@@ -504,6 +504,12 @@ def has_resolvable_address() -> bool:
|
||||
if has_ip_address():
|
||||
return True
|
||||
|
||||
# The dashboard pre-resolves the device and passes the IPs via
|
||||
# --mdns-address-cache/--dns-address-cache; honor a cached address even when the
|
||||
# device has mDNS disabled (e.g. a .local host found via ping).
|
||||
if CORE.address_cache and CORE.address_cache.get_addresses(CORE.address):
|
||||
return True
|
||||
|
||||
if has_mdns():
|
||||
return True
|
||||
|
||||
@@ -1464,12 +1470,29 @@ _LEGACY_REDACTION_REMOVAL = "2026.12.0"
|
||||
|
||||
def _redact_with_legacy_fallback(output: str) -> str:
|
||||
unmarked: set[str] = set()
|
||||
# Track the top-level ``substitutions:`` block. Its keys are arbitrary
|
||||
# user-chosen names with no schema validator, so the ``cv.sensitive(...)``
|
||||
# migration named in the warning can't be applied to them. Their values are
|
||||
# still redacted, but emitting the (unactionable) deprecation warning would
|
||||
# only confuse users.
|
||||
in_substitutions = False
|
||||
|
||||
def _replace(m: re.Match[str]) -> str:
|
||||
unmarked.add(m.group("key"))
|
||||
return f"{m.group('key')}: \\033[8m{m.group('val')}\\033[28m"
|
||||
|
||||
output = _LEGACY_REDACTION_RE.sub(_replace, output)
|
||||
lines = output.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
# A non-indented, non-blank line is a top-level key that opens or
|
||||
# closes the substitutions block.
|
||||
if line and not line[0].isspace():
|
||||
in_substitutions = line.startswith(f"{CONF_SUBSTITUTIONS}:")
|
||||
m = _LEGACY_REDACTION_RE.search(line)
|
||||
if m is None:
|
||||
continue
|
||||
if not in_substitutions:
|
||||
unmarked.add(m.group("key"))
|
||||
lines[i] = (
|
||||
f"{line[: m.start()]}{m.group('key')}: "
|
||||
f"\\033[8m{m.group('val')}\\033[28m{line[m.end() :]}"
|
||||
)
|
||||
output = "\n".join(lines)
|
||||
for key in sorted(unmarked):
|
||||
_LOGGER.warning(
|
||||
"Field '%s' is being redacted by a legacy substring heuristic. "
|
||||
@@ -1765,6 +1788,21 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
# Native ESP-IDF derives idedata from the build's compile_commands.json,
|
||||
# so the configuration must already be compiled.
|
||||
from esphome.espidf import toolchain as espidf_toolchain
|
||||
|
||||
idedata = espidf_toolchain.get_idedata()
|
||||
if idedata is None:
|
||||
_LOGGER.error(
|
||||
"No idedata available; compile the configuration first",
|
||||
)
|
||||
return 1
|
||||
|
||||
print(json.dumps(idedata, indent=2) + "\n")
|
||||
return 0
|
||||
|
||||
if not CORE.using_toolchain_platformio:
|
||||
_LOGGER.error(
|
||||
"The idedata command is not compatible with %s toolchain",
|
||||
|
||||
@@ -8,6 +8,17 @@ import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
|
||||
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
||||
# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
|
||||
# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
|
||||
# i.e. after IDF appends its default and before the options are consumed, and
|
||||
# applies project-wide like PlatformIO build_unflags.
|
||||
CPP_STANDARD_TEMPLATE = """\
|
||||
idf_build_get_property(esphome_cxx_compile_options CXX_COMPILE_OPTIONS)
|
||||
list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")
|
||||
list(APPEND esphome_cxx_compile_options "-std={standard}")
|
||||
idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")"""
|
||||
|
||||
|
||||
def get_available_components() -> list[str] | None:
|
||||
"""Get list of built-in ESP-IDF components from project_description.json.
|
||||
@@ -84,6 +95,12 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
for flag in project_compile_opts
|
||||
)
|
||||
|
||||
cpp_standard_options = (
|
||||
CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard)
|
||||
if CORE.cpp_standard
|
||||
else ""
|
||||
)
|
||||
|
||||
# Per-project list exposed as a CMake variable so converted PIO libs
|
||||
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
|
||||
# project-specific names into their cached CMakeLists.
|
||||
@@ -140,6 +157,8 @@ set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||
|
||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
{cpp_standard_options}
|
||||
|
||||
{extra_compile_options}
|
||||
|
||||
{managed_components_property}
|
||||
@@ -200,9 +219,6 @@ idf_component_register(
|
||||
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
||||
)
|
||||
|
||||
# Apply C++ standard
|
||||
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
|
||||
|
||||
# ESPHome linker options
|
||||
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
{link_opts_str}
|
||||
|
||||
@@ -33,12 +33,27 @@ def format_ini(data: dict[str, str | list[str]]) -> str:
|
||||
return content
|
||||
|
||||
|
||||
# All -std= variants a platform/framework may set by default, in both the GNU
|
||||
# and strict dialects; unflagged so the cg.set_cpp_standard() value is the
|
||||
# only standard left in the build.
|
||||
CPP_STD_VARIANTS = [
|
||||
f"{prefix}{year}"
|
||||
for year in ("11", "14", "17", "20", "23", "26", "2a", "2b", "2c")
|
||||
for prefix in ("gnu++", "c++")
|
||||
]
|
||||
|
||||
|
||||
def get_ini_content():
|
||||
CORE.add_platformio_option(
|
||||
"lib_deps",
|
||||
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
||||
+ ["${common.lib_deps}"],
|
||||
)
|
||||
if CORE.cpp_standard:
|
||||
for variant in CPP_STD_VARIANTS:
|
||||
if variant != CORE.cpp_standard:
|
||||
CORE.add_build_unflag(f"-std={variant}")
|
||||
CORE.add_build_flag(f"-std={CORE.cpp_standard}")
|
||||
# Sort to avoid changing build flags order
|
||||
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
||||
|
||||
|
||||
@@ -66,15 +66,18 @@ float ADCSensor::sample() {
|
||||
}
|
||||
|
||||
uint8_t pin = this->pin_->get_pin();
|
||||
#ifdef CYW43_USES_VSYS_PIN
|
||||
#if defined(CYW43_USES_VSYS_PIN) && defined(USE_WIFI)
|
||||
if (pin == PICO_VSYS_PIN) {
|
||||
// Measuring VSYS on Raspberry Pico W needs to be wrapped with
|
||||
// `cyw43_thread_enter()`/`cyw43_thread_exit()` as discussed in
|
||||
// https://github.com/raspberrypi/pico-sdk/issues/1222, since Wifi chip and
|
||||
// VSYS ADC both share GPIO29
|
||||
// VSYS ADC both share GPIO29.
|
||||
// The USE_WIFI guard is required because CYW43_USES_VSYS_PIN can be defined
|
||||
// transitively (e.g. via lwip_wrap.h) even on non-WiFi boards where the CYW43
|
||||
// driver is never initialized; calling cyw43_thread_enter() there hard-faults.
|
||||
cyw43_thread_enter();
|
||||
}
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
#endif // defined(CYW43_USES_VSYS_PIN) && defined(USE_WIFI)
|
||||
|
||||
adc_gpio_init(pin);
|
||||
adc_select_input(pin - 26);
|
||||
@@ -84,11 +87,11 @@ float ADCSensor::sample() {
|
||||
aggr.add_sample(raw);
|
||||
}
|
||||
|
||||
#ifdef CYW43_USES_VSYS_PIN
|
||||
#if defined(CYW43_USES_VSYS_PIN) && defined(USE_WIFI)
|
||||
if (pin == PICO_VSYS_PIN) {
|
||||
cyw43_thread_exit();
|
||||
}
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
#endif // defined(CYW43_USES_VSYS_PIN) && defined(USE_WIFI)
|
||||
|
||||
if (this->output_raw_) {
|
||||
return aggr.aggregate();
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
CODEOWNERS = ["@kpfleming"]
|
||||
|
||||
@@ -87,14 +87,24 @@ void ADE7880::update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_
|
||||
sensor->publish_state(f(val));
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
void ADE7880::update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f) {
|
||||
if (sensor == nullptr) {
|
||||
void ADE7880::update_active_energy_(PowerChannel *channel, uint16_t a_register) {
|
||||
if (channel->forward_active_energy == nullptr && channel->reverse_active_energy == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
float val = this->read_s32_register16_(a_register);
|
||||
sensor->publish_state(f(val));
|
||||
// The ADE7880 has no separate forward/reverse active energy accumulators. The xWATTHR registers
|
||||
// accumulate signed energy since the last read (positive = imported/forward, negative = exported/
|
||||
// reverse), so split the value by sign into the forward and reverse running totals.
|
||||
float val = this->read_s32_register16_(a_register) / 14400.0f;
|
||||
if (val >= 0.0f) {
|
||||
if (channel->forward_active_energy != nullptr) {
|
||||
channel->forward_active_energy->publish_state(channel->forward_active_energy_total += val);
|
||||
}
|
||||
} else {
|
||||
if (channel->reverse_active_energy != nullptr) {
|
||||
channel->reverse_active_energy->publish_state(channel->reverse_active_energy_total -= val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ADE7880::update() {
|
||||
@@ -117,12 +127,7 @@ void ADE7880::update() {
|
||||
this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; });
|
||||
this->update_sensor_from_s16_register16_(chan->power_factor, APF,
|
||||
[](float val) { return std::abs(val / -327.68f); });
|
||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
|
||||
return chan->forward_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) {
|
||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_active_energy_(chan, AWATTHR);
|
||||
}
|
||||
|
||||
if (this->channel_b_ != nullptr) {
|
||||
@@ -133,12 +138,7 @@ void ADE7880::update() {
|
||||
this->update_sensor_from_s24zp_register16_(chan->apparent_power, BVA, [](float val) { return val / 100.0f; });
|
||||
this->update_sensor_from_s16_register16_(chan->power_factor, BPF,
|
||||
[](float val) { return std::abs(val / -327.68f); });
|
||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
|
||||
return chan->forward_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) {
|
||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_active_energy_(chan, BWATTHR);
|
||||
}
|
||||
|
||||
if (this->channel_c_ != nullptr) {
|
||||
@@ -149,12 +149,7 @@ void ADE7880::update() {
|
||||
this->update_sensor_from_s24zp_register16_(chan->apparent_power, CVA, [](float val) { return val / 100.0f; });
|
||||
this->update_sensor_from_s16_register16_(chan->power_factor, CPF,
|
||||
[](float val) { return std::abs(val / -327.68f); });
|
||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
|
||||
return chan->forward_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) {
|
||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_active_energy_(chan, CWATTHR);
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "update took %" PRIu32 " ms", millis() - start);
|
||||
|
||||
@@ -105,7 +105,8 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent {
|
||||
// the callable will be passed a 'float' value and is expected to return a 'float'
|
||||
template<typename F> void update_sensor_from_s24zp_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
|
||||
template<typename F> void update_sensor_from_s16_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
|
||||
template<typename F> void update_sensor_from_s32_register16_(sensor::Sensor *sensor, uint16_t a_register, F &&f);
|
||||
|
||||
void update_active_energy_(PowerChannel *channel, uint16_t a_register);
|
||||
|
||||
void reset_device_();
|
||||
|
||||
|
||||
@@ -84,9 +84,7 @@ constexpr uint16_t CWATTHR = 0xE402;
|
||||
constexpr uint16_t AFWATTHR = 0xE403;
|
||||
constexpr uint16_t BFWATTHR = 0xE404;
|
||||
constexpr uint16_t CFWATTHR = 0xE405;
|
||||
constexpr uint16_t ARWATTHR = 0xE406;
|
||||
constexpr uint16_t BRWATTHR = 0xE407;
|
||||
constexpr uint16_t CRWATTHR = 0xE408;
|
||||
// 0xE406-0xE408 are reserved on the ADE7880 (it does not implement total reactive energy accumulation)
|
||||
constexpr uint16_t AFVARHR = 0xE409;
|
||||
constexpr uint16_t BFVARHR = 0xE40A;
|
||||
constexpr uint16_t CFVARHR = 0xE40B;
|
||||
|
||||
@@ -21,7 +21,7 @@ from esphome.const import (
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"]
|
||||
CODEOWNERS = ["@ncareau", "@jeromelaban"]
|
||||
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ async def to_code(config):
|
||||
)
|
||||
if data.mp3_support:
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-mp3", ref="0.2.1")
|
||||
add_idf_component(name="esphome/micro-mp3", ref="0.2.3")
|
||||
_emit_memory_pair(
|
||||
data.mp3.buffer_memory,
|
||||
"CONFIG_MP3_DECODER_PREFER_PSRAM",
|
||||
|
||||
@@ -68,11 +68,15 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
|
||||
void loop() override;
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
void register_connection(BluetoothConnection *connection) {
|
||||
// maybe_unused: in a passive proxy (active: false) MAX is 0, the body below is removed, and connection is unused.
|
||||
void register_connection([[maybe_unused]] BluetoothConnection *connection) {
|
||||
// Guard the always-false comparison (-Wtype-limits) in a passive proxy (active: false), where MAX is 0.
|
||||
#if BLUETOOTH_PROXY_MAX_CONNECTIONS > 0
|
||||
if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
|
||||
this->connections_[this->connection_count_++] = connection;
|
||||
connection->proxy_ = this;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.motion import MotionComponent
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
CONF_BMI270_ID = "bmi270_id"
|
||||
# C++ namespace / class
|
||||
bmi270_ns = cg.esphome_ns.namespace("bmi270")
|
||||
BMI270Component = bmi270_ns.class_("BMI270Component", MotionComponent, i2c.I2CDevice)
|
||||
@@ -0,0 +1,209 @@
|
||||
#include "bmi270.h"
|
||||
#include "bmi270_config.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::bmi270 {
|
||||
|
||||
static const char *const TAG = "bmi270";
|
||||
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
static const size_t MAX_I2C_BUFFER_SIZE = 32;
|
||||
#else
|
||||
static const size_t MAX_I2C_BUFFER_SIZE = 256;
|
||||
#endif
|
||||
|
||||
// Configuration blob upload
|
||||
// The BMI270 requires a firmware config blob to be written to its internal
|
||||
// memory after every power-on before sensors can be used.
|
||||
|
||||
bool BMI270Component::load_config_file_() {
|
||||
// 1. Disable advanced power-save so the config port is accessible
|
||||
if (!this->write_byte(BMI270_REG_PWR_CONF, 0x00))
|
||||
return false;
|
||||
delay(1);
|
||||
|
||||
// 2. Prepare config load: write 0x00 to INIT_CTRL to start
|
||||
if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x00))
|
||||
return false;
|
||||
|
||||
// 3. Burst-write the config in pages
|
||||
const uint8_t *cfg = BMI270_CONFIG_FILE;
|
||||
constexpr size_t cfg_len = sizeof(BMI270_CONFIG_FILE);
|
||||
size_t index = 0;
|
||||
|
||||
while (index != cfg_len) {
|
||||
// Set the page address in INIT_ADDR registers
|
||||
uint8_t addr_lsb = (uint8_t) ((index / 2) & 0x0F);
|
||||
uint8_t addr_msb = (uint8_t) ((index / 2) >> 4);
|
||||
if (!this->write_byte(BMI270_REG_INIT_ADDR_0, addr_lsb))
|
||||
return false;
|
||||
if (!this->write_byte(BMI270_REG_INIT_ADDR_0 + 1, addr_msb))
|
||||
return false;
|
||||
|
||||
// Write a burst of up to the maximum allowed size
|
||||
size_t burst = clamp_at_most(cfg_len - index, MAX_I2C_BUFFER_SIZE);
|
||||
if (this->write_register(BMI270_REG_INIT_DATA, cfg + index, burst) != i2c::ERROR_OK)
|
||||
return false;
|
||||
|
||||
index += burst;
|
||||
}
|
||||
|
||||
// 4. Signal end of config load
|
||||
if (!this->write_byte(BMI270_REG_INIT_CTRL, 0x01))
|
||||
return false;
|
||||
delay(20); // spec: wait ≥20 ms for init to complete
|
||||
|
||||
// 5. Check INTERNAL_STATUS: bit[0:3] should be 0x01 ("initialisation OK")
|
||||
uint8_t status = 0;
|
||||
if (!this->read_byte(BMI270_REG_INTERNAL_STATUS, &status))
|
||||
return false;
|
||||
if ((status & 0x0F) != 0x01) {
|
||||
ESP_LOGE(TAG, "Config load failed: INTERNAL_STATUS=0x%02X (expected 0x01)", status);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// setup() ─
|
||||
|
||||
void BMI270Component::setup() {
|
||||
MotionComponent::setup();
|
||||
// 1. Verify chip ID
|
||||
uint8_t chip_id = 0;
|
||||
if (!this->read_byte(BMI270_REG_CHIP_ID, &chip_id)) {
|
||||
ESP_LOGE(TAG, "Failed to read chip ID – check wiring / address");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (chip_id != BMI270_CHIP_ID_VALUE) {
|
||||
ESP_LOGE(TAG, "Wrong chip ID: 0x%02X (expected 0x%02X)", chip_id, BMI270_CHIP_ID_VALUE);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Chip ID: 0x%02X", chip_id);
|
||||
|
||||
// 2. Soft-reset via CMD register (0x7E = 0xB6)
|
||||
if (!this->write_byte(0x7E, 0xB6)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(20);
|
||||
|
||||
// 4. Upload the configuration blob
|
||||
if (!load_config_file_()) {
|
||||
ESP_LOGE(TAG, "Config file upload failed");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Config blob uploaded ✓");
|
||||
|
||||
// 5. Configure accelerometer
|
||||
// ACC_CONF: ODR | BWP(0x2 = normal avg4) | perf_mode(1)
|
||||
uint8_t acc_conf = (uint8_t) (accel_odr_) | (0x2 << 4) | (1 << 7);
|
||||
if (!this->write_byte(BMI270_REG_ACC_CONF, acc_conf)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (!this->write_byte(BMI270_REG_ACC_RANGE, (uint8_t) accel_range_)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. Configure gyroscope
|
||||
// GYR_CONF: ODR | BWP(0x2 = normal) | noise_perf(1) | filter_perf(1)
|
||||
uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (0x2 << 4) | (1 << 6) | (1 << 7);
|
||||
if (!this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (!this->write_byte(BMI270_REG_GYR_RANGE, (uint8_t) gyro_range_)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// 7. Enable accelerometer, gyroscope, and temperature sensor
|
||||
// PWR_CTRL bits: temp_en[3] | gyr_en[2] | acc_en[1]
|
||||
if (!this->write_byte(BMI270_REG_PWR_CTRL, 0x0E)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(5);
|
||||
|
||||
// 8. Re-enable advanced power save (optional; keeps current low between reads)
|
||||
// Disabled here for simplicity – leave in performance mode
|
||||
if (!this->write_byte(BMI270_REG_PWR_CONF, 0x02)) { // bit1 = fifo_self_wakeup
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(TAG, "BMI270 initialised successfully");
|
||||
}
|
||||
|
||||
void BMI270Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BMI270 IMU:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, " Communication failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
|
||||
static constexpr const char *const GYRO_RANGE_STRS[] = {"±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s"};
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
|
||||
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
|
||||
MotionComponent::dump_config();
|
||||
}
|
||||
|
||||
// update() ─
|
||||
// Reads all 6 axes + temperature in one block
|
||||
|
||||
bool BMI270Component::update_data(motion::MotionData &data) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
// Accelerometer: registers 0x0C–0x11 (6 bytes: x_lsb, x_msb, y_lsb, y_msb, z_lsb, z_msb)
|
||||
uint8_t raw_data[REG_READ_LEN];
|
||||
if (!this->read_bytes(BMI270_REG_DATA_8, raw_data, REG_READ_LEN)) {
|
||||
ESP_LOGW(TAG, "Failed to read IMU data");
|
||||
return false;
|
||||
}
|
||||
// Scale factor: LSB/g depends on range
|
||||
// raw is a signed 16-bit value; full-scale = range_g * 2^15 lsb
|
||||
static constexpr float ACCEL_SCALE[] = {
|
||||
2.0f / 32768.0f,
|
||||
4.0f / 32768.0f,
|
||||
8.0f / 32768.0f,
|
||||
16.0f / 32768.0f,
|
||||
};
|
||||
float scale = ACCEL_SCALE[this->accel_range_];
|
||||
|
||||
data.acceleration[motion::X_AXIS] = (int16_t) ((raw_data[1] << 8) | raw_data[0]) * scale;
|
||||
data.acceleration[motion::Y_AXIS] = (int16_t) ((raw_data[3] << 8) | raw_data[2]) * scale;
|
||||
data.acceleration[motion::Z_AXIS] = (int16_t) ((raw_data[5] << 8) | raw_data[4]) * scale;
|
||||
|
||||
// Gyroscope: registers 0x12–0x17 (6 bytes)
|
||||
// Scale: full-scale range / 2^15
|
||||
static constexpr float GYRO_SCALE[] = {
|
||||
2000.0f / 32768.0f, 1000.0f / 32768.0f, 500.0f / 32768.0f, 250.0f / 32768.0f, 125.0f / 32768.0f,
|
||||
};
|
||||
static constexpr uint8_t GYR_OFFS = BMI270_REG_DATA_14 - BMI270_REG_DATA_8;
|
||||
scale = GYRO_SCALE[this->gyro_range_];
|
||||
|
||||
data.angular_rate[motion::X_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 1] << 8) | raw_data[GYR_OFFS + 0]) * scale;
|
||||
data.angular_rate[motion::Y_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 3] << 8) | raw_data[GYR_OFFS + 2]) * scale;
|
||||
data.angular_rate[motion::Z_AXIS] = (int16_t) ((raw_data[GYR_OFFS + 5] << 8) | raw_data[GYR_OFFS + 4]) * scale;
|
||||
|
||||
if (this->temperature_callback_.empty())
|
||||
return true;
|
||||
// Temperature: registers 0x22–0x23
|
||||
// Formula from datasheet: T[°C] = raw / 512 + 23
|
||||
static constexpr uint8_t TEMP_OFFS = BMI270_REG_TEMP_0 - BMI270_REG_DATA_8;
|
||||
int16_t raw_t = (int16_t) ((raw_data[TEMP_OFFS + 1] << 8) | raw_data[TEMP_OFFS + 0]);
|
||||
float temperature = (raw_t / 512.0f) + 23.0f;
|
||||
this->temperature_callback_.call(temperature);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::bmi270
|
||||
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/motion/motion_component.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include <functional>
|
||||
|
||||
namespace esphome::bmi270 {
|
||||
|
||||
// Register map
|
||||
static const uint8_t BMI270_REG_CHIP_ID = 0x00;
|
||||
static const uint8_t BMI270_REG_ERR_REG = 0x02;
|
||||
static const uint8_t BMI270_REG_STATUS = 0x03;
|
||||
static const uint8_t BMI270_REG_DATA_8 = 0x0C; // ACC_X LSB
|
||||
static const uint8_t BMI270_REG_DATA_14 = 0x12; // GYR_X LSB
|
||||
static const uint8_t BMI270_REG_TEMP_0 = 0x22;
|
||||
static const uint8_t BMI270_REG_TEMP_MSB = 0x23; // temperature (2 bytes big-endian ish)
|
||||
|
||||
static constexpr uint8_t REG_READ_LEN =
|
||||
BMI270_REG_TEMP_MSB - BMI270_REG_DATA_8 +
|
||||
1; // 0x23 - 0x0C + 1 = 0x18 bytes total for accel(6) + gyro(6) + temp(2) + padding(4)
|
||||
|
||||
static const uint8_t BMI270_REG_PWR_CONF = 0x7C;
|
||||
static const uint8_t BMI270_REG_PWR_CTRL = 0x7D;
|
||||
static const uint8_t BMI270_REG_INIT_CTRL = 0x59;
|
||||
static const uint8_t BMI270_REG_INIT_DATA = 0x5E;
|
||||
static const uint8_t BMI270_REG_INIT_ADDR_0 = 0x5B;
|
||||
static const uint8_t BMI270_REG_INTERNAL_STATUS = 0x21;
|
||||
static const uint8_t BMI270_REG_ACC_CONF = 0x40;
|
||||
static const uint8_t BMI270_REG_ACC_RANGE = 0x41;
|
||||
static const uint8_t BMI270_REG_GYR_CONF = 0x42;
|
||||
static const uint8_t BMI270_REG_GYR_RANGE = 0x43;
|
||||
|
||||
static const uint8_t BMI270_CHIP_ID_VALUE = 0x24;
|
||||
|
||||
// Accelerometer range options
|
||||
enum BMI270AccelRange : uint8_t {
|
||||
BMI270_ACCEL_RANGE_2G = 0x00,
|
||||
BMI270_ACCEL_RANGE_4G = 0x01,
|
||||
BMI270_ACCEL_RANGE_8G = 0x02,
|
||||
BMI270_ACCEL_RANGE_16G = 0x03,
|
||||
};
|
||||
|
||||
// Accelerometer ODR options
|
||||
enum BMI270AccelODR : uint8_t {
|
||||
BMI270_ACCEL_ODR_12_5 = 0x05,
|
||||
BMI270_ACCEL_ODR_25 = 0x06,
|
||||
BMI270_ACCEL_ODR_50 = 0x07,
|
||||
BMI270_ACCEL_ODR_100 = 0x08,
|
||||
BMI270_ACCEL_ODR_200 = 0x09,
|
||||
BMI270_ACCEL_ODR_400 = 0x0A,
|
||||
BMI270_ACCEL_ODR_800 = 0x0B,
|
||||
BMI270_ACCEL_ODR_1600 = 0x0C,
|
||||
};
|
||||
|
||||
// Gyroscope range options
|
||||
enum BMI270GyroRange : uint8_t {
|
||||
BMI270_GYRO_RANGE_2000 = 0x00,
|
||||
BMI270_GYRO_RANGE_1000 = 0x01,
|
||||
BMI270_GYRO_RANGE_500 = 0x02,
|
||||
BMI270_GYRO_RANGE_250 = 0x03,
|
||||
BMI270_GYRO_RANGE_125 = 0x04,
|
||||
};
|
||||
|
||||
// Gyroscope ODR options
|
||||
enum BMI270GyroODR : uint8_t {
|
||||
BMI270_GYRO_ODR_25 = 0x06,
|
||||
BMI270_GYRO_ODR_50 = 0x07,
|
||||
BMI270_GYRO_ODR_100 = 0x08,
|
||||
BMI270_GYRO_ODR_200 = 0x09,
|
||||
BMI270_GYRO_ODR_400 = 0x0A,
|
||||
BMI270_GYRO_ODR_800 = 0x0B,
|
||||
BMI270_GYRO_ODR_1600 = 0x0C,
|
||||
BMI270_GYRO_ODR_3200 = 0x0D,
|
||||
};
|
||||
|
||||
// ---Data class
|
||||
|
||||
// Main component class
|
||||
class BMI270Component : public motion::MotionComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
// Lifecycle
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
// Configuration setters
|
||||
void set_accel_range(BMI270AccelRange r) { this->accel_range_ = r; }
|
||||
void set_accel_odr(BMI270AccelODR o) { this->accel_odr_ = o; }
|
||||
void set_gyro_range(BMI270GyroRange r) { this->gyro_range_ = r; }
|
||||
void set_gyro_odr(BMI270GyroODR o) { this->gyro_odr_ = o; }
|
||||
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
|
||||
|
||||
protected:
|
||||
bool update_data(motion::MotionData &data) override;
|
||||
bool load_config_file_();
|
||||
|
||||
// Config
|
||||
BMI270AccelRange accel_range_{BMI270_ACCEL_RANGE_4G};
|
||||
BMI270AccelODR accel_odr_{BMI270_ACCEL_ODR_100};
|
||||
BMI270GyroRange gyro_range_{BMI270_GYRO_RANGE_2000};
|
||||
BMI270GyroODR gyro_odr_{BMI270_GYRO_ODR_200};
|
||||
|
||||
LazyCallbackManager<void(float)> temperature_callback_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::bmi270
|
||||
@@ -0,0 +1,483 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::bmi270 {
|
||||
|
||||
/**
|
||||
BMI270 configuration file (chip ID 0x24, firmware v2.86.1)
|
||||
Source: Bosch Sensortec BMI270_SensorAPI (BSD-3-Clause)
|
||||
https://github.com/boschsensortec/BMI270_SensorAPI
|
||||
|
||||
Copyright (c) 2023 Bosch Sensortec GmbH. All rights reserved.
|
||||
|
||||
BSD-3-Clause
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
This blob MUST be written to the chip's internal INIT_DATA register
|
||||
after every power cycle, before any sensor data can be read.
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
static constexpr uint8_t BMI270_CONFIG_FILE[] = {
|
||||
0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x3d, 0xb1, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x91, 0x03, 0x80, 0x2e, 0xbc,
|
||||
0xb0, 0x80, 0x2e, 0xa3, 0x03, 0xc8, 0x2e, 0x00, 0x2e, 0x80, 0x2e, 0x00, 0xb0, 0x50, 0x30, 0x21, 0x2e, 0x59, 0xf5,
|
||||
0x10, 0x30, 0x21, 0x2e, 0x6a, 0xf5, 0x80, 0x2e, 0x3b, 0x03, 0x00, 0x00, 0x00, 0x00, 0x08, 0x19, 0x01, 0x00, 0x22,
|
||||
0x00, 0x75, 0x00, 0x00, 0x10, 0x00, 0x10, 0xd1, 0x00, 0xb3, 0x43, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0xe0, 0x5f, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x92, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x19, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
|
||||
0xe0, 0xaa, 0x38, 0x05, 0xe0, 0x90, 0x30, 0xfa, 0x00, 0x96, 0x00, 0x4b, 0x09, 0x11, 0x00, 0x11, 0x00, 0x02, 0x00,
|
||||
0x2d, 0x01, 0xd4, 0x7b, 0x3b, 0x01, 0xdb, 0x7a, 0x04, 0x00, 0x3f, 0x7b, 0xcd, 0x6c, 0xc3, 0x04, 0x85, 0x09, 0xc3,
|
||||
0x04, 0xec, 0xe6, 0x0c, 0x46, 0x01, 0x00, 0x27, 0x00, 0x19, 0x00, 0x96, 0x00, 0xa0, 0x00, 0x01, 0x00, 0x0c, 0x00,
|
||||
0xf0, 0x3c, 0x00, 0x01, 0x01, 0x00, 0x03, 0x00, 0x01, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x32, 0x00, 0x05, 0x00, 0xee,
|
||||
0x06, 0x04, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x04, 0x00, 0xa8, 0x05, 0xee, 0x06, 0x00, 0x04, 0xbc, 0x02, 0xb3, 0x00,
|
||||
0x85, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0xb4, 0x00, 0x01, 0x00, 0xb9, 0x00, 0x01, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x80, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0xde,
|
||||
0x00, 0xeb, 0x00, 0xda, 0x00, 0x00, 0x0c, 0xff, 0x0f, 0x00, 0x04, 0xc0, 0x00, 0x5b, 0xf5, 0xc9, 0x01, 0x1e, 0xf2,
|
||||
0x80, 0x00, 0x3f, 0xff, 0x19, 0xf4, 0x58, 0xf5, 0x66, 0xf5, 0x64, 0xf5, 0xc0, 0xf1, 0xf0, 0x00, 0xe0, 0x00, 0xcd,
|
||||
0x01, 0xd3, 0x01, 0xdb, 0x01, 0xff, 0x7f, 0xff, 0x01, 0xe4, 0x00, 0x74, 0xf7, 0xf3, 0x00, 0xfa, 0x00, 0xff, 0x3f,
|
||||
0xca, 0x03, 0x6c, 0x38, 0x56, 0xfe, 0x44, 0xfd, 0xbc, 0x02, 0xf9, 0x06, 0x00, 0xfc, 0x12, 0x02, 0xae, 0x01, 0x58,
|
||||
0xfa, 0x9a, 0xfd, 0x77, 0x05, 0xbb, 0x02, 0x96, 0x01, 0x95, 0x01, 0x7f, 0x01, 0x82, 0x01, 0x89, 0x01, 0x87, 0x01,
|
||||
0x88, 0x01, 0x8a, 0x01, 0x8c, 0x01, 0x8f, 0x01, 0x8d, 0x01, 0x92, 0x01, 0x91, 0x01, 0xdd, 0x00, 0x9f, 0x01, 0x7e,
|
||||
0x01, 0xdb, 0x00, 0xb6, 0x01, 0x70, 0x69, 0x26, 0xd3, 0x9c, 0x07, 0x1f, 0x05, 0x9d, 0x00, 0x00, 0x08, 0xbc, 0x05,
|
||||
0x37, 0xfa, 0xa2, 0x01, 0xaa, 0x01, 0xa1, 0x01, 0xa8, 0x01, 0xa0, 0x01, 0xa8, 0x05, 0xb4, 0x01, 0xb4, 0x01, 0xce,
|
||||
0x00, 0xd0, 0x00, 0xfc, 0x00, 0xc5, 0x01, 0xff, 0xfb, 0xb1, 0x00, 0x00, 0x38, 0x00, 0x30, 0xfd, 0xf5, 0xfc, 0xf5,
|
||||
0xcd, 0x01, 0xa0, 0x00, 0x5f, 0xff, 0x00, 0x40, 0xff, 0x00, 0x00, 0x80, 0x6d, 0x0f, 0xeb, 0x00, 0x7f, 0xff, 0xc2,
|
||||
0xf5, 0x68, 0xf7, 0xb3, 0xf1, 0x67, 0x0f, 0x5b, 0x0f, 0x61, 0x0f, 0x80, 0x0f, 0x58, 0xf7, 0x5b, 0xf7, 0x83, 0x0f,
|
||||
0x86, 0x00, 0x72, 0x0f, 0x85, 0x0f, 0xc6, 0xf1, 0x7f, 0x0f, 0x6c, 0xf7, 0x00, 0xe0, 0x00, 0xff, 0xd1, 0xf5, 0x87,
|
||||
0x0f, 0x8a, 0x0f, 0xff, 0x03, 0xf0, 0x3f, 0x8b, 0x00, 0x8e, 0x00, 0x90, 0x00, 0xb9, 0x00, 0x2d, 0xf5, 0xca, 0xf5,
|
||||
0xcb, 0x01, 0x20, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x50, 0x98, 0x2e,
|
||||
0xd7, 0x0e, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30, 0xf0, 0x7f, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x00,
|
||||
0x2e, 0x01, 0x80, 0x08, 0xa2, 0xfb, 0x2f, 0x98, 0x2e, 0xba, 0x03, 0x21, 0x2e, 0x19, 0x00, 0x01, 0x2e, 0xee, 0x00,
|
||||
0x00, 0xb2, 0x07, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x03, 0x2f, 0x01, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x07,
|
||||
0xcc, 0x01, 0x2e, 0xdd, 0x00, 0x00, 0xb2, 0x27, 0x2f, 0x05, 0x2e, 0x8a, 0x00, 0x05, 0x52, 0x98, 0x2e, 0xc7, 0xc1,
|
||||
0x03, 0x2e, 0xe9, 0x00, 0x40, 0xb2, 0xf0, 0x7f, 0x08, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x04, 0x2f, 0x00,
|
||||
0x30, 0x21, 0x2e, 0xe9, 0x00, 0x98, 0x2e, 0xb4, 0xb1, 0x01, 0x2e, 0x18, 0x00, 0x00, 0xb2, 0x10, 0x2f, 0x05, 0x50,
|
||||
0x98, 0x2e, 0x4d, 0xc3, 0x05, 0x50, 0x98, 0x2e, 0x5a, 0xc7, 0x98, 0x2e, 0xf9, 0xb4, 0x98, 0x2e, 0x54, 0xb2, 0x98,
|
||||
0x2e, 0x67, 0xb6, 0x98, 0x2e, 0x17, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x01, 0x2e, 0xef, 0x00, 0x00, 0xb2,
|
||||
0x04, 0x2f, 0x98, 0x2e, 0x7a, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0xef, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0xae, 0x0b,
|
||||
0x2f, 0x01, 0x2e, 0xdd, 0x00, 0x00, 0xb2, 0x07, 0x2f, 0x05, 0x52, 0x98, 0x2e, 0x8e, 0x0e, 0x00, 0xb2, 0x02, 0x2f,
|
||||
0x10, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x01, 0x2e, 0x7d, 0x00, 0x00, 0x90, 0x90, 0x2e, 0xf1, 0x02, 0x01, 0x2e, 0xd7,
|
||||
0x00, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x2f, 0x0e, 0x00, 0x30, 0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, 0x7b, 0x00,
|
||||
0x00, 0xb2, 0x12, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x00, 0x90, 0x02, 0x2f, 0x98, 0x2e, 0x1f, 0x0e, 0x09, 0x2d, 0x98,
|
||||
0x2e, 0x81, 0x0d, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0x90, 0x02, 0x2f, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x00, 0x30,
|
||||
0x21, 0x2e, 0x7b, 0x00, 0x01, 0x2e, 0x7c, 0x00, 0x00, 0xb2, 0x90, 0x2e, 0x09, 0x03, 0x01, 0x2e, 0x7c, 0x00, 0x01,
|
||||
0x31, 0x01, 0x08, 0x00, 0xb2, 0x04, 0x2f, 0x98, 0x2e, 0x47, 0xcb, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x81, 0x30,
|
||||
0x01, 0x2e, 0x7c, 0x00, 0x01, 0x08, 0x00, 0xb2, 0x61, 0x2f, 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0xd4, 0x00, 0x98,
|
||||
0xbc, 0x98, 0xb8, 0x05, 0xb2, 0x0f, 0x58, 0x23, 0x2f, 0x07, 0x90, 0x09, 0x54, 0x00, 0x30, 0x37, 0x2f, 0x15, 0x41,
|
||||
0x04, 0x41, 0xdc, 0xbe, 0x44, 0xbe, 0xdc, 0xba, 0x2c, 0x01, 0x61, 0x00, 0x0f, 0x56, 0x4a, 0x0f, 0x0c, 0x2f, 0xd1,
|
||||
0x42, 0x94, 0xb8, 0xc1, 0x42, 0x11, 0x30, 0x05, 0x2e, 0x6a, 0xf7, 0x2c, 0xbd, 0x2f, 0xb9, 0x80, 0xb2, 0x08, 0x22,
|
||||
0x98, 0x2e, 0xc3, 0xb7, 0x21, 0x2d, 0x61, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x98, 0x2e, 0xc3, 0xb7, 0x00, 0x30, 0x21,
|
||||
0x2e, 0x5a, 0xf5, 0x18, 0x2d, 0xe1, 0x7f, 0x50, 0x30, 0x98, 0x2e, 0xfa, 0x03, 0x0f, 0x52, 0x07, 0x50, 0x50, 0x42,
|
||||
0x70, 0x30, 0x0d, 0x54, 0x42, 0x42, 0x7e, 0x82, 0xe2, 0x6f, 0x80, 0xb2, 0x42, 0x42, 0x05, 0x2f, 0x21, 0x2e, 0xd4,
|
||||
0x00, 0x10, 0x30, 0x98, 0x2e, 0xc3, 0xb7, 0x03, 0x2d, 0x60, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x01, 0x2e, 0xd4, 0x00,
|
||||
0x06, 0x90, 0x18, 0x2f, 0x01, 0x2e, 0x76, 0x00, 0x0b, 0x54, 0x07, 0x52, 0xe0, 0x7f, 0x98, 0x2e, 0x7a, 0xc1, 0xe1,
|
||||
0x6f, 0x08, 0x1a, 0x40, 0x30, 0x08, 0x2f, 0x21, 0x2e, 0xd4, 0x00, 0x20, 0x30, 0x98, 0x2e, 0xaf, 0xb7, 0x50, 0x32,
|
||||
0x98, 0x2e, 0xfa, 0x03, 0x05, 0x2d, 0x98, 0x2e, 0x38, 0x0e, 0x00, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x00, 0x30, 0x21,
|
||||
0x2e, 0x7c, 0x00, 0x18, 0x2d, 0x01, 0x2e, 0xd4, 0x00, 0x03, 0xaa, 0x01, 0x2f, 0x98, 0x2e, 0x45, 0x0e, 0x01, 0x2e,
|
||||
0xd4, 0x00, 0x3f, 0x80, 0x03, 0xa2, 0x01, 0x2f, 0x00, 0x2e, 0x02, 0x2d, 0x98, 0x2e, 0x5b, 0x0e, 0x30, 0x30, 0x98,
|
||||
0x2e, 0xce, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7d, 0x00, 0x50, 0x32, 0x98, 0x2e, 0xfa, 0x03, 0x01, 0x2e, 0x77, 0x00,
|
||||
0x00, 0xb2, 0x24, 0x2f, 0x98, 0x2e, 0xf5, 0xcb, 0x03, 0x2e, 0xd5, 0x00, 0x11, 0x54, 0x01, 0x0a, 0xbc, 0x84, 0x83,
|
||||
0x86, 0x21, 0x2e, 0xc9, 0x01, 0xe0, 0x40, 0x13, 0x52, 0xc4, 0x40, 0x82, 0x40, 0xa8, 0xb9, 0x52, 0x42, 0x43, 0xbe,
|
||||
0x53, 0x42, 0x04, 0x0a, 0x50, 0x42, 0xe1, 0x7f, 0xf0, 0x31, 0x41, 0x40, 0xf2, 0x6f, 0x25, 0xbd, 0x08, 0x08, 0x02,
|
||||
0x0a, 0xd0, 0x7f, 0x98, 0x2e, 0xa8, 0xcf, 0x06, 0xbc, 0xd1, 0x6f, 0xe2, 0x6f, 0x08, 0x0a, 0x80, 0x42, 0x98, 0x2e,
|
||||
0x58, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0xee, 0x00, 0x21, 0x2e, 0x77, 0x00, 0x21, 0x2e, 0xdd, 0x00, 0x80, 0x2e, 0xf4,
|
||||
0x01, 0x1a, 0x24, 0x22, 0x00, 0x80, 0x2e, 0xec, 0x01, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50,
|
||||
0xfb, 0x6f, 0x01, 0x30, 0x71, 0x54, 0x11, 0x42, 0x42, 0x0e, 0xfc, 0x2f, 0xc0, 0x2e, 0x01, 0x42, 0xf0, 0x5f, 0x80,
|
||||
0x2e, 0x00, 0xc1, 0xfd, 0x2d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9a, 0x01,
|
||||
0x34, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x20, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x06, 0x32, 0x0f, 0x2e, 0x61, 0xf5, 0xfe, 0x09, 0xc0, 0xb3, 0x04,
|
||||
0x2f, 0x17, 0x30, 0x2f, 0x2e, 0xef, 0x00, 0x2d, 0x2e, 0x61, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e,
|
||||
0x20, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x46, 0x30, 0x0f, 0x2e, 0xa4, 0xf1, 0xbe, 0x09, 0x80, 0xb3, 0x06, 0x2f, 0x0d,
|
||||
0x2e, 0xd4, 0x00, 0x84, 0xaf, 0x02, 0x2f, 0x16, 0x30, 0x2d, 0x2e, 0x7b, 0x00, 0x86, 0x30, 0x2d, 0x2e, 0x60, 0xf5,
|
||||
0xf6, 0x6f, 0xe7, 0x6f, 0xe0, 0x5f, 0xc8, 0x2e, 0x01, 0x2e, 0x77, 0xf7, 0x09, 0xbc, 0x0f, 0xb8, 0x00, 0xb2, 0x10,
|
||||
0x50, 0xfb, 0x7f, 0x10, 0x30, 0x0b, 0x2f, 0x03, 0x2e, 0x8a, 0x00, 0x96, 0xbc, 0x9f, 0xb8, 0x40, 0xb2, 0x05, 0x2f,
|
||||
0x03, 0x2e, 0x68, 0xf7, 0x9e, 0xbc, 0x9f, 0xb8, 0x40, 0xb2, 0x07, 0x2f, 0x03, 0x2e, 0x7e, 0x00, 0x41, 0x90, 0x01,
|
||||
0x2f, 0x98, 0x2e, 0xdc, 0x03, 0x03, 0x2c, 0x00, 0x30, 0x21, 0x2e, 0x7e, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, 0xb8, 0x2e,
|
||||
0x20, 0x50, 0xe0, 0x7f, 0xfb, 0x7f, 0x00, 0x2e, 0x27, 0x50, 0x98, 0x2e, 0x3b, 0xc8, 0x29, 0x50, 0x98, 0x2e, 0xa7,
|
||||
0xc8, 0x01, 0x50, 0x98, 0x2e, 0x55, 0xcc, 0xe1, 0x6f, 0x2b, 0x50, 0x98, 0x2e, 0xe0, 0xc9, 0xfb, 0x6f, 0x00, 0x30,
|
||||
0xe0, 0x5f, 0x21, 0x2e, 0x7e, 0x00, 0xb8, 0x2e, 0x73, 0x50, 0x01, 0x30, 0x57, 0x54, 0x11, 0x42, 0x42, 0x0e, 0xfc,
|
||||
0x2f, 0xb8, 0x2e, 0x21, 0x2e, 0x59, 0xf5, 0x10, 0x30, 0xc0, 0x2e, 0x21, 0x2e, 0x4a, 0xf1, 0x90, 0x50, 0xf7, 0x7f,
|
||||
0xe6, 0x7f, 0xd5, 0x7f, 0xc4, 0x7f, 0xb3, 0x7f, 0xa1, 0x7f, 0x90, 0x7f, 0x82, 0x7f, 0x7b, 0x7f, 0x98, 0x2e, 0x35,
|
||||
0xb7, 0x00, 0xb2, 0x90, 0x2e, 0x97, 0xb0, 0x03, 0x2e, 0x8f, 0x00, 0x07, 0x2e, 0x91, 0x00, 0x05, 0x2e, 0xb1, 0x00,
|
||||
0x3f, 0xba, 0x9f, 0xb8, 0x01, 0x2e, 0xb1, 0x00, 0xa3, 0xbd, 0x4c, 0x0a, 0x05, 0x2e, 0xb1, 0x00, 0x04, 0xbe, 0xbf,
|
||||
0xb9, 0xcb, 0x0a, 0x4f, 0xba, 0x22, 0xbd, 0x01, 0x2e, 0xb3, 0x00, 0xdc, 0x0a, 0x2f, 0xb9, 0x03, 0x2e, 0xb8, 0x00,
|
||||
0x0a, 0xbe, 0x9a, 0x0a, 0xcf, 0xb9, 0x9b, 0xbc, 0x01, 0x2e, 0x97, 0x00, 0x9f, 0xb8, 0x93, 0x0a, 0x0f, 0xbc, 0x91,
|
||||
0x0a, 0x0f, 0xb8, 0x90, 0x0a, 0x25, 0x2e, 0x18, 0x00, 0x05, 0x2e, 0xc1, 0xf5, 0x2e, 0xbd, 0x2e, 0xb9, 0x01, 0x2e,
|
||||
0x19, 0x00, 0x31, 0x30, 0x8a, 0x04, 0x00, 0x90, 0x07, 0x2f, 0x01, 0x2e, 0xd4, 0x00, 0x04, 0xa2, 0x03, 0x2f, 0x01,
|
||||
0x2e, 0x18, 0x00, 0x00, 0xb2, 0x0c, 0x2f, 0x19, 0x50, 0x05, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x05, 0x2e, 0x78, 0x00,
|
||||
0x80, 0x90, 0x10, 0x30, 0x01, 0x2f, 0x21, 0x2e, 0x78, 0x00, 0x25, 0x2e, 0xdd, 0x00, 0x98, 0x2e, 0x3e, 0xb7, 0x00,
|
||||
0xb2, 0x02, 0x30, 0x01, 0x30, 0x04, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x00, 0x2f, 0x21, 0x30, 0x01, 0x2e,
|
||||
0xea, 0x00, 0x08, 0x1a, 0x0e, 0x2f, 0x23, 0x2e, 0xea, 0x00, 0x33, 0x30, 0x1b, 0x50, 0x0b, 0x09, 0x01, 0x40, 0x17,
|
||||
0x56, 0x46, 0xbe, 0x4b, 0x08, 0x4c, 0x0a, 0x01, 0x42, 0x0a, 0x80, 0x15, 0x52, 0x01, 0x42, 0x00, 0x2e, 0x01, 0x2e,
|
||||
0x18, 0x00, 0x00, 0xb2, 0x1f, 0x2f, 0x03, 0x2e, 0xc0, 0xf5, 0xf0, 0x30, 0x48, 0x08, 0x47, 0xaa, 0x74, 0x30, 0x07,
|
||||
0x2e, 0x7a, 0x00, 0x61, 0x22, 0x4b, 0x1a, 0x05, 0x2f, 0x07, 0x2e, 0x66, 0xf5, 0xbf, 0xbd, 0xbf, 0xb9, 0xc0, 0x90,
|
||||
0x0b, 0x2f, 0x1d, 0x56, 0x2b, 0x30, 0xd2, 0x42, 0xdb, 0x42, 0x01, 0x04, 0xc2, 0x42, 0x04, 0xbd, 0xfe, 0x80, 0x81,
|
||||
0x84, 0x23, 0x2e, 0x7a, 0x00, 0x02, 0x42, 0x02, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x05, 0x2e, 0xd6, 0x00, 0x81, 0x84,
|
||||
0x25, 0x2e, 0xd6, 0x00, 0x02, 0x31, 0x25, 0x2e, 0x60, 0xf5, 0x05, 0x2e, 0x8a, 0x00, 0x0b, 0x50, 0x90, 0x08, 0x80,
|
||||
0xb2, 0x0b, 0x2f, 0x05, 0x2e, 0xca, 0xf5, 0xf0, 0x3e, 0x90, 0x08, 0x25, 0x2e, 0xca, 0xf5, 0x05, 0x2e, 0x59, 0xf5,
|
||||
0xe0, 0x3f, 0x90, 0x08, 0x25, 0x2e, 0x59, 0xf5, 0x90, 0x6f, 0xa1, 0x6f, 0xb3, 0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0xe6,
|
||||
0x6f, 0xf7, 0x6f, 0x7b, 0x6f, 0x82, 0x6f, 0x70, 0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0x90, 0x7f, 0xe5, 0x7f, 0xd4, 0x7f,
|
||||
0xc3, 0x7f, 0xb1, 0x7f, 0xa2, 0x7f, 0x87, 0x7f, 0xf6, 0x7f, 0x7b, 0x7f, 0x00, 0x2e, 0x01, 0x2e, 0x60, 0xf5, 0x60,
|
||||
0x7f, 0x98, 0x2e, 0x35, 0xb7, 0x02, 0x30, 0x63, 0x6f, 0x15, 0x52, 0x50, 0x7f, 0x62, 0x7f, 0x5a, 0x2c, 0x02, 0x32,
|
||||
0x1a, 0x09, 0x00, 0xb3, 0x14, 0x2f, 0x00, 0xb2, 0x03, 0x2f, 0x09, 0x2e, 0x18, 0x00, 0x00, 0x91, 0x0c, 0x2f, 0x43,
|
||||
0x7f, 0x98, 0x2e, 0x97, 0xb7, 0x1f, 0x50, 0x02, 0x8a, 0x02, 0x32, 0x04, 0x30, 0x25, 0x2e, 0x64, 0xf5, 0x15, 0x52,
|
||||
0x50, 0x6f, 0x43, 0x6f, 0x44, 0x43, 0x25, 0x2e, 0x60, 0xf5, 0xd9, 0x08, 0xc0, 0xb2, 0x36, 0x2f, 0x98, 0x2e, 0x3e,
|
||||
0xb7, 0x00, 0xb2, 0x06, 0x2f, 0x01, 0x2e, 0x19, 0x00, 0x00, 0xb2, 0x02, 0x2f, 0x50, 0x6f, 0x00, 0x90, 0x0a, 0x2f,
|
||||
0x01, 0x2e, 0x79, 0x00, 0x00, 0x90, 0x19, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x79, 0x00, 0x00, 0x30, 0x98, 0x2e, 0xdc,
|
||||
0x03, 0x13, 0x2d, 0x01, 0x2e, 0xc3, 0xf5, 0x0c, 0xbc, 0x0f, 0xb8, 0x12, 0x30, 0x10, 0x04, 0x03, 0xb0, 0x26, 0x25,
|
||||
0x21, 0x50, 0x03, 0x52, 0x98, 0x2e, 0x4d, 0xb7, 0x10, 0x30, 0x21, 0x2e, 0xee, 0x00, 0x02, 0x30, 0x60, 0x7f, 0x25,
|
||||
0x2e, 0x79, 0x00, 0x60, 0x6f, 0x00, 0x90, 0x05, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0xea, 0x00, 0x15, 0x50, 0x21, 0x2e,
|
||||
0x64, 0xf5, 0x15, 0x52, 0x23, 0x2e, 0x60, 0xf5, 0x02, 0x32, 0x50, 0x6f, 0x00, 0x90, 0x02, 0x2f, 0x03, 0x30, 0x27,
|
||||
0x2e, 0x78, 0x00, 0x07, 0x2e, 0x60, 0xf5, 0x1a, 0x09, 0x00, 0x91, 0xa3, 0x2f, 0x19, 0x09, 0x00, 0x91, 0xa0, 0x2f,
|
||||
0x90, 0x6f, 0xa2, 0x6f, 0xb1, 0x6f, 0xc3, 0x6f, 0xd4, 0x6f, 0xe5, 0x6f, 0x7b, 0x6f, 0xf6, 0x6f, 0x87, 0x6f, 0x40,
|
||||
0x5f, 0xc8, 0x2e, 0xc0, 0x50, 0xe7, 0x7f, 0xf6, 0x7f, 0x26, 0x30, 0x0f, 0x2e, 0x61, 0xf5, 0x2f, 0x2e, 0x7c, 0x00,
|
||||
0x0f, 0x2e, 0x7c, 0x00, 0xbe, 0x09, 0xa2, 0x7f, 0x80, 0x7f, 0x80, 0xb3, 0xd5, 0x7f, 0xc4, 0x7f, 0xb3, 0x7f, 0x91,
|
||||
0x7f, 0x7b, 0x7f, 0x0b, 0x2f, 0x23, 0x50, 0x1a, 0x25, 0x12, 0x40, 0x42, 0x7f, 0x74, 0x82, 0x12, 0x40, 0x52, 0x7f,
|
||||
0x00, 0x2e, 0x00, 0x40, 0x60, 0x7f, 0x98, 0x2e, 0x6a, 0xd6, 0x81, 0x30, 0x01, 0x2e, 0x7c, 0x00, 0x01, 0x08, 0x00,
|
||||
0xb2, 0x42, 0x2f, 0x03, 0x2e, 0x89, 0x00, 0x01, 0x2e, 0x89, 0x00, 0x97, 0xbc, 0x06, 0xbc, 0x9f, 0xb8, 0x0f, 0xb8,
|
||||
0x00, 0x90, 0x23, 0x2e, 0xd8, 0x00, 0x10, 0x30, 0x01, 0x30, 0x2a, 0x2f, 0x03, 0x2e, 0xd4, 0x00, 0x44, 0xb2, 0x05,
|
||||
0x2f, 0x47, 0xb2, 0x00, 0x30, 0x2d, 0x2f, 0x21, 0x2e, 0x7c, 0x00, 0x2b, 0x2d, 0x03, 0x2e, 0xfd, 0xf5, 0x9e, 0xbc,
|
||||
0x9f, 0xb8, 0x40, 0x90, 0x14, 0x2f, 0x03, 0x2e, 0xfc, 0xf5, 0x99, 0xbc, 0x9f, 0xb8, 0x40, 0x90, 0x0e, 0x2f, 0x03,
|
||||
0x2e, 0x49, 0xf1, 0x25, 0x54, 0x4a, 0x08, 0x40, 0x90, 0x08, 0x2f, 0x98, 0x2e, 0x35, 0xb7, 0x00, 0xb2, 0x10, 0x30,
|
||||
0x03, 0x2f, 0x50, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x10, 0x2d, 0x98, 0x2e, 0xaf, 0xb7, 0x00, 0x30, 0x21, 0x2e, 0x7c,
|
||||
0x00, 0x0a, 0x2d, 0x05, 0x2e, 0x69, 0xf7, 0x2d, 0xbd, 0x2f, 0xb9, 0x80, 0xb2, 0x01, 0x2f, 0x21, 0x2e, 0x7d, 0x00,
|
||||
0x23, 0x2e, 0x7c, 0x00, 0xe0, 0x31, 0x21, 0x2e, 0x61, 0xf5, 0xf6, 0x6f, 0xe7, 0x6f, 0x80, 0x6f, 0xa2, 0x6f, 0xb3,
|
||||
0x6f, 0xc4, 0x6f, 0xd5, 0x6f, 0x7b, 0x6f, 0x91, 0x6f, 0x40, 0x5f, 0xc8, 0x2e, 0x60, 0x51, 0x0a, 0x25, 0x36, 0x88,
|
||||
0xf4, 0x7f, 0xeb, 0x7f, 0x00, 0x32, 0x31, 0x52, 0x32, 0x30, 0x13, 0x30, 0x98, 0x2e, 0x15, 0xcb, 0x0a, 0x25, 0x33,
|
||||
0x84, 0xd2, 0x7f, 0x43, 0x30, 0x05, 0x50, 0x2d, 0x52, 0x98, 0x2e, 0x95, 0xc1, 0xd2, 0x6f, 0x27, 0x52, 0x98, 0x2e,
|
||||
0xd7, 0xc7, 0x2a, 0x25, 0xb0, 0x86, 0xc0, 0x7f, 0xd3, 0x7f, 0xaf, 0x84, 0x29, 0x50, 0xf1, 0x6f, 0x98, 0x2e, 0x4d,
|
||||
0xc8, 0x2a, 0x25, 0xae, 0x8a, 0xaa, 0x88, 0xf2, 0x6e, 0x2b, 0x50, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x7f, 0x98, 0x2e,
|
||||
0xb6, 0xc8, 0xe0, 0x6e, 0x00, 0xb2, 0x32, 0x2f, 0x33, 0x54, 0x83, 0x86, 0xf1, 0x6f, 0xc3, 0x7f, 0x04, 0x30, 0x30,
|
||||
0x30, 0xf4, 0x7f, 0xd0, 0x7f, 0xb2, 0x7f, 0xe3, 0x30, 0xc5, 0x6f, 0x56, 0x40, 0x45, 0x41, 0x28, 0x08, 0x03, 0x14,
|
||||
0x0e, 0xb4, 0x08, 0xbc, 0x82, 0x40, 0x10, 0x0a, 0x2f, 0x54, 0x26, 0x05, 0x91, 0x7f, 0x44, 0x28, 0xa3, 0x7f, 0x98,
|
||||
0x2e, 0xd9, 0xc0, 0x08, 0xb9, 0x33, 0x30, 0x53, 0x09, 0xc1, 0x6f, 0xd3, 0x6f, 0xf4, 0x6f, 0x83, 0x17, 0x47, 0x40,
|
||||
0x6c, 0x15, 0xb2, 0x6f, 0xbe, 0x09, 0x75, 0x0b, 0x90, 0x42, 0x45, 0x42, 0x51, 0x0e, 0x32, 0xbc, 0x02, 0x89, 0xa1,
|
||||
0x6f, 0x7e, 0x86, 0xf4, 0x7f, 0xd0, 0x7f, 0xb2, 0x7f, 0x04, 0x30, 0x91, 0x6f, 0xd6, 0x2f, 0xeb, 0x6f, 0xa0, 0x5e,
|
||||
0xb8, 0x2e, 0x03, 0x2e, 0x97, 0x00, 0x1b, 0xbc, 0x60, 0x50, 0x9f, 0xbc, 0x0c, 0xb8, 0xf0, 0x7f, 0x40, 0xb2, 0xeb,
|
||||
0x7f, 0x2b, 0x2f, 0x03, 0x2e, 0x7f, 0x00, 0x41, 0x40, 0x01, 0x2e, 0xc8, 0x00, 0x01, 0x1a, 0x11, 0x2f, 0x37, 0x58,
|
||||
0x23, 0x2e, 0xc8, 0x00, 0x10, 0x41, 0xa0, 0x7f, 0x38, 0x81, 0x01, 0x41, 0xd0, 0x7f, 0xb1, 0x7f, 0x98, 0x2e, 0x64,
|
||||
0xcf, 0xd0, 0x6f, 0x07, 0x80, 0xa1, 0x6f, 0x11, 0x42, 0x00, 0x2e, 0xb1, 0x6f, 0x01, 0x42, 0x11, 0x30, 0x01, 0x2e,
|
||||
0xfc, 0x00, 0x00, 0xa8, 0x03, 0x30, 0xcb, 0x22, 0x4a, 0x25, 0x01, 0x2e, 0x7f, 0x00, 0x3c, 0x89, 0x35, 0x52, 0x05,
|
||||
0x54, 0x98, 0x2e, 0xc4, 0xce, 0xc1, 0x6f, 0xf0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x04, 0x2d, 0x01, 0x30, 0xf0, 0x6f,
|
||||
0x98, 0x2e, 0x95, 0xcf, 0xeb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e, 0x03, 0x2e, 0xb3, 0x00, 0x02, 0x32, 0xf0, 0x30, 0x03,
|
||||
0x31, 0x30, 0x50, 0x8a, 0x08, 0x08, 0x08, 0xcb, 0x08, 0xe0, 0x7f, 0x80, 0xb2, 0xf3, 0x7f, 0xdb, 0x7f, 0x25, 0x2f,
|
||||
0x03, 0x2e, 0xca, 0x00, 0x41, 0x90, 0x04, 0x2f, 0x01, 0x30, 0x23, 0x2e, 0xca, 0x00, 0x98, 0x2e, 0x3f, 0x03, 0xc0,
|
||||
0xb2, 0x05, 0x2f, 0x03, 0x2e, 0xda, 0x00, 0x00, 0x30, 0x41, 0x04, 0x23, 0x2e, 0xda, 0x00, 0x98, 0x2e, 0x92, 0xb2,
|
||||
0x10, 0x25, 0xf0, 0x6f, 0x00, 0xb2, 0x05, 0x2f, 0x01, 0x2e, 0xda, 0x00, 0x02, 0x30, 0x10, 0x04, 0x21, 0x2e, 0xda,
|
||||
0x00, 0x40, 0xb2, 0x01, 0x2f, 0x23, 0x2e, 0xc8, 0x01, 0xdb, 0x6f, 0xe0, 0x6f, 0xd0, 0x5f, 0x80, 0x2e, 0x95, 0xcf,
|
||||
0x01, 0x30, 0xe0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0x11, 0x30, 0x23, 0x2e, 0xca, 0x00, 0xdb, 0x6f, 0xd0, 0x5f, 0xb8,
|
||||
0x2e, 0xd0, 0x50, 0x0a, 0x25, 0x33, 0x84, 0x55, 0x50, 0xd2, 0x7f, 0xe2, 0x7f, 0x03, 0x8c, 0xc0, 0x7f, 0xbb, 0x7f,
|
||||
0x00, 0x30, 0x05, 0x5a, 0x39, 0x54, 0x51, 0x41, 0xa5, 0x7f, 0x96, 0x7f, 0x80, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0x05,
|
||||
0x30, 0xf5, 0x7f, 0x20, 0x25, 0x91, 0x6f, 0x3b, 0x58, 0x3d, 0x5c, 0x3b, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xc1, 0x6f,
|
||||
0xd5, 0x6f, 0x52, 0x40, 0x50, 0x43, 0xc1, 0x7f, 0xd5, 0x7f, 0x10, 0x25, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98,
|
||||
0x2e, 0x74, 0xc0, 0x86, 0x6f, 0x30, 0x28, 0x92, 0x6f, 0x82, 0x8c, 0xa5, 0x6f, 0x6f, 0x52, 0x69, 0x0e, 0x39, 0x54,
|
||||
0xdb, 0x2f, 0x19, 0xa0, 0x15, 0x30, 0x03, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x81, 0x01, 0x0a, 0x2d, 0x01, 0x2e, 0x81,
|
||||
0x01, 0x05, 0x28, 0x42, 0x36, 0x21, 0x2e, 0x81, 0x01, 0x02, 0x0e, 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x57, 0x50,
|
||||
0x12, 0x30, 0x01, 0x40, 0x98, 0x2e, 0xfe, 0xc9, 0x51, 0x6f, 0x0b, 0x5c, 0x8e, 0x0e, 0x3b, 0x6f, 0x57, 0x58, 0x02,
|
||||
0x30, 0x21, 0x2e, 0x95, 0x01, 0x45, 0x6f, 0x2a, 0x8d, 0xd2, 0x7f, 0xcb, 0x7f, 0x13, 0x2f, 0x02, 0x30, 0x3f, 0x50,
|
||||
0xd2, 0x7f, 0xa8, 0x0e, 0x0e, 0x2f, 0xc0, 0x6f, 0x53, 0x54, 0x02, 0x00, 0x51, 0x54, 0x42, 0x0e, 0x10, 0x30, 0x59,
|
||||
0x52, 0x02, 0x30, 0x01, 0x2f, 0x00, 0x2e, 0x03, 0x2d, 0x50, 0x42, 0x42, 0x42, 0x12, 0x30, 0xd2, 0x7f, 0x80, 0xb2,
|
||||
0x03, 0x2f, 0x00, 0x30, 0x21, 0x2e, 0x80, 0x01, 0x12, 0x2d, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x80, 0x05, 0x2e, 0x80,
|
||||
0x01, 0x11, 0x30, 0x91, 0x28, 0x00, 0x40, 0x25, 0x2e, 0x80, 0x01, 0x10, 0x0e, 0x05, 0x2f, 0x01, 0x2e, 0x7f, 0x01,
|
||||
0x01, 0x90, 0x01, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0x00, 0x2e, 0xa0, 0x41, 0x01, 0x90, 0xa6, 0x7f, 0x90, 0x2e, 0xe3,
|
||||
0xb4, 0x01, 0x2e, 0x95, 0x01, 0x00, 0xa8, 0x90, 0x2e, 0xe3, 0xb4, 0x5b, 0x54, 0x95, 0x80, 0x82, 0x40, 0x80, 0xb2,
|
||||
0x02, 0x40, 0x2d, 0x8c, 0x3f, 0x52, 0x96, 0x7f, 0x90, 0x2e, 0xc2, 0xb3, 0x29, 0x0e, 0x76, 0x2f, 0x01, 0x2e, 0xc9,
|
||||
0x00, 0x00, 0x40, 0x81, 0x28, 0x45, 0x52, 0xb3, 0x30, 0x98, 0x2e, 0x0f, 0xca, 0x5d, 0x54, 0x80, 0x7f, 0x00, 0x2e,
|
||||
0xa1, 0x40, 0x72, 0x7f, 0x82, 0x80, 0x82, 0x40, 0x60, 0x7f, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74,
|
||||
0xc0, 0x62, 0x6f, 0x05, 0x30, 0x87, 0x40, 0xc0, 0x91, 0x04, 0x30, 0x05, 0x2f, 0x05, 0x2e, 0x83, 0x01, 0x80, 0xb2,
|
||||
0x14, 0x30, 0x00, 0x2f, 0x04, 0x30, 0x05, 0x2e, 0xc9, 0x00, 0x73, 0x6f, 0x81, 0x40, 0xe2, 0x40, 0x69, 0x04, 0x11,
|
||||
0x0f, 0xe1, 0x40, 0x16, 0x30, 0xfe, 0x29, 0xcb, 0x40, 0x02, 0x2f, 0x83, 0x6f, 0x83, 0x0f, 0x22, 0x2f, 0x47, 0x56,
|
||||
0x13, 0x0f, 0x12, 0x30, 0x77, 0x2f, 0x49, 0x54, 0x42, 0x0e, 0x12, 0x30, 0x73, 0x2f, 0x00, 0x91, 0x0a, 0x2f, 0x01,
|
||||
0x2e, 0x8b, 0x01, 0x19, 0xa8, 0x02, 0x30, 0x6c, 0x2f, 0x63, 0x50, 0x00, 0x2e, 0x17, 0x42, 0x05, 0x42, 0x68, 0x2c,
|
||||
0x12, 0x30, 0x0b, 0x25, 0x08, 0x0f, 0x50, 0x30, 0x02, 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x03, 0x2d, 0x40, 0x30, 0x21,
|
||||
0x2e, 0x83, 0x01, 0x2b, 0x2e, 0x85, 0x01, 0x5a, 0x2c, 0x12, 0x30, 0x00, 0x91, 0x2b, 0x25, 0x04, 0x2f, 0x63, 0x50,
|
||||
0x02, 0x30, 0x17, 0x42, 0x17, 0x2c, 0x02, 0x42, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e, 0x74, 0xc0, 0x05,
|
||||
0x2e, 0xc9, 0x00, 0x81, 0x84, 0x5b, 0x30, 0x82, 0x40, 0x37, 0x2e, 0x83, 0x01, 0x02, 0x0e, 0x07, 0x2f, 0x5f, 0x52,
|
||||
0x40, 0x30, 0x62, 0x40, 0x41, 0x40, 0x91, 0x0e, 0x01, 0x2f, 0x21, 0x2e, 0x83, 0x01, 0x05, 0x30, 0x2b, 0x2e, 0x85,
|
||||
0x01, 0x12, 0x30, 0x36, 0x2c, 0x16, 0x30, 0x15, 0x25, 0x81, 0x7f, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0x98, 0x2e,
|
||||
0x74, 0xc0, 0x19, 0xa2, 0x16, 0x30, 0x15, 0x2f, 0x05, 0x2e, 0x97, 0x01, 0x80, 0x6f, 0x82, 0x0e, 0x05, 0x2f, 0x01,
|
||||
0x2e, 0x86, 0x01, 0x06, 0x28, 0x21, 0x2e, 0x86, 0x01, 0x0b, 0x2d, 0x03, 0x2e, 0x87, 0x01, 0x5f, 0x54, 0x4e, 0x28,
|
||||
0x91, 0x42, 0x00, 0x2e, 0x82, 0x40, 0x90, 0x0e, 0x01, 0x2f, 0x21, 0x2e, 0x88, 0x01, 0x02, 0x30, 0x13, 0x2c, 0x05,
|
||||
0x30, 0xc0, 0x6f, 0x08, 0x1c, 0xa8, 0x0f, 0x16, 0x30, 0x05, 0x30, 0x5b, 0x50, 0x09, 0x2f, 0x02, 0x80, 0x2d, 0x2e,
|
||||
0x82, 0x01, 0x05, 0x42, 0x05, 0x80, 0x00, 0x2e, 0x02, 0x42, 0x3e, 0x80, 0x00, 0x2e, 0x06, 0x42, 0x02, 0x30, 0x90,
|
||||
0x6f, 0x3e, 0x88, 0x01, 0x40, 0x04, 0x41, 0x4c, 0x28, 0x01, 0x42, 0x07, 0x80, 0x10, 0x25, 0x24, 0x40, 0x00, 0x40,
|
||||
0x00, 0xa8, 0xf5, 0x22, 0x23, 0x29, 0x44, 0x42, 0x7a, 0x82, 0x7e, 0x88, 0x43, 0x40, 0x04, 0x41, 0x00, 0xab, 0xf5,
|
||||
0x23, 0xdf, 0x28, 0x43, 0x42, 0xd9, 0xa0, 0x14, 0x2f, 0x00, 0x90, 0x02, 0x2f, 0xd2, 0x6f, 0x81, 0xb2, 0x05, 0x2f,
|
||||
0x63, 0x54, 0x06, 0x28, 0x90, 0x42, 0x85, 0x42, 0x09, 0x2c, 0x02, 0x30, 0x5b, 0x50, 0x03, 0x80, 0x29, 0x2e, 0x7e,
|
||||
0x01, 0x2b, 0x2e, 0x82, 0x01, 0x05, 0x42, 0x12, 0x30, 0x2b, 0x2e, 0x83, 0x01, 0x45, 0x82, 0x00, 0x2e, 0x40, 0x40,
|
||||
0x7a, 0x82, 0x02, 0xa0, 0x08, 0x2f, 0x63, 0x50, 0x3b, 0x30, 0x15, 0x42, 0x05, 0x42, 0x37, 0x80, 0x37, 0x2e, 0x7e,
|
||||
0x01, 0x05, 0x42, 0x12, 0x30, 0x01, 0x2e, 0xc9, 0x00, 0x02, 0x8c, 0x40, 0x40, 0x84, 0x41, 0x7a, 0x8c, 0x04, 0x0f,
|
||||
0x03, 0x2f, 0x01, 0x2e, 0x8b, 0x01, 0x19, 0xa4, 0x04, 0x2f, 0x2b, 0x2e, 0x82, 0x01, 0x98, 0x2e, 0xf3, 0x03, 0x12,
|
||||
0x30, 0x81, 0x90, 0x61, 0x52, 0x08, 0x2f, 0x65, 0x42, 0x65, 0x42, 0x43, 0x80, 0x39, 0x84, 0x82, 0x88, 0x05, 0x42,
|
||||
0x45, 0x42, 0x85, 0x42, 0x05, 0x43, 0x00, 0x2e, 0x80, 0x41, 0x00, 0x90, 0x90, 0x2e, 0xe1, 0xb4, 0x65, 0x54, 0xc1,
|
||||
0x6f, 0x80, 0x40, 0x00, 0xb2, 0x43, 0x58, 0x69, 0x50, 0x44, 0x2f, 0x55, 0x5c, 0xb7, 0x87, 0x8c, 0x0f, 0x0d, 0x2e,
|
||||
0x96, 0x01, 0xc4, 0x40, 0x36, 0x2f, 0x41, 0x56, 0x8b, 0x0e, 0x2a, 0x2f, 0x0b, 0x52, 0xa1, 0x0e, 0x0a, 0x2f, 0x05,
|
||||
0x2e, 0x8f, 0x01, 0x14, 0x25, 0x98, 0x2e, 0xfe, 0xc9, 0x4b, 0x54, 0x02, 0x0f, 0x69, 0x50, 0x05, 0x30, 0x65, 0x54,
|
||||
0x15, 0x2f, 0x03, 0x2e, 0x8e, 0x01, 0x4d, 0x5c, 0x8e, 0x0f, 0x3a, 0x2f, 0x05, 0x2e, 0x8f, 0x01, 0x98, 0x2e, 0xfe,
|
||||
0xc9, 0x4f, 0x54, 0x82, 0x0f, 0x05, 0x30, 0x69, 0x50, 0x65, 0x54, 0x30, 0x2f, 0x6d, 0x52, 0x15, 0x30, 0x42, 0x8c,
|
||||
0x45, 0x42, 0x04, 0x30, 0x2b, 0x2c, 0x84, 0x43, 0x6b, 0x52, 0x42, 0x8c, 0x00, 0x2e, 0x85, 0x43, 0x15, 0x30, 0x24,
|
||||
0x2c, 0x45, 0x42, 0x8e, 0x0f, 0x20, 0x2f, 0x0d, 0x2e, 0x8e, 0x01, 0xb1, 0x0e, 0x1c, 0x2f, 0x23, 0x2e, 0x8e, 0x01,
|
||||
0x1a, 0x2d, 0x0e, 0x0e, 0x17, 0x2f, 0xa1, 0x0f, 0x15, 0x2f, 0x23, 0x2e, 0x8d, 0x01, 0x13, 0x2d, 0x98, 0x2e, 0x74,
|
||||
0xc0, 0x43, 0x54, 0xc2, 0x0e, 0x0a, 0x2f, 0x65, 0x50, 0x04, 0x80, 0x0b, 0x30, 0x06, 0x82, 0x0b, 0x42, 0x79, 0x80,
|
||||
0x41, 0x40, 0x12, 0x30, 0x25, 0x2e, 0x8c, 0x01, 0x01, 0x42, 0x05, 0x30, 0x69, 0x50, 0x65, 0x54, 0x84, 0x82, 0x43,
|
||||
0x84, 0xbe, 0x8c, 0x84, 0x40, 0x86, 0x41, 0x26, 0x29, 0x94, 0x42, 0xbe, 0x8e, 0xd5, 0x7f, 0x19, 0xa1, 0x43, 0x40,
|
||||
0x0b, 0x2e, 0x8c, 0x01, 0x84, 0x40, 0xc7, 0x41, 0x5d, 0x29, 0x27, 0x29, 0x45, 0x42, 0x84, 0x42, 0xc2, 0x7f, 0x01,
|
||||
0x2f, 0xc0, 0xb3, 0x1d, 0x2f, 0x05, 0x2e, 0x94, 0x01, 0x99, 0xa0, 0x01, 0x2f, 0x80, 0xb3, 0x13, 0x2f, 0x80, 0xb3,
|
||||
0x18, 0x2f, 0xc0, 0xb3, 0x16, 0x2f, 0x12, 0x40, 0x01, 0x40, 0x92, 0x7f, 0x98, 0x2e, 0x74, 0xc0, 0x92, 0x6f, 0x10,
|
||||
0x0f, 0x20, 0x30, 0x03, 0x2f, 0x10, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0x0a, 0x2d, 0x21, 0x2e, 0x7e, 0x01, 0x07, 0x2d,
|
||||
0x20, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0x03, 0x2d, 0x10, 0x30, 0x21, 0x2e, 0x7e, 0x01, 0xc2, 0x6f, 0x01, 0x2e, 0xc9,
|
||||
0x00, 0xbc, 0x84, 0x02, 0x80, 0x82, 0x40, 0x00, 0x40, 0x90, 0x0e, 0xd5, 0x6f, 0x02, 0x2f, 0x15, 0x30, 0x98, 0x2e,
|
||||
0xf3, 0x03, 0x41, 0x91, 0x05, 0x30, 0x07, 0x2f, 0x67, 0x50, 0x3d, 0x80, 0x2b, 0x2e, 0x8f, 0x01, 0x05, 0x42, 0x04,
|
||||
0x80, 0x00, 0x2e, 0x05, 0x42, 0x02, 0x2c, 0x00, 0x30, 0x00, 0x30, 0xa2, 0x6f, 0x98, 0x8a, 0x86, 0x40, 0x80, 0xa7,
|
||||
0x05, 0x2f, 0x98, 0x2e, 0xf3, 0x03, 0xc0, 0x30, 0x21, 0x2e, 0x95, 0x01, 0x06, 0x25, 0x1a, 0x25, 0xe2, 0x6f, 0x76,
|
||||
0x82, 0x96, 0x40, 0x56, 0x43, 0x51, 0x0e, 0xfb, 0x2f, 0xbb, 0x6f, 0x30, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xb8, 0x00,
|
||||
0x01, 0x31, 0x41, 0x08, 0x40, 0xb2, 0x20, 0x50, 0xf2, 0x30, 0x02, 0x08, 0xfb, 0x7f, 0x01, 0x30, 0x10, 0x2f, 0x05,
|
||||
0x2e, 0xcc, 0x00, 0x81, 0x90, 0xe0, 0x7f, 0x03, 0x2f, 0x23, 0x2e, 0xcc, 0x00, 0x98, 0x2e, 0x55, 0xb6, 0x98, 0x2e,
|
||||
0x1d, 0xb5, 0x10, 0x25, 0xfb, 0x6f, 0xe0, 0x6f, 0xe0, 0x5f, 0x80, 0x2e, 0x95, 0xcf, 0x98, 0x2e, 0x95, 0xcf, 0x10,
|
||||
0x30, 0x21, 0x2e, 0xcc, 0x00, 0xfb, 0x6f, 0xe0, 0x5f, 0xb8, 0x2e, 0x00, 0x51, 0x05, 0x58, 0xeb, 0x7f, 0x2a, 0x25,
|
||||
0x89, 0x52, 0x6f, 0x5a, 0x89, 0x50, 0x13, 0x41, 0x06, 0x40, 0xb3, 0x01, 0x16, 0x42, 0xcb, 0x16, 0x06, 0x40, 0xf3,
|
||||
0x02, 0x13, 0x42, 0x65, 0x0e, 0xf5, 0x2f, 0x05, 0x40, 0x14, 0x30, 0x2c, 0x29, 0x04, 0x42, 0x08, 0xa1, 0x00, 0x30,
|
||||
0x90, 0x2e, 0x52, 0xb6, 0xb3, 0x88, 0xb0, 0x8a, 0xb6, 0x84, 0xa4, 0x7f, 0xc4, 0x7f, 0xb5, 0x7f, 0xd5, 0x7f, 0x92,
|
||||
0x7f, 0x73, 0x30, 0x04, 0x30, 0x55, 0x40, 0x42, 0x40, 0x8a, 0x17, 0xf3, 0x08, 0x6b, 0x01, 0x90, 0x02, 0x53, 0xb8,
|
||||
0x4b, 0x82, 0xad, 0xbe, 0x71, 0x7f, 0x45, 0x0a, 0x09, 0x54, 0x84, 0x7f, 0x98, 0x2e, 0xd9, 0xc0, 0xa3, 0x6f, 0x7b,
|
||||
0x54, 0xd0, 0x42, 0xa3, 0x7f, 0xf2, 0x7f, 0x60, 0x7f, 0x20, 0x25, 0x71, 0x6f, 0x75, 0x5a, 0x77, 0x58, 0x79, 0x5c,
|
||||
0x75, 0x56, 0x98, 0x2e, 0x67, 0xcc, 0xb1, 0x6f, 0x62, 0x6f, 0x50, 0x42, 0xb1, 0x7f, 0xb3, 0x30, 0x10, 0x25, 0x98,
|
||||
0x2e, 0x0f, 0xca, 0x84, 0x6f, 0x20, 0x29, 0x71, 0x6f, 0x92, 0x6f, 0xa5, 0x6f, 0x76, 0x82, 0x6a, 0x0e, 0x73, 0x30,
|
||||
0x00, 0x30, 0xd0, 0x2f, 0xd2, 0x6f, 0xd1, 0x7f, 0xb4, 0x7f, 0x98, 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x02,
|
||||
0x0a, 0xc2, 0x6f, 0xc0, 0x7f, 0x98, 0x2e, 0x2b, 0xb7, 0x15, 0xbd, 0x0b, 0xb8, 0x42, 0x0a, 0xc0, 0x6f, 0x08, 0x17,
|
||||
0x41, 0x18, 0x89, 0x16, 0xe1, 0x18, 0xd0, 0x18, 0xa1, 0x7f, 0x27, 0x25, 0x16, 0x25, 0x98, 0x2e, 0x79, 0xc0, 0x8b,
|
||||
0x54, 0x90, 0x7f, 0xb3, 0x30, 0x82, 0x40, 0x80, 0x90, 0x0d, 0x2f, 0x7d, 0x52, 0x92, 0x6f, 0x98, 0x2e, 0x0f, 0xca,
|
||||
0xb2, 0x6f, 0x90, 0x0e, 0x06, 0x2f, 0x8b, 0x50, 0x14, 0x30, 0x42, 0x6f, 0x51, 0x6f, 0x14, 0x42, 0x12, 0x42, 0x01,
|
||||
0x42, 0x00, 0x2e, 0x31, 0x6f, 0x98, 0x2e, 0x74, 0xc0, 0x41, 0x6f, 0x80, 0x7f, 0x98, 0x2e, 0x74, 0xc0, 0x82, 0x6f,
|
||||
0x10, 0x04, 0x43, 0x52, 0x01, 0x0f, 0x05, 0x2e, 0xcb, 0x00, 0x00, 0x30, 0x04, 0x30, 0x21, 0x2f, 0x51, 0x6f, 0x43,
|
||||
0x58, 0x8c, 0x0e, 0x04, 0x30, 0x1c, 0x2f, 0x85, 0x88, 0x41, 0x6f, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30, 0x16, 0x2f,
|
||||
0x84, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x0f, 0x2f, 0x82, 0x88, 0x31, 0x6f, 0x04,
|
||||
0x41, 0x04, 0x05, 0x8c, 0x0e, 0x04, 0x30, 0x08, 0x2f, 0x83, 0x88, 0x00, 0x2e, 0x04, 0x41, 0x8c, 0x0f, 0x04, 0x30,
|
||||
0x02, 0x2f, 0x21, 0x2e, 0xad, 0x01, 0x14, 0x30, 0x00, 0x91, 0x14, 0x2f, 0x03, 0x2e, 0xa1, 0x01, 0x41, 0x90, 0x0e,
|
||||
0x2f, 0x03, 0x2e, 0xad, 0x01, 0x14, 0x30, 0x4c, 0x28, 0x23, 0x2e, 0xad, 0x01, 0x46, 0xa0, 0x06, 0x2f, 0x81, 0x84,
|
||||
0x8d, 0x52, 0x48, 0x82, 0x82, 0x40, 0x21, 0x2e, 0xa1, 0x01, 0x42, 0x42, 0x5c, 0x2c, 0x02, 0x30, 0x05, 0x2e, 0xaa,
|
||||
0x01, 0x80, 0xb2, 0x02, 0x30, 0x55, 0x2f, 0x03, 0x2e, 0xa9, 0x01, 0x92, 0x6f, 0xb3, 0x30, 0x98, 0x2e, 0x0f, 0xca,
|
||||
0xb2, 0x6f, 0x90, 0x0f, 0x00, 0x30, 0x02, 0x30, 0x4a, 0x2f, 0xa2, 0x6f, 0x87, 0x52, 0x91, 0x00, 0x85, 0x52, 0x51,
|
||||
0x0e, 0x02, 0x2f, 0x00, 0x2e, 0x43, 0x2c, 0x02, 0x30, 0xc2, 0x6f, 0x7f, 0x52, 0x91, 0x0e, 0x02, 0x30, 0x3c, 0x2f,
|
||||
0x51, 0x6f, 0x81, 0x54, 0x98, 0x2e, 0xfe, 0xc9, 0x10, 0x25, 0xb3, 0x30, 0x21, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x32,
|
||||
0x6f, 0xc0, 0x7f, 0xb3, 0x30, 0x12, 0x25, 0x98, 0x2e, 0x0f, 0xca, 0x42, 0x6f, 0xb0, 0x7f, 0xb3, 0x30, 0x12, 0x25,
|
||||
0x98, 0x2e, 0x0f, 0xca, 0xb2, 0x6f, 0x90, 0x28, 0x83, 0x52, 0x98, 0x2e, 0xfe, 0xc9, 0xc2, 0x6f, 0x90, 0x0f, 0x00,
|
||||
0x30, 0x02, 0x30, 0x1d, 0x2f, 0x05, 0x2e, 0xa1, 0x01, 0x80, 0xb2, 0x12, 0x30, 0x0f, 0x2f, 0x42, 0x6f, 0x03, 0x2e,
|
||||
0xab, 0x01, 0x91, 0x0e, 0x02, 0x30, 0x12, 0x2f, 0x52, 0x6f, 0x03, 0x2e, 0xac, 0x01, 0x91, 0x0f, 0x02, 0x30, 0x0c,
|
||||
0x2f, 0x21, 0x2e, 0xaa, 0x01, 0x0a, 0x2c, 0x12, 0x30, 0x03, 0x2e, 0xcb, 0x00, 0x8d, 0x58, 0x08, 0x89, 0x41, 0x40,
|
||||
0x11, 0x43, 0x00, 0x43, 0x25, 0x2e, 0xa1, 0x01, 0xd4, 0x6f, 0x8f, 0x52, 0x00, 0x43, 0x3a, 0x89, 0x00, 0x2e, 0x10,
|
||||
0x43, 0x10, 0x43, 0x61, 0x0e, 0xfb, 0x2f, 0x03, 0x2e, 0xa0, 0x01, 0x11, 0x1a, 0x02, 0x2f, 0x02, 0x25, 0x21, 0x2e,
|
||||
0xa0, 0x01, 0xeb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x91, 0x52, 0x10, 0x30, 0x02, 0x30, 0x95, 0x56, 0x52, 0x42, 0x4b,
|
||||
0x0e, 0xfc, 0x2f, 0x8d, 0x54, 0x88, 0x82, 0x93, 0x56, 0x80, 0x42, 0x53, 0x42, 0x40, 0x42, 0x42, 0x86, 0x83, 0x54,
|
||||
0xc0, 0x2e, 0xc2, 0x42, 0x00, 0x2e, 0xa3, 0x52, 0x00, 0x51, 0x52, 0x40, 0x47, 0x40, 0x1a, 0x25, 0x01, 0x2e, 0x97,
|
||||
0x00, 0x8f, 0xbe, 0x72, 0x86, 0xfb, 0x7f, 0x0b, 0x30, 0x7c, 0xbf, 0xa5, 0x50, 0x10, 0x08, 0xdf, 0xba, 0x70, 0x88,
|
||||
0xf8, 0xbf, 0xcb, 0x42, 0xd3, 0x7f, 0x6c, 0xbb, 0xfc, 0xbb, 0xc5, 0x0a, 0x90, 0x7f, 0x1b, 0x7f, 0x0b, 0x43, 0xc0,
|
||||
0xb2, 0xe5, 0x7f, 0xb7, 0x7f, 0xa6, 0x7f, 0xc4, 0x7f, 0x90, 0x2e, 0x1c, 0xb7, 0x07, 0x2e, 0xd2, 0x00, 0xc0, 0xb2,
|
||||
0x0b, 0x2f, 0x97, 0x52, 0x01, 0x2e, 0xcd, 0x00, 0x82, 0x7f, 0x98, 0x2e, 0xbb, 0xcc, 0x0b, 0x30, 0x37, 0x2e, 0xd2,
|
||||
0x00, 0x82, 0x6f, 0x90, 0x6f, 0x1a, 0x25, 0x00, 0xb2, 0x8b, 0x7f, 0x14, 0x2f, 0xa6, 0xbd, 0x25, 0xbd, 0xb6, 0xb9,
|
||||
0x2f, 0xb9, 0x80, 0xb2, 0xd4, 0xb0, 0x0c, 0x2f, 0x99, 0x54, 0x9b, 0x56, 0x0b, 0x30, 0x0b, 0x2e, 0xb1, 0x00, 0xa1,
|
||||
0x58, 0x9b, 0x42, 0xdb, 0x42, 0x6c, 0x09, 0x2b, 0x2e, 0xb1, 0x00, 0x8b, 0x42, 0xcb, 0x42, 0x86, 0x7f, 0x73, 0x84,
|
||||
0xa7, 0x56, 0xc3, 0x08, 0x39, 0x52, 0x05, 0x50, 0x72, 0x7f, 0x63, 0x7f, 0x98, 0x2e, 0xc2, 0xc0, 0xe1, 0x6f, 0x62,
|
||||
0x6f, 0xd1, 0x0a, 0x01, 0x2e, 0xcd, 0x00, 0xd5, 0x6f, 0xc4, 0x6f, 0x72, 0x6f, 0x97, 0x52, 0x9d, 0x5c, 0x98, 0x2e,
|
||||
0x06, 0xcd, 0x23, 0x6f, 0x90, 0x6f, 0x99, 0x52, 0xc0, 0xb2, 0x04, 0xbd, 0x54, 0x40, 0xaf, 0xb9, 0x45, 0x40, 0xe1,
|
||||
0x7f, 0x02, 0x30, 0x06, 0x2f, 0xc0, 0xb2, 0x02, 0x30, 0x03, 0x2f, 0x9b, 0x5c, 0x12, 0x30, 0x94, 0x43, 0x85, 0x43,
|
||||
0x03, 0xbf, 0x6f, 0xbb, 0x80, 0xb3, 0x20, 0x2f, 0x06, 0x6f, 0x26, 0x01, 0x16, 0x6f, 0x6e, 0x03, 0x45, 0x42, 0xc0,
|
||||
0x90, 0x29, 0x2e, 0xce, 0x00, 0x9b, 0x52, 0x14, 0x2f, 0x9b, 0x5c, 0x00, 0x2e, 0x93, 0x41, 0x86, 0x41, 0xe3, 0x04,
|
||||
0xae, 0x07, 0x80, 0xab, 0x04, 0x2f, 0x80, 0x91, 0x0a, 0x2f, 0x86, 0x6f, 0x73, 0x0f, 0x07, 0x2f, 0x83, 0x6f, 0xc0,
|
||||
0xb2, 0x04, 0x2f, 0x54, 0x42, 0x45, 0x42, 0x12, 0x30, 0x04, 0x2c, 0x11, 0x30, 0x02, 0x2c, 0x11, 0x30, 0x11, 0x30,
|
||||
0x02, 0xbc, 0x0f, 0xb8, 0xd2, 0x7f, 0x00, 0xb2, 0x0a, 0x2f, 0x01, 0x2e, 0xfc, 0x00, 0x05, 0x2e, 0xc7, 0x01, 0x10,
|
||||
0x1a, 0x02, 0x2f, 0x21, 0x2e, 0xc7, 0x01, 0x03, 0x2d, 0x02, 0x2c, 0x01, 0x30, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e,
|
||||
0x95, 0xcf, 0xd1, 0x6f, 0xa0, 0x6f, 0x98, 0x2e, 0x95, 0xcf, 0xe2, 0x6f, 0x9f, 0x52, 0x01, 0x2e, 0xce, 0x00, 0x82,
|
||||
0x40, 0x50, 0x42, 0x0c, 0x2c, 0x42, 0x42, 0x11, 0x30, 0x23, 0x2e, 0xd2, 0x00, 0x01, 0x30, 0xb0, 0x6f, 0x98, 0x2e,
|
||||
0x95, 0xcf, 0xa0, 0x6f, 0x01, 0x30, 0x98, 0x2e, 0x95, 0xcf, 0x00, 0x2e, 0xfb, 0x6f, 0x00, 0x5f, 0xb8, 0x2e, 0x83,
|
||||
0x86, 0x01, 0x30, 0x00, 0x30, 0x94, 0x40, 0x24, 0x18, 0x06, 0x00, 0x53, 0x0e, 0x4f, 0x02, 0xf9, 0x2f, 0xb8, 0x2e,
|
||||
0xa9, 0x52, 0x00, 0x2e, 0x60, 0x40, 0x41, 0x40, 0x0d, 0xbc, 0x98, 0xbc, 0xc0, 0x2e, 0x01, 0x0a, 0x0f, 0xb8, 0xab,
|
||||
0x52, 0x53, 0x3c, 0x52, 0x40, 0x40, 0x40, 0x4b, 0x00, 0x82, 0x16, 0x26, 0xb9, 0x01, 0xb8, 0x41, 0x40, 0x10, 0x08,
|
||||
0x97, 0xb8, 0x01, 0x08, 0xc0, 0x2e, 0x11, 0x30, 0x01, 0x08, 0x43, 0x86, 0x25, 0x40, 0x04, 0x40, 0xd8, 0xbe, 0x2c,
|
||||
0x0b, 0x22, 0x11, 0x54, 0x42, 0x03, 0x80, 0x4b, 0x0e, 0xf6, 0x2f, 0xb8, 0x2e, 0x9f, 0x50, 0x10, 0x50, 0xad, 0x52,
|
||||
0x05, 0x2e, 0xd3, 0x00, 0xfb, 0x7f, 0x00, 0x2e, 0x13, 0x40, 0x93, 0x42, 0x41, 0x0e, 0xfb, 0x2f, 0x98, 0x2e, 0xa5,
|
||||
0xb7, 0x98, 0x2e, 0x87, 0xcf, 0x01, 0x2e, 0xd9, 0x00, 0x00, 0xb2, 0xfb, 0x6f, 0x0b, 0x2f, 0x01, 0x2e, 0x69, 0xf7,
|
||||
0xb1, 0x3f, 0x01, 0x08, 0x01, 0x30, 0xf0, 0x5f, 0x23, 0x2e, 0xd9, 0x00, 0x21, 0x2e, 0x69, 0xf7, 0x80, 0x2e, 0x7a,
|
||||
0xb7, 0xf0, 0x5f, 0xb8, 0x2e, 0x01, 0x2e, 0xc0, 0xf8, 0x03, 0x2e, 0xfc, 0xf5, 0x15, 0x54, 0xaf, 0x56, 0x82, 0x08,
|
||||
0x0b, 0x2e, 0x69, 0xf7, 0xcb, 0x0a, 0xb1, 0x58, 0x80, 0x90, 0xdd, 0xbe, 0x4c, 0x08, 0x5f, 0xb9, 0x59, 0x22, 0x80,
|
||||
0x90, 0x07, 0x2f, 0x03, 0x34, 0xc3, 0x08, 0xf2, 0x3a, 0x0a, 0x08, 0x02, 0x35, 0xc0, 0x90, 0x4a, 0x0a, 0x48, 0x22,
|
||||
0xc0, 0x2e, 0x23, 0x2e, 0xfc, 0xf5, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x56, 0xc7, 0x98, 0x2e, 0x49, 0xc3, 0x10,
|
||||
0x30, 0xfb, 0x6f, 0xf0, 0x5f, 0x21, 0x2e, 0xcc, 0x00, 0x21, 0x2e, 0xca, 0x00, 0xb8, 0x2e, 0x03, 0x2e, 0xd3, 0x00,
|
||||
0x16, 0xb8, 0x02, 0x34, 0x4a, 0x0c, 0x21, 0x2e, 0x2d, 0xf5, 0xc0, 0x2e, 0x23, 0x2e, 0xd3, 0x00, 0x03, 0xbc, 0x21,
|
||||
0x2e, 0xd5, 0x00, 0x03, 0x2e, 0xd5, 0x00, 0x40, 0xb2, 0x10, 0x30, 0x21, 0x2e, 0x77, 0x00, 0x01, 0x30, 0x05, 0x2f,
|
||||
0x05, 0x2e, 0xd8, 0x00, 0x80, 0x90, 0x01, 0x2f, 0x23, 0x2e, 0x6f, 0xf5, 0xc0, 0x2e, 0x21, 0x2e, 0xd9, 0x00, 0x11,
|
||||
0x30, 0x81, 0x08, 0x01, 0x2e, 0x6a, 0xf7, 0x71, 0x3f, 0x23, 0xbd, 0x01, 0x08, 0x02, 0x0a, 0xc0, 0x2e, 0x21, 0x2e,
|
||||
0x6a, 0xf7, 0x30, 0x25, 0x00, 0x30, 0x21, 0x2e, 0x5a, 0xf5, 0x10, 0x50, 0x21, 0x2e, 0x7b, 0x00, 0x21, 0x2e, 0x7c,
|
||||
0x00, 0xfb, 0x7f, 0x98, 0x2e, 0xc3, 0xb7, 0x40, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0xfb, 0x6f, 0xf0, 0x5f, 0x03, 0x25,
|
||||
0x80, 0x2e, 0xaf, 0xb7, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x01, 0x2e, 0x5d, 0xf7, 0x08, 0xbc, 0x80, 0xac, 0x0e, 0xbb, 0x02, 0x2f,
|
||||
0x00, 0x30, 0x41, 0x04, 0x82, 0x06, 0xc0, 0xa4, 0x00, 0x30, 0x11, 0x2f, 0x40, 0xa9, 0x03, 0x2f, 0x40, 0x91, 0x0d,
|
||||
0x2f, 0x00, 0xa7, 0x0b, 0x2f, 0x80, 0xb3, 0xb3, 0x58, 0x02, 0x2f, 0x90, 0xa1, 0x26, 0x13, 0x20, 0x23, 0x80, 0x90,
|
||||
0x10, 0x30, 0x01, 0x2f, 0xcc, 0x0e, 0x00, 0x2f, 0x00, 0x30, 0xb8, 0x2e, 0xb5, 0x50, 0x18, 0x08, 0x08, 0xbc, 0x88,
|
||||
0xb6, 0x0d, 0x17, 0xc6, 0xbd, 0x56, 0xbc, 0xb7, 0x58, 0xda, 0xba, 0x04, 0x01, 0x1d, 0x0a, 0x10, 0x50, 0x05, 0x30,
|
||||
0x32, 0x25, 0x45, 0x03, 0xfb, 0x7f, 0xf6, 0x30, 0x21, 0x25, 0x98, 0x2e, 0x37, 0xca, 0x16, 0xb5, 0x9a, 0xbc, 0x06,
|
||||
0xb8, 0x80, 0xa8, 0x41, 0x0a, 0x0e, 0x2f, 0x80, 0x90, 0x02, 0x2f, 0x2d, 0x50, 0x48, 0x0f, 0x09, 0x2f, 0xbf, 0xa0,
|
||||
0x04, 0x2f, 0xbf, 0x90, 0x06, 0x2f, 0xb7, 0x54, 0xca, 0x0f, 0x03, 0x2f, 0x00, 0x2e, 0x02, 0x2c, 0xb7, 0x52, 0x2d,
|
||||
0x52, 0xf2, 0x33, 0x98, 0x2e, 0xd9, 0xc0, 0xfb, 0x6f, 0xf1, 0x37, 0xc0, 0x2e, 0x01, 0x08, 0xf0, 0x5f, 0xbf, 0x56,
|
||||
0xb9, 0x54, 0xd0, 0x40, 0xc4, 0x40, 0x0b, 0x2e, 0xfd, 0xf3, 0xbf, 0x52, 0x90, 0x42, 0x94, 0x42, 0x95, 0x42, 0x05,
|
||||
0x30, 0xc1, 0x50, 0x0f, 0x88, 0x06, 0x40, 0x04, 0x41, 0x96, 0x42, 0xc5, 0x42, 0x48, 0xbe, 0x73, 0x30, 0x0d, 0x2e,
|
||||
0xd8, 0x00, 0x4f, 0xba, 0x84, 0x42, 0x03, 0x42, 0x81, 0xb3, 0x02, 0x2f, 0x2b, 0x2e, 0x6f, 0xf5, 0x06, 0x2d, 0x05,
|
||||
0x2e, 0x77, 0xf7, 0xbd, 0x56, 0x93, 0x08, 0x25, 0x2e, 0x77, 0xf7, 0xbb, 0x54, 0x25, 0x2e, 0xc2, 0xf5, 0x07, 0x2e,
|
||||
0xfd, 0xf3, 0x42, 0x30, 0xb4, 0x33, 0xda, 0x0a, 0x4c, 0x00, 0x27, 0x2e, 0xfd, 0xf3, 0x43, 0x40, 0xd4, 0x3f, 0xdc,
|
||||
0x08, 0x43, 0x42, 0x00, 0x2e, 0x00, 0x2e, 0x43, 0x40, 0x24, 0x30, 0xdc, 0x0a, 0x43, 0x42, 0x04, 0x80, 0x03, 0x2e,
|
||||
0xfd, 0xf3, 0x4a, 0x0a, 0x23, 0x2e, 0xfd, 0xf3, 0x61, 0x34, 0xc0, 0x2e, 0x01, 0x42, 0x00, 0x2e, 0x60, 0x50, 0x1a,
|
||||
0x25, 0x7a, 0x86, 0xe0, 0x7f, 0xf3, 0x7f, 0x03, 0x25, 0xc3, 0x52, 0x41, 0x84, 0xdb, 0x7f, 0x33, 0x30, 0x98, 0x2e,
|
||||
0x16, 0xc2, 0x1a, 0x25, 0x7d, 0x82, 0xf0, 0x6f, 0xe2, 0x6f, 0x32, 0x25, 0x16, 0x40, 0x94, 0x40, 0x26, 0x01, 0x85,
|
||||
0x40, 0x8e, 0x17, 0xc4, 0x42, 0x6e, 0x03, 0x95, 0x42, 0x41, 0x0e, 0xf4, 0x2f, 0xdb, 0x6f, 0xa0, 0x5f, 0xb8, 0x2e,
|
||||
0xb0, 0x51, 0xfb, 0x7f, 0x98, 0x2e, 0xe8, 0x0d, 0x5a, 0x25, 0x98, 0x2e, 0x0f, 0x0e, 0xcb, 0x58, 0x32, 0x87, 0xc4,
|
||||
0x7f, 0x65, 0x89, 0x6b, 0x8d, 0xc5, 0x5a, 0x65, 0x7f, 0xe1, 0x7f, 0x83, 0x7f, 0xa6, 0x7f, 0x74, 0x7f, 0xd0, 0x7f,
|
||||
0xb6, 0x7f, 0x94, 0x7f, 0x17, 0x30, 0xc7, 0x52, 0xc9, 0x54, 0x51, 0x7f, 0x00, 0x2e, 0x85, 0x6f, 0x42, 0x7f, 0x00,
|
||||
0x2e, 0x51, 0x41, 0x45, 0x81, 0x42, 0x41, 0x13, 0x40, 0x3b, 0x8a, 0x00, 0x40, 0x4b, 0x04, 0xd0, 0x06, 0xc0, 0xac,
|
||||
0x85, 0x7f, 0x02, 0x2f, 0x02, 0x30, 0x51, 0x04, 0xd3, 0x06, 0x41, 0x84, 0x05, 0x30, 0x5d, 0x02, 0xc9, 0x16, 0xdf,
|
||||
0x08, 0xd3, 0x00, 0x8d, 0x02, 0xaf, 0xbc, 0xb1, 0xb9, 0x59, 0x0a, 0x65, 0x6f, 0x11, 0x43, 0xa1, 0xb4, 0x52, 0x41,
|
||||
0x53, 0x41, 0x01, 0x43, 0x34, 0x7f, 0x65, 0x7f, 0x26, 0x31, 0xe5, 0x6f, 0xd4, 0x6f, 0x98, 0x2e, 0x37, 0xca, 0x32,
|
||||
0x6f, 0x75, 0x6f, 0x83, 0x40, 0x42, 0x41, 0x23, 0x7f, 0x12, 0x7f, 0xf6, 0x30, 0x40, 0x25, 0x51, 0x25, 0x98, 0x2e,
|
||||
0x37, 0xca, 0x14, 0x6f, 0x20, 0x05, 0x70, 0x6f, 0x25, 0x6f, 0x69, 0x07, 0xa2, 0x6f, 0x31, 0x6f, 0x0b, 0x30, 0x04,
|
||||
0x42, 0x9b, 0x42, 0x8b, 0x42, 0x55, 0x42, 0x32, 0x7f, 0x40, 0xa9, 0xc3, 0x6f, 0x71, 0x7f, 0x02, 0x30, 0xd0, 0x40,
|
||||
0xc3, 0x7f, 0x03, 0x2f, 0x40, 0x91, 0x15, 0x2f, 0x00, 0xa7, 0x13, 0x2f, 0x00, 0xa4, 0x11, 0x2f, 0x84, 0xbd, 0x98,
|
||||
0x2e, 0x79, 0xca, 0x55, 0x6f, 0xb7, 0x54, 0x54, 0x41, 0x82, 0x00, 0xf3, 0x3f, 0x45, 0x41, 0xcb, 0x02, 0xf6, 0x30,
|
||||
0x98, 0x2e, 0x37, 0xca, 0x35, 0x6f, 0xa4, 0x6f, 0x41, 0x43, 0x03, 0x2c, 0x00, 0x43, 0xa4, 0x6f, 0x35, 0x6f, 0x17,
|
||||
0x30, 0x42, 0x6f, 0x51, 0x6f, 0x93, 0x40, 0x42, 0x82, 0x00, 0x41, 0xc3, 0x00, 0x03, 0x43, 0x51, 0x7f, 0x00, 0x2e,
|
||||
0x94, 0x40, 0x41, 0x41, 0x4c, 0x02, 0xc4, 0x6f, 0xd1, 0x56, 0x63, 0x0e, 0x74, 0x6f, 0x51, 0x43, 0xa5, 0x7f, 0x8a,
|
||||
0x2f, 0x09, 0x2e, 0xd8, 0x00, 0x01, 0xb3, 0x21, 0x2f, 0xcb, 0x58, 0x90, 0x6f, 0x13, 0x41, 0xb6, 0x6f, 0xe4, 0x7f,
|
||||
0x00, 0x2e, 0x91, 0x41, 0x14, 0x40, 0x92, 0x41, 0x15, 0x40, 0x17, 0x2e, 0x6f, 0xf5, 0xb6, 0x7f, 0xd0, 0x7f, 0xcb,
|
||||
0x7f, 0x98, 0x2e, 0x00, 0x0c, 0x07, 0x15, 0xc2, 0x6f, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0xc3, 0xa3, 0xc1, 0x8f,
|
||||
0xe4, 0x6f, 0xd0, 0x6f, 0xe6, 0x2f, 0x14, 0x30, 0x05, 0x2e, 0x6f, 0xf5, 0x14, 0x0b, 0x29, 0x2e, 0x6f, 0xf5, 0x18,
|
||||
0x2d, 0xcd, 0x56, 0x04, 0x32, 0xb5, 0x6f, 0x1c, 0x01, 0x51, 0x41, 0x52, 0x41, 0xc3, 0x40, 0xb5, 0x7f, 0xe4, 0x7f,
|
||||
0x98, 0x2e, 0x1f, 0x0c, 0xe4, 0x6f, 0x21, 0x87, 0x00, 0x43, 0x04, 0x32, 0xcf, 0x54, 0x5a, 0x0e, 0xef, 0x2f, 0x15,
|
||||
0x54, 0x09, 0x2e, 0x77, 0xf7, 0x22, 0x0b, 0x29, 0x2e, 0x77, 0xf7, 0xfb, 0x6f, 0x50, 0x5e, 0xb8, 0x2e, 0x10, 0x50,
|
||||
0x01, 0x2e, 0xd4, 0x00, 0x00, 0xb2, 0xfb, 0x7f, 0x51, 0x2f, 0x01, 0xb2, 0x48, 0x2f, 0x02, 0xb2, 0x42, 0x2f, 0x03,
|
||||
0x90, 0x56, 0x2f, 0xd7, 0x52, 0x79, 0x80, 0x42, 0x40, 0x81, 0x84, 0x00, 0x40, 0x42, 0x42, 0x98, 0x2e, 0x93, 0x0c,
|
||||
0xd9, 0x54, 0xd7, 0x50, 0xa1, 0x40, 0x98, 0xbd, 0x82, 0x40, 0x3e, 0x82, 0xda, 0x0a, 0x44, 0x40, 0x8b, 0x16, 0xe3,
|
||||
0x00, 0x53, 0x42, 0x00, 0x2e, 0x43, 0x40, 0x9a, 0x02, 0x52, 0x42, 0x00, 0x2e, 0x41, 0x40, 0x15, 0x54, 0x4a, 0x0e,
|
||||
0x3a, 0x2f, 0x3a, 0x82, 0x00, 0x30, 0x41, 0x40, 0x21, 0x2e, 0x85, 0x0f, 0x40, 0xb2, 0x0a, 0x2f, 0x98, 0x2e, 0xb1,
|
||||
0x0c, 0x98, 0x2e, 0x45, 0x0e, 0x98, 0x2e, 0x5b, 0x0e, 0xfb, 0x6f, 0xf0, 0x5f, 0x00, 0x30, 0x80, 0x2e, 0xce, 0xb7,
|
||||
0xdd, 0x52, 0xd3, 0x54, 0x42, 0x42, 0x4f, 0x84, 0x73, 0x30, 0xdb, 0x52, 0x83, 0x42, 0x1b, 0x30, 0x6b, 0x42, 0x23,
|
||||
0x30, 0x27, 0x2e, 0xd7, 0x00, 0x37, 0x2e, 0xd4, 0x00, 0x21, 0x2e, 0xd6, 0x00, 0x7a, 0x84, 0x17, 0x2c, 0x42, 0x42,
|
||||
0x30, 0x30, 0x21, 0x2e, 0xd4, 0x00, 0x12, 0x2d, 0x21, 0x30, 0x00, 0x30, 0x23, 0x2e, 0xd4, 0x00, 0x21, 0x2e, 0x7b,
|
||||
0xf7, 0x0b, 0x2d, 0x17, 0x30, 0x98, 0x2e, 0x51, 0x0c, 0xd5, 0x50, 0x0c, 0x82, 0x72, 0x30, 0x2f, 0x2e, 0xd4, 0x00,
|
||||
0x25, 0x2e, 0x7b, 0xf7, 0x40, 0x42, 0x00, 0x2e, 0xfb, 0x6f, 0xf0, 0x5f, 0xb8, 0x2e, 0x70, 0x50, 0x0a, 0x25, 0x39,
|
||||
0x86, 0xfb, 0x7f, 0xe1, 0x32, 0x62, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xb5, 0x56, 0xa5, 0x6f, 0xab, 0x08, 0x91, 0x6f,
|
||||
0x4b, 0x08, 0xdf, 0x56, 0xc4, 0x6f, 0x23, 0x09, 0x4d, 0xba, 0x93, 0xbc, 0x8c, 0x0b, 0xd1, 0x6f, 0x0b, 0x09, 0xcb,
|
||||
0x52, 0xe1, 0x5e, 0x56, 0x42, 0xaf, 0x09, 0x4d, 0xba, 0x23, 0xbd, 0x94, 0x0a, 0xe5, 0x6f, 0x68, 0xbb, 0xeb, 0x08,
|
||||
0xbd, 0xb9, 0x63, 0xbe, 0xfb, 0x6f, 0x52, 0x42, 0xe3, 0x0a, 0xc0, 0x2e, 0x43, 0x42, 0x90, 0x5f, 0xd1, 0x50, 0x03,
|
||||
0x2e, 0x25, 0xf3, 0x13, 0x40, 0x00, 0x40, 0x9b, 0xbc, 0x9b, 0xb4, 0x08, 0xbd, 0xb8, 0xb9, 0x98, 0xbc, 0xda, 0x0a,
|
||||
0x08, 0xb6, 0x89, 0x16, 0xc0, 0x2e, 0x19, 0x00, 0x62, 0x02, 0x10, 0x50, 0xfb, 0x7f, 0x98, 0x2e, 0x81, 0x0d, 0x01,
|
||||
0x2e, 0xd4, 0x00, 0x31, 0x30, 0x08, 0x04, 0xfb, 0x6f, 0x01, 0x30, 0xf0, 0x5f, 0x23, 0x2e, 0xd6, 0x00, 0x21, 0x2e,
|
||||
0xd7, 0x00, 0xb8, 0x2e, 0x01, 0x2e, 0xd7, 0x00, 0x03, 0x2e, 0xd6, 0x00, 0x48, 0x0e, 0x01, 0x2f, 0x80, 0x2e, 0x1f,
|
||||
0x0e, 0xb8, 0x2e, 0xe3, 0x50, 0x21, 0x34, 0x01, 0x42, 0x82, 0x30, 0xc1, 0x32, 0x25, 0x2e, 0x62, 0xf5, 0x01, 0x00,
|
||||
0x22, 0x30, 0x01, 0x40, 0x4a, 0x0a, 0x01, 0x42, 0xb8, 0x2e, 0xe3, 0x54, 0xf0, 0x3b, 0x83, 0x40, 0xd8, 0x08, 0xe5,
|
||||
0x52, 0x83, 0x42, 0x00, 0x30, 0x83, 0x30, 0x50, 0x42, 0xc4, 0x32, 0x27, 0x2e, 0x64, 0xf5, 0x94, 0x00, 0x50, 0x42,
|
||||
0x40, 0x42, 0xd3, 0x3f, 0x84, 0x40, 0x7d, 0x82, 0xe3, 0x08, 0x40, 0x42, 0x83, 0x42, 0xb8, 0x2e, 0xdd, 0x52, 0x00,
|
||||
0x30, 0x40, 0x42, 0x7c, 0x86, 0xb9, 0x52, 0x09, 0x2e, 0x70, 0x0f, 0xbf, 0x54, 0xc4, 0x42, 0xd3, 0x86, 0x54, 0x40,
|
||||
0x55, 0x40, 0x94, 0x42, 0x85, 0x42, 0x21, 0x2e, 0xd7, 0x00, 0x42, 0x40, 0x25, 0x2e, 0xfd, 0xf3, 0xc0, 0x42, 0x7e,
|
||||
0x82, 0x05, 0x2e, 0x7d, 0x00, 0x80, 0xb2, 0x14, 0x2f, 0x05, 0x2e, 0x89, 0x00, 0x27, 0xbd, 0x2f, 0xb9, 0x80, 0x90,
|
||||
0x02, 0x2f, 0x21, 0x2e, 0x6f, 0xf5, 0x0c, 0x2d, 0x07, 0x2e, 0x71, 0x0f, 0x14, 0x30, 0x1c, 0x09, 0x05, 0x2e, 0x77,
|
||||
0xf7, 0xbd, 0x56, 0x47, 0xbe, 0x93, 0x08, 0x94, 0x0a, 0x25, 0x2e, 0x77, 0xf7, 0xe7, 0x54, 0x50, 0x42, 0x4a, 0x0e,
|
||||
0xfc, 0x2f, 0xb8, 0x2e, 0x50, 0x50, 0x02, 0x30, 0x43, 0x86, 0xe5, 0x50, 0xfb, 0x7f, 0xe3, 0x7f, 0xd2, 0x7f, 0xc0,
|
||||
0x7f, 0xb1, 0x7f, 0x00, 0x2e, 0x41, 0x40, 0x00, 0x40, 0x48, 0x04, 0x98, 0x2e, 0x74, 0xc0, 0x1e, 0xaa, 0xd3, 0x6f,
|
||||
0x14, 0x30, 0xb1, 0x6f, 0xe3, 0x22, 0xc0, 0x6f, 0x52, 0x40, 0xe4, 0x6f, 0x4c, 0x0e, 0x12, 0x42, 0xd3, 0x7f, 0xeb,
|
||||
0x2f, 0x03, 0x2e, 0x86, 0x0f, 0x40, 0x90, 0x11, 0x30, 0x03, 0x2f, 0x23, 0x2e, 0x86, 0x0f, 0x02, 0x2c, 0x00, 0x30,
|
||||
0xd0, 0x6f, 0xfb, 0x6f, 0xb0, 0x5f, 0xb8, 0x2e, 0x40, 0x50, 0xf1, 0x7f, 0x0a, 0x25, 0x3c, 0x86, 0xeb, 0x7f, 0x41,
|
||||
0x33, 0x22, 0x30, 0x98, 0x2e, 0xc2, 0xc4, 0xd3, 0x6f, 0xf4, 0x30, 0xdc, 0x09, 0x47, 0x58, 0xc2, 0x6f, 0x94, 0x09,
|
||||
0xeb, 0x58, 0x6a, 0xbb, 0xdc, 0x08, 0xb4, 0xb9, 0xb1, 0xbd, 0xe9, 0x5a, 0x95, 0x08, 0x21, 0xbd, 0xf6, 0xbf, 0x77,
|
||||
0x0b, 0x51, 0xbe, 0xf1, 0x6f, 0xeb, 0x6f, 0x52, 0x42, 0x54, 0x42, 0xc0, 0x2e, 0x43, 0x42, 0xc0, 0x5f, 0x50, 0x50,
|
||||
0xf5, 0x50, 0x31, 0x30, 0x11, 0x42, 0xfb, 0x7f, 0x7b, 0x30, 0x0b, 0x42, 0x11, 0x30, 0x02, 0x80, 0x23, 0x33, 0x01,
|
||||
0x42, 0x03, 0x00, 0x07, 0x2e, 0x80, 0x03, 0x05, 0x2e, 0xd3, 0x00, 0x23, 0x52, 0xe2, 0x7f, 0xd3, 0x7f, 0xc0, 0x7f,
|
||||
0x98, 0x2e, 0xb6, 0x0e, 0xd1, 0x6f, 0x08, 0x0a, 0x1a, 0x25, 0x7b, 0x86, 0xd0, 0x7f, 0x01, 0x33, 0x12, 0x30, 0x98,
|
||||
0x2e, 0xc2, 0xc4, 0xd1, 0x6f, 0x08, 0x0a, 0x00, 0xb2, 0x0d, 0x2f, 0xe3, 0x6f, 0x01, 0x2e, 0x80, 0x03, 0x51, 0x30,
|
||||
0xc7, 0x86, 0x23, 0x2e, 0x21, 0xf2, 0x08, 0xbc, 0xc0, 0x42, 0x98, 0x2e, 0xa5, 0xb7, 0x00, 0x2e, 0x00, 0x2e, 0xd0,
|
||||
0x2e, 0xb0, 0x6f, 0x0b, 0xb8, 0x03, 0x2e, 0x1b, 0x00, 0x08, 0x1a, 0xb0, 0x7f, 0x70, 0x30, 0x04, 0x2f, 0x21, 0x2e,
|
||||
0x21, 0xf2, 0x00, 0x2e, 0x00, 0x2e, 0xd0, 0x2e, 0x98, 0x2e, 0x6d, 0xc0, 0x98, 0x2e, 0x5d, 0xc0, 0xed, 0x50, 0x98,
|
||||
0x2e, 0x44, 0xcb, 0xef, 0x50, 0x98, 0x2e, 0x46, 0xc3, 0xf1, 0x50, 0x98, 0x2e, 0x53, 0xc7, 0x35, 0x50, 0x98, 0x2e,
|
||||
0x64, 0xcf, 0x10, 0x30, 0x98, 0x2e, 0xdc, 0x03, 0x20, 0x26, 0xc0, 0x6f, 0x02, 0x31, 0x12, 0x42, 0xab, 0x33, 0x0b,
|
||||
0x42, 0x37, 0x80, 0x01, 0x30, 0x01, 0x42, 0xf3, 0x37, 0xf7, 0x52, 0xfb, 0x50, 0x44, 0x40, 0xa2, 0x0a, 0x42, 0x42,
|
||||
0x8b, 0x31, 0x09, 0x2e, 0x5e, 0xf7, 0xf9, 0x54, 0xe3, 0x08, 0x83, 0x42, 0x1b, 0x42, 0x23, 0x33, 0x4b, 0x00, 0xbc,
|
||||
0x84, 0x0b, 0x40, 0x33, 0x30, 0x83, 0x42, 0x0b, 0x42, 0xe0, 0x7f, 0xd1, 0x7f, 0x98, 0x2e, 0x58, 0xb7, 0xd1, 0x6f,
|
||||
0x80, 0x30, 0x40, 0x42, 0x03, 0x30, 0xe0, 0x6f, 0xf3, 0x54, 0x04, 0x30, 0x00, 0x2e, 0x00, 0x2e, 0x01, 0x89, 0x62,
|
||||
0x0e, 0xfa, 0x2f, 0x43, 0x42, 0x11, 0x30, 0xfb, 0x6f, 0xc0, 0x2e, 0x01, 0x42, 0xb0, 0x5f, 0xc1, 0x4a, 0x00, 0x00,
|
||||
0x6d, 0x57, 0x00, 0x00, 0x77, 0x8e, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xff, 0xd3, 0xff, 0xff, 0xff, 0xe5, 0xff, 0xff,
|
||||
0xff, 0xee, 0xe1, 0xff, 0xff, 0x7c, 0x13, 0x00, 0x00, 0x46, 0xe6, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||
0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1,
|
||||
0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00,
|
||||
0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e,
|
||||
0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80, 0x2e, 0x00, 0xc1, 0x80,
|
||||
0x2e, 0x00, 0xc1
|
||||
|
||||
};
|
||||
|
||||
} // namespace esphome::bmi270
|
||||
@@ -0,0 +1,91 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import (
|
||||
CONF_ACCELEROMETER_ODR,
|
||||
CONF_ACCELEROMETER_RANGE,
|
||||
CONF_GYROSCOPE_ODR,
|
||||
CONF_GYROSCOPE_RANGE,
|
||||
)
|
||||
from esphome.components.motion import motion_schema, new_motion_component
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from . import BMI270Component, bmi270_ns
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
# Enum proxies (must match the C++ enum values exactly)
|
||||
BMI270AccelRange = bmi270_ns.enum("BMI270AccelRange")
|
||||
ACCEL_RANGE_OPTIONS = {
|
||||
"2G": BMI270AccelRange.BMI270_ACCEL_RANGE_2G,
|
||||
"4G": BMI270AccelRange.BMI270_ACCEL_RANGE_4G,
|
||||
"8G": BMI270AccelRange.BMI270_ACCEL_RANGE_8G,
|
||||
"16G": BMI270AccelRange.BMI270_ACCEL_RANGE_16G,
|
||||
}
|
||||
|
||||
BMI270GyroRange = bmi270_ns.enum("BMI270GyroRange")
|
||||
GYRO_RANGE_OPTIONS = {
|
||||
"2000DPS": BMI270GyroRange.BMI270_GYRO_RANGE_2000,
|
||||
"1000DPS": BMI270GyroRange.BMI270_GYRO_RANGE_1000,
|
||||
"500DPS": BMI270GyroRange.BMI270_GYRO_RANGE_500,
|
||||
"250DPS": BMI270GyroRange.BMI270_GYRO_RANGE_250,
|
||||
"125DPS": BMI270GyroRange.BMI270_GYRO_RANGE_125,
|
||||
}
|
||||
|
||||
BMI270AccelODR = bmi270_ns.enum("BMI270AccelODR")
|
||||
ACCEL_ODR_OPTIONS = {
|
||||
"12_5HZ": BMI270AccelODR.BMI270_ACCEL_ODR_12_5,
|
||||
"25HZ": BMI270AccelODR.BMI270_ACCEL_ODR_25,
|
||||
"50HZ": BMI270AccelODR.BMI270_ACCEL_ODR_50,
|
||||
"100HZ": BMI270AccelODR.BMI270_ACCEL_ODR_100,
|
||||
"200HZ": BMI270AccelODR.BMI270_ACCEL_ODR_200,
|
||||
"400HZ": BMI270AccelODR.BMI270_ACCEL_ODR_400,
|
||||
"800HZ": BMI270AccelODR.BMI270_ACCEL_ODR_800,
|
||||
"1600HZ": BMI270AccelODR.BMI270_ACCEL_ODR_1600,
|
||||
}
|
||||
|
||||
BMI270GyroODR = bmi270_ns.enum("BMI270GyroODR")
|
||||
GYRO_ODR_OPTIONS = {
|
||||
"25HZ": BMI270GyroODR.BMI270_GYRO_ODR_25,
|
||||
"50HZ": BMI270GyroODR.BMI270_GYRO_ODR_50,
|
||||
"100HZ": BMI270GyroODR.BMI270_GYRO_ODR_100,
|
||||
"200HZ": BMI270GyroODR.BMI270_GYRO_ODR_200,
|
||||
"400HZ": BMI270GyroODR.BMI270_GYRO_ODR_400,
|
||||
"800HZ": BMI270GyroODR.BMI270_GYRO_ODR_800,
|
||||
"1600HZ": BMI270GyroODR.BMI270_GYRO_ODR_1600,
|
||||
"3200HZ": BMI270GyroODR.BMI270_GYRO_ODR_3200,
|
||||
}
|
||||
|
||||
# Top-level CONFIG_SCHEMA
|
||||
CONFIG_SCHEMA = (
|
||||
motion_schema(BMI270Component, has_accel=True, has_gyro=True)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_ACCELEROMETER_RANGE, default="4G"): cv.enum(
|
||||
ACCEL_RANGE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_ACCELEROMETER_ODR, default="100HZ"): cv.enum(
|
||||
ACCEL_ODR_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_GYROSCOPE_RANGE, default="2000DPS"): cv.enum(
|
||||
GYRO_RANGE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_GYROSCOPE_ODR, default="200HZ"): cv.enum(
|
||||
GYRO_ODR_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x68))
|
||||
)
|
||||
|
||||
|
||||
# Code generation
|
||||
async def to_code(config):
|
||||
var = await new_motion_component(config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
# Accelerometer sensors
|
||||
# Hardware configuration
|
||||
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
|
||||
cg.add(var.set_accel_odr(config[CONF_ACCELEROMETER_ODR]))
|
||||
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
|
||||
cg.add(var.set_gyro_odr(config[CONF_GYROSCOPE_ODR]))
|
||||
@@ -0,0 +1,41 @@
|
||||
# YAML config keys
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_THERMOMETER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from . import CONF_BMI270_ID, BMI270Component
|
||||
|
||||
AUTO_LOAD = ["bmi270"]
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_TYPE): cv.one_of(CONF_TEMPERATURE),
|
||||
cv.GenerateID(CONF_BMI270_ID): cv.use_id(BMI270Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
parent = await cg.get_variable(config[CONF_BMI270_ID])
|
||||
data = MockObj("data")
|
||||
value_lambda = await cg.process_lambda(
|
||||
var.publish_state(data),
|
||||
[(cg.float_, str(data))],
|
||||
)
|
||||
cg.add(parent.add_temperature_listener(value_lambda))
|
||||
@@ -5,6 +5,8 @@ CODEOWNERS = ["@esphome/core"]
|
||||
BYTE_ORDER_LITTLE = "little_endian"
|
||||
BYTE_ORDER_BIG = "big_endian"
|
||||
|
||||
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
|
||||
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
|
||||
CONF_B_CONSTANT = "b_constant"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
@@ -13,6 +15,8 @@ CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DATA_BITS = "data_bits"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_GYROSCOPE_ODR = "gyroscope_odr"
|
||||
CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
CONF_LIBRETINY = "libretiny"
|
||||
CONF_LOOP = "loop"
|
||||
|
||||
@@ -1,57 +1,258 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266
|
||||
import logging
|
||||
import re
|
||||
|
||||
CODEOWNERS = ["@SimonFischer04"]
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_PATTERN,
|
||||
CONF_PRIORITY,
|
||||
CONF_RECEIVE_TIMEOUT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@SimonFischer04", "@Tomer27cz", "@latonita", "@PolarGoose"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_DLMS_METER_ID = "dlms_meter_id"
|
||||
CONF_DECRYPTION_KEY = "decryption_key"
|
||||
CONF_AUTH_KEY = "auth_key"
|
||||
CONF_OBIS_CODE = "obis_code"
|
||||
CONF_CUSTOM_PATTERNS = "custom_patterns"
|
||||
CONF_SKIP_CRC = "skip_crc"
|
||||
CONF_DEFAULT_OBIS = "default_obis"
|
||||
CONF_PROVIDER = "provider"
|
||||
|
||||
PROVIDERS = {"generic": 0, "netznoe": 1}
|
||||
|
||||
dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter")
|
||||
DlmsMeterComponent = dlms_meter_component_ns.class_(
|
||||
"DlmsMeterComponent", cg.Component, uart.UARTDevice
|
||||
)
|
||||
|
||||
|
||||
def validate_key(value):
|
||||
value = cv.string_strict(value)
|
||||
if len(value) != 32:
|
||||
raise cv.Invalid("Decryption key must be 32 hex characters (16 bytes)")
|
||||
try:
|
||||
return [int(value[i : i + 2], 16) for i in range(0, 32, 2)]
|
||||
except ValueError as exc:
|
||||
raise cv.Invalid("Decryption key must be hex values from 00 to FF") from exc
|
||||
def obis_code(value):
|
||||
# Normalize the OBIS code to the strict A.B.C.D.E.F format
|
||||
bytes_list = parse_obis_code_bytes(value)
|
||||
return ".".join(str(b) for b in bytes_list)
|
||||
|
||||
|
||||
def parse_obis_code_bytes(value):
|
||||
value = cv.string(value)
|
||||
normalized = re.sub(r"[\-\:\*]", ".", value)
|
||||
parts = normalized.split(".")
|
||||
if len(parts) < 5 or len(parts) > 6:
|
||||
raise cv.Invalid("OBIS code must have 5 or 6 parts")
|
||||
try:
|
||||
bytes_list = [int(p) for p in parts]
|
||||
except ValueError as exc:
|
||||
raise cv.Invalid("OBIS code parts must be integers") from exc
|
||||
for b in bytes_list:
|
||||
if b < 0 or b > 255:
|
||||
raise cv.Invalid("OBIS code parts must be between 0 and 255")
|
||||
if len(bytes_list) == 5:
|
||||
bytes_list.append(255)
|
||||
return bytes_list
|
||||
|
||||
|
||||
def custom_pattern_dict(value):
|
||||
if isinstance(value, str):
|
||||
return {CONF_PATTERN: value}
|
||||
return value
|
||||
|
||||
|
||||
def validate_custom_pattern(value):
|
||||
if CONF_DEFAULT_OBIS in value and CONF_NAME not in value:
|
||||
raise cv.Invalid(f"'{CONF_DEFAULT_OBIS}' requires '{CONF_NAME}' to be set")
|
||||
return value
|
||||
|
||||
|
||||
def validate_provider_deprecation(config):
|
||||
if CONF_PROVIDER in config:
|
||||
provider = str(config[CONF_PROVIDER]).lower()
|
||||
if provider == "netznoe":
|
||||
_LOGGER.warning(
|
||||
"The 'provider: netznoe' option is deprecated and will be removed in 2026.11.0. "
|
||||
"The required custom patterns have been added automatically for this release, but you must update your configuration.\n"
|
||||
"Please remove the 'provider' key and explicitly replace it with the following:\n\n"
|
||||
"custom_patterns:\n"
|
||||
' - pattern: "L, TSTR"\n'
|
||||
' name: "MeterID"\n'
|
||||
' default_obis: "0.0.96.1.0.255"\n'
|
||||
' - pattern: "F, TDTM"\n'
|
||||
' name: "DateTime"\n'
|
||||
' default_obis: "0.0.1.0.0.255"\n'
|
||||
)
|
||||
patterns = config.get(CONF_CUSTOM_PATTERNS, [])
|
||||
|
||||
# Ensure "L, TSTR" for MeterID is present
|
||||
if not any(p.get(CONF_PATTERN) == "L, TSTR" for p in patterns):
|
||||
patterns.append(
|
||||
{
|
||||
CONF_PATTERN: "L, TSTR",
|
||||
CONF_NAME: "MeterID",
|
||||
CONF_DEFAULT_OBIS: [0, 0, 96, 1, 0, 255],
|
||||
CONF_PRIORITY: 0,
|
||||
}
|
||||
)
|
||||
|
||||
# Ensure "F, TDTM" for DateTime is present
|
||||
if not any(p.get(CONF_PATTERN) == "F, TDTM" for p in patterns):
|
||||
patterns.append(
|
||||
{
|
||||
CONF_PATTERN: "F, TDTM",
|
||||
CONF_NAME: "DateTime",
|
||||
CONF_DEFAULT_OBIS: [0, 0, 1, 0, 0, 255],
|
||||
CONF_PRIORITY: 0,
|
||||
}
|
||||
)
|
||||
|
||||
config[CONF_CUSTOM_PATTERNS] = patterns
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"The 'provider' option is deprecated and will be removed in 2026.11.0. "
|
||||
"The dlms_parser library now handles quirks dynamically. "
|
||||
"Please remove this option from your configuration."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CUSTOM_PATTERN_SCHEMA = cv.All(
|
||||
custom_pattern_dict,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PATTERN): cv.string,
|
||||
cv.Optional(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_PRIORITY, default=0): cv.int_,
|
||||
cv.Optional(CONF_DEFAULT_OBIS): parse_obis_code_bytes,
|
||||
}
|
||||
),
|
||||
validate_custom_pattern,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_DECRYPTION_KEY): validate_key,
|
||||
cv.Optional(CONF_PROVIDER, default="generic"): cv.enum(
|
||||
PROVIDERS, lower=True
|
||||
cv.Optional(CONF_DECRYPTION_KEY): lambda value: cv.bind_key(
|
||||
value, name="Decryption key"
|
||||
),
|
||||
cv.Optional(CONF_AUTH_KEY): lambda value: cv.bind_key(
|
||||
value, name="Authentication key"
|
||||
),
|
||||
cv.Optional(CONF_CUSTOM_PATTERNS): cv.ensure_list(CUSTOM_PATTERN_SCHEMA),
|
||||
cv.Optional(CONF_SKIP_CRC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PROVIDER): cv.string,
|
||||
cv.Optional(
|
||||
CONF_RECEIVE_TIMEOUT, default="1000ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32]),
|
||||
validate_provider_deprecation,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"dlms_meter", baud_rate=2400, require_rx=True
|
||||
)
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("dlms_meter", require_rx=True)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
dec_key_expr = cg.RawExpression("std::nullopt")
|
||||
if dec_key := config.get(CONF_DECRYPTION_KEY):
|
||||
key_bytes = [str(int(dec_key[i : i + 2], 16)) for i in range(0, 32, 2)]
|
||||
dec_key_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 16>{{{', '.join(key_bytes)}}}"
|
||||
)
|
||||
|
||||
auth_key_expr = cg.RawExpression("std::nullopt")
|
||||
if auth_key := config.get(CONF_AUTH_KEY):
|
||||
key_bytes = [str(int(auth_key[i : i + 2], 16)) for i in range(0, 32, 2)]
|
||||
auth_key_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 16>{{{', '.join(key_bytes)}}}"
|
||||
)
|
||||
|
||||
patterns = []
|
||||
if custom_patterns := config.get(CONF_CUSTOM_PATTERNS):
|
||||
for p in custom_patterns:
|
||||
name_expr = cg.RawExpression("std::nullopt")
|
||||
if name_val := p.get(CONF_NAME):
|
||||
name_expr = name_val
|
||||
|
||||
if obis_vals := p.get(CONF_DEFAULT_OBIS):
|
||||
obis_expr = cg.RawExpression(
|
||||
f"std::array<uint8_t, 6>{{{obis_vals[0]}, {obis_vals[1]}, {obis_vals[2]}, {obis_vals[3]}, {obis_vals[4]}, {obis_vals[5]}}}"
|
||||
)
|
||||
else:
|
||||
obis_expr = cg.RawExpression("std::nullopt")
|
||||
|
||||
patterns.append(
|
||||
cg.ArrayInitializer(
|
||||
p[CONF_PATTERN],
|
||||
name_expr,
|
||||
p.get(CONF_PRIORITY, 0),
|
||||
obis_expr,
|
||||
)
|
||||
)
|
||||
|
||||
patterns_expr = (
|
||||
cg.ArrayInitializer(*patterns) if patterns else cg.RawExpression("{}")
|
||||
)
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
config[CONF_RECEIVE_TIMEOUT],
|
||||
config[CONF_SKIP_CRC],
|
||||
dec_key_expr,
|
||||
auth_key_expr,
|
||||
patterns_expr,
|
||||
)
|
||||
|
||||
hub_id = config[CONF_ID].id
|
||||
|
||||
sensor_count = 0
|
||||
for sens_conf in CORE.config.get("sensor", []):
|
||||
if (
|
||||
sens_conf.get("platform") == "dlms_meter"
|
||||
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||
):
|
||||
if CONF_OBIS_CODE in sens_conf:
|
||||
sensor_count += 1
|
||||
else:
|
||||
from .sensor import NUMERIC_KEYS
|
||||
|
||||
sensor_count += sum(1 for key in NUMERIC_KEYS if key in sens_conf)
|
||||
|
||||
text_sensor_count = 0
|
||||
for sens_conf in CORE.config.get("text_sensor", []):
|
||||
if (
|
||||
sens_conf.get("platform") == "dlms_meter"
|
||||
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||
):
|
||||
if CONF_OBIS_CODE in sens_conf:
|
||||
text_sensor_count += 1
|
||||
else:
|
||||
from .text_sensor import TEXT_KEYS
|
||||
|
||||
text_sensor_count += sum(1 for key in TEXT_KEYS if key in sens_conf)
|
||||
|
||||
binary_sensor_count = 0
|
||||
for sens_conf in CORE.config.get("binary_sensor", []):
|
||||
if (
|
||||
sens_conf.get("platform") == "dlms_meter"
|
||||
and sens_conf.get(CONF_DLMS_METER_ID).id == hub_id
|
||||
):
|
||||
binary_sensor_count += 1
|
||||
|
||||
cg.add_define("DLMS_MAX_SENSORS", sensor_count)
|
||||
cg.add_define("DLMS_MAX_TEXT_SENSORS", text_sensor_count)
|
||||
cg.add_define("DLMS_MAX_BINARY_SENSORS", binary_sensor_count)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
key = ", ".join(str(b) for b in config[CONF_DECRYPTION_KEY])
|
||||
cg.add(var.set_decryption_key(cg.RawExpression(f"{{{key}}}")))
|
||||
cg.add(var.set_provider(PROVIDERS[config[CONF_PROVIDER]]))
|
||||
|
||||
if CORE.is_esp32:
|
||||
esp32.add_idf_component(name="esphome/dlms_parser", ref="1.1.0")
|
||||
else:
|
||||
cg.add_library("esphome/dlms_parser", "1.1.0")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
cg.add(hub.register_binary_sensor(config[CONF_OBIS_CODE], var))
|
||||
@@ -1,71 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
/*
|
||||
+-------------------------------+
|
||||
| Ciphering Service |
|
||||
+-------------------------------+
|
||||
| System Title Length |
|
||||
+-------------------------------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| System |
|
||||
| Title |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+-------------------------------+
|
||||
| Length | (1 or 3 Bytes)
|
||||
+-------------------------------+
|
||||
| Security Control Byte |
|
||||
+-------------------------------+
|
||||
| |
|
||||
| Frame |
|
||||
| Counter |
|
||||
| |
|
||||
+-------------------------------+
|
||||
| |
|
||||
~ ~
|
||||
Encrypted Payload
|
||||
~ ~
|
||||
| |
|
||||
+-------------------------------+
|
||||
|
||||
Ciphering Service: 0xDB (General-Glo-Ciphering)
|
||||
System Title Length: 0x08
|
||||
System Title: Unique ID of meter
|
||||
Length: 1 Byte=Length <= 127, 3 Bytes=Length > 127 (0x82 & 2 Bytes length)
|
||||
Security Control Byte:
|
||||
- Bit 3…0: Security_Suite_Id
|
||||
- Bit 4: "A" subfield: indicates that authentication is applied
|
||||
- Bit 5: "E" subfield: indicates that encryption is applied
|
||||
- Bit 6: Key_Set subfield: 0 = Unicast, 1 = Broadcast
|
||||
- Bit 7: Indicates the use of compression.
|
||||
*/
|
||||
|
||||
static constexpr uint8_t DLMS_HEADER_LENGTH = 16;
|
||||
static constexpr uint8_t DLMS_HEADER_EXT_OFFSET = 2; // Extra offset for extended length header
|
||||
static constexpr uint8_t DLMS_CIPHER_OFFSET = 0;
|
||||
static constexpr uint8_t DLMS_SYST_OFFSET = 1;
|
||||
static constexpr uint8_t DLMS_LENGTH_OFFSET = 10;
|
||||
static constexpr uint8_t TWO_BYTE_LENGTH = 0x82;
|
||||
static constexpr uint8_t DLMS_LENGTH_CORRECTION = 5; // Header bytes included in length field
|
||||
static constexpr uint8_t DLMS_SECBYTE_OFFSET = 11;
|
||||
static constexpr uint8_t DLMS_FRAMECOUNTER_OFFSET = 12;
|
||||
static constexpr uint8_t DLMS_FRAMECOUNTER_LENGTH = 4;
|
||||
static constexpr uint8_t DLMS_PAYLOAD_OFFSET = 16;
|
||||
static constexpr uint8_t GLO_CIPHERING = 0xDB;
|
||||
static constexpr uint8_t DATA_NOTIFICATION = 0x0F;
|
||||
static constexpr uint8_t TIMESTAMP_DATETIME = 0x0C;
|
||||
static constexpr uint16_t MAX_MESSAGE_LENGTH = 512; // Maximum size of message (when having 2 bytes length in header).
|
||||
|
||||
// Provider specific quirks
|
||||
static constexpr uint8_t NETZ_NOE_MAGIC_BYTE = 0x81; // Magic length byte used by Netz NOE
|
||||
static constexpr uint8_t NETZ_NOE_EXPECTED_MESSAGE_LENGTH = 0xF8;
|
||||
static constexpr uint8_t NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE = 0x20;
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
@@ -1,516 +1,236 @@
|
||||
#include "dlms_meter.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
||||
#include <bearssl/bearssl.h>
|
||||
#elif defined(USE_ESP32)
|
||||
#include <esp_idf_version.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
#include <psa/crypto.h>
|
||||
#else
|
||||
#include "mbedtls/esp_config.h"
|
||||
#include "mbedtls/gcm.h"
|
||||
#endif
|
||||
#endif
|
||||
#include <cstdio>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
static constexpr const char *TAG = "dlms_meter";
|
||||
static const char *const TAG = "dlms_meter";
|
||||
static void log_callback(dlms_parser::LogLevel level, const char *fmt, va_list args) {
|
||||
std::array<char, 256> buf;
|
||||
vsnprintf(buf.data(), buf.size(), fmt, args);
|
||||
switch (level) {
|
||||
case dlms_parser::LogLevel::ERROR:
|
||||
ESP_LOGE(TAG, "%s", buf.data());
|
||||
break;
|
||||
case dlms_parser::LogLevel::WARNING:
|
||||
ESP_LOGW(TAG, "%s", buf.data());
|
||||
break;
|
||||
case dlms_parser::LogLevel::INFO:
|
||||
ESP_LOGI(TAG, "%s", buf.data());
|
||||
break;
|
||||
case dlms_parser::LogLevel::VERBOSE:
|
||||
ESP_LOGV(TAG, "%s", buf.data());
|
||||
break;
|
||||
case dlms_parser::LogLevel::VERY_VERBOSE:
|
||||
ESP_LOGVV(TAG, "%s", buf.data());
|
||||
break;
|
||||
case dlms_parser::LogLevel::DEBUG:
|
||||
ESP_LOGD(TAG, "%s", buf.data());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DlmsMeterComponent::DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check,
|
||||
std::optional<std::array<uint8_t, 16>> decryption_key,
|
||||
std::optional<std::array<uint8_t, 16>> authentication_key,
|
||||
std::vector<CustomPattern> custom_patterns)
|
||||
: receive_timeout_ms_(receive_timeout_ms),
|
||||
skip_crc_check_(skip_crc_check),
|
||||
custom_patterns_(std::move(custom_patterns)),
|
||||
parser_(&decryptor_) {
|
||||
dlms_parser::Logger::set_log_function(log_callback);
|
||||
|
||||
if (decryption_key.has_value()) {
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
ESP_LOGE(TAG, "Decryption is not supported on this platform (no compatible crypto library found)");
|
||||
#else
|
||||
auto opt_key = dlms_parser::Aes128GcmDecryptionKey::from_bytes(decryption_key.value());
|
||||
if (opt_key) {
|
||||
this->parser_.set_decryption_key(*opt_key);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to set decryption key: invalid key format");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (authentication_key.has_value()) {
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
ESP_LOGE(TAG, "Authentication is not supported on this platform (no compatible crypto library found)");
|
||||
#else
|
||||
auto opt_key = dlms_parser::Aes128GcmAuthenticationKey::from_bytes(authentication_key.value());
|
||||
if (opt_key) {
|
||||
this->parser_.set_authentication_key(*opt_key);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to set authentication key: invalid key format");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
this->parser_.set_skip_crc_check(this->skip_crc_check_);
|
||||
|
||||
this->parser_.load_default_patterns();
|
||||
for (const auto &pattern : this->custom_patterns_) {
|
||||
if (pattern.default_obis.has_value() && pattern.name.has_value()) {
|
||||
this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority,
|
||||
pattern.default_obis.value());
|
||||
} else if (pattern.name.has_value()) {
|
||||
this->parser_.register_pattern(pattern.name->c_str(), pattern.pattern.c_str(), pattern.priority);
|
||||
} else {
|
||||
this->parser_.register_pattern(pattern.pattern.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DlmsMeterComponent::setup() { this->flush_rx_buffer_(); }
|
||||
|
||||
void DlmsMeterComponent::dump_config() {
|
||||
const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic";
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"DLMS Meter:\n"
|
||||
" Provider: %s\n"
|
||||
" Read Timeout: %" PRIu32 " ms",
|
||||
provider_name, this->read_timeout_);
|
||||
#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_);
|
||||
DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, )
|
||||
#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_);
|
||||
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, )
|
||||
ESP_LOGCONFIG(TAG, "DLMS Meter:");
|
||||
ESP_LOGCONFIG(TAG, " Receive Timeout: %u ms", this->receive_timeout_ms_);
|
||||
ESP_LOGCONFIG(TAG, " Skip CRC Check: %s", YESNO(this->skip_crc_check_));
|
||||
|
||||
for (const auto &pattern : this->custom_patterns_) {
|
||||
if (pattern.default_obis.has_value() && pattern.name.has_value()) {
|
||||
const auto &obis = pattern.default_obis.value();
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d, default_obis: %d.%d.%d.%d.%d.%d)",
|
||||
pattern.pattern.c_str(), pattern.name->c_str(), pattern.priority, obis[0], obis[1], obis[2],
|
||||
obis[3], obis[4], obis[5]);
|
||||
} else if (pattern.name.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s' (name: %s, priority: %d)", pattern.pattern.c_str(),
|
||||
pattern.name->c_str(), pattern.priority);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Custom Pattern: '%s'", pattern.pattern.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
for (const auto &entry : this->sensors_) {
|
||||
LOG_SENSOR(" ", "Numeric Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
for (const auto &entry : this->text_sensors_) {
|
||||
LOG_TEXT_SENSOR(" ", "Text Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (const auto &entry : this->binary_sensors_) {
|
||||
LOG_BINARY_SENSOR(" ", "Binary Sensor (OBIS)", entry.sensor);
|
||||
ESP_LOGCONFIG(TAG, " OBIS: %s", entry.obis_code.c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void DlmsMeterComponent::loop() {
|
||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||
size_t avail = this->available();
|
||||
if (avail > 0) {
|
||||
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
||||
if (remaining == 0) {
|
||||
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
||||
} else {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// Cap reads to remaining buffer capacity.
|
||||
if (avail > remaining) {
|
||||
avail = remaining;
|
||||
}
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
|
||||
this->mbus_payload_.clear();
|
||||
if (!this->parse_mbus_(this->mbus_payload_))
|
||||
return;
|
||||
|
||||
uint16_t message_length;
|
||||
uint8_t systitle_length;
|
||||
uint16_t header_offset;
|
||||
if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset))
|
||||
return;
|
||||
|
||||
if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) {
|
||||
ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Decrypt in place and then decode the OBIS codes
|
||||
if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset))
|
||||
return;
|
||||
this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length);
|
||||
this->read_rx_buffer_();
|
||||
if (this->bytes_accumulated_ > 0 &&
|
||||
App.get_loop_component_start_time() - this->last_rx_char_time_ > this->receive_timeout_ms_) {
|
||||
this->process_frame_();
|
||||
}
|
||||
}
|
||||
|
||||
bool DlmsMeterComponent::parse_mbus_(std::vector<uint8_t> &mbus_payload) {
|
||||
ESP_LOGV(TAG, "Parsing M-Bus frames");
|
||||
uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames
|
||||
|
||||
while (frame_offset < this->receive_buffer_.size()) {
|
||||
// Ensure enough bytes remain for the minimal intro header before accessing indices
|
||||
if (this->receive_buffer_.size() - frame_offset < MBUS_HEADER_INTRO_LENGTH) {
|
||||
ESP_LOGE(TAG, "MBUS: Not enough data for frame header (need %d, have %d)", MBUS_HEADER_INTRO_LENGTH,
|
||||
(this->receive_buffer_.size() - frame_offset));
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check start bytes
|
||||
if (this->receive_buffer_[frame_offset + MBUS_START1_OFFSET] != START_BYTE_LONG_FRAME ||
|
||||
this->receive_buffer_[frame_offset + MBUS_START2_OFFSET] != START_BYTE_LONG_FRAME) {
|
||||
ESP_LOGE(TAG, "MBUS: Start bytes do not match");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Both length bytes must be identical
|
||||
if (this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET] !=
|
||||
this->receive_buffer_[frame_offset + MBUS_LENGTH2_OFFSET]) {
|
||||
ESP_LOGE(TAG, "MBUS: Length bytes do not match");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t frame_length = this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET]; // Get length of this frame
|
||||
|
||||
// Check if received data is enough for the given frame length
|
||||
if (this->receive_buffer_.size() - frame_offset <
|
||||
frame_length + 3) { // length field inside packet does not account for second start- + checksum- + stop- byte
|
||||
ESP_LOGE(TAG, "MBUS: Frame too big for received data");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we have full frame (header + payload + checksum + stop byte) before accessing stop byte
|
||||
size_t required_total =
|
||||
frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH; // payload + header + 2 footer bytes
|
||||
if (this->receive_buffer_.size() - frame_offset < required_total) {
|
||||
ESP_LOGE(TAG, "MBUS: Incomplete frame (need %d, have %d)", (unsigned int) required_total,
|
||||
this->receive_buffer_.size() - frame_offset);
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH - 1] !=
|
||||
STOP_BYTE) {
|
||||
ESP_LOGE(TAG, "MBUS: Invalid stop byte");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify checksum: sum of all bytes starting at MBUS_HEADER_INTRO_LENGTH, take last byte
|
||||
uint8_t checksum = 0; // use uint8_t so only the 8 least significant bits are stored
|
||||
for (uint16_t i = 0; i < frame_length; i++) {
|
||||
checksum += this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + i];
|
||||
}
|
||||
if (checksum != this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]) {
|
||||
ESP_LOGE(TAG, "MBUS: Invalid checksum: %x != %x", checksum,
|
||||
this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]);
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
mbus_payload.insert(mbus_payload.end(), &this->receive_buffer_[frame_offset + MBUS_FULL_HEADER_LENGTH],
|
||||
&this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + frame_length]);
|
||||
|
||||
frame_offset += MBUS_HEADER_INTRO_LENGTH + frame_length + MBUS_FOOTER_LENGTH;
|
||||
void DlmsMeterComponent::flush_rx_buffer_() {
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DlmsMeterComponent::parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length,
|
||||
uint8_t &systitle_length, uint16_t &header_offset) {
|
||||
ESP_LOGV(TAG, "Parsing DLMS header");
|
||||
if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) {
|
||||
ESP_LOGE(TAG, "DLMS: Payload too short");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
void DlmsMeterComponent::read_rx_buffer_() {
|
||||
int available = this->available();
|
||||
if (available == 0)
|
||||
return;
|
||||
|
||||
if (this->bytes_accumulated_ + available > this->rx_buffer_.size()) {
|
||||
ESP_LOGW(TAG, "RX Buffer overflow. Frame too large! Dropping frame.");
|
||||
this->bytes_accumulated_ = 0;
|
||||
|
||||
this->flush_rx_buffer_();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB)
|
||||
ESP_LOGE(TAG, "DLMS: Unsupported cipher");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
bool success = this->read_array(this->rx_buffer_.data() + this->bytes_accumulated_, available);
|
||||
if (!success) {
|
||||
ESP_LOGW(TAG, "UART read failed. Dropping frame.");
|
||||
this->bytes_accumulated_ = 0;
|
||||
this->flush_rx_buffer_();
|
||||
return;
|
||||
}
|
||||
|
||||
systitle_length = mbus_payload[DLMS_SYST_OFFSET];
|
||||
this->bytes_accumulated_ += available;
|
||||
|
||||
if (systitle_length != 0x08) { // Only system titles with length of 8 are supported
|
||||
ESP_LOGE(TAG, "DLMS: Unsupported system title length");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
message_length = mbus_payload[DLMS_LENGTH_OFFSET];
|
||||
header_offset = 0;
|
||||
|
||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
||||
// for some reason EVN seems to set the standard "length" field to 0x81 and then the actual length is in the next
|
||||
// byte. Check some bytes to see if received data still matches expectation
|
||||
if (message_length == NETZ_NOE_MAGIC_BYTE &&
|
||||
mbus_payload[DLMS_LENGTH_OFFSET + 1] == NETZ_NOE_EXPECTED_MESSAGE_LENGTH &&
|
||||
mbus_payload[DLMS_LENGTH_OFFSET + 2] == NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE) {
|
||||
message_length = mbus_payload[DLMS_LENGTH_OFFSET + 1];
|
||||
header_offset = 1;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Wrong Length - Security Control Byte sequence detected for provider EVN");
|
||||
}
|
||||
} else {
|
||||
if (message_length == TWO_BYTE_LENGTH) {
|
||||
message_length = encode_uint16(mbus_payload[DLMS_LENGTH_OFFSET + 1], mbus_payload[DLMS_LENGTH_OFFSET + 2]);
|
||||
header_offset = DLMS_HEADER_EXT_OFFSET;
|
||||
}
|
||||
}
|
||||
if (message_length < DLMS_LENGTH_CORRECTION) {
|
||||
ESP_LOGE(TAG, "DLMS: Message length too short: %u", message_length);
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
message_length -= DLMS_LENGTH_CORRECTION; // Correct message length due to part of header being included in length
|
||||
|
||||
if (mbus_payload.size() - DLMS_HEADER_LENGTH - header_offset != message_length) {
|
||||
ESP_LOGV(TAG, "DLMS: Length mismatch - payload=%d, header=%d, offset=%d, message=%d", mbus_payload.size(),
|
||||
DLMS_HEADER_LENGTH, header_offset, message_length);
|
||||
ESP_LOGE(TAG, "DLMS: Message has invalid length");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != 0x21 &&
|
||||
mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] !=
|
||||
0x20) { // Only certain security suite is supported (0x21 || 0x20)
|
||||
ESP_LOGE(TAG, "DLMS: Unsupported security control byte");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
this->last_rx_char_time_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
|
||||
uint16_t header_offset) {
|
||||
ESP_LOGV(TAG, "Decrypting payload");
|
||||
uint8_t iv[12]; // Reserve space for the IV, always 12 bytes
|
||||
// Copy system title to IV (System title is before length; no header offset needed!)
|
||||
// Add 1 to the offset in order to skip the system title length byte
|
||||
memcpy(&iv[0], &mbus_payload[DLMS_SYST_OFFSET + 1], systitle_length);
|
||||
memcpy(&iv[8], &mbus_payload[header_offset + DLMS_FRAMECOUNTER_OFFSET],
|
||||
DLMS_FRAMECOUNTER_LENGTH); // Copy frame counter to IV
|
||||
void DlmsMeterComponent::process_frame_() {
|
||||
ESP_LOGV(TAG, "Processing frame of size: %zu bytes", this->bytes_accumulated_);
|
||||
|
||||
uint8_t *payload_ptr = &mbus_payload[header_offset + DLMS_PAYLOAD_OFFSET];
|
||||
auto callback = [this](const char *obis_code, float float_val, const char *str_val, bool is_numeric) {
|
||||
this->on_data_(obis_code, float_val, str_val, is_numeric);
|
||||
};
|
||||
|
||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
||||
br_gcm_context gcm_ctx;
|
||||
br_aes_ct_ctr_keys bc;
|
||||
br_aes_ct_ctr_init(&bc, this->decryption_key_.data(), this->decryption_key_.size());
|
||||
br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32);
|
||||
br_gcm_reset(&gcm_ctx, iv, sizeof(iv));
|
||||
br_gcm_flip(&gcm_ctx);
|
||||
br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
|
||||
#elif defined(USE_ESP32)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// PSA Crypto multipart AEAD (no tag verification, matching legacy behavior)
|
||||
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
|
||||
psa_set_key_bits(&attributes, this->decryption_key_.size() * 8);
|
||||
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
|
||||
psa_set_key_algorithm(&attributes, PSA_ALG_GCM);
|
||||
this->parser_.parse({this->rx_buffer_.data(), this->bytes_accumulated_}, callback);
|
||||
|
||||
mbedtls_svc_key_id_t key_id;
|
||||
bool decrypt_failed = true;
|
||||
if (psa_import_key(&attributes, this->decryption_key_.data(), this->decryption_key_.size(), &key_id) == PSA_SUCCESS) {
|
||||
psa_aead_operation_t op = PSA_AEAD_OPERATION_INIT;
|
||||
if (psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM) == PSA_SUCCESS &&
|
||||
psa_aead_set_nonce(&op, iv, sizeof(iv)) == PSA_SUCCESS) {
|
||||
size_t outlen = 0;
|
||||
if (psa_aead_update(&op, payload_ptr, message_length, payload_ptr, message_length, &outlen) == PSA_SUCCESS &&
|
||||
outlen == message_length) {
|
||||
decrypt_failed = false;
|
||||
this->bytes_accumulated_ = 0;
|
||||
}
|
||||
|
||||
void DlmsMeterComponent::on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric) {
|
||||
int updated_count = 0;
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
if (is_numeric) {
|
||||
for (auto &item : this->sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(float_val);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
psa_aead_abort(&op);
|
||||
psa_destroy_key(key_id);
|
||||
}
|
||||
if (decrypt_failed) {
|
||||
ESP_LOGE(TAG, "Decryption failed");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
size_t outlen = 0;
|
||||
mbedtls_gcm_context gcm_ctx;
|
||||
mbedtls_gcm_init(&gcm_ctx);
|
||||
mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8);
|
||||
mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv));
|
||||
auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen);
|
||||
mbedtls_gcm_free(&gcm_ctx);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "Decryption failed with error: %d", ret);
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
#error "Invalid Platform"
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (!is_numeric && str_val != nullptr) {
|
||||
for (auto &item : this->text_sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(str_val);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) {
|
||||
ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_length) {
|
||||
ESP_LOGV(TAG, "Decoding payload");
|
||||
MeterData data{};
|
||||
uint16_t current_position = DECODER_START_OFFSET;
|
||||
bool power_factor_found = false;
|
||||
|
||||
while (current_position + OBIS_CODE_OFFSET <= message_length) {
|
||||
if (plaintext[current_position + OBIS_TYPE_OFFSET] != DataType::OCTET_STRING) {
|
||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header type: %x", plaintext[current_position + OBIS_TYPE_OFFSET]);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t obis_code_length = plaintext[current_position + OBIS_LENGTH_OFFSET];
|
||||
if (obis_code_length != OBIS_CODE_LENGTH_STANDARD && obis_code_length != OBIS_CODE_LENGTH_EXTENDED) {
|
||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS header length: %x", obis_code_length);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
if (current_position + OBIS_CODE_OFFSET + obis_code_length > message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for OBIS code");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *obis_code = &plaintext[current_position + OBIS_CODE_OFFSET];
|
||||
uint8_t obis_medium = obis_code[OBIS_A];
|
||||
uint16_t obis_cd = encode_uint16(obis_code[OBIS_C], obis_code[OBIS_D]);
|
||||
|
||||
bool timestamp_found = false;
|
||||
bool meter_number_found = false;
|
||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
||||
// Do not advance Position when reading the Timestamp at DECODER_START_OFFSET
|
||||
if ((obis_code_length == OBIS_CODE_LENGTH_EXTENDED) && (current_position == DECODER_START_OFFSET)) {
|
||||
timestamp_found = true;
|
||||
} else if (power_factor_found) {
|
||||
meter_number_found = true;
|
||||
power_factor_found = false;
|
||||
} else {
|
||||
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code and position
|
||||
}
|
||||
} else {
|
||||
current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code, position and type
|
||||
}
|
||||
if (!timestamp_found && !meter_number_found && obis_medium != Medium::ELECTRICITY &&
|
||||
obis_medium != Medium::ABSTRACT) {
|
||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS medium: %x", obis_medium);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (current_position >= message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for data type");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
float value = 0.0f;
|
||||
uint8_t value_size = 0;
|
||||
uint8_t data_type = plaintext[current_position];
|
||||
current_position++;
|
||||
|
||||
switch (data_type) {
|
||||
case DataType::DOUBLE_LONG_UNSIGNED: {
|
||||
value_size = 4;
|
||||
if (current_position + value_size > message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for DOUBLE_LONG_UNSIGNED");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
value = encode_uint32(plaintext[current_position + 0], plaintext[current_position + 1],
|
||||
plaintext[current_position + 2], plaintext[current_position + 3]);
|
||||
current_position += value_size;
|
||||
break;
|
||||
}
|
||||
case DataType::LONG_UNSIGNED: {
|
||||
value_size = 2;
|
||||
if (current_position + value_size > message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for LONG_UNSIGNED");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
value = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
|
||||
current_position += value_size;
|
||||
break;
|
||||
}
|
||||
case DataType::OCTET_STRING: {
|
||||
uint8_t data_length = plaintext[current_position];
|
||||
current_position++; // Advance past string length
|
||||
if (current_position + data_length > message_length) {
|
||||
ESP_LOGE(TAG, "OBIS: Buffer too short for OCTET_STRING");
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
// Handle timestamp (normal OBIS code or NETZNOE special case)
|
||||
if (obis_cd == OBIS_TIMESTAMP || timestamp_found) {
|
||||
if (data_length < 8) {
|
||||
ESP_LOGE(TAG, "OBIS: Timestamp data too short: %u", data_length);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
uint16_t year = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]);
|
||||
uint8_t month = plaintext[current_position + 2];
|
||||
uint8_t day = plaintext[current_position + 3];
|
||||
uint8_t hour = plaintext[current_position + 5];
|
||||
uint8_t minute = plaintext[current_position + 6];
|
||||
uint8_t second = plaintext[current_position + 7];
|
||||
if (year > 9999 || month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59) {
|
||||
ESP_LOGE(TAG, "Invalid timestamp values: %04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, minute,
|
||||
second);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
snprintf(data.timestamp, sizeof(data.timestamp), "%04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour,
|
||||
minute, second);
|
||||
} else if (meter_number_found) {
|
||||
snprintf(data.meternumber, sizeof(data.meternumber), "%.*s", data_length, &plaintext[current_position]);
|
||||
}
|
||||
current_position += data_length;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGE(TAG, "OBIS: Unsupported OBIS data type: %x", data_type);
|
||||
this->receive_buffer_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip break after data
|
||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
||||
// Don't skip the break on the first timestamp, as there's none
|
||||
if (!timestamp_found) {
|
||||
current_position += 2;
|
||||
}
|
||||
} else {
|
||||
current_position += 2;
|
||||
}
|
||||
|
||||
// Check for additional data (scaler-unit structure)
|
||||
if (current_position < message_length && plaintext[current_position] == DataType::INTEGER) {
|
||||
// Apply scaler: real_value = raw_value × 10^scaler
|
||||
if (current_position + 1 < message_length) {
|
||||
int8_t scaler = static_cast<int8_t>(plaintext[current_position + 1]);
|
||||
if (scaler != 0) {
|
||||
value *= pow10_int(scaler);
|
||||
}
|
||||
}
|
||||
|
||||
// on EVN Meters there is no additional break
|
||||
if (this->provider_ == PROVIDER_NETZNOE) {
|
||||
current_position += 4;
|
||||
} else {
|
||||
current_position += 6;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED)
|
||||
if (value_size > 0) {
|
||||
switch (obis_cd) {
|
||||
case OBIS_VOLTAGE_L1:
|
||||
data.voltage_l1 = value;
|
||||
break;
|
||||
case OBIS_VOLTAGE_L2:
|
||||
data.voltage_l2 = value;
|
||||
break;
|
||||
case OBIS_VOLTAGE_L3:
|
||||
data.voltage_l3 = value;
|
||||
break;
|
||||
case OBIS_CURRENT_L1:
|
||||
data.current_l1 = value;
|
||||
break;
|
||||
case OBIS_CURRENT_L2:
|
||||
data.current_l2 = value;
|
||||
break;
|
||||
case OBIS_CURRENT_L3:
|
||||
data.current_l3 = value;
|
||||
break;
|
||||
case OBIS_ACTIVE_POWER_PLUS:
|
||||
data.active_power_plus = value;
|
||||
break;
|
||||
case OBIS_ACTIVE_POWER_MINUS:
|
||||
data.active_power_minus = value;
|
||||
break;
|
||||
case OBIS_ACTIVE_ENERGY_PLUS:
|
||||
data.active_energy_plus = value;
|
||||
break;
|
||||
case OBIS_ACTIVE_ENERGY_MINUS:
|
||||
data.active_energy_minus = value;
|
||||
break;
|
||||
case OBIS_REACTIVE_ENERGY_PLUS:
|
||||
data.reactive_energy_plus = value;
|
||||
break;
|
||||
case OBIS_REACTIVE_ENERGY_MINUS:
|
||||
data.reactive_energy_minus = value;
|
||||
break;
|
||||
case OBIS_POWER_FACTOR:
|
||||
data.power_factor = value;
|
||||
power_factor_found = true;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (is_numeric) {
|
||||
bool state = float_val != 0.0f;
|
||||
for (auto &item : this->binary_sensors_) {
|
||||
if (item.obis_code == obis_code) {
|
||||
item.sensor->publish_state(state);
|
||||
updated_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->receive_buffer_.clear();
|
||||
|
||||
ESP_LOGI(TAG, "Received valid data");
|
||||
this->publish_sensors(data);
|
||||
this->status_clear_warning();
|
||||
if (updated_count == 0) {
|
||||
ESP_LOGV(TAG, "Received OBIS %s, but no sensors are registered for it.", obis_code);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void DlmsMeterComponent::register_sensor(const std::string &obis_code, sensor::Sensor *sensor) {
|
||||
this->sensors_.push_back({obis_code, sensor});
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void DlmsMeterComponent::register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor) {
|
||||
this->text_sensors_.push_back({obis_code, sensor});
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void DlmsMeterComponent::register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor) {
|
||||
this->binary_sensors_.push_back({obis_code, sensor});
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
|
||||
@@ -2,95 +2,150 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
#include "mbus.h"
|
||||
#include "dlms.h"
|
||||
#include "obis.h"
|
||||
#include <dlms_parser/dlms_parser.h>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
#include <dlms_parser/decryption/aes_128_gcm_decryptor_tfpsa.h>
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
#if __has_include(<mbedtls/esp_config.h>)
|
||||
#include <mbedtls/esp_config.h>
|
||||
#endif
|
||||
#include <dlms_parser/decryption/aes_128_gcm_decryptor_mbedtls.h>
|
||||
#elif __has_include(<bearssl/bearssl.h>)
|
||||
#include <dlms_parser/decryption/aes_128_gcm_decryptor_bearssl.h>
|
||||
#else
|
||||
#define DLMS_METER_NO_CRYPTO
|
||||
#endif
|
||||
|
||||
#ifndef DLMS_MAX_SENSORS
|
||||
static constexpr uint8_t DLMS_MAX_SENSORS = 0;
|
||||
#endif
|
||||
#ifndef DLMS_MAX_TEXT_SENSORS
|
||||
static constexpr uint8_t DLMS_MAX_TEXT_SENSORS = 0;
|
||||
#endif
|
||||
#ifndef DLMS_MAX_BINARY_SENSORS
|
||||
static constexpr uint8_t DLMS_MAX_BINARY_SENSORS = 0;
|
||||
#endif
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
#ifndef DLMS_METER_SENSOR_LIST
|
||||
#define DLMS_METER_SENSOR_LIST(F, SEP)
|
||||
#endif
|
||||
|
||||
#ifndef DLMS_METER_TEXT_SENSOR_LIST
|
||||
#define DLMS_METER_TEXT_SENSOR_LIST(F, SEP)
|
||||
#endif
|
||||
|
||||
struct MeterData {
|
||||
float voltage_l1 = 0.0f; // Voltage L1
|
||||
float voltage_l2 = 0.0f; // Voltage L2
|
||||
float voltage_l3 = 0.0f; // Voltage L3
|
||||
float current_l1 = 0.0f; // Current L1
|
||||
float current_l2 = 0.0f; // Current L2
|
||||
float current_l3 = 0.0f; // Current L3
|
||||
float active_power_plus = 0.0f; // Active power taken from grid
|
||||
float active_power_minus = 0.0f; // Active power put into grid
|
||||
float active_energy_plus = 0.0f; // Active energy taken from grid
|
||||
float active_energy_minus = 0.0f; // Active energy put into grid
|
||||
float reactive_energy_plus = 0.0f; // Reactive energy taken from grid
|
||||
float reactive_energy_minus = 0.0f; // Reactive energy put into grid
|
||||
char timestamp[27]{}; // Text sensor for the timestamp value
|
||||
|
||||
// Netz NOE
|
||||
float power_factor = 0.0f; // Power Factor
|
||||
char meternumber[13]{}; // Text sensor for the meterNumber value
|
||||
#ifdef DLMS_METER_NO_CRYPTO
|
||||
// Fallback dummy decryptor for platforms without supported crypto (e.g., Zephyr during clang-tidy)
|
||||
class Aes128GcmDecryptorDummy : public dlms_parser::Aes128GcmDecryptor {
|
||||
public:
|
||||
void set_decryption_key(const dlms_parser::Aes128GcmDecryptionKey &key) override {}
|
||||
bool decrypt_in_place(std::span<const uint8_t> iv, std::span<uint8_t> ciphertext_and_plaintext,
|
||||
std::span<const uint8_t> aad, std::span<const uint8_t> tag) override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
// Provider constants
|
||||
enum Providers : uint32_t { PROVIDER_GENERIC = 0x00, PROVIDER_NETZNOE = 0x01 };
|
||||
#if __has_include(<psa/crypto.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorTfPsa;
|
||||
#elif !defined(USE_ESP8266) && __has_include(<mbedtls/gcm.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorMbedTls;
|
||||
#elif __has_include(<bearssl/bearssl.h>)
|
||||
using Aes128GcmDecryptorImpl = dlms_parser::Aes128GcmDecryptorBearSsl;
|
||||
#else
|
||||
using Aes128GcmDecryptorImpl = Aes128GcmDecryptorDummy;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
struct SensorItem {
|
||||
std::string obis_code;
|
||||
sensor::Sensor *sensor;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
struct TextSensorItem {
|
||||
std::string obis_code;
|
||||
text_sensor::TextSensor *sensor;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct BinarySensorItem {
|
||||
std::string obis_code;
|
||||
binary_sensor::BinarySensor *sensor;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct CustomPattern {
|
||||
std::string pattern;
|
||||
std::optional<std::string> name;
|
||||
int priority{0};
|
||||
std::optional<std::array<uint8_t, 6>> default_obis;
|
||||
};
|
||||
|
||||
class DlmsMeterComponent : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
DlmsMeterComponent() = default;
|
||||
DlmsMeterComponent(uint32_t receive_timeout_ms, bool skip_crc_check,
|
||||
std::optional<std::array<uint8_t, 16>> decryption_key,
|
||||
std::optional<std::array<uint8_t, 16>> authentication_key,
|
||||
std::vector<CustomPattern> custom_patterns);
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
void set_decryption_key(const std::array<uint8_t, 16> &key) { this->decryption_key_ = key; }
|
||||
void set_provider(uint32_t provider) { this->provider_ = provider; }
|
||||
|
||||
void publish_sensors(MeterData &data) {
|
||||
#define DLMS_METER_PUBLISH_SENSOR(s) \
|
||||
if (this->s##_sensor_ != nullptr) \
|
||||
s##_sensor_->publish_state(data.s);
|
||||
DLMS_METER_SENSOR_LIST(DLMS_METER_PUBLISH_SENSOR, )
|
||||
|
||||
#define DLMS_METER_PUBLISH_TEXT_SENSOR(s) \
|
||||
if (this->s##_text_sensor_ != nullptr) \
|
||||
s##_text_sensor_->publish_state(data.s);
|
||||
DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_PUBLISH_TEXT_SENSOR, )
|
||||
}
|
||||
|
||||
DLMS_METER_SENSOR_LIST(SUB_SENSOR, )
|
||||
DLMS_METER_TEXT_SENSOR_LIST(SUB_TEXT_SENSOR, )
|
||||
#ifdef USE_SENSOR
|
||||
void register_sensor(const std::string &obis_code, sensor::Sensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void register_text_sensor(const std::string &obis_code, text_sensor::TextSensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void register_binary_sensor(const std::string &obis_code, binary_sensor::BinarySensor *sensor);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool parse_mbus_(std::vector<uint8_t> &mbus_payload);
|
||||
bool parse_dlms_(const std::vector<uint8_t> &mbus_payload, uint16_t &message_length, uint8_t &systitle_length,
|
||||
uint16_t &header_offset);
|
||||
bool decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t message_length, uint8_t systitle_length,
|
||||
uint16_t header_offset);
|
||||
void decode_obis_(uint8_t *plaintext, uint16_t message_length);
|
||||
void read_rx_buffer_();
|
||||
void flush_rx_buffer_();
|
||||
void process_frame_();
|
||||
void on_data_(const char *obis_code, float float_val, const char *str_val, bool is_numeric);
|
||||
|
||||
std::vector<uint8_t> receive_buffer_; // Stores the packet currently being received
|
||||
std::vector<uint8_t> mbus_payload_; // Parsed M-Bus payload, reused to avoid heap churn
|
||||
uint32_t last_read_ = 0; // Timestamp when data was last read
|
||||
uint32_t read_timeout_ = 1000; // Time to wait after last byte before considering data complete
|
||||
std::array<uint8_t, 2048> rx_buffer_;
|
||||
size_t bytes_accumulated_{0};
|
||||
uint32_t last_rx_char_time_{0};
|
||||
|
||||
uint32_t provider_ = PROVIDER_GENERIC; // Provider of the meter / your grid operator
|
||||
std::array<uint8_t, 16> decryption_key_;
|
||||
uint32_t receive_timeout_ms_{1000};
|
||||
bool skip_crc_check_{false};
|
||||
|
||||
std::vector<CustomPattern> custom_patterns_;
|
||||
|
||||
Aes128GcmDecryptorImpl decryptor_;
|
||||
dlms_parser::DlmsParser parser_;
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
StaticVector<SensorItem, DLMS_MAX_SENSORS> sensors_;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
StaticVector<TextSensorItem, DLMS_MAX_TEXT_SENSORS> text_sensors_;
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
StaticVector<BinarySensorItem, DLMS_MAX_BINARY_SENSORS> binary_sensors_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
/*
|
||||
+----------------------------------------------------+ -
|
||||
| Start Character [0x68] | \
|
||||
+----------------------------------------------------+ |
|
||||
| Data Length (L) | |
|
||||
+----------------------------------------------------+ |
|
||||
| Data Length Repeat (L) | |
|
||||
+----------------------------------------------------+ > M-Bus Data link layer
|
||||
| Start Character Repeat [0x68] | |
|
||||
+----------------------------------------------------+ |
|
||||
| Control/Function Field (C) | |
|
||||
+----------------------------------------------------+ |
|
||||
| Address Field (A) | /
|
||||
+----------------------------------------------------+ -
|
||||
| Control Information Field (CI) | \
|
||||
+----------------------------------------------------+ |
|
||||
| Source Transport Service Access Point (STSAP) | > DLMS/COSEM M-Bus transport layer
|
||||
+----------------------------------------------------+ |
|
||||
| Destination Transport Service Access Point (DTSAP) | /
|
||||
+----------------------------------------------------+ -
|
||||
| | \
|
||||
~ ~ |
|
||||
Data > DLMS/COSEM Application Layer
|
||||
~ ~ |
|
||||
| | /
|
||||
+----------------------------------------------------+ -
|
||||
| Checksum | \
|
||||
+----------------------------------------------------+ > M-Bus Data link layer
|
||||
| Stop Character [0x16] | /
|
||||
+----------------------------------------------------+ -
|
||||
|
||||
Data_Length = L - C - A - CI
|
||||
Each line (except Data) is one Byte
|
||||
|
||||
Possible Values found in publicly available docs:
|
||||
- C: 0x53/0x73 (SND_UD)
|
||||
- A: FF (Broadcast)
|
||||
- CI: 0x00-0x1F/0x60/0x61/0x7C/0x7D
|
||||
- STSAP: 0x01 (Management Logical Device ID 1 of the meter)
|
||||
- DTSAP: 0x67 (Consumer Information Push Client ID 103)
|
||||
*/
|
||||
|
||||
// MBUS start bytes for different telegram formats:
|
||||
// - Single Character: 0xE5 (length=1)
|
||||
// - Short Frame: 0x10 (length=5)
|
||||
// - Control Frame: 0x68 (length=9)
|
||||
// - Long Frame: 0x68 (length=9+data_length)
|
||||
// This component currently only uses Long Frame.
|
||||
static constexpr uint8_t START_BYTE_SINGLE_CHARACTER = 0xE5;
|
||||
static constexpr uint8_t START_BYTE_SHORT_FRAME = 0x10;
|
||||
static constexpr uint8_t START_BYTE_CONTROL_FRAME = 0x68;
|
||||
static constexpr uint8_t START_BYTE_LONG_FRAME = 0x68;
|
||||
static constexpr uint8_t MBUS_HEADER_INTRO_LENGTH = 4; // Header length for the intro (0x68, length, length, 0x68)
|
||||
static constexpr uint8_t MBUS_FULL_HEADER_LENGTH = 9; // Total header length
|
||||
static constexpr uint8_t MBUS_FOOTER_LENGTH = 2; // Footer after frame
|
||||
static constexpr uint8_t MBUS_MAX_FRAME_LENGTH = 250; // Maximum size of frame
|
||||
static constexpr uint8_t MBUS_START1_OFFSET = 0; // Offset of first start byte
|
||||
static constexpr uint8_t MBUS_LENGTH1_OFFSET = 1; // Offset of first length byte
|
||||
static constexpr uint8_t MBUS_LENGTH2_OFFSET = 2; // Offset of (duplicated) second length byte
|
||||
static constexpr uint8_t MBUS_START2_OFFSET = 3; // Offset of (duplicated) second start byte
|
||||
static constexpr uint8_t STOP_BYTE = 0x16;
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
@@ -1,94 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
// Data types as per specification
|
||||
enum DataType {
|
||||
NULL_DATA = 0x00,
|
||||
BOOLEAN = 0x03,
|
||||
BIT_STRING = 0x04,
|
||||
DOUBLE_LONG = 0x05,
|
||||
DOUBLE_LONG_UNSIGNED = 0x06,
|
||||
OCTET_STRING = 0x09,
|
||||
VISIBLE_STRING = 0x0A,
|
||||
UTF8_STRING = 0x0C,
|
||||
BINARY_CODED_DECIMAL = 0x0D,
|
||||
INTEGER = 0x0F,
|
||||
LONG = 0x10,
|
||||
UNSIGNED = 0x11,
|
||||
LONG_UNSIGNED = 0x12,
|
||||
LONG64 = 0x14,
|
||||
LONG64_UNSIGNED = 0x15,
|
||||
ENUM = 0x16,
|
||||
FLOAT32 = 0x17,
|
||||
FLOAT64 = 0x18,
|
||||
DATE_TIME = 0x19,
|
||||
DATE = 0x1A,
|
||||
TIME = 0x1B,
|
||||
|
||||
ARRAY = 0x01,
|
||||
STRUCTURE = 0x02,
|
||||
COMPACT_ARRAY = 0x13
|
||||
};
|
||||
|
||||
enum Medium {
|
||||
ABSTRACT = 0x00,
|
||||
ELECTRICITY = 0x01,
|
||||
HEAT_COST_ALLOCATOR = 0x04,
|
||||
COOLING = 0x05,
|
||||
HEAT = 0x06,
|
||||
GAS = 0x07,
|
||||
COLD_WATER = 0x08,
|
||||
HOT_WATER = 0x09,
|
||||
OIL = 0x10,
|
||||
COMPRESSED_AIR = 0x11,
|
||||
NITROGEN = 0x12
|
||||
};
|
||||
|
||||
// Data structure
|
||||
static constexpr uint8_t DECODER_START_OFFSET = 20; // Skip header, timestamp and break block
|
||||
static constexpr uint8_t OBIS_TYPE_OFFSET = 0;
|
||||
static constexpr uint8_t OBIS_LENGTH_OFFSET = 1;
|
||||
static constexpr uint8_t OBIS_CODE_OFFSET = 2;
|
||||
static constexpr uint8_t OBIS_CODE_LENGTH_STANDARD = 0x06; // 6-byte OBIS code (A.B.C.D.E.F)
|
||||
static constexpr uint8_t OBIS_CODE_LENGTH_EXTENDED = 0x0C; // 12-byte extended OBIS code
|
||||
static constexpr uint8_t OBIS_A = 0;
|
||||
static constexpr uint8_t OBIS_B = 1;
|
||||
static constexpr uint8_t OBIS_C = 2;
|
||||
static constexpr uint8_t OBIS_D = 3;
|
||||
static constexpr uint8_t OBIS_E = 4;
|
||||
static constexpr uint8_t OBIS_F = 5;
|
||||
|
||||
// Metadata
|
||||
static constexpr uint16_t OBIS_TIMESTAMP = 0x0100;
|
||||
static constexpr uint16_t OBIS_SERIAL_NUMBER = 0x6001;
|
||||
static constexpr uint16_t OBIS_DEVICE_NAME = 0x2A00;
|
||||
|
||||
// Voltage
|
||||
static constexpr uint16_t OBIS_VOLTAGE_L1 = 0x2007;
|
||||
static constexpr uint16_t OBIS_VOLTAGE_L2 = 0x3407;
|
||||
static constexpr uint16_t OBIS_VOLTAGE_L3 = 0x4807;
|
||||
|
||||
// Current
|
||||
static constexpr uint16_t OBIS_CURRENT_L1 = 0x1F07;
|
||||
static constexpr uint16_t OBIS_CURRENT_L2 = 0x3307;
|
||||
static constexpr uint16_t OBIS_CURRENT_L3 = 0x4707;
|
||||
|
||||
// Power
|
||||
static constexpr uint16_t OBIS_ACTIVE_POWER_PLUS = 0x0107;
|
||||
static constexpr uint16_t OBIS_ACTIVE_POWER_MINUS = 0x0207;
|
||||
|
||||
// Active energy
|
||||
static constexpr uint16_t OBIS_ACTIVE_ENERGY_PLUS = 0x0108;
|
||||
static constexpr uint16_t OBIS_ACTIVE_ENERGY_MINUS = 0x0208;
|
||||
|
||||
// Reactive energy
|
||||
static constexpr uint16_t OBIS_REACTIVE_ENERGY_PLUS = 0x0308;
|
||||
static constexpr uint16_t OBIS_REACTIVE_ENERGY_MINUS = 0x0408;
|
||||
|
||||
// Netz NOE specific
|
||||
static constexpr uint16_t OBIS_POWER_FACTOR = 0x0D07;
|
||||
|
||||
} // namespace esphome::dlms_meter
|
||||
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
@@ -16,109 +17,142 @@ from esphome.const import (
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
|
||||
AUTO_LOAD = ["dlms_meter"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
NUMERIC_KEYS = {
|
||||
"voltage_l1": "1.0.32.7.0.255",
|
||||
"voltage_l2": "1.0.52.7.0.255",
|
||||
"voltage_l3": "1.0.72.7.0.255",
|
||||
"current_l1": "1.0.31.7.0.255",
|
||||
"current_l2": "1.0.51.7.0.255",
|
||||
"current_l3": "1.0.71.7.0.255",
|
||||
"active_power_plus": "1.0.1.7.0.255",
|
||||
"active_power_minus": "1.0.2.7.0.255",
|
||||
"active_energy_plus": "1.0.1.8.0.255",
|
||||
"active_energy_minus": "1.0.2.8.0.255",
|
||||
"reactive_energy_plus": "1.0.3.8.0.255",
|
||||
"reactive_energy_minus": "1.0.4.8.0.255",
|
||||
"power_factor": "1.0.13.7.0.255",
|
||||
}
|
||||
|
||||
DYNAMIC_SCHEMA = sensor.sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Optional("voltage_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("voltage_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("voltage_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_power_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_power_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_energy_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("active_energy_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
# Netz NOE
|
||||
cv.Optional("power_factor"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_POWER_FACTOR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def deprecation_warning(config):
|
||||
_LOGGER.warning(
|
||||
"The dlms_meter sensor schema using predefined keys (e.g., 'voltage_l1') is deprecated and will be removed in 2026.11.0. "
|
||||
"Please update your configuration to use the new schema with 'obis_code'."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
OLD_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Optional("voltage_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("voltage_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("voltage_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l1"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l2"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("current_l3"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_power_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_power_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional("active_energy_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("active_energy_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("reactive_energy_plus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("reactive_energy_minus"): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT_HOURS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
cv.Optional("power_factor"): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_POWER_FACTOR,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
deprecation_warning,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
|
||||
sensors = []
|
||||
for key, conf in config.items():
|
||||
if not isinstance(conf, dict):
|
||||
continue
|
||||
id = conf[CONF_ID]
|
||||
if id and id.type == sensor.Sensor:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
|
||||
sensors.append(f"F({key})")
|
||||
|
||||
if sensors:
|
||||
cg.add_define(
|
||||
"DLMS_METER_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
|
||||
)
|
||||
if obis := config.get(CONF_OBIS_CODE):
|
||||
var = await sensor.new_sensor(config)
|
||||
cg.add(hub.register_sensor(obis, var))
|
||||
else:
|
||||
for key, obis_val in NUMERIC_KEYS.items():
|
||||
if sensor_config := config.get(key):
|
||||
sens = await sensor.new_sensor(sensor_config)
|
||||
cg.add(hub.register_sensor(obis_val, sens))
|
||||
|
||||
@@ -1,37 +1,59 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
from .. import CONF_DLMS_METER_ID, DlmsMeterComponent
|
||||
from .. import CONF_DLMS_METER_ID, CONF_OBIS_CODE, DlmsMeterComponent, obis_code
|
||||
|
||||
AUTO_LOAD = ["dlms_meter"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
DEPENDENCIES = ["dlms_meter"]
|
||||
|
||||
TEXT_KEYS = {
|
||||
"timestamp": "0.0.1.0.0.255",
|
||||
"meternumber": "0.0.96.1.0.255",
|
||||
}
|
||||
|
||||
DYNAMIC_SCHEMA = text_sensor.text_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
|
||||
# Netz NOE
|
||||
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
|
||||
cv.Required(CONF_OBIS_CODE): obis_code,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
def deprecation_warning(config):
|
||||
_LOGGER.warning(
|
||||
"The dlms_meter text_sensor schema using predefined keys (e.g., 'timestamp') is deprecated and will be removed in 2026.11.0. "
|
||||
"Please update your configuration to use the new schema with 'obis_code'."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
OLD_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent),
|
||||
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
|
||||
cv.Optional("meternumber"): text_sensor.text_sensor_schema(),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
deprecation_warning,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Any(DYNAMIC_SCHEMA, OLD_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_DLMS_METER_ID])
|
||||
|
||||
text_sensors = []
|
||||
for key, conf in config.items():
|
||||
if not isinstance(conf, dict):
|
||||
continue
|
||||
id = conf[CONF_ID]
|
||||
if id and id.type == text_sensor.TextSensor:
|
||||
sens = await text_sensor.new_text_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
|
||||
text_sensors.append(f"F({key})")
|
||||
|
||||
if text_sensors:
|
||||
cg.add_define(
|
||||
"DLMS_METER_TEXT_SENSOR_LIST(F, sep)",
|
||||
cg.RawExpression(" sep ".join(text_sensors)),
|
||||
)
|
||||
if obis := config.get(CONF_OBIS_CODE):
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
cg.add(hub.register_text_sensor(obis, var))
|
||||
else:
|
||||
for key, obis_val in TEXT_KEYS.items():
|
||||
if text_sensor_config := config.get(key):
|
||||
sens = await text_sensor.new_text_sensor(text_sensor_config)
|
||||
cg.add(hub.register_text_sensor(obis_val, sens))
|
||||
|
||||
@@ -87,7 +87,7 @@ async def to_code(config):
|
||||
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
|
||||
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
|
||||
|
||||
cg.add_library("esphome/dsmr_parser", "1.8.0")
|
||||
cg.add_library("esphome/dsmr_parser", "1.9.0")
|
||||
|
||||
|
||||
def final_validate(config: ConfigType) -> ConfigType:
|
||||
|
||||
@@ -5,7 +5,11 @@ from esphome import core, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation
|
||||
from esphome.components.mipi import flatten_sequence, map_sequence
|
||||
from esphome.components.mipi import (
|
||||
flatten_sequence,
|
||||
map_sequence,
|
||||
model_schema_extractor,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import update_interval
|
||||
from esphome.const import (
|
||||
@@ -111,6 +115,7 @@ def model_schema(config):
|
||||
)
|
||||
|
||||
|
||||
@model_schema_extractor(MODELS, model_schema)
|
||||
def customise_schema(config):
|
||||
"""
|
||||
Create a customised config schema for a specific model and validate the configuration.
|
||||
|
||||
@@ -173,8 +173,14 @@ bool ES8388::set_mute_state_(bool mute_state) {
|
||||
ES8388_ERROR_CHECK(this->read_byte(ES8388_DACCONTROL3, &value));
|
||||
ESP_LOGV(TAG, "Read ES8388_DACCONTROL3: 0x%02X", value);
|
||||
|
||||
// Only toggle the DACMute bit; the other bits of this register hold unrelated
|
||||
// DAC settings that must be preserved. Previously muting overwrote the whole
|
||||
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
|
||||
// could not be unmuted again.
|
||||
if (mute_state) {
|
||||
value = 0x3C;
|
||||
value |= ES8388_DACCONTROL3_DAC_MUTE;
|
||||
} else {
|
||||
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
|
||||
|
||||
@@ -38,6 +38,7 @@ static const uint8_t ES8388_ADCCONTROL14 = 0x16;
|
||||
static const uint8_t ES8388_DACCONTROL1 = 0x17;
|
||||
static const uint8_t ES8388_DACCONTROL2 = 0x18;
|
||||
static const uint8_t ES8388_DACCONTROL3 = 0x19;
|
||||
static const uint8_t ES8388_DACCONTROL3_DAC_MUTE = 0x04; // DACMute, bit 2 of DACCONTROL3
|
||||
static const uint8_t ES8388_DACCONTROL4 = 0x1a;
|
||||
static const uint8_t ES8388_DACCONTROL5 = 0x1b;
|
||||
static const uint8_t ES8388_DACCONTROL6 = 0x1c;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections.abc import Callable, Iterable
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import itertools
|
||||
@@ -6,6 +7,7 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
from esphome import yaml_util
|
||||
import esphome.codegen as cg
|
||||
@@ -52,6 +54,7 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
from esphome.espidf.component import generate_idf_components
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
from esphome.writer import clean_build, clean_cmake_cache
|
||||
|
||||
@@ -496,6 +499,32 @@ def get_esp32_variant(core_obj=None):
|
||||
return (core_obj or CORE).data[KEY_ESP32][KEY_VARIANT]
|
||||
|
||||
|
||||
def variant_filtered_enum(
|
||||
by_variant: dict[str, Iterable[Any]], **kwargs: Any
|
||||
) -> Callable[[Any], Any]:
|
||||
"""Build a ``one_of`` validator whose valid set depends on the active variant.
|
||||
|
||||
``by_variant`` maps each ESP32 variant constant to the iterable of values that
|
||||
are valid on that variant. At validation time the value is checked against the
|
||||
set allowed for the current target variant. For schema extraction the inverted
|
||||
``{value: [variants, ...]}`` map is returned instead, so the language-schema
|
||||
dump can tag every option with the variants that accept it and frontends can
|
||||
filter to the user's selected variant.
|
||||
"""
|
||||
by_value: dict[str, list[str]] = {}
|
||||
for variant, values in by_variant.items():
|
||||
for value in values:
|
||||
by_value.setdefault(str(value), []).append(variant)
|
||||
|
||||
@schema_extractor("variant_enum")
|
||||
def validator(value: Any) -> Any:
|
||||
if value is SCHEMA_EXTRACT:
|
||||
return by_value
|
||||
return cv.one_of(*by_variant.get(get_esp32_variant(), ()), **kwargs)(value)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def get_board(core_obj=None):
|
||||
return (core_obj or CORE).data[KEY_ESP32][KEY_BOARD]
|
||||
|
||||
@@ -1615,8 +1644,14 @@ FLASH_SIZES = [
|
||||
]
|
||||
|
||||
CONF_FLASH_SIZE = "flash_size"
|
||||
CONF_FLASH_MODE = "flash_mode"
|
||||
CONF_FLASH_FREQUENCY = "flash_frequency"
|
||||
CONF_CPU_FREQUENCY = "cpu_frequency"
|
||||
CONF_PARTITIONS = "partitions"
|
||||
FLASH_MODES = ["qio", "qout", "dio", "dout", "opi"]
|
||||
FLASH_FREQUENCIES = [
|
||||
f"{freq}MHZ" for freq in (120, 80, 64, 60, 48, 40, 32, 30, 26, 24, 20, 16)
|
||||
]
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -1630,6 +1665,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
|
||||
*FLASH_SIZES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_FLASH_MODE): cv.one_of(*FLASH_MODES, lower=True),
|
||||
cv.Optional(CONF_FLASH_FREQUENCY): cv.one_of(
|
||||
*FLASH_FREQUENCIES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_PARTITIONS): cv.Any(
|
||||
cv.file_,
|
||||
cv.ensure_list(
|
||||
@@ -1866,6 +1905,12 @@ async def to_code(config):
|
||||
"board_upload.maximum_size",
|
||||
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
||||
)
|
||||
if flash_mode := config.get(CONF_FLASH_MODE):
|
||||
cg.add_platformio_option("board_build.flash_mode", flash_mode)
|
||||
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
|
||||
cg.add_platformio_option(
|
||||
"board_build.f_flash", f"{flash_frequency[:-3]}000000L"
|
||||
)
|
||||
|
||||
if CONF_SOURCE in conf:
|
||||
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
||||
@@ -2016,6 +2061,14 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
|
||||
)
|
||||
if flash_mode := config.get(CONF_FLASH_MODE):
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True
|
||||
)
|
||||
if flash_frequency := config.get(CONF_FLASH_FREQUENCY):
|
||||
add_idf_sdkconfig_option(
|
||||
f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True
|
||||
)
|
||||
|
||||
# ESP32-P4: ESP-IDF 5.5.3 changed the default of ESP32P4_SELECTS_REV_LESS_V3
|
||||
# from y to n. PlatformIO uses sections.ld.in (for rev <3) or
|
||||
@@ -2263,14 +2316,14 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", True)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_SIGNING_KEY",
|
||||
str(signed_ota[CONF_SIGNING_KEY].resolve()),
|
||||
signed_ota[CONF_SIGNING_KEY].resolve().as_posix(),
|
||||
)
|
||||
else:
|
||||
# Public key mode — verification only, external signing required
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_VERIFICATION_KEY",
|
||||
str(signed_ota[CONF_VERIFICATION_KEY].resolve()),
|
||||
signed_ota[CONF_VERIFICATION_KEY].resolve().as_posix(),
|
||||
)
|
||||
|
||||
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
|
||||
|
||||
@@ -224,6 +224,17 @@ def merge_factory_bin(source, target, env):
|
||||
flash_size = env.BoardConfig().get("upload.flash_size", "4MB")
|
||||
chip = env.BoardConfig().get("build.mcu", "esp32")
|
||||
|
||||
# PlatformIO's esp-idf builder already creates a correct firmware.factory.bin (right
|
||||
# artifact names and partition offsets, including custom partition tables). The merge
|
||||
# below is only a fallback and cannot honor custom layouts, so don't overwrite an image
|
||||
# PlatformIO already produced. Post-build actions only run when firmware.bin is rebuilt,
|
||||
# and PlatformIO's combined-image builder runs before us in that batch, so an existing
|
||||
# file here is current.
|
||||
output_path = firmware_path.with_suffix(".factory.bin")
|
||||
if output_path.exists():
|
||||
print(f"{output_path.name} already created by PlatformIO - skipping merge")
|
||||
return
|
||||
|
||||
sections = []
|
||||
flasher_args_path = build_dir / "flasher_args.json"
|
||||
|
||||
@@ -291,7 +302,6 @@ def merge_factory_bin(source, target, env):
|
||||
print("No valid flash sections found — skipping .factory.bin creation.")
|
||||
return
|
||||
|
||||
output_path = firmware_path.with_suffix(".factory.bin")
|
||||
python_exe = f'"{env.subst("$PYTHONEXE")}"'
|
||||
cmd = [
|
||||
python_exe,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
Import("env") # noqa: F821
|
||||
|
||||
# Remove custom_sdkconfig from the board config as it causes
|
||||
@@ -7,3 +9,8 @@ if "espidf.custom_sdkconfig" in board:
|
||||
del board._manifest["espidf"]["custom_sdkconfig"]
|
||||
if not board._manifest["espidf"]:
|
||||
del board._manifest["espidf"]
|
||||
|
||||
# Referenced by rules in esphome/idf_component.yml; an unset env var is a
|
||||
# fatal error there. Always 0: in PlatformIO builds arduino is not a managed
|
||||
# IDF component.
|
||||
os.environ.setdefault("ESPHOME_ARDUINO_COMPONENT", "0")
|
||||
|
||||
@@ -77,13 +77,15 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
|
||||
// Set initial value
|
||||
this->parent_->set_value(this->buffer_.value(x...));
|
||||
// Set the listener for read events
|
||||
this->parent_->on_read([this, x...](uint16_t id) {
|
||||
// ``mutable`` keeps by-copy captures non-const for triggers passing args by reference
|
||||
// (e.g. climate on_control's ClimateCall&). See #17142.
|
||||
this->parent_->on_read([this, x...](uint16_t id) mutable {
|
||||
// Set the value of the characteristic every time it is read
|
||||
this->parent_->set_value(this->buffer_.value(x...));
|
||||
});
|
||||
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
|
||||
BLECharacteristicSetValueActionManager::get_instance()->set_listener(
|
||||
this->parent_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); });
|
||||
this->parent_, [this, x...]() mutable { this->parent_->set_value(this->buffer_.value(x...)); });
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -257,7 +257,7 @@ async def to_code(config):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1")
|
||||
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.8")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.9")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -338,6 +338,14 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
||||
this->incoming_data_.clear();
|
||||
return;
|
||||
}
|
||||
if (wifi::global_wifi_component->is_disabled()) {
|
||||
// Wi-Fi is disabled, so we can't provision. Respond immediately
|
||||
// instead of letting the client wait out its provisioning timeout.
|
||||
ESP_LOGW(TAG, "Wi-Fi is disabled; cannot provision");
|
||||
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
|
||||
this->incoming_data_.clear();
|
||||
return;
|
||||
}
|
||||
wifi::WiFiAP sta{};
|
||||
sta.set_ssid(command.ssid.c_str());
|
||||
sta.set_password(command.password.c_str());
|
||||
|
||||
@@ -492,6 +492,15 @@ def _parse_register(config, regex, line):
|
||||
STACKTRACE_ESP8266_EXCEPTION_TYPE_RE = re.compile(r"[eE]xception \((\d+)\):")
|
||||
STACKTRACE_ESP8266_PC_RE = re.compile(r"epc1=0x(4[0-9a-fA-F]{7})")
|
||||
STACKTRACE_ESP8266_EXCVADDR_RE = re.compile(r"excvaddr=0x(4[0-9a-fA-F]{7})")
|
||||
# Structured crash handler output (crash_handler.cpp) from a previous boot:
|
||||
# PC: 0x40220060
|
||||
# EXCVADDR: 0x0000008A
|
||||
# BT0: 0x40212345
|
||||
STACKTRACE_ESP8266_CRASH_PC_RE = re.compile(r".*PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})")
|
||||
STACKTRACE_ESP8266_CRASH_EXCVADDR_RE = re.compile(
|
||||
r".*EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})"
|
||||
)
|
||||
STACKTRACE_ESP8266_CRASH_BT_RE = re.compile(r"BT\d+:\s*0x([0-9a-fA-F]{8})")
|
||||
STACKTRACE_BAD_ALLOC_RE = re.compile(
|
||||
r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$"
|
||||
)
|
||||
@@ -508,10 +517,17 @@ def process_stacktrace(config, line, backtrace_state):
|
||||
"Exception type: %s", ESP8266_EXCEPTION_CODES.get(code, "unknown")
|
||||
)
|
||||
|
||||
# ESP8266 PC/EXCVADDR
|
||||
# ESP8266 PC/EXCVADDR (legacy Arduino postmortem)
|
||||
_parse_register(config, STACKTRACE_ESP8266_PC_RE, line)
|
||||
_parse_register(config, STACKTRACE_ESP8266_EXCVADDR_RE, line)
|
||||
|
||||
# ESP8266 structured crash handler (crash_handler.cpp) from previous boot
|
||||
_parse_register(config, STACKTRACE_ESP8266_CRASH_PC_RE, line)
|
||||
_parse_register(config, STACKTRACE_ESP8266_CRASH_EXCVADDR_RE, line)
|
||||
match = re.search(STACKTRACE_ESP8266_CRASH_BT_RE, line)
|
||||
if match is not None:
|
||||
_decode_pc(config, match.group(1))
|
||||
|
||||
# bad alloc
|
||||
match = re.match(STACKTRACE_BAD_ALLOC_RE, line)
|
||||
if match is not None:
|
||||
|
||||
@@ -28,9 +28,6 @@ namespace esphome::espnow {
|
||||
|
||||
static constexpr const char *TAG = "espnow";
|
||||
|
||||
static const esp_err_t CONFIG_ESPNOW_WAKE_WINDOW = 50;
|
||||
static const esp_err_t CONFIG_ESPNOW_WAKE_INTERVAL = 100;
|
||||
|
||||
ESPNowComponent *global_esp_now = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static const LogString *espnow_error_to_str(esp_err_t error) {
|
||||
@@ -97,6 +94,15 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
|
||||
}
|
||||
|
||||
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
// Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a
|
||||
// v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B),
|
||||
// but our receive buffer is ESP_NOW_MAX_DATA_LEN (250 B); copying a larger
|
||||
// frame would overflow packet_.receive.data.
|
||||
if (size < 0 || size > ESP_NOW_MAX_DATA_LEN) {
|
||||
global_esp_now->receive_packet_queue_.increment_dropped_count();
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate an event from the pool
|
||||
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
|
||||
if (packet == nullptr) {
|
||||
@@ -204,11 +210,6 @@ void ESPNowComponent::enable_() {
|
||||
|
||||
esp_wifi_get_mac(WIFI_IF_STA, this->own_address_);
|
||||
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW);
|
||||
esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL);
|
||||
#endif
|
||||
|
||||
this->state_ = ESPNOW_STATE_ENABLED;
|
||||
|
||||
for (auto peer : this->peers_) {
|
||||
@@ -311,7 +312,9 @@ void ESPNowComponent::loop() {
|
||||
ESP_LOGV(TAG, ">>> [%s] %s", addr_buf, LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status)));
|
||||
#endif
|
||||
if (this->current_send_packet_ != nullptr) {
|
||||
this->current_send_packet_->callback_(packet->packet_.sent.status);
|
||||
if (this->current_send_packet_->callback_ != nullptr) {
|
||||
this->current_send_packet_->callback_(packet->packet_.sent.status);
|
||||
}
|
||||
this->send_packet_pool_.release(this->current_send_packet_);
|
||||
this->current_send_packet_ = nullptr; // Reset current packet after sending
|
||||
}
|
||||
@@ -333,13 +336,13 @@ void ESPNowComponent::loop() {
|
||||
// Log dropped received packets periodically
|
||||
uint16_t received_dropped = this->receive_packet_queue_.get_and_reset_dropped_count();
|
||||
if (received_dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u received packets due to buffer overflow", received_dropped);
|
||||
ESP_LOGW(TAG, "Dropped %u received packets (queue full or oversized frame)", received_dropped);
|
||||
}
|
||||
|
||||
// Log dropped send packets periodically
|
||||
uint16_t send_dropped = this->send_packet_queue_.get_and_reset_dropped_count();
|
||||
if (send_dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u send packets due to buffer overflow", send_dropped);
|
||||
ESP_LOGW(TAG, "Dropped %u send packets (queue full)", send_dropped);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ async def new_fastled_light(config):
|
||||
ref="d44c800a9e876a8394caefc2ce4915dd96dac77b",
|
||||
)
|
||||
cg.add_library("SPI", None)
|
||||
# FastLED's RMT5 driver hard-codes intr_priority=3, which conflicts with
|
||||
# esphome's RMT channels (remote_transmitter etc., priority 0): the IDF
|
||||
# driver rejects FastLED's channel and show() then hangs ~3s with no
|
||||
# output. Override to 0 so it shares the interrupt. See #17063.
|
||||
cg.add_build_flag("-DFL_RMT5_INTERRUPT_LEVEL=0")
|
||||
else:
|
||||
cg.add_library("fastled/FastLED", "3.9.16")
|
||||
await light.register_light(var, config)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import light, output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B
|
||||
from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B, CONF_UPDATE_INTERVAL
|
||||
|
||||
from .. import hbridge_ns
|
||||
|
||||
CODEOWNERS = ["@DotNetDann"]
|
||||
|
||||
HBridgeLightOutput = hbridge_ns.class_(
|
||||
"HBridgeLightOutput", cg.Component, light.LightOutput
|
||||
"HBridgeLightOutput", cg.PollingComponent, light.LightOutput
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
|
||||
@@ -16,12 +16,14 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(HBridgeLightOutput),
|
||||
cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput),
|
||||
cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput),
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="8ms"): cv.update_interval,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
cg.add(var.set_update_interval(config.pop(CONF_UPDATE_INTERVAL)))
|
||||
await cg.register_component(var, config)
|
||||
await light.register_light(var, config)
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::hbridge {
|
||||
|
||||
class HBridgeLightOutput : public Component, public light::LightOutput {
|
||||
class HBridgeLightOutput final : public PollingComponent, public light::LightOutput {
|
||||
public:
|
||||
void set_pina_pin(output::FloatOutput *pina_pin) { this->pina_pin_ = pina_pin; }
|
||||
void set_pinb_pin(output::FloatOutput *pinb_pin) { this->pinb_pin_ = pinb_pin; }
|
||||
@@ -20,11 +19,12 @@ class HBridgeLightOutput : public Component, public light::LightOutput {
|
||||
return traits;
|
||||
}
|
||||
|
||||
void setup() override { this->disable_loop(); }
|
||||
void setup() override { this->stop_poller(); }
|
||||
|
||||
void loop() override {
|
||||
// Only called when both channels are active — alternate H-bridge direction
|
||||
// each iteration to multiplex cold and warm white.
|
||||
void update() override {
|
||||
// Flip the H-bridge direction to multiplex cold/warm white. update_interval must stay
|
||||
// slower than the output's PWM period (flipping faster collapses the output onto one
|
||||
// channel) but fast enough to avoid flicker (issue #17030).
|
||||
if (!this->forward_direction_) {
|
||||
this->pina_pin_->set_level(this->pina_duty_);
|
||||
this->pinb_pin_->set_level(0);
|
||||
@@ -46,13 +46,17 @@ class HBridgeLightOutput : public Component, public light::LightOutput {
|
||||
this->pinb_duty_ = new_pinb;
|
||||
|
||||
if (new_pina != 0.0f && new_pinb != 0.0f) {
|
||||
// Both channels active — need loop to alternate H-bridge direction
|
||||
this->high_freq_.start();
|
||||
this->enable_loop();
|
||||
// Both channels active — multiplex the H-bridge direction via the poller.
|
||||
if (!this->multiplexing_) {
|
||||
this->multiplexing_ = true;
|
||||
this->start_poller();
|
||||
}
|
||||
} else {
|
||||
// Zero or one channel active — drive pins directly, no multiplexing needed
|
||||
this->high_freq_.stop();
|
||||
this->disable_loop();
|
||||
// Zero or one channel active — drive pins directly, no multiplexing needed.
|
||||
if (this->multiplexing_) {
|
||||
this->multiplexing_ = false;
|
||||
this->stop_poller();
|
||||
}
|
||||
this->pina_pin_->set_level(new_pina);
|
||||
this->pinb_pin_->set_level(new_pinb);
|
||||
}
|
||||
@@ -64,7 +68,7 @@ class HBridgeLightOutput : public Component, public light::LightOutput {
|
||||
float pina_duty_{0};
|
||||
float pinb_duty_{0};
|
||||
bool forward_direction_{false};
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
bool multiplexing_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::hbridge
|
||||
|
||||
@@ -126,6 +126,6 @@ async def to_code(config):
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
||||
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.41")
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.42")
|
||||
if CORE.is_libretiny or CORE.is_esp32:
|
||||
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])
|
||||
|
||||
@@ -22,7 +22,9 @@ void ImprovSerialComponent::setup() {
|
||||
|
||||
if (wifi::global_wifi_component->has_sta()) {
|
||||
this->state_ = improv::STATE_PROVISIONED;
|
||||
} else {
|
||||
} else if (!wifi::global_wifi_component->is_disabled()) {
|
||||
// Respect Wi-Fi's disabled state; forcing a scan while disabled throws
|
||||
// the wifi component into an invalid state from which it cannot recover.
|
||||
wifi::global_wifi_component->start_scanning();
|
||||
}
|
||||
}
|
||||
@@ -230,6 +232,13 @@ bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
|
||||
bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) {
|
||||
switch (command.command) {
|
||||
case improv::WIFI_SETTINGS: {
|
||||
if (wifi::global_wifi_component->is_disabled()) {
|
||||
// Wi-Fi is disabled, so we can't provision. Respond immediately
|
||||
// instead of letting the client wait out its provisioning timeout.
|
||||
ESP_LOGW(TAG, "Wi-Fi is disabled; cannot provision");
|
||||
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
|
||||
return true;
|
||||
}
|
||||
wifi::WiFiAP sta{};
|
||||
sta.set_ssid(command.ssid.c_str());
|
||||
sta.set_password(command.password.c_str());
|
||||
@@ -245,6 +254,14 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
return true;
|
||||
}
|
||||
case improv::GET_CURRENT_STATE:
|
||||
if (wifi::global_wifi_component->is_disabled()) {
|
||||
// Wi-Fi is disabled; report the Improv "stopped" state so a client can tell
|
||||
// the user that provisioning is unavailable. Reported transiently without
|
||||
// disturbing our internal provisioning state machine, so a later `wifi.enable`
|
||||
// still reports the correct state.
|
||||
this->send_current_state_(improv::STATE_STOPPED);
|
||||
return true;
|
||||
}
|
||||
this->set_state_(this->state_);
|
||||
if (this->state_ == improv::STATE_PROVISIONED) {
|
||||
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::GET_CURRENT_STATE);
|
||||
@@ -299,6 +316,10 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
|
||||
void ImprovSerialComponent::set_state_(improv::State state) {
|
||||
this->state_ = state;
|
||||
this->send_current_state_(state);
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::send_current_state_(improv::State state) {
|
||||
this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE;
|
||||
this->tx_header_[TX_DATA_IDX] = state;
|
||||
this->write_data_();
|
||||
|
||||
@@ -57,6 +57,7 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
|
||||
bool parse_improv_payload_(improv::ImprovCommand &command);
|
||||
|
||||
void set_state_(improv::State state);
|
||||
void send_current_state_(improv::State state);
|
||||
void set_error_(improv::Error error);
|
||||
void send_response_(std::vector<uint8_t> &response);
|
||||
void on_wifi_connect_timeout_();
|
||||
|
||||
@@ -175,6 +175,10 @@ void Logger::process_messages_() {
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// Process any buffered messages when available
|
||||
if (this->log_buffer_.has_messages()) {
|
||||
// Prevent main-task logs emitted by listener callbacks (e.g. the API send path) from re-entering
|
||||
// and corrupting the shared tx_buffer_ / API shared_write_buffer_ while we are draining here.
|
||||
// Mirrors the guard held by log_message_to_buffer_and_send_ on the synchronous logging path.
|
||||
RecursionGuard guard(this->main_task_recursion_guard_);
|
||||
logger::TaskLogBuffer::LogMessage *message;
|
||||
uint16_t text_length;
|
||||
while (this->log_buffer_.borrow_message_main_loop(message, text_length)) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.motion import MotionComponent
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
CONF_LSM6DS_ID = "lsm6ds_id"
|
||||
# C++ namespace / class
|
||||
|
||||
lsm6ds_ns = cg.esphome_ns.namespace("lsm6ds")
|
||||
LSM6DSComponent = lsm6ds_ns.class_(
|
||||
"LSM6DSComponent",
|
||||
MotionComponent,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "lsm6ds.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::lsm6ds {
|
||||
|
||||
static const char *const TAG = "lsm6ds";
|
||||
|
||||
static const struct {
|
||||
uint8_t who_am_i;
|
||||
const char *const name;
|
||||
} CHIP_IDS[] = {{0x69, "LSMDSO"}, {0x6A, "LSM6DS3"}};
|
||||
|
||||
void LSM6DSComponent::setup() {
|
||||
MotionComponent::setup();
|
||||
uint8_t who_am_i = 0;
|
||||
if (this->read_register(LSM6DS_REG_WHO_AM_I, &who_am_i, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Failed to read WHO_AM_I — check wiring and I2C address");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
const char *chip_name = nullptr;
|
||||
for (const auto &chip : CHIP_IDS) {
|
||||
if (chip.who_am_i == who_am_i) {
|
||||
chip_name = chip.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (chip_name == nullptr) {
|
||||
ESP_LOGE(TAG, "Unknown WHO_AM_I: 0x%02X", who_am_i);
|
||||
this->mark_failed(LOG_STR("Unknown WHO_AM_I value"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Found %s (WHO_AM_I = 0x%02X)", chip_name, who_am_i);
|
||||
this->chip_name_ = chip_name;
|
||||
|
||||
// 2. Software reset — clears all registers to defaults
|
||||
if (this->write_register(LSM6DS_REG_CTRL3_C, &CTRL3_C_SW_RESET, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Software reset failed"));
|
||||
return;
|
||||
}
|
||||
// Datasheet: reset bit self-clears after boot (typ. 50 µs);
|
||||
delay(2);
|
||||
|
||||
// 3. Enable auto-increment and block data update (BDU).
|
||||
// BDU prevents reading a high-byte from one sample and a low-byte from the next.
|
||||
// IF_INC is set by default after reset but we set it explicitly for clarity.
|
||||
uint8_t ctrl3 = CTRL3_C_IF_INC | CTRL3_C_BDU;
|
||||
if (this->write_register(LSM6DS_REG_CTRL3_C, &ctrl3, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Config failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Configure accelerometer: ODR in bits[7:4], FS in bits[3:2]
|
||||
// Anti-aliasing filter bandwidth is left at power-on default (bits[1:0] = 00 = ODR/2).
|
||||
uint8_t ctrl1_xl = (uint8_t) (this->accel_odr_ << 4) | (uint8_t) (this->accel_range_ << 2);
|
||||
if (this->write_register(LSM6DS_REG_CTRL1_XL, &ctrl1_xl, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Failed to configure accelerometer"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Configure gyroscope: ODR in bits[7:4], FS_G + FS_125 in bits[3:0]
|
||||
// For ±125 dps: FS_G[2:1]=00 and FS_125(bit1)=1, so gyro_range_ encodes the full nibble.
|
||||
uint8_t ctrl2_g = (uint8_t) (this->gyro_odr_ << 4) | (uint8_t) (this->gyro_range_);
|
||||
if (this->write_register(LSM6DS_REG_CTRL2_G, &ctrl2_g, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Failed to configure gyroscope"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 6. Ensure accelerometer is in high-performance mode (CTRL6_C bit 4 = XL_HM_MODE = 0)
|
||||
// and gyroscope is in high-performance mode (CTRL7_G bit 7 = G_HM_MODE = 0).
|
||||
// Both default to 0 (high-performance) after reset, but write explicitly.
|
||||
uint8_t zero = 0x00;
|
||||
if (this->write_register(LSM6DS_REG_CTRL6_C, &zero, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (this->write_register(LSM6DS_REG_CTRL7_G, &zero, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void LSM6DSComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"LSM6DS IMU:\n"
|
||||
" Chip type: %s\n",
|
||||
this->chip_name_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
// Accel range — index into the sensitivity table (datasheet Table 3)
|
||||
static const char *const ACCEL_RANGE_STR[] = {"±2g", "±16g", "±4g", "±8g"};
|
||||
|
||||
const char *gyro_str;
|
||||
switch (this->gyro_range_) {
|
||||
case LSM6DS_GYRO_RANGE_125:
|
||||
gyro_str = "±125dps";
|
||||
break;
|
||||
case LSM6DS_GYRO_RANGE_250:
|
||||
gyro_str = "±250dps";
|
||||
break;
|
||||
case LSM6DS_GYRO_RANGE_500:
|
||||
gyro_str = "±500dps";
|
||||
break;
|
||||
case LSM6DS_GYRO_RANGE_1000:
|
||||
gyro_str = "±1000dps";
|
||||
break;
|
||||
case LSM6DS_GYRO_RANGE_2000:
|
||||
gyro_str = "±2000dps";
|
||||
break;
|
||||
default:
|
||||
gyro_str = "unknown";
|
||||
break;
|
||||
}
|
||||
auto accel_odr = this->accel_odr_ == 0 ? 0 : 13 * (1 << (this->accel_odr_ - 1));
|
||||
auto gyro_odr = this->gyro_odr_ == 0 ? 0 : 13 * (1 << (this->gyro_odr_ - 1));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Accel range : %s\n"
|
||||
" Accel data rate : %dHz\n"
|
||||
" Gyro range : %s\n"
|
||||
" Gyro data rate : %dHz",
|
||||
ACCEL_RANGE_STR[this->accel_range_], accel_odr, gyro_str, gyro_odr);
|
||||
}
|
||||
|
||||
// update_data()
|
||||
// Called by MotionComponent::update() on each polling interval.
|
||||
// Reads gyro XYZ and accel XYZ in a single 12-byte burst (registers 0x22–0x2D).
|
||||
// Values are in g (accel) and °/s (gyro) — MotionComponent handles axis mapping
|
||||
// and sensor publishing.
|
||||
|
||||
bool LSM6DSComponent::update_data(motion::MotionData &data) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
// Single burst: gyro X/Y/Z (0x22–0x27) then accel X/Y/Z (0x28–0x2D)
|
||||
uint8_t raw[LSM6DS_BURST_LEN];
|
||||
if (!this->read_bytes(LSM6DS_REG_OUTX_L_G, raw, LSM6DS_BURST_LEN)) {
|
||||
this->status_set_error(LOG_STR("Failed to read IMU data"));
|
||||
return false;
|
||||
}
|
||||
this->status_clear_error();
|
||||
|
||||
// Gyroscope
|
||||
// Sensitivity (mdps/LSB) from datasheet Table 3.
|
||||
// Multiply by 1e-3 to convert mdps → dps (°/s).
|
||||
static constexpr float GYRO_SCALE[] = {
|
||||
8.75e-3f, // 0x00 — ±250 dps
|
||||
8.75e-3f, // 0x01 — unused (maps to 250 as fallback)
|
||||
4.375e-3f, // 0x02 — ±125 dps (FS_125 bit set)
|
||||
8.75e-3f, // 0x03 — unused
|
||||
17.50e-3f, // 0x04 — ±500 dps
|
||||
17.50e-3f, // 0x05 — unused
|
||||
8.75e-3f, // 0x06 — unused
|
||||
8.75e-3f, // 0x07 — unused
|
||||
35.0e-3f, // 0x08 — ±1000 dps
|
||||
35.0e-3f, // 0x09 — unused
|
||||
17.50e-3f, // 0x0A — unused
|
||||
17.50e-3f, // 0x0B — unused
|
||||
70.0e-3f, // 0x0C — ±2000 dps
|
||||
};
|
||||
float gyro_scale = GYRO_SCALE[this->gyro_range_];
|
||||
|
||||
data.angular_rate[motion::X_AXIS] = (int16_t) ((raw[1] << 8) | raw[0]) * gyro_scale;
|
||||
data.angular_rate[motion::Y_AXIS] = (int16_t) ((raw[3] << 8) | raw[2]) * gyro_scale;
|
||||
data.angular_rate[motion::Z_AXIS] = (int16_t) ((raw[5] << 8) | raw[4]) * gyro_scale;
|
||||
|
||||
// Accelerometer
|
||||
// Sensitivity (mg/LSB) from datasheet Table 3.
|
||||
// Multiply by 1e-3 to convert mg → g.
|
||||
// Note: FS_XL register values are non-monotonic (0=2g, 1=16g, 2=4g, 3=8g).
|
||||
static constexpr float ACCEL_SCALE[] = {
|
||||
0.061e-3f, // 0x00 — ±2g
|
||||
0.488e-3f, // 0x01 — ±16g
|
||||
0.122e-3f, // 0x02 — ±4g
|
||||
0.244e-3f, // 0x03 — ±8g
|
||||
};
|
||||
float accel_scale = ACCEL_SCALE[this->accel_range_];
|
||||
|
||||
data.acceleration[motion::X_AXIS] =
|
||||
(int16_t) ((raw[LSM6DS_ACCEL_OFFSET + 1] << 8) | raw[LSM6DS_ACCEL_OFFSET + 0]) * accel_scale;
|
||||
data.acceleration[motion::Y_AXIS] =
|
||||
(int16_t) ((raw[LSM6DS_ACCEL_OFFSET + 3] << 8) | raw[LSM6DS_ACCEL_OFFSET + 2]) * accel_scale;
|
||||
data.acceleration[motion::Z_AXIS] =
|
||||
(int16_t) ((raw[LSM6DS_ACCEL_OFFSET + 5] << 8) | raw[LSM6DS_ACCEL_OFFSET + 4]) * accel_scale;
|
||||
|
||||
// Temperature (lazy — only read if a listener is registered)
|
||||
// Kept as a separate 2-byte read to avoid extending the burst to 14 bytes when
|
||||
// temperature is not needed.
|
||||
// Formula: T(°C) = (raw / 256.0) + 25.0 (datasheet Table 90, OUT_TEMP register)
|
||||
if (!this->temperature_callback_.empty()) {
|
||||
uint8_t raw_t[2];
|
||||
if (this->read_bytes(LSM6DS_REG_OUT_TEMP_L, raw_t, 2)) {
|
||||
int16_t temp_raw = (int16_t) ((raw_t[1] << 8) | raw_t[0]);
|
||||
float temperature = (temp_raw / 256.0f) + 25.0f;
|
||||
this->temperature_callback_.call(temperature);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::lsm6ds
|
||||
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/motion/motion_component.h"
|
||||
|
||||
namespace esphome::lsm6ds {
|
||||
|
||||
// ── Register map (datasheet DocID030071 Rev 3, Table 19) ────────────────────
|
||||
static const uint8_t LSM6DS_REG_WHO_AM_I = 0x0F;
|
||||
static const uint8_t LSM6DS_REG_CTRL1_XL = 0x10; // Accel ODR + FS
|
||||
static const uint8_t LSM6DS_REG_CTRL2_G = 0x11; // Gyro ODR + FS
|
||||
static const uint8_t LSM6DS_REG_CTRL3_C = 0x12; // SW_RESET, BDU, IF_INC
|
||||
static const uint8_t LSM6DS_REG_CTRL6_C = 0x15; // Accel HP disable, Gyro LPF1
|
||||
static const uint8_t LSM6DS_REG_CTRL7_G = 0x16; // Gyro HP disable
|
||||
static const uint8_t LSM6DS_REG_STATUS = 0x1E; // XLDA, GDA, TDA
|
||||
static const uint8_t LSM6DS_REG_OUT_TEMP_L = 0x20; // Temperature LSB
|
||||
static const uint8_t LSM6DS_REG_OUTX_L_G = 0x22; // Gyro X LSB (burst start)
|
||||
static const uint8_t LSM6DS_REG_OUTX_L_XL = 0x28; // Accel X LSB
|
||||
|
||||
// Burst read from 0x22 to 0x2D inclusive: gyro XYZ (6 bytes) + accel XYZ (6 bytes)
|
||||
static const uint8_t LSM6DS_BURST_LEN = 12;
|
||||
static const uint8_t LSM6DS_ACCEL_OFFSET = 6; // 0x28 - 0x22
|
||||
|
||||
// ── CTRL3_C bit fields ───────────────────────────────────────────────────────
|
||||
static const uint8_t CTRL3_C_SW_RESET = (1 << 0);
|
||||
static const uint8_t CTRL3_C_IF_INC = (1 << 2); // auto-increment address on burst (default 1)
|
||||
static const uint8_t CTRL3_C_BDU = (1 << 6); // block data update
|
||||
|
||||
// ── Accelerometer full-scale range ──────────────────────────────────────────
|
||||
// CTRL1_XL bits [3:2] — FS_XL[1:0]
|
||||
// Note: 0x01 = ±16g is intentional per Table 52 — the mapping is non-monotonic
|
||||
enum LSM6DSAccelRange : uint8_t {
|
||||
LSM6DS_ACCEL_RANGE_2G = 0x00, // ±2 g, 0.061 mg/LSB
|
||||
LSM6DS_ACCEL_RANGE_16G = 0x01, // ±16 g, 0.488 mg/LSB
|
||||
LSM6DS_ACCEL_RANGE_4G = 0x02, // ±4 g, 0.122 mg/LSB
|
||||
LSM6DS_ACCEL_RANGE_8G = 0x03, // ±8 g, 0.244 mg/LSB
|
||||
};
|
||||
|
||||
// ── Accelerometer output data rate ──────────────────────────────────────────
|
||||
// CTRL1_XL bits [7:4] — ODR_XL[3:0]
|
||||
enum LSM6DSAccelODR : uint8_t {
|
||||
LSM6DS_ACCEL_ODR_OFF = 0x00,
|
||||
LSM6DS_ACCEL_ODR_12_5 = 0x01, // 12.5 Hz
|
||||
LSM6DS_ACCEL_ODR_26 = 0x02, // 26 Hz
|
||||
LSM6DS_ACCEL_ODR_52 = 0x03, // 52 Hz
|
||||
LSM6DS_ACCEL_ODR_104 = 0x04, // 104 Hz
|
||||
LSM6DS_ACCEL_ODR_208 = 0x05, // 208 Hz
|
||||
LSM6DS_ACCEL_ODR_416 = 0x06, // 416 Hz
|
||||
LSM6DS_ACCEL_ODR_833 = 0x07, // 833 Hz
|
||||
LSM6DS_ACCEL_ODR_1666 = 0x08, // 1666 Hz
|
||||
LSM6DS_ACCEL_ODR_3332 = 0x09, // 3332 Hz
|
||||
LSM6DS_ACCEL_ODR_6664 = 0x0A, // 6664 Hz
|
||||
};
|
||||
|
||||
// ── Gyroscope full-scale range ───────────────────────────────────────────────
|
||||
// CTRL2_G bits [3:0] — FS_G[2:1] and FS_125 (bit 1)
|
||||
// The FS_125 bit (bit 1) enables the ±125 dps range independently of FS_G.
|
||||
// For all other ranges, bits [3:2] select the range and bit 1 = 0.
|
||||
enum LSM6DSGyroRange : uint8_t {
|
||||
LSM6DS_GYRO_RANGE_125 = 0x02, // ±125 dps, 4.375 mdps/LSB (FS_125=1)
|
||||
LSM6DS_GYRO_RANGE_250 = 0x00, // ±250 dps, 8.75 mdps/LSB
|
||||
LSM6DS_GYRO_RANGE_500 = 0x04, // ±500 dps, 17.50 mdps/LSB
|
||||
LSM6DS_GYRO_RANGE_1000 = 0x08, // ±1000 dps, 35 mdps/LSB
|
||||
LSM6DS_GYRO_RANGE_2000 = 0x0C, // ±2000 dps, 70 mdps/LSB
|
||||
};
|
||||
|
||||
// ── Gyroscope output data rate ───────────────────────────────────────────────
|
||||
// CTRL2_G bits [7:4] — ODR_G[3:0]
|
||||
enum LSM6DSGyroODR : uint8_t {
|
||||
LSM6DS_GYRO_ODR_OFF = 0x00,
|
||||
LSM6DS_GYRO_ODR_12_5 = 0x01, // 12.5 Hz
|
||||
LSM6DS_GYRO_ODR_26 = 0x02, // 26 Hz
|
||||
LSM6DS_GYRO_ODR_52 = 0x03, // 52 Hz
|
||||
LSM6DS_GYRO_ODR_104 = 0x04, // 104 Hz
|
||||
LSM6DS_GYRO_ODR_208 = 0x05, // 208 Hz
|
||||
LSM6DS_GYRO_ODR_416 = 0x06, // 416 Hz
|
||||
LSM6DS_GYRO_ODR_833 = 0x07, // 833 Hz
|
||||
LSM6DS_GYRO_ODR_1666 = 0x08, // 1666 Hz
|
||||
LSM6DS_GYRO_ODR_3332 = 0x09, // 3332 Hz
|
||||
LSM6DS_GYRO_ODR_6664 = 0x0A, // 6664 Hz
|
||||
};
|
||||
|
||||
// ── Main component class ─────────────────────────────────────────────────────
|
||||
class LSM6DSComponent : public motion::MotionComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
// Configuration setters (called from Python codegen)
|
||||
void set_accel_range(LSM6DSAccelRange r) { this->accel_range_ = r; }
|
||||
void set_accel_odr(LSM6DSAccelODR o) { this->accel_odr_ = o; }
|
||||
void set_gyro_range(LSM6DSGyroRange r) { this->gyro_range_ = r; }
|
||||
void set_gyro_odr(LSM6DSGyroODR o) { this->gyro_odr_ = o; }
|
||||
|
||||
template<typename F> void add_temperature_listener(F &&cb) { this->temperature_callback_.add(std::forward<F>(cb)); }
|
||||
|
||||
protected:
|
||||
const char *chip_name_{"Unknown"};
|
||||
bool update_data(motion::MotionData &data) override;
|
||||
|
||||
LSM6DSAccelRange accel_range_{LSM6DS_ACCEL_RANGE_4G};
|
||||
LSM6DSAccelODR accel_odr_{LSM6DS_ACCEL_ODR_104};
|
||||
LSM6DSGyroRange gyro_range_{LSM6DS_GYRO_RANGE_2000};
|
||||
LSM6DSGyroODR gyro_odr_{LSM6DS_GYRO_ODR_208};
|
||||
|
||||
LazyCallbackManager<void(float)> temperature_callback_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::lsm6ds
|
||||
@@ -0,0 +1,106 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import (
|
||||
CONF_ACCELEROMETER_ODR,
|
||||
CONF_ACCELEROMETER_RANGE,
|
||||
CONF_GYROSCOPE_ODR,
|
||||
CONF_GYROSCOPE_RANGE,
|
||||
)
|
||||
from esphome.components.motion import motion_schema, new_motion_component
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from . import LSM6DSComponent, lsm6ds_ns
|
||||
|
||||
# ── Dependency declarations ──────────────────────────────────────────────────
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "lsm6ds"
|
||||
|
||||
# ── C++ namespace / class ────────────────────────────────────────────────────
|
||||
# ── Enum proxies ─────────────────────────────────────────────────────────────
|
||||
LSM6DSAccelRange = lsm6ds_ns.enum("LSM6DSAccelRange")
|
||||
ACCEL_RANGE_OPTIONS = {
|
||||
"2G": LSM6DSAccelRange.LSM6DS_ACCEL_RANGE_2G,
|
||||
"4G": LSM6DSAccelRange.LSM6DS_ACCEL_RANGE_4G,
|
||||
"8G": LSM6DSAccelRange.LSM6DS_ACCEL_RANGE_8G,
|
||||
"16G": LSM6DSAccelRange.LSM6DS_ACCEL_RANGE_16G,
|
||||
}
|
||||
|
||||
LSM6DSAccelODR = lsm6ds_ns.enum("LSM6DSAccelODR")
|
||||
ACCEL_ODR_OPTIONS = {
|
||||
"OFF": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_OFF,
|
||||
"12_5HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_12_5,
|
||||
"26HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_26,
|
||||
"52HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_52,
|
||||
"104HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_104,
|
||||
"208HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_208,
|
||||
"416HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_416,
|
||||
"833HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_833,
|
||||
"1666HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_1666,
|
||||
"3332HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_3332,
|
||||
"6664HZ": LSM6DSAccelODR.LSM6DS_ACCEL_ODR_6664,
|
||||
}
|
||||
|
||||
LSM6DSGyroRange = lsm6ds_ns.enum("LSM6DSGyroRange")
|
||||
GYRO_RANGE_OPTIONS = {
|
||||
"125DPS": LSM6DSGyroRange.LSM6DS_GYRO_RANGE_125,
|
||||
"250DPS": LSM6DSGyroRange.LSM6DS_GYRO_RANGE_250,
|
||||
"500DPS": LSM6DSGyroRange.LSM6DS_GYRO_RANGE_500,
|
||||
"1000DPS": LSM6DSGyroRange.LSM6DS_GYRO_RANGE_1000,
|
||||
"2000DPS": LSM6DSGyroRange.LSM6DS_GYRO_RANGE_2000,
|
||||
}
|
||||
|
||||
LSM6DSGyroODR = lsm6ds_ns.enum("LSM6DSGyroODR")
|
||||
GYRO_ODR_OPTIONS = {
|
||||
"OFF": LSM6DSGyroODR.LSM6DS_GYRO_ODR_OFF,
|
||||
"12_5HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_12_5,
|
||||
"26HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_26,
|
||||
"52HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_52,
|
||||
"104HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_104,
|
||||
"208HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_208,
|
||||
"416HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_416,
|
||||
"833HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_833,
|
||||
"1666HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_1666,
|
||||
"3332HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_3332,
|
||||
"6664HZ": LSM6DSGyroODR.LSM6DS_GYRO_ODR_6664,
|
||||
}
|
||||
|
||||
# ── CONFIG_SCHEMA ─────────────────────────────────────────────────────────────
|
||||
# Extend the motion platform schema which provides:
|
||||
# - accel_x/y/z sensor schemas
|
||||
# - gyro_x/y/z sensor schemas
|
||||
# - axis_mapping schema + validation
|
||||
# - update_interval / polling
|
||||
CONFIG_SCHEMA = (
|
||||
motion_schema(LSM6DSComponent, has_accel=True, has_gyro=True)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_ACCELEROMETER_RANGE, default="4G"): cv.enum(
|
||||
ACCEL_RANGE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_ACCELEROMETER_ODR, default="104HZ"): cv.enum(
|
||||
ACCEL_ODR_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_GYROSCOPE_RANGE, default="2000DPS"): cv.enum(
|
||||
GYRO_RANGE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_GYROSCOPE_ODR, default="208HZ"): cv.enum(
|
||||
GYRO_ODR_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x6A))
|
||||
)
|
||||
|
||||
|
||||
# ── Code generation ──────────────────────────────────────────────────────────
|
||||
async def to_code(config):
|
||||
var = await new_motion_component(config)
|
||||
|
||||
# Let the motion platform handle sensor wiring, axis mapping, and polling
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
# Chip-specific hardware configuration
|
||||
cg.add(var.set_accel_range(config[CONF_ACCELEROMETER_RANGE]))
|
||||
cg.add(var.set_accel_odr(config[CONF_ACCELEROMETER_ODR]))
|
||||
cg.add(var.set_gyro_range(config[CONF_GYROSCOPE_RANGE]))
|
||||
cg.add(var.set_gyro_odr(config[CONF_GYROSCOPE_ODR]))
|
||||
@@ -0,0 +1,39 @@
|
||||
# YAML config keys
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_THERMOMETER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from . import CONF_LSM6DS_ID, LSM6DSComponent
|
||||
|
||||
CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_TYPE): CONF_TEMPERATURE,
|
||||
cv.GenerateID(CONF_LSM6DS_ID): cv.use_id(LSM6DSComponent),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
parent = await cg.get_variable(config[CONF_LSM6DS_ID])
|
||||
data = MockObj("data")
|
||||
value_lambda = await cg.process_lambda(
|
||||
var.publish_state(data),
|
||||
[(cg.float_, str(data))],
|
||||
)
|
||||
cg.add(parent.add_temperature_listener(value_lambda))
|
||||
@@ -47,6 +47,7 @@ from esphome.core import CORE, ID, Lambda
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.writer import clean_build
|
||||
from esphome.yaml_util import load_yaml
|
||||
|
||||
@@ -75,10 +76,14 @@ from .schemas import (
|
||||
BASE_PROPS,
|
||||
DISP_BG_SCHEMA,
|
||||
FULL_STYLE_SCHEMA,
|
||||
SET_STATE_SCHEMA,
|
||||
STATE_SCHEMA,
|
||||
STYLE_REMAP,
|
||||
STYLE_SCHEMA,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
container_schema,
|
||||
container_schema_value,
|
||||
obj_dict,
|
||||
)
|
||||
from .styles import styles_to_code, theme_to_code
|
||||
@@ -113,6 +118,14 @@ from .widgets.page import ( # page_spec used in LVGL_SCHEMA
|
||||
page_spec,
|
||||
)
|
||||
|
||||
# These style schemas live in .schemas but are imported here so they land in
|
||||
# this module's namespace, where script/build_language_schema.py registers them
|
||||
# as *named* schemas and emits `extends` references — instead of inlining the
|
||||
# ~80-property STYLE_SCHEMA at every widget x part x state, which bloated the
|
||||
# dumped lvgl schema ~23x (17 MB vs ~750 KB). They are not otherwise used in
|
||||
# this file; this tuple keeps the imports live (and self-documents why).
|
||||
_SCHEMA_DUMPER_NAMED_SCHEMAS = (STYLE_SCHEMA, STATE_SCHEMA, SET_STATE_SCHEMA)
|
||||
|
||||
# Widget registration happens via WidgetType.__init__ in individual widget files
|
||||
# The imports below trigger creation of the widget types
|
||||
# Action registration (lvgl.{widget}.update) happens automatically
|
||||
@@ -559,94 +572,106 @@ def _theme_schema(value: dict) -> dict:
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validation
|
||||
|
||||
LVGL_SCHEMA = cv.All(
|
||||
container_schema(
|
||||
obj_spec,
|
||||
cv.polling_component_schema("1s")
|
||||
.extend(
|
||||
{
|
||||
**{
|
||||
cv.Optional(event): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
Trigger.template(lv_obj_t_ptr, lv_event_t_ptr)
|
||||
),
|
||||
}
|
||||
)
|
||||
for event in df.LV_SCREEN_EVENT_TRIGGERS
|
||||
+ df.LV_DISPLAY_EVENT_TRIGGERS
|
||||
},
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent),
|
||||
cv.GenerateID(CONF_ALIGN_TO_LAMBDA_ID): cv.declare_id(lv_lambda_t),
|
||||
cv.GenerateID(df.CONF_DISPLAYS): display_schema,
|
||||
cv.Optional(CONF_COLOR_DEPTH, default=16): cv.one_of(16),
|
||||
cv.Optional(
|
||||
df.CONF_DEFAULT_FONT, default="montserrat_14"
|
||||
): lvalid.lv_font,
|
||||
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
|
||||
cv.Optional(
|
||||
df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
||||
*df.LV_LOG_LEVELS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_BYTE_ORDER): cv.one_of(
|
||||
"big_endian", "little_endian", lower=True
|
||||
),
|
||||
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
|
||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}).extend(
|
||||
FULL_STYLE_SCHEMA
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_ON_IDLE): validate_automation(
|
||||
# The options accepted at the top level of an `lvgl:` block, on top of the base
|
||||
# object schema that `container_schema(obj_spec, ...)` supplies. Held in a
|
||||
# module-level name (rather than inline) so the schema-extractor wrapper on
|
||||
# CONFIG_SCHEMA below can hand the language-schema dumper the same composed
|
||||
# schema the runtime validates against.
|
||||
LVGL_TOP_LEVEL_SCHEMA = (
|
||||
cv.polling_component_schema("1s")
|
||||
.extend(
|
||||
{
|
||||
**{
|
||||
cv.Optional(event): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
|
||||
cv.Required(CONF_TIMEOUT): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
Trigger.template(lv_obj_t_ptr, lv_event_t_ptr)
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PAGES): cv.ensure_list(container_schema(page_spec)),
|
||||
**{
|
||||
cv.Optional(x): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlainTrigger),
|
||||
},
|
||||
single=True,
|
||||
)
|
||||
for x in SIMPLE_TRIGGERS
|
||||
},
|
||||
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
|
||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||
cv.Optional(df.CONF_BOTTOM_LAYER): container_schema(obj_spec),
|
||||
cv.Optional(
|
||||
df.CONF_TRANSPARENCY_KEY, default=0x000400
|
||||
): lvalid.lv_color,
|
||||
cv.Optional(df.CONF_THEME): _theme_schema,
|
||||
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
|
||||
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
|
||||
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
|
||||
cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG,
|
||||
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
|
||||
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(DISP_BG_SCHEMA),
|
||||
),
|
||||
)
|
||||
for event in df.LV_SCREEN_EVENT_TRIGGERS + df.LV_DISPLAY_EVENT_TRIGGERS
|
||||
},
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent),
|
||||
cv.GenerateID(CONF_ALIGN_TO_LAMBDA_ID): cv.declare_id(lv_lambda_t),
|
||||
cv.GenerateID(df.CONF_DISPLAYS): display_schema,
|
||||
cv.Optional(CONF_COLOR_DEPTH, default=16): cv.one_of(16),
|
||||
cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font,
|
||||
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
||||
*df.LV_LOG_LEVELS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_BYTE_ORDER): cv.one_of(
|
||||
"big_endian", "little_endian", lower=True
|
||||
),
|
||||
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
|
||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}).extend(
|
||||
FULL_STYLE_SCHEMA
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_ON_IDLE): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
|
||||
cv.Required(CONF_TIMEOUT): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PAGES): cv.ensure_list(container_schema(page_spec)),
|
||||
**{
|
||||
cv.Optional(x): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlainTrigger),
|
||||
},
|
||||
single=True,
|
||||
)
|
||||
for x in SIMPLE_TRIGGERS
|
||||
},
|
||||
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
|
||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||
cv.Optional(df.CONF_BOTTOM_LAYER): container_schema(obj_spec),
|
||||
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
|
||||
cv.Optional(df.CONF_THEME): _theme_schema,
|
||||
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
|
||||
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
|
||||
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
|
||||
cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG,
|
||||
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
|
||||
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(DISP_BG_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
LVGL_SCHEMA = cv.All(
|
||||
container_schema(obj_spec, LVGL_TOP_LEVEL_SCHEMA),
|
||||
cv.has_at_most_one_key(CONF_PAGES, df.CONF_LAYOUT),
|
||||
add_hello_world,
|
||||
)
|
||||
|
||||
|
||||
@schema_extractor("schema")
|
||||
def lvgl_config_schema(config):
|
||||
"""
|
||||
Can't use cv.ensure_list here because it converts an empty config to an empty list,
|
||||
rather than a default config.
|
||||
"""
|
||||
if config is SCHEMA_EXTRACT:
|
||||
# CONFIG_SCHEMA is this callable wrapping `cv.All` over a container_schema
|
||||
# closure, so the language-schema dumper can't see the top-level `lvgl:`
|
||||
# fields (it would emit an empty schema). Hand it the same composed
|
||||
# obj + top-level schema the runtime validates against, plus the
|
||||
# `widgets:` key (added per-value by append_layout_schema at runtime, so
|
||||
# otherwise invisible to the dumper). Validation of real configs (the
|
||||
# branches below) is unchanged.
|
||||
return container_schema_value(obj_spec, LVGL_TOP_LEVEL_SCHEMA).extend(
|
||||
{cv.Optional(df.CONF_WIDGETS): any_widget_schema()}
|
||||
)
|
||||
if not config or isinstance(config, dict):
|
||||
return [LVGL_SCHEMA(config)]
|
||||
return cv.Schema([LVGL_SCHEMA])(config)
|
||||
|
||||
@@ -22,7 +22,11 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.core.config import StartupTrigger
|
||||
from esphome.schema_extractors import EnableSchemaExtraction
|
||||
from esphome.schema_extractors import (
|
||||
SCHEMA_EXTRACT,
|
||||
EnableSchemaExtraction,
|
||||
schema_extractor,
|
||||
)
|
||||
|
||||
from . import defines as df, lv_validation as lvalid
|
||||
from .defines import (
|
||||
@@ -627,6 +631,25 @@ _CONTAINER_SCHEMA_CACHE: dict[
|
||||
] = {}
|
||||
|
||||
|
||||
def container_schema_value(widget_type: WidgetType, extras: Any = None) -> cv.Schema:
|
||||
"""
|
||||
Build the static schema that :func:`container_schema` validates against, i.e.
|
||||
everything except the value-dependent ``append_layout_schema`` applied at
|
||||
validation time.
|
||||
|
||||
Factored out and exposed so the language-schema dumper can extract a
|
||||
representative schema for a widget — and for the top-level ``lvgl:`` block,
|
||||
whose ``CONFIG_SCHEMA`` is a callable that otherwise hides this behind the
|
||||
:func:`container_schema` validator closure.
|
||||
"""
|
||||
schema = obj_schema(widget_type).extend(
|
||||
{cv.GenerateID(): cv.declare_id(widget_type.w_type)}
|
||||
)
|
||||
if extras:
|
||||
schema = schema.extend(extras)
|
||||
return schema.extend(widget_type.schema)
|
||||
|
||||
|
||||
def container_schema(
|
||||
widget_type: WidgetType, extras: Any = None
|
||||
) -> Callable[[Any], Any]:
|
||||
@@ -649,12 +672,7 @@ def container_schema(
|
||||
def get_schema() -> cv.Schema:
|
||||
nonlocal cached_schema
|
||||
if cached_schema is None:
|
||||
schema = obj_schema(widget_type).extend(
|
||||
{cv.GenerateID(): cv.declare_id(widget_type.w_type)}
|
||||
)
|
||||
if extras:
|
||||
schema = schema.extend(extras)
|
||||
cached_schema = schema.extend(widget_type.schema)
|
||||
cached_schema = container_schema_value(widget_type, extras)
|
||||
return cached_schema
|
||||
|
||||
def validator(value: Any) -> Any:
|
||||
@@ -678,7 +696,23 @@ def any_widget_schema(extras=None):
|
||||
:return: A validator for the Widgets key
|
||||
"""
|
||||
|
||||
@schema_extractor("schema")
|
||||
def validator(value):
|
||||
if value is SCHEMA_EXTRACT:
|
||||
# The widgets: list is built per-value at validation time, so the
|
||||
# language-schema dumper sees nothing. Enumerate every registered
|
||||
# widget type as an optional key (a widget item is really a
|
||||
# single-key mapping; over-listing them lets editors complete any
|
||||
# widget — `esphome config` enforces exactly one). extras carries the
|
||||
# layout child options where applicable.
|
||||
return cv.ensure_list(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(name): container_schema_value(widget_type, extras)
|
||||
for name, widget_type in WIDGET_TYPES.items()
|
||||
}
|
||||
)
|
||||
)
|
||||
if isinstance(value, dict):
|
||||
# Convert to list
|
||||
is_dict = True
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
# Various configuration constants for MIPI displays
|
||||
# Various utility functions for MIPI DBI configuration
|
||||
|
||||
from collections.abc import Callable
|
||||
import functools
|
||||
from typing import Any, Self
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from esphome.components.const import CONF_COLOR_DEPTH
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns
|
||||
import esphome.config_validation as cv
|
||||
@@ -18,6 +22,7 @@ from esphome.const import (
|
||||
CONF_LAMBDA,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_OFFSET_WIDTH,
|
||||
CONF_PAGES,
|
||||
@@ -27,6 +32,7 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
|
||||
LOGGER = cv.logging.getLogger(__name__)
|
||||
|
||||
@@ -114,6 +120,7 @@ CSCON = 0xF0
|
||||
PWCTR6 = 0xF6
|
||||
ADJCTL3 = 0xF7
|
||||
PAGESEL = 0xFE
|
||||
PAGESEL1 = 0xFF
|
||||
|
||||
MADCTL_MY = 0x80 # Bit 7 Bottom to top
|
||||
MADCTL_MX = 0x40 # Bit 6 Right to left
|
||||
@@ -133,6 +140,8 @@ MADCTL_FLIP_FLAG = 0x100 # meta-flag to indicate use of axis flips
|
||||
# Special constant for delays in command sequences
|
||||
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
|
||||
|
||||
CONF_PAD_HEIGHT = "pad_height"
|
||||
CONF_PAD_WIDTH = "pad_width"
|
||||
CONF_PIXEL_MODE = "pixel_mode"
|
||||
CONF_USE_AXIS_FLIPS = "use_axis_flips"
|
||||
|
||||
@@ -196,6 +205,8 @@ def dimension_schema(rounding):
|
||||
rounding
|
||||
),
|
||||
cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding),
|
||||
cv.Optional(CONF_PAD_WIDTH): validate_dimension(rounding),
|
||||
cv.Optional(CONF_PAD_HEIGHT): validate_dimension(rounding),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -239,6 +250,54 @@ def delay(ms):
|
||||
return DELAY_FLAG, ms
|
||||
|
||||
|
||||
# Generic placeholder model present in every DriverChip registry; skipped when
|
||||
# choosing a representative model for schema extraction.
|
||||
_CUSTOM_MODEL = "CUSTOM"
|
||||
|
||||
|
||||
def model_schema_extractor(
|
||||
models: dict[str, Any],
|
||||
model_schema: Callable[[dict[str, Any]], Any],
|
||||
extra: dict[str, Any] | None = None,
|
||||
) -> Callable[[Callable[[Any], Any]], Callable[[Any], Any]]:
|
||||
"""
|
||||
Decorate a model-driven display CONFIG_SCHEMA so the language-schema dumper
|
||||
can extract it.
|
||||
|
||||
The schema is generated per ``model`` at validation time, so the static
|
||||
dumper has nothing to walk. When the dumper passes SCHEMA_EXTRACT, resolve a
|
||||
representative schema for a real model (the generic "CUSTOM" placeholder
|
||||
over-constrains fields like init_sequence) plus any *extra* keys the model
|
||||
needs, e.g. a bus mode, and hand that back; runtime validation is untouched.
|
||||
"""
|
||||
|
||||
def decorate(config_schema: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
||||
@schema_extractor("schema")
|
||||
@functools.wraps(config_schema)
|
||||
def wrapper(config: Any) -> Any:
|
||||
if config is not SCHEMA_EXTRACT:
|
||||
return config_schema(config)
|
||||
names = sorted(models)
|
||||
representative = next((n for n in names if n != _CUSTOM_MODEL), names[0])
|
||||
schema = model_schema({CONF_MODEL: representative, **(extra or {})})
|
||||
if isinstance(schema, vol.All):
|
||||
schema = next(
|
||||
(v for v in schema.validators if isinstance(v, vol.Schema)),
|
||||
schema,
|
||||
)
|
||||
if isinstance(schema, vol.Schema):
|
||||
# The resolved schema pins ``model`` to the representative; expose
|
||||
# the full model list so the dumped enum offers every model.
|
||||
schema = schema.extend(
|
||||
{cv.Required(CONF_MODEL): cv.one_of(*names, upper=True)}
|
||||
)
|
||||
return schema
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
class DriverChip:
|
||||
"""
|
||||
A class representing a MIPI DBI driver chip model.
|
||||
@@ -257,6 +316,36 @@ class DriverChip:
|
||||
name = name.upper()
|
||||
self.name = name
|
||||
self.initsequence = initsequence
|
||||
if CONF_NATIVE_WIDTH in defaults:
|
||||
if CONF_WIDTH not in defaults:
|
||||
defaults[CONF_WIDTH] = (
|
||||
defaults[CONF_NATIVE_WIDTH]
|
||||
- defaults.get(CONF_OFFSET_WIDTH, 0)
|
||||
- defaults.get(CONF_PAD_WIDTH, 0)
|
||||
)
|
||||
else:
|
||||
native_width = (
|
||||
defaults.get(CONF_WIDTH, 0)
|
||||
+ defaults.get(CONF_OFFSET_WIDTH, 0)
|
||||
+ defaults.get(CONF_PAD_WIDTH, 0)
|
||||
)
|
||||
if native_width != 0:
|
||||
defaults[CONF_NATIVE_WIDTH] = native_width
|
||||
if CONF_NATIVE_HEIGHT in defaults:
|
||||
if CONF_HEIGHT not in defaults:
|
||||
defaults[CONF_HEIGHT] = (
|
||||
defaults[CONF_NATIVE_HEIGHT]
|
||||
- defaults.get(CONF_OFFSET_HEIGHT, 0)
|
||||
- defaults.get(CONF_PAD_HEIGHT, 0)
|
||||
)
|
||||
else:
|
||||
native_height = (
|
||||
defaults.get(CONF_HEIGHT, 0)
|
||||
+ defaults.get(CONF_OFFSET_HEIGHT, 0)
|
||||
+ defaults.get(CONF_PAD_HEIGHT, 0)
|
||||
)
|
||||
if native_height != 0:
|
||||
defaults[CONF_NATIVE_HEIGHT] = native_height
|
||||
self.defaults = defaults
|
||||
DriverChip.models[name] = self
|
||||
|
||||
@@ -282,18 +371,6 @@ class DriverChip:
|
||||
initsequence = list(kwargs.pop("initsequence", self.initsequence))
|
||||
initsequence.extend(kwargs.pop("add_init_sequence", ()))
|
||||
defaults = self.defaults.copy()
|
||||
if (
|
||||
CONF_WIDTH in defaults
|
||||
and CONF_OFFSET_WIDTH in kwargs
|
||||
and CONF_NATIVE_WIDTH not in defaults
|
||||
):
|
||||
defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH]
|
||||
if (
|
||||
CONF_HEIGHT in defaults
|
||||
and CONF_OFFSET_HEIGHT in kwargs
|
||||
and CONF_NATIVE_HEIGHT not in defaults
|
||||
):
|
||||
defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT]
|
||||
defaults.update(kwargs)
|
||||
return self.__class__(name, initsequence=tuple(initsequence), **defaults)
|
||||
|
||||
@@ -311,6 +388,16 @@ class DriverChip:
|
||||
return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
|
||||
return {CONF_MIRROR_X, CONF_MIRROR_Y}
|
||||
|
||||
def has_hardware_transform(self, config) -> bool:
|
||||
"""
|
||||
Check if the model supports hardware transforms for the given configuration.
|
||||
"""
|
||||
return config.get(CONF_TRANSFORM) != CONF_DISABLED and self.transforms == {
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_SWAP_XY,
|
||||
}
|
||||
|
||||
def option(self, name, fallback=False) -> cv.Optional:
|
||||
return cv.Optional(name, default=self.get_default(name, fallback))
|
||||
|
||||
@@ -331,48 +418,89 @@ class DriverChip:
|
||||
return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms
|
||||
return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms
|
||||
|
||||
def get_dimensions(self, config, swap: bool = True) -> tuple[int, int, int, int]:
|
||||
def get_dimensions(
|
||||
self, config, swap: bool = True
|
||||
) -> tuple[int, int, int, int, int, int]:
|
||||
"""
|
||||
Return the dimensions of the current model.
|
||||
:param config: The current configuration
|
||||
:param swap: If width/height should be swapped when axes are swapped.
|
||||
:return:
|
||||
:return: A tuple (width, height, offset_width, offset_height, pad_width, pad_height).
|
||||
"""
|
||||
|
||||
transform = self.get_transform(config)
|
||||
if CONF_DIMENSIONS in config:
|
||||
# Explicit dimensions, just use as is
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
native_width = self.get_default(CONF_NATIVE_WIDTH, 0)
|
||||
native_height = self.get_default(CONF_NATIVE_HEIGHT, 0)
|
||||
if transform.get(CONF_SWAP_XY) is True:
|
||||
native_width, native_height = native_height, native_width
|
||||
width = dimensions[CONF_WIDTH]
|
||||
height = dimensions[CONF_HEIGHT]
|
||||
offset_width = dimensions[CONF_OFFSET_WIDTH]
|
||||
offset_height = dimensions[CONF_OFFSET_HEIGHT]
|
||||
return width, height, offset_width, offset_height
|
||||
(width, height) = dimensions
|
||||
return width, height, 0, 0
|
||||
if CONF_PAD_WIDTH in dimensions:
|
||||
pad_width = dimensions[CONF_PAD_WIDTH]
|
||||
native_width = width + offset_width + pad_width
|
||||
elif native_width == 0:
|
||||
pad_width = 0
|
||||
native_width = width + offset_width
|
||||
else:
|
||||
pad_width = native_width - width - offset_width
|
||||
if CONF_PAD_HEIGHT in dimensions:
|
||||
pad_height = dimensions[CONF_PAD_HEIGHT]
|
||||
native_height = height + offset_height + pad_height
|
||||
elif native_height == 0:
|
||||
pad_height = 0
|
||||
native_height = height + offset_height
|
||||
else:
|
||||
pad_height = native_height - height - offset_height
|
||||
if (
|
||||
pad_width + offset_width >= native_width
|
||||
or pad_height + offset_height >= native_height
|
||||
):
|
||||
raise cv.Invalid("Dimensions exceed native size", [CONF_DIMENSIONS])
|
||||
if pad_width < 0 or pad_height < 0:
|
||||
raise cv.Invalid("Invalid offsets", [CONF_DIMENSIONS])
|
||||
|
||||
return width, height, offset_width, offset_height, pad_width, pad_height
|
||||
|
||||
# Must be a tuple
|
||||
width, height = dimensions
|
||||
return width, height, 0, 0, 0, 0
|
||||
|
||||
# Default dimensions, use model defaults
|
||||
transform = self.get_transform(config)
|
||||
|
||||
width = self.get_default(CONF_WIDTH)
|
||||
height = self.get_default(CONF_HEIGHT)
|
||||
native_width = self.get_default(CONF_NATIVE_WIDTH, 0)
|
||||
native_height = self.get_default(CONF_NATIVE_HEIGHT, 0)
|
||||
offset_width = self.get_default(CONF_OFFSET_WIDTH, 0)
|
||||
offset_height = self.get_default(CONF_OFFSET_HEIGHT, 0)
|
||||
pad_width = self.get_default(
|
||||
CONF_PAD_WIDTH, native_width - width - offset_width
|
||||
)
|
||||
pad_height = self.get_default(
|
||||
CONF_PAD_HEIGHT, native_height - height - offset_height
|
||||
)
|
||||
|
||||
if pad_width < 0 or pad_height < 0:
|
||||
raise cv.Invalid("Offsets exceed native size", [CONF_DIMENSIONS])
|
||||
|
||||
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
|
||||
# the offset is asymmetric
|
||||
if transform.get(CONF_MIRROR_X):
|
||||
native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2)
|
||||
offset_width = native_width - width - offset_width
|
||||
offset_width, pad_width = pad_width, offset_width
|
||||
if transform.get(CONF_MIRROR_Y):
|
||||
native_height = self.get_default(
|
||||
CONF_NATIVE_HEIGHT, height + offset_height * 2
|
||||
)
|
||||
offset_height = native_height - height - offset_height
|
||||
# Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer
|
||||
offset_height, pad_height = pad_height, offset_height
|
||||
# Swap default dimensions if swap_xy is set, or if rotation is 90/270, and we are not using a buffer
|
||||
if swap and transform.get(CONF_SWAP_XY) is True:
|
||||
width, height = height, width
|
||||
offset_height, offset_width = offset_width, offset_height
|
||||
return width, height, offset_width, offset_height
|
||||
pad_width, pad_height = pad_height, pad_width
|
||||
return width, height, offset_width, offset_height, pad_width, pad_height
|
||||
|
||||
def get_base_transform(self, config):
|
||||
transform = config.get(
|
||||
@@ -396,20 +524,8 @@ class DriverChip:
|
||||
|
||||
def get_transform(self, config) -> dict[str, bool]:
|
||||
transform = self.get_base_transform(config)
|
||||
can_transform = self.rotation_as_transform(config)
|
||||
# Can we use the MADCTL register to set the rotation?
|
||||
if can_transform and CONF_TRANSFORM not in config:
|
||||
rotation = config[CONF_ROTATION]
|
||||
if rotation == 180:
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
elif rotation == 90:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
else:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
transform[CONF_TRANSFORM] = True
|
||||
transform[CONF_TRANSFORM] = self.rotation_as_transform(config)
|
||||
return transform
|
||||
|
||||
def swap_xy_schema(self):
|
||||
@@ -444,8 +560,8 @@ class DriverChip:
|
||||
return madctl
|
||||
|
||||
def add_madctl(self, sequence: list, config: dict):
|
||||
# Add the MADCTL command to the sequence based on the configuration.
|
||||
# This takes into account rotation if it can be implemented in the transform
|
||||
# Add the MADCTL command to the sequence based on the base configuration.
|
||||
# Rotation is not applied here, it will be done at runtime.
|
||||
transform = self.get_transform(config)
|
||||
madctl = self.get_madctl(transform, config)
|
||||
sequence.append((MADCTL, madctl & 0xFF))
|
||||
|
||||
@@ -32,6 +32,7 @@ from esphome.components.mipi import (
|
||||
dimension_schema,
|
||||
get_color_depth,
|
||||
map_sequence,
|
||||
model_schema_extractor,
|
||||
power_of_two,
|
||||
requires_buffer,
|
||||
)
|
||||
@@ -161,6 +162,7 @@ def model_schema(config):
|
||||
)
|
||||
|
||||
|
||||
@model_schema_extractor(MODELS, model_schema)
|
||||
def _config_schema(config):
|
||||
config = cv.Schema(
|
||||
{
|
||||
@@ -170,7 +172,9 @@ def _config_schema(config):
|
||||
)(config)
|
||||
config = model_schema(config)(config)
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
display.add_metadata(
|
||||
config[CONF_ID],
|
||||
width,
|
||||
@@ -204,7 +208,9 @@ async def to_code(config):
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
color_depth = COLOR_DEPTHS[get_color_depth(config)]
|
||||
pixel_mode = int(config[CONF_PIXEL_MODE].removesuffix("bit"))
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode)
|
||||
|
||||
sequence = model.get_sequence(config)
|
||||
|
||||
@@ -71,6 +71,7 @@ DriverChip(
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
initsequence=[
|
||||
(0x01,),
|
||||
(0x60, 0x71, 0x23, 0xa2),
|
||||
(0x60, 0x71, 0x23, 0xa3),
|
||||
(0x60, 0x71, 0x23, 0xa4),
|
||||
|
||||
@@ -30,6 +30,7 @@ from esphome.components.mipi import (
|
||||
DriverChip,
|
||||
dimension_schema,
|
||||
map_sequence,
|
||||
model_schema_extractor,
|
||||
power_of_two,
|
||||
requires_buffer,
|
||||
)
|
||||
@@ -219,6 +220,7 @@ def model_schema(config):
|
||||
return schema
|
||||
|
||||
|
||||
@model_schema_extractor(MODELS, model_schema)
|
||||
def _config_schema(config):
|
||||
config = cv.Schema(
|
||||
{
|
||||
@@ -233,7 +235,9 @@ def _config_schema(config):
|
||||
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
|
||||
)(config)
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
display.add_metadata(
|
||||
config[CONF_ID],
|
||||
width,
|
||||
@@ -271,7 +275,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
async def to_code(config):
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
var = cg.new_Pvariable(config[CONF_ID], width, height)
|
||||
cg.add(var.set_model(model.name))
|
||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||
|
||||
@@ -17,16 +17,19 @@ from esphome.components.mipi import (
|
||||
MADCTL,
|
||||
MODE_BGR,
|
||||
MODE_RGB,
|
||||
PAGESEL,
|
||||
PAGESEL1,
|
||||
PIXFMT,
|
||||
DriverChip,
|
||||
dimension_schema,
|
||||
get_color_depth,
|
||||
map_sequence,
|
||||
model_schema_extractor,
|
||||
power_of_two,
|
||||
requires_buffer,
|
||||
)
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||
from esphome.components.spi import CONF_SPI_MODE, TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import ALLOW_EXTRA
|
||||
from esphome.const import (
|
||||
@@ -120,7 +123,9 @@ def denominator(config):
|
||||
"""
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
frac = config.get(CONF_BUFFER_SIZE)
|
||||
_width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
_width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
if frac is None or frac > 0.75 or height < 32:
|
||||
return 1
|
||||
try:
|
||||
@@ -168,11 +173,28 @@ def model_schema(config):
|
||||
]
|
||||
if bus_mode == TYPE_SINGLE:
|
||||
other_options.append(CONF_SPI_16)
|
||||
# Calculate default SPI mode. Mode3 for octal bus or single bus with no cs pin, mode0 otherwise.
|
||||
spi_mode = (
|
||||
cv.UNDEFINED if CONF_SPI_MODE in config else model.get_default(CONF_SPI_MODE)
|
||||
)
|
||||
if not spi_mode:
|
||||
if bus_mode == TYPE_OCTAL or (
|
||||
bus_mode == TYPE_SINGLE
|
||||
and config.get(CONF_CS_PIN, model.get_default(CONF_CS_PIN)) is False
|
||||
):
|
||||
spi_mode = "MODE3"
|
||||
if bus_mode == TYPE_SINGLE:
|
||||
LOGGER.warning(
|
||||
"No SPI mode specified, defaulting to MODE3 due to lack of CS pin. If you experience issues, try setting SPI mode explicitly to MODE0 or MODE3."
|
||||
)
|
||||
else:
|
||||
spi_mode = "MODE0"
|
||||
|
||||
schema = (
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_mode="MODE3" if bus_mode == TYPE_OCTAL else "MODE0",
|
||||
default_mode=spi_mode,
|
||||
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
|
||||
mode=bus_mode,
|
||||
)
|
||||
@@ -227,6 +249,7 @@ def model_schema(config):
|
||||
return schema
|
||||
|
||||
|
||||
@model_schema_extractor(MODELS, model_schema, extra={CONF_BUS_MODE: TYPE_SINGLE})
|
||||
def customise_schema(config):
|
||||
"""
|
||||
Create a customised config schema for a specific model and validate the configuration.
|
||||
@@ -255,14 +278,16 @@ def customise_schema(config):
|
||||
# Check for invalid combinations of MADCTL config
|
||||
if init_sequence := config.get(CONF_INIT_SEQUENCE):
|
||||
commands = [x[0] for x in init_sequence]
|
||||
if MADCTL in commands and CONF_TRANSFORM in config:
|
||||
raise cv.Invalid(
|
||||
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
|
||||
)
|
||||
if PIXFMT in commands:
|
||||
raise cv.Invalid(
|
||||
f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically"
|
||||
)
|
||||
# If there is page swapping, we can't rely on recognising common commands
|
||||
if PAGESEL not in commands and PAGESEL1 not in commands:
|
||||
if MADCTL in commands and CONF_TRANSFORM in config:
|
||||
raise cv.Invalid(
|
||||
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
|
||||
)
|
||||
if PIXFMT in commands:
|
||||
raise cv.Invalid(
|
||||
f"PIXFMT ({PIXFMT:#X}) should not be in the init sequence, it will be set automatically"
|
||||
)
|
||||
|
||||
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
|
||||
raise cv.Invalid("DC pin is not supported in quad mode")
|
||||
@@ -270,15 +295,9 @@ def customise_schema(config):
|
||||
raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
|
||||
denominator(config)
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
has_hardware_transform = config.get(
|
||||
CONF_TRANSFORM
|
||||
) != CONF_DISABLED and model.transforms == {
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_SWAP_XY,
|
||||
}
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(
|
||||
config, not has_hardware_transform
|
||||
has_hardware_transform = model.has_hardware_transform(config)
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config, not has_hardware_transform)
|
||||
)
|
||||
display.add_metadata(
|
||||
config[CONF_ID],
|
||||
@@ -311,14 +330,17 @@ def _final_validate(config):
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
|
||||
# Always call this to check dimensions during validation
|
||||
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
|
||||
model.get_dimensions(config)
|
||||
)
|
||||
|
||||
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
|
||||
# If PSRAM is not enabled, choose a small buffer size by default
|
||||
if not requires_buffer(config):
|
||||
return # No need to pick a size
|
||||
color_depth = get_color_depth(config)
|
||||
frac = denominator(config)
|
||||
width, height, _offset_width, _offset_height = model.get_dimensions(config)
|
||||
|
||||
buffer_size = color_depth // 8 * width * height // frac
|
||||
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
|
||||
fraction = min(20000.0, buffer_size // 4) / buffer_size
|
||||
@@ -338,15 +360,9 @@ def get_instance(config):
|
||||
:return: type, template arguments
|
||||
"""
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
has_hardware_transform = config.get(
|
||||
CONF_TRANSFORM
|
||||
) != CONF_DISABLED and model.transforms == {
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_SWAP_XY,
|
||||
}
|
||||
width, height, offset_width, offset_height = model.get_dimensions(
|
||||
config, not has_hardware_transform
|
||||
has_hardware_transform = model.has_hardware_transform(config)
|
||||
width, height, offset_width, offset_height, pad_width, pad_height = (
|
||||
model.get_dimensions(config, not has_hardware_transform)
|
||||
)
|
||||
|
||||
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
|
||||
@@ -372,6 +388,8 @@ def get_instance(config):
|
||||
height,
|
||||
offset_width,
|
||||
offset_height,
|
||||
pad_width,
|
||||
pad_height,
|
||||
madctl,
|
||||
has_hardware_transform,
|
||||
]
|
||||
@@ -407,6 +425,8 @@ async def to_code(config):
|
||||
dc_pin = await cg.gpio_pin_expression(dc_pin)
|
||||
cg.add(var.set_dc_pin(dc_pin))
|
||||
|
||||
if config.get(CONF_INVERT_COLORS):
|
||||
cg.add(var.set_invert_colors(True))
|
||||
if lamb := config.get(CONF_LAMBDA):
|
||||
lambda_ = await cg.process_lambda(
|
||||
lamb, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
|
||||
@@ -81,10 +81,15 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
||||
* @tparam HEIGHT Height of the display in pixels
|
||||
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
||||
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
||||
* @tparam PAD_WIDTH Additional pixels recognised by the controller after the offset and width
|
||||
* @tparam PAD_HEIGHT Additional lines recognised by the controller after the offset and width
|
||||
* @tparam MADCTL The base MADCTL value for the display, with no rotation bits set.
|
||||
* @tparam HAS_HARDWARE_ROTATION Whether the display supports hardware rotation.
|
||||
* buffer
|
||||
*/
|
||||
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, bool HAS_HARDWARE_ROTATION>
|
||||
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, int PAD_WIDTH, int PAD_HEIGHT, uint16_t MADCTL,
|
||||
bool HAS_HARDWARE_ROTATION>
|
||||
class MipiSpi : public display::Display,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
@@ -126,17 +131,6 @@ class MipiSpi : public display::Display,
|
||||
return HEIGHT;
|
||||
}
|
||||
|
||||
// If hardware rotation is in use, the actual display width/height changes with rotation
|
||||
int get_width_internal() override {
|
||||
if constexpr (HAS_HARDWARE_ROTATION)
|
||||
return get_width();
|
||||
return WIDTH;
|
||||
}
|
||||
int get_height_internal() override {
|
||||
if constexpr (HAS_HARDWARE_ROTATION)
|
||||
return get_height();
|
||||
return HEIGHT;
|
||||
}
|
||||
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||
|
||||
// reset the display, and write the init sequence
|
||||
@@ -157,6 +151,9 @@ class MipiSpi : public display::Display,
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
} else {
|
||||
// no reset pin, send software reset command
|
||||
this->write_command_(SW_RESET_CMD);
|
||||
}
|
||||
|
||||
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
||||
@@ -182,7 +179,6 @@ class MipiSpi : public display::Display,
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
auto arg_byte = vec[index];
|
||||
switch (cmd) {
|
||||
case SLEEP_OUT: {
|
||||
// are we ready, boots?
|
||||
@@ -193,13 +189,6 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
} break;
|
||||
|
||||
case INVERT_ON:
|
||||
this->invert_colors_ = true;
|
||||
break;
|
||||
case BRIGHTNESS:
|
||||
this->brightness_ = arg_byte;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -233,14 +222,25 @@ class MipiSpi : public display::Display,
|
||||
}
|
||||
|
||||
void dump_config() override {
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
|
||||
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
|
||||
HAS_HARDWARE_ROTATION);
|
||||
internal_dump_config(this->model_, this->get_width(), this->get_height(), this->get_offset_width_(),
|
||||
this->get_offset_height_(), (uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8,
|
||||
IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, this->mode_,
|
||||
this->data_rate_, BUS_TYPE, HAS_HARDWARE_ROTATION);
|
||||
}
|
||||
|
||||
protected:
|
||||
/* METHODS */
|
||||
// If hardware rotation is in use, the actual display width/height changes with rotation
|
||||
int get_width_internal() override {
|
||||
if constexpr (HAS_HARDWARE_ROTATION)
|
||||
return get_width();
|
||||
return WIDTH;
|
||||
}
|
||||
int get_height_internal() override {
|
||||
if constexpr (HAS_HARDWARE_ROTATION)
|
||||
return get_height();
|
||||
return HEIGHT;
|
||||
}
|
||||
// convenience functions to write commands with or without data
|
||||
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
|
||||
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
|
||||
@@ -330,20 +330,34 @@ class MipiSpi : public display::Display,
|
||||
this->write_command_(MADCTL_CMD, madctl);
|
||||
}
|
||||
|
||||
uint16_t get_offset_width_() {
|
||||
uint16_t get_offset_width_() const {
|
||||
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return OFFSET_HEIGHT;
|
||||
switch (this->rotation_) {
|
||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||
return OFFSET_HEIGHT;
|
||||
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||
return PAD_WIDTH;
|
||||
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||
return PAD_HEIGHT;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return OFFSET_WIDTH;
|
||||
}
|
||||
|
||||
uint16_t get_offset_height_() {
|
||||
uint16_t get_offset_height_() const {
|
||||
if constexpr (HAS_HARDWARE_ROTATION) {
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
|
||||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
|
||||
return OFFSET_WIDTH;
|
||||
switch (this->rotation_) {
|
||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||
return PAD_WIDTH;
|
||||
case display::DISPLAY_ROTATION_180_DEGREES:
|
||||
return PAD_HEIGHT;
|
||||
case display::DISPLAY_ROTATION_270_DEGREES:
|
||||
return OFFSET_WIDTH;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return OFFSET_HEIGHT;
|
||||
}
|
||||
@@ -396,7 +410,7 @@ class MipiSpi : public display::Display,
|
||||
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h, 8);
|
||||
}
|
||||
} else {
|
||||
for (size_t y = 0; y != static_cast<size_t>(h); y++) {
|
||||
for (size_t y = 0; y != h; y++) {
|
||||
if constexpr (BUS_TYPE == BUS_TYPE_SINGLE || BUS_TYPE == BUS_TYPE_SINGLE_16) {
|
||||
this->write_array(ptr, w);
|
||||
} else if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||
@@ -492,19 +506,23 @@ class MipiSpi : public display::Display,
|
||||
* @tparam BUFFERPIXEL Color depth of the buffer
|
||||
* @tparam DISPLAYPIXEL Color depth of the display
|
||||
* @tparam BUS_TYPE The type of the interface bus (single, quad, octal)
|
||||
* @tparam ROTATION The rotation of the display
|
||||
* @tparam WIDTH Width of the display in pixels
|
||||
* @tparam HEIGHT Height of the display in pixels
|
||||
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
||||
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
||||
* @tparam PAD_WIDTH Additional pixels recognised by the controller after the offset and width
|
||||
* @tparam PAD_HEIGHT Additional lines recognised by the controller after the offset and width
|
||||
* @tparam MADCTL The base MADCTL value for the display, with no rotation bits set.
|
||||
* @tparam HAS_HARDWARE_ROTATION Whether the display supports hardware rotation.
|
||||
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
|
||||
* @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even)
|
||||
*/
|
||||
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL,
|
||||
bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING>
|
||||
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
||||
OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> {
|
||||
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, int PAD_WIDTH, int PAD_HEIGHT,
|
||||
uint16_t MADCTL, bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING>
|
||||
class MipiSpiBuffer
|
||||
: public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||
OFFSET_HEIGHT, PAD_WIDTH, PAD_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> {
|
||||
public:
|
||||
// these values define the buffer size needed to write in accordance with the chip pixel alignment
|
||||
// requirements. If the required rounding does not divide the width and height, we round up to the next multiple and
|
||||
@@ -515,7 +533,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
|
||||
void dump_config() override {
|
||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
MADCTL, HAS_HARDWARE_ROTATION>::dump_config();
|
||||
PAD_WIDTH, PAD_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION>::dump_config();
|
||||
esph_log_config(TAG,
|
||||
" Rotation: %d°\n"
|
||||
" Buffer pixels: %d bits\n"
|
||||
@@ -528,7 +546,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
|
||||
void setup() override {
|
||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
|
||||
MADCTL, HAS_HARDWARE_ROTATION>::setup();
|
||||
PAD_WIDTH, PAD_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION>::setup();
|
||||
RAMAllocator<BUFFERTYPE> allocator{};
|
||||
this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION);
|
||||
if (this->buffer_ == nullptr) {
|
||||
|
||||
@@ -24,13 +24,11 @@ from esphome.components.mipi import (
|
||||
PWSET,
|
||||
PWSETN,
|
||||
SETEXTC,
|
||||
SWRESET,
|
||||
VMCTR,
|
||||
VMCTR1,
|
||||
VMCTR2,
|
||||
VSCRSADD,
|
||||
DriverChip,
|
||||
delay,
|
||||
)
|
||||
from esphome.components.spi import TYPE_OCTAL
|
||||
|
||||
@@ -179,6 +177,9 @@ ILI9342 = DriverChip(
|
||||
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
|
||||
ILI9341.extend(
|
||||
"M5CORE2",
|
||||
# Reset native dimensions due to axis swap.
|
||||
native_width=320,
|
||||
native_height=240,
|
||||
width=320,
|
||||
height=240,
|
||||
mirror_x=False,
|
||||
@@ -364,7 +365,6 @@ ST7796 = DriverChip(
|
||||
width=320,
|
||||
height=480,
|
||||
initsequence=(
|
||||
(SWRESET,),
|
||||
(CSCON, 0xC3),
|
||||
(CSCON, 0x96),
|
||||
(VMCTR1, 0x1C),
|
||||
@@ -725,8 +725,6 @@ DriverChip(
|
||||
width=128,
|
||||
height=160,
|
||||
initsequence=(
|
||||
SWRESET,
|
||||
delay(10),
|
||||
(FRMCTR1, 0x01, 0x2C, 0x2D),
|
||||
(FRMCTR2, 0x01, 0x2C, 0x2D),
|
||||
(FRMCTR3, 0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D),
|
||||
@@ -783,6 +781,31 @@ ST7796.extend(
|
||||
bus_mode=TYPE_OCTAL,
|
||||
mirror_x=True,
|
||||
reset_pin=4,
|
||||
dc_pin=0,
|
||||
dc_pin={"number": 0, "ignore_strapping_warning": True},
|
||||
invert_colors=True,
|
||||
)
|
||||
|
||||
ST7789V.extend(
|
||||
"GEEKMAGIC-SMALLTV",
|
||||
data_rate="40MHz",
|
||||
height=240,
|
||||
width=240,
|
||||
offset_width=0,
|
||||
offset_height=0,
|
||||
invert_colors=True,
|
||||
buffer_size=0.125,
|
||||
reset_pin=2,
|
||||
dc_pin=0,
|
||||
)
|
||||
ST7789V.extend(
|
||||
"GEEKMAGIC-SMALLTV-PRO",
|
||||
data_rate="40MHz",
|
||||
height=240,
|
||||
width=240,
|
||||
offset_width=0,
|
||||
offset_height=0,
|
||||
invert_colors=True,
|
||||
buffer_size=0.125,
|
||||
reset_pin=4,
|
||||
dc_pin=2,
|
||||
)
|
||||
|
||||
@@ -177,6 +177,20 @@ CO5300.extend(
|
||||
reset_pin=39,
|
||||
)
|
||||
|
||||
# Waveshare ESP32-S3 Touch AMOLED 2.16" (CO5300 controller)
|
||||
# Pin assignments are the same as the 1.75" devkit: CS=12, RESET=39. Width/height set to 480x480.
|
||||
CO5300.extend(
|
||||
"WAVESHARE-ESP32-S3-TOUCH-AMOLED-2.16",
|
||||
width=480,
|
||||
height=480,
|
||||
pixel_mode="16bit",
|
||||
offset_height=0,
|
||||
offset_width=0,
|
||||
cs_pin=12,
|
||||
reset_pin=39,
|
||||
data_rate="40MHz",
|
||||
)
|
||||
|
||||
AXS15231.extend(
|
||||
"WAVESHARE-ESP32-S3-TOUCH-LCD-3.49",
|
||||
width=172,
|
||||
@@ -255,3 +269,16 @@ ST7789V.extend(
|
||||
cs_pin=14,
|
||||
dc_pin={"number": 15, "ignore_strapping_warning": True},
|
||||
)
|
||||
|
||||
ST7789V.extend(
|
||||
"WAVESHARE-ESP32-S3-GEEK",
|
||||
cs_pin=10,
|
||||
dc_pin=8,
|
||||
reset_pin=9,
|
||||
width=135,
|
||||
height=240,
|
||||
offset_width=52,
|
||||
offset_height=40,
|
||||
invert_colors=True,
|
||||
data_rate="40MHz",
|
||||
)
|
||||
|
||||
@@ -119,7 +119,7 @@ struct IPAddress {
|
||||
IPAddress(const std::string &in_address) { ipaddr_aton(in_address.c_str(), &ip_addr_); }
|
||||
IPAddress(ip4_addr_t *other_ip) {
|
||||
memcpy((void *) &ip_addr_, (void *) other_ip, sizeof(ip4_addr_t));
|
||||
#if USE_ESP32 && LWIP_IPV6
|
||||
#if LWIP_IPV6
|
||||
ip_addr_.type = IPADDR_TYPE_V4;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ from .const import (
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
)
|
||||
from .framework import check_and_install
|
||||
|
||||
# force import gpio to register pin schema
|
||||
from .gpio import nrf52_pin_to_code # noqa: F401
|
||||
@@ -562,3 +563,15 @@ def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) ->
|
||||
_LOGGER.error("LR: %s", _addr2line(addr2line, elf, lr))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def run_compile(args, config: ConfigType) -> bool:
|
||||
if CORE.using_toolchain_platformio:
|
||||
return False
|
||||
if not CORE.using_toolchain_sdk_nrf:
|
||||
raise EsphomeError(
|
||||
"Unsupported toolchain for nRF52. "
|
||||
"Supported toolchains are 'platformio' and 'sdk-nrf'."
|
||||
)
|
||||
check_and_install()
|
||||
raise EsphomeError("Native build for nRF52 is not implemented yet")
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import tempfile
|
||||
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.framework_helpers import (
|
||||
archive_extract_all,
|
||||
create_venv,
|
||||
download_from_mirrors,
|
||||
get_python_env_executable_path,
|
||||
rmdir,
|
||||
run_command_ok,
|
||||
str_to_lst_of_str,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_WEST_VERSION = "1.5.0"
|
||||
_TOOLCHAIN_VERSION = "0.17.4"
|
||||
|
||||
SDK_NG_TOOLCHAIN_MIRRORS = str_to_lst_of_str(
|
||||
os.environ.get(
|
||||
"ESPHOME_SDK_NG_TOOLCHAIN_MIRRORS",
|
||||
"https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v{VERSION}/toolchain_{sysname}-{machine}_arm-zephyr-eabi.{extension}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _get_tools_path() -> Path:
|
||||
return CORE.data_dir / "sdk-nrf"
|
||||
|
||||
|
||||
def _get_python_env_path(version: str) -> Path:
|
||||
return _get_tools_path() / "penvs" / version
|
||||
|
||||
|
||||
def _get_framework_path(version: str) -> Path:
|
||||
return _get_tools_path() / "frameworks" / f"{version}"
|
||||
|
||||
|
||||
def _get_toolchain_path(version: str) -> Path:
|
||||
return _get_tools_path() / "toolchains" / f"{version}"
|
||||
|
||||
|
||||
# onexc/dir_fd were added to shutil.rmtree in 3.12; the 3.11 branch uses onerror.
|
||||
_SITECUSTOMIZE = """\
|
||||
import os, stat, shutil, sys
|
||||
_orig = shutil.rmtree
|
||||
def _handler(func, path, exc):
|
||||
os.chmod(path, stat.S_IWRITE); func(path)
|
||||
if sys.version_info >= (3, 12):
|
||||
def _rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
|
||||
if onerror is None and onexc is None:
|
||||
onexc = _handler
|
||||
return _orig(path, ignore_errors=ignore_errors, onerror=onerror, onexc=onexc, dir_fd=dir_fd)
|
||||
else:
|
||||
def _rmtree(path, ignore_errors=False, onerror=None):
|
||||
if onerror is None:
|
||||
onerror = _handler
|
||||
return _orig(path, ignore_errors=ignore_errors, onerror=onerror)
|
||||
shutil.rmtree = _rmtree
|
||||
"""
|
||||
|
||||
|
||||
def _install_sitecustomize(python_env_path: Path) -> None:
|
||||
"""Patch shutil.rmtree inside the penv to handle read-only files.
|
||||
|
||||
west init's shutil.move falls back to copytree+rmtree on Windows, and
|
||||
rmtree dies on the read-only .idx/.pack files git just wrote into
|
||||
manifest-tmp. Dropping a sitecustomize.py into the venv applies the
|
||||
same fix esphome.helpers.rmtree uses, but inside the subprocess.
|
||||
"""
|
||||
if os.name != "nt":
|
||||
return
|
||||
site_packages = python_env_path / "Lib" / "site-packages"
|
||||
site_packages.mkdir(parents=True, exist_ok=True)
|
||||
(site_packages / "sitecustomize.py").write_text(_SITECUSTOMIZE, encoding="utf-8")
|
||||
|
||||
|
||||
def _get_toolchain_platform_info() -> tuple[str, str, str]:
|
||||
"""Return (sysname, machine, extension) for the current host."""
|
||||
extension = "tar.xz"
|
||||
sysname = platform.system().lower()
|
||||
machine = platform.machine()
|
||||
if machine == "arm64":
|
||||
machine = "aarch64"
|
||||
if sysname == "darwin":
|
||||
sysname = "macos"
|
||||
elif sysname == "windows":
|
||||
machine = "x86_64"
|
||||
extension = "7z"
|
||||
return sysname, machine, extension
|
||||
|
||||
|
||||
def check_and_install() -> None:
|
||||
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
version = f"v{framework_ver.major}.{framework_ver.minor}.{framework_ver.patch}"
|
||||
python_env_path = _get_python_env_path(version)
|
||||
env_python_path = get_python_env_executable_path(python_env_path, "python")
|
||||
sentinel = python_env_path / ".ready"
|
||||
install_venv = not sentinel.exists()
|
||||
if install_venv:
|
||||
rmdir(python_env_path, msg=f"Clean up {version} Python environment")
|
||||
|
||||
create_venv(python_env_path, msg=f"{version}")
|
||||
|
||||
_install_sitecustomize(python_env_path)
|
||||
|
||||
_LOGGER.info("Installing west %s ...", _WEST_VERSION)
|
||||
cmd = [str(env_python_path), "-m", "pip", "install", f"west=={_WEST_VERSION}"]
|
||||
if not run_command_ok(cmd):
|
||||
raise EsphomeError(f"Install west for {version} Python environment failure")
|
||||
sentinel.touch()
|
||||
|
||||
framework_path = _get_framework_path(version)
|
||||
sentinel = framework_path / ".ready"
|
||||
if install_venv or not sentinel.exists():
|
||||
rmdir(framework_path, msg=f"Clean up {version} framework environment")
|
||||
_LOGGER.info("Initializing nRF Connect SDK %s ...", version)
|
||||
cmd = [
|
||||
str(env_python_path),
|
||||
"-m",
|
||||
"west",
|
||||
"init",
|
||||
"-m",
|
||||
"https://github.com/nrfconnect/sdk-nrf",
|
||||
"--mr",
|
||||
f"{version}",
|
||||
str(framework_path),
|
||||
]
|
||||
if not run_command_ok(cmd):
|
||||
raise EsphomeError(f"Can't initialize nRF Connect SDK {version}")
|
||||
_LOGGER.info("Updating nRF Connect SDK %s (this may take a while) ...", version)
|
||||
cmd = [
|
||||
str(env_python_path),
|
||||
"-m",
|
||||
"west",
|
||||
"update",
|
||||
"--narrow",
|
||||
"--fetch-opt=--depth=1",
|
||||
]
|
||||
if not run_command_ok(cmd, cwd=framework_path):
|
||||
raise EsphomeError(f"Can't update nRF Connect SDK {version}")
|
||||
sentinel.touch()
|
||||
|
||||
toolchains_dir = _get_toolchain_path(_TOOLCHAIN_VERSION)
|
||||
sentinel = toolchains_dir / ".ready"
|
||||
if not sentinel.exists():
|
||||
rmdir(
|
||||
toolchains_dir, msg=f"Clean up {_TOOLCHAIN_VERSION} toolchain environment"
|
||||
)
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
_LOGGER.info("Downloading %s toolchain ...", _TOOLCHAIN_VERSION)
|
||||
|
||||
sysname, machine, extension = _get_toolchain_platform_info()
|
||||
|
||||
download_from_mirrors(
|
||||
SDK_NG_TOOLCHAIN_MIRRORS,
|
||||
{
|
||||
"VERSION": _TOOLCHAIN_VERSION,
|
||||
"sysname": sysname,
|
||||
"machine": machine,
|
||||
"extension": extension,
|
||||
},
|
||||
tmp.file,
|
||||
)
|
||||
archive_extract_all(tmp.file, toolchains_dir, progress_header="Extracting")
|
||||
sentinel.touch()
|
||||
@@ -7,9 +7,13 @@ static const char *const TAG = "opentherm.output";
|
||||
|
||||
void opentherm::OpenthermOutput::write_state(float state) {
|
||||
ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_);
|
||||
this->state = state < 0.003 && this->zero_means_zero_
|
||||
? 0.0
|
||||
: clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
|
||||
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
|
||||
bool zero_means_zero = this->zero_means_zero_;
|
||||
#else
|
||||
bool zero_means_zero = false;
|
||||
#endif
|
||||
this->state =
|
||||
state < 0.003 && zero_means_zero ? 0.0 : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_);
|
||||
this->has_state_ = true;
|
||||
ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state);
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ bool OpenThreadComponent::teardown() {
|
||||
ESP_LOGW(TAG, "Failed to acquire OpenThread lock during teardown, leaking memory");
|
||||
return true;
|
||||
}
|
||||
otInstance *instance = lock->get_instance();
|
||||
otInstance *instance = lock.get_instance();
|
||||
otSrpClientClearHostAndServices(instance);
|
||||
otSrpClientBuffersFreeAllServices(instance);
|
||||
global_openthread_component = nullptr;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user