mirror of
https://github.com/esphome/esphome.git
synced 2025-10-20 18:53:47 +01:00
Merge remote-tracking branch 'upstream/dev' into ci_impact_analysis
This commit is contained in:
@@ -1 +1 @@
|
|||||||
049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f
|
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248
|
||||||
|
140
.github/workflows/ci.yml
vendored
140
.github/workflows/ci.yml
vendored
@@ -114,8 +114,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.11"
|
- "3.11"
|
||||||
- "3.12"
|
- "3.14"
|
||||||
- "3.13"
|
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
- macOS-latest
|
- macOS-latest
|
||||||
@@ -124,13 +123,9 @@ jobs:
|
|||||||
# Minimize CI resource usage
|
# Minimize CI resource usage
|
||||||
# by only running the Python version
|
# by only running the Python version
|
||||||
# version used for docker images on Windows and macOS
|
# version used for docker images on Windows and macOS
|
||||||
- python-version: "3.13"
|
- python-version: "3.14"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python-version: "3.12"
|
- python-version: "3.14"
|
||||||
os: windows-latest
|
|
||||||
- python-version: "3.13"
|
|
||||||
os: macOS-latest
|
|
||||||
- python-version: "3.12"
|
|
||||||
os: macOS-latest
|
os: macOS-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
needs:
|
needs:
|
||||||
@@ -178,6 +173,7 @@ jobs:
|
|||||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||||
|
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||||
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||||
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
||||||
steps:
|
steps:
|
||||||
@@ -207,6 +203,7 @@ jobs:
|
|||||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||||
|
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||||
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
||||||
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
@@ -360,48 +357,13 @@ jobs:
|
|||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
test-build-components:
|
|
||||||
name: Component test ${{ matrix.file }}
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- common
|
|
||||||
- determine-jobs
|
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
max-parallel: 2
|
|
||||||
matrix:
|
|
||||||
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }}
|
|
||||||
steps:
|
|
||||||
- name: Cache apt packages
|
|
||||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
|
|
||||||
with:
|
|
||||||
packages: libsdl2-dev
|
|
||||||
version: 1.0
|
|
||||||
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
|
||||||
- name: Restore Python
|
|
||||||
uses: ./.github/actions/restore-python
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
|
||||||
- name: Validate config for ${{ matrix.file }}
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
python3 script/test_build_components.py -e config -c ${{ matrix.file }}
|
|
||||||
- name: Compile config for ${{ matrix.file }}
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
python3 script/test_build_components.py -e compile -c ${{ matrix.file }}
|
|
||||||
|
|
||||||
test-build-components-splitter:
|
test-build-components-splitter:
|
||||||
name: Split components for intelligent grouping (40 weighted per batch)
|
name: Split components for intelligent grouping (40 weighted per batch)
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||||
outputs:
|
outputs:
|
||||||
matrix: ${{ steps.split.outputs.components }}
|
matrix: ${{ steps.split.outputs.components }}
|
||||||
steps:
|
steps:
|
||||||
@@ -420,8 +382,18 @@ jobs:
|
|||||||
# Use intelligent splitter that groups components with same bus configs
|
# Use intelligent splitter that groups components with same bus configs
|
||||||
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
|
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
|
||||||
|
|
||||||
|
# Only isolate directly changed components when targeting dev branch
|
||||||
|
# For beta/release branches, group everything for faster CI
|
||||||
|
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
|
||||||
|
directly_changed='[]'
|
||||||
|
echo "Target branch: ${{ github.base_ref }} - grouping all components"
|
||||||
|
else
|
||||||
|
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
|
||||||
|
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Splitting components intelligently..."
|
echo "Splitting components intelligently..."
|
||||||
output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github)
|
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
|
||||||
|
|
||||||
echo "$output" >> $GITHUB_OUTPUT
|
echo "$output" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
@@ -432,10 +404,10 @@ jobs:
|
|||||||
- common
|
- common
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
- test-build-components-splitter
|
- test-build-components-splitter
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }}
|
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
|
||||||
matrix:
|
matrix:
|
||||||
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
|
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
|
||||||
steps:
|
steps:
|
||||||
@@ -463,41 +435,80 @@ jobs:
|
|||||||
- name: Validate and compile components with intelligent grouping
|
- name: Validate and compile components with intelligent grouping
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
# Use /mnt for build files (70GB available vs ~29GB on /)
|
|
||||||
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
|
|
||||||
sudo mkdir -p /mnt/platformio
|
|
||||||
sudo chown $USER:$USER /mnt/platformio
|
|
||||||
mkdir -p ~/.platformio
|
|
||||||
sudo mount --bind /mnt/platformio ~/.platformio
|
|
||||||
|
|
||||||
# Bind mount test build directory to /mnt
|
# Check if /mnt has more free space than / before bind mounting
|
||||||
sudo mkdir -p /mnt/test_build_components_build
|
# Extract available space in KB for comparison
|
||||||
sudo chown $USER:$USER /mnt/test_build_components_build
|
root_avail=$(df -k / | awk 'NR==2 {print $4}')
|
||||||
mkdir -p tests/test_build_components/build
|
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
|
||||||
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
|
|
||||||
|
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
|
||||||
|
|
||||||
|
# Only use /mnt if it has more space than /
|
||||||
|
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
|
||||||
|
echo "Using /mnt for build files (more space available)"
|
||||||
|
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
|
||||||
|
sudo mkdir -p /mnt/platformio
|
||||||
|
sudo chown $USER:$USER /mnt/platformio
|
||||||
|
mkdir -p ~/.platformio
|
||||||
|
sudo mount --bind /mnt/platformio ~/.platformio
|
||||||
|
|
||||||
|
# Bind mount test build directory to /mnt
|
||||||
|
sudo mkdir -p /mnt/test_build_components_build
|
||||||
|
sudo chown $USER:$USER /mnt/test_build_components_build
|
||||||
|
mkdir -p tests/test_build_components/build
|
||||||
|
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
|
||||||
|
else
|
||||||
|
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Convert space-separated components to comma-separated for Python script
|
# Convert space-separated components to comma-separated for Python script
|
||||||
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
|
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
|
||||||
|
|
||||||
echo "Testing components: $components_csv"
|
# Only isolate directly changed components when targeting dev branch
|
||||||
|
# For beta/release branches, group everything for faster CI
|
||||||
|
#
|
||||||
|
# WHY ISOLATE DIRECTLY CHANGED COMPONENTS?
|
||||||
|
# - Isolated tests run WITHOUT --testing-mode, enabling full validation
|
||||||
|
# - This catches pin conflicts and other issues in directly changed code
|
||||||
|
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
|
||||||
|
# - Dependencies are safe to group since they weren't modified in this PR
|
||||||
|
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
|
||||||
|
directly_changed_csv=""
|
||||||
|
echo "Testing components: $components_csv"
|
||||||
|
echo "Target branch: ${{ github.base_ref }} - grouping all components"
|
||||||
|
else
|
||||||
|
directly_changed_csv=$(echo '${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | jq -r 'join(",")')
|
||||||
|
echo "Testing components: $components_csv"
|
||||||
|
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components: $directly_changed_csv"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Run config validation with grouping
|
# Show disk space before validation (after bind mounts setup)
|
||||||
python3 script/test_build_components.py -e config -c "$components_csv" -f
|
echo "Disk space before config validation:"
|
||||||
|
df -h
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run config validation with grouping and isolation
|
||||||
|
python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Config validation passed! Starting compilation..."
|
echo "Config validation passed! Starting compilation..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Run compilation with grouping
|
# Show disk space before compilation
|
||||||
python3 script/test_build_components.py -e compile -c "$components_csv" -f
|
echo "Disk space before compilation:"
|
||||||
|
df -h
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run compilation with grouping and isolation
|
||||||
|
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
|
||||||
|
|
||||||
pre-commit-ci-lite:
|
pre-commit-ci-lite:
|
||||||
name: pre-commit.ci lite
|
name: pre-commit.ci lite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
|
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -635,7 +646,6 @@ jobs:
|
|||||||
- integration-tests
|
- integration-tests
|
||||||
- clang-tidy
|
- clang-tidy
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
- test-build-components
|
|
||||||
- test-build-components-splitter
|
- test-build-components-splitter
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
- pre-commit-ci-lite
|
- pre-commit-ci-lite
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
|
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -86,6 +86,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
|
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
@@ -11,7 +11,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.14.0
|
rev: v0.14.1
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
@@ -62,6 +62,7 @@ esphome/components/bedjet/fan/* @jhansche
|
|||||||
esphome/components/bedjet/sensor/* @javawizard @jhansche
|
esphome/components/bedjet/sensor/* @javawizard @jhansche
|
||||||
esphome/components/beken_spi_led_strip/* @Mat931
|
esphome/components/beken_spi_led_strip/* @Mat931
|
||||||
esphome/components/bh1750/* @OttoWinter
|
esphome/components/bh1750/* @OttoWinter
|
||||||
|
esphome/components/bh1900nux/* @B48D81EFCC
|
||||||
esphome/components/binary_sensor/* @esphome/core
|
esphome/components/binary_sensor/* @esphome/core
|
||||||
esphome/components/bk72xx/* @kuba2k2
|
esphome/components/bk72xx/* @kuba2k2
|
||||||
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
|
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
|
||||||
|
@@ -117,6 +117,17 @@ class Purpose(StrEnum):
|
|||||||
LOGGING = "logging"
|
LOGGING = "logging"
|
||||||
|
|
||||||
|
|
||||||
|
class PortType(StrEnum):
|
||||||
|
SERIAL = "SERIAL"
|
||||||
|
NETWORK = "NETWORK"
|
||||||
|
MQTT = "MQTT"
|
||||||
|
MQTTIP = "MQTTIP"
|
||||||
|
|
||||||
|
|
||||||
|
# Magic MQTT port types that require special handling
|
||||||
|
_MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP})
|
||||||
|
|
||||||
|
|
||||||
def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
|
def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
|
||||||
"""Resolve an address using cache if available, otherwise return the address itself."""
|
"""Resolve an address using cache if available, otherwise return the address itself."""
|
||||||
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)):
|
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)):
|
||||||
@@ -280,16 +291,67 @@ def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str
|
|||||||
return mqtt.get_esphome_device_ip(config, username, password, client_id)
|
return mqtt.get_esphome_device_ip(config, username, password, client_id)
|
||||||
|
|
||||||
|
|
||||||
_PORT_TO_PORT_TYPE = {
|
def _resolve_network_devices(
|
||||||
"MQTT": "MQTT",
|
devices: list[str], config: ConfigType, args: ArgsProtocol
|
||||||
"MQTTIP": "MQTTIP",
|
) -> list[str]:
|
||||||
}
|
"""Resolve device list, converting MQTT magic strings to actual IP addresses.
|
||||||
|
|
||||||
|
This function filters the devices list to:
|
||||||
|
- Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup
|
||||||
|
- Deduplicate addresses while preserving order
|
||||||
|
- Only resolve MQTT once even if multiple MQTT strings are present
|
||||||
|
- If MQTT resolution fails, log a warning and continue with other devices
|
||||||
|
|
||||||
|
Args:
|
||||||
|
devices: List of device identifiers (IPs, hostnames, or magic strings)
|
||||||
|
config: ESPHome configuration
|
||||||
|
args: Command-line arguments containing MQTT credentials
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of network addresses suitable for connection attempts
|
||||||
|
"""
|
||||||
|
network_devices: list[str] = []
|
||||||
|
mqtt_resolved: bool = False
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
port_type = get_port_type(device)
|
||||||
|
if port_type in _MQTT_PORT_TYPES:
|
||||||
|
# Only resolve MQTT once, even if multiple MQTT entries
|
||||||
|
if not mqtt_resolved:
|
||||||
|
try:
|
||||||
|
mqtt_ips = mqtt_get_ip(
|
||||||
|
config, args.username, args.password, args.client_id
|
||||||
|
)
|
||||||
|
network_devices.extend(mqtt_ips)
|
||||||
|
except EsphomeError as err:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"MQTT IP discovery failed (%s), will try other devices if available",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
mqtt_resolved = True
|
||||||
|
elif device not in network_devices:
|
||||||
|
# Regular network address or IP - add if not already present
|
||||||
|
network_devices.append(device)
|
||||||
|
|
||||||
|
return network_devices
|
||||||
|
|
||||||
|
|
||||||
def get_port_type(port: str) -> str:
|
def get_port_type(port: str) -> PortType:
|
||||||
|
"""Determine the type of port/device identifier.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
|
||||||
|
PortType.MQTT for MQTT logging
|
||||||
|
PortType.MQTTIP for MQTT IP lookup
|
||||||
|
PortType.NETWORK for IP addresses, hostnames, or mDNS names
|
||||||
|
"""
|
||||||
if port.startswith("/") or port.startswith("COM"):
|
if port.startswith("/") or port.startswith("COM"):
|
||||||
return "SERIAL"
|
return PortType.SERIAL
|
||||||
return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
|
if port == "MQTT":
|
||||||
|
return PortType.MQTT
|
||||||
|
if port == "MQTTIP":
|
||||||
|
return PortType.MQTTIP
|
||||||
|
return PortType.NETWORK
|
||||||
|
|
||||||
|
|
||||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||||
@@ -489,7 +551,7 @@ def upload_using_platformio(config: ConfigType, port: str):
|
|||||||
|
|
||||||
|
|
||||||
def check_permissions(port: str):
|
def check_permissions(port: str):
|
||||||
if os.name == "posix" and get_port_type(port) == "SERIAL":
|
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
|
||||||
# Check if we can open selected serial port
|
# Check if we can open selected serial port
|
||||||
if not os.access(port, os.F_OK):
|
if not os.access(port, os.F_OK):
|
||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
@@ -517,7 +579,7 @@ def upload_program(
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if get_port_type(host) == "SERIAL":
|
if get_port_type(host) == PortType.SERIAL:
|
||||||
check_permissions(host)
|
check_permissions(host)
|
||||||
|
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
@@ -544,17 +606,16 @@ def upload_program(
|
|||||||
from esphome import espota2
|
from esphome import espota2
|
||||||
|
|
||||||
remote_port = int(ota_conf[CONF_PORT])
|
remote_port = int(ota_conf[CONF_PORT])
|
||||||
password = ota_conf.get(CONF_PASSWORD, "")
|
password = ota_conf.get(CONF_PASSWORD)
|
||||||
if getattr(args, "file", None) is not None:
|
if getattr(args, "file", None) is not None:
|
||||||
binary = Path(args.file)
|
binary = Path(args.file)
|
||||||
else:
|
else:
|
||||||
binary = CORE.firmware_bin
|
binary = CORE.firmware_bin
|
||||||
|
|
||||||
# MQTT address resolution
|
# Resolve MQTT magic strings to actual IP addresses
|
||||||
if get_port_type(host) in ("MQTT", "MQTTIP"):
|
network_devices = _resolve_network_devices(devices, config, args)
|
||||||
devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
|
|
||||||
|
|
||||||
return espota2.run_ota(devices, remote_port, password, binary)
|
return espota2.run_ota(network_devices, remote_port, password, binary)
|
||||||
|
|
||||||
|
|
||||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||||
@@ -569,33 +630,22 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
|
|||||||
raise EsphomeError("Logger is not configured!")
|
raise EsphomeError("Logger is not configured!")
|
||||||
|
|
||||||
port = devices[0]
|
port = devices[0]
|
||||||
|
port_type = get_port_type(port)
|
||||||
|
|
||||||
if get_port_type(port) == "SERIAL":
|
if port_type == PortType.SERIAL:
|
||||||
check_permissions(port)
|
check_permissions(port)
|
||||||
return run_miniterm(config, port, args)
|
return run_miniterm(config, port, args)
|
||||||
|
|
||||||
port_type = get_port_type(port)
|
|
||||||
|
|
||||||
# Check if we should use API for logging
|
# Check if we should use API for logging
|
||||||
if has_api():
|
# Resolve MQTT magic strings to actual IP addresses
|
||||||
addresses_to_use: list[str] | None = None
|
if has_api() and (
|
||||||
|
network_devices := _resolve_network_devices(devices, config, args)
|
||||||
|
):
|
||||||
|
from esphome.components.api.client import run_logs
|
||||||
|
|
||||||
if port_type == "NETWORK":
|
return run_logs(config, network_devices)
|
||||||
# Network addresses (IPs, mDNS names, or regular DNS hostnames) can be used
|
|
||||||
# The resolve_ip_address() function in helpers.py handles all types
|
|
||||||
addresses_to_use = devices
|
|
||||||
elif port_type in ("MQTT", "MQTTIP") and has_mqtt_ip_lookup():
|
|
||||||
# Use MQTT IP lookup for MQTT/MQTTIP types
|
|
||||||
addresses_to_use = mqtt_get_ip(
|
|
||||||
config, args.username, args.password, args.client_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if addresses_to_use is not None:
|
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
|
||||||
from esphome.components.api.client import run_logs
|
|
||||||
|
|
||||||
return run_logs(config, addresses_to_use)
|
|
||||||
|
|
||||||
if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
|
|
||||||
from esphome import mqtt
|
from esphome import mqtt
|
||||||
|
|
||||||
return mqtt.show_logs(
|
return mqtt.show_logs(
|
||||||
|
@@ -380,12 +380,19 @@ async def homeassistant_service_to_code(
|
|||||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||||
cg.add(var.set_service(templ))
|
cg.add(var.set_service(templ))
|
||||||
|
|
||||||
|
# Initialize FixedVectors with exact sizes from config
|
||||||
|
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||||
for key, value in config[CONF_DATA].items():
|
for key, value in config[CONF_DATA].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data(key, templ))
|
cg.add(var.add_data(key, templ))
|
||||||
|
|
||||||
|
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data_template(key, templ))
|
cg.add(var.add_data_template(key, templ))
|
||||||
|
|
||||||
|
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||||
for key, value in config[CONF_VARIABLES].items():
|
for key, value in config[CONF_VARIABLES].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_variable(key, templ))
|
cg.add(var.add_variable(key, templ))
|
||||||
@@ -458,15 +465,23 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
|||||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||||
cg.add(var.set_service(templ))
|
cg.add(var.set_service(templ))
|
||||||
|
|
||||||
|
# Initialize FixedVectors with exact sizes from config
|
||||||
|
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||||
for key, value in config[CONF_DATA].items():
|
for key, value in config[CONF_DATA].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data(key, templ))
|
cg.add(var.add_data(key, templ))
|
||||||
|
|
||||||
|
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_data_template(key, templ))
|
cg.add(var.add_data_template(key, templ))
|
||||||
|
|
||||||
|
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||||
for key, value in config[CONF_VARIABLES].items():
|
for key, value in config[CONF_VARIABLES].items():
|
||||||
templ = await cg.templatable(value, args, None)
|
templ = await cg.templatable(value, args, None)
|
||||||
cg.add(var.add_variable(key, templ))
|
cg.add(var.add_variable(key, templ))
|
||||||
|
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|
||||||
@@ -489,6 +504,8 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
|||||||
serv = await cg.get_variable(config[CONF_ID])
|
serv = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||||
cg.add(var.set_service("esphome.tag_scanned"))
|
cg.add(var.set_service("esphome.tag_scanned"))
|
||||||
|
# Initialize FixedVector with exact size (1 data field)
|
||||||
|
cg.add(var.init_data(1))
|
||||||
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
||||||
cg.add(var.add_data("tag_id", templ))
|
cg.add(var.add_data("tag_id", templ))
|
||||||
return var
|
return var
|
||||||
|
@@ -776,9 +776,9 @@ message HomeassistantActionRequest {
|
|||||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||||
|
|
||||||
string service = 1;
|
string service = 1;
|
||||||
repeated HomeassistantServiceMap data = 2;
|
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
|
||||||
repeated HomeassistantServiceMap data_template = 3;
|
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
|
||||||
repeated HomeassistantServiceMap variables = 4;
|
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
|
||||||
bool is_event = 5;
|
bool is_event = 5;
|
||||||
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
||||||
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||||
@@ -866,7 +866,7 @@ message ListEntitiesServicesResponse {
|
|||||||
|
|
||||||
string name = 1;
|
string name = 1;
|
||||||
fixed32 key = 2;
|
fixed32 key = 2;
|
||||||
repeated ListEntitiesServicesArgument args = 3;
|
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||||
}
|
}
|
||||||
message ExecuteServiceArgument {
|
message ExecuteServiceArgument {
|
||||||
option (ifdef) = "USE_API_SERVICES";
|
option (ifdef) = "USE_API_SERVICES";
|
||||||
@@ -876,10 +876,10 @@ message ExecuteServiceArgument {
|
|||||||
string string_ = 4;
|
string string_ = 4;
|
||||||
// ESPHome 1.14 (api v1.3) make int a signed value
|
// ESPHome 1.14 (api v1.3) make int a signed value
|
||||||
sint32 int_ = 5;
|
sint32 int_ = 5;
|
||||||
repeated bool bool_array = 6 [packed=false];
|
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];
|
||||||
repeated sint32 int_array = 7 [packed=false];
|
repeated sint32 int_array = 7 [packed=false, (fixed_vector) = true];
|
||||||
repeated float float_array = 8 [packed=false];
|
repeated float float_array = 8 [packed=false, (fixed_vector) = true];
|
||||||
repeated string string_array = 9;
|
repeated string string_array = 9 [(fixed_vector) = true];
|
||||||
}
|
}
|
||||||
message ExecuteServiceRequest {
|
message ExecuteServiceRequest {
|
||||||
option (id) = 42;
|
option (id) = 42;
|
||||||
@@ -888,7 +888,7 @@ message ExecuteServiceRequest {
|
|||||||
option (ifdef) = "USE_API_SERVICES";
|
option (ifdef) = "USE_API_SERVICES";
|
||||||
|
|
||||||
fixed32 key = 1;
|
fixed32 key = 1;
|
||||||
repeated ExecuteServiceArgument args = 2;
|
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== CAMERA ====================
|
// ==================== CAMERA ====================
|
||||||
@@ -987,8 +987,8 @@ message ListEntitiesClimateResponse {
|
|||||||
string name = 3;
|
string name = 3;
|
||||||
reserved 4; // Deprecated: was string unique_id
|
reserved 4; // Deprecated: was string unique_id
|
||||||
|
|
||||||
bool supports_current_temperature = 5;
|
bool supports_current_temperature = 5; // Deprecated: use feature_flags
|
||||||
bool supports_two_point_target_temperature = 6;
|
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
|
||||||
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
|
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
|
||||||
float visual_min_temperature = 8;
|
float visual_min_temperature = 8;
|
||||||
float visual_max_temperature = 9;
|
float visual_max_temperature = 9;
|
||||||
@@ -997,7 +997,7 @@ message ListEntitiesClimateResponse {
|
|||||||
// is if CLIMATE_PRESET_AWAY exists is supported_presets
|
// is if CLIMATE_PRESET_AWAY exists is supported_presets
|
||||||
// Deprecated in API version 1.5
|
// Deprecated in API version 1.5
|
||||||
bool legacy_supports_away = 11 [deprecated=true];
|
bool legacy_supports_away = 11 [deprecated=true];
|
||||||
bool supports_action = 12;
|
bool supports_action = 12; // Deprecated: use feature_flags
|
||||||
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
|
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
|
||||||
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
|
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
|
||||||
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
|
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
|
||||||
@@ -1007,11 +1007,12 @@ message ListEntitiesClimateResponse {
|
|||||||
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
|
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||||
EntityCategory entity_category = 20;
|
EntityCategory entity_category = 20;
|
||||||
float visual_current_temperature_step = 21;
|
float visual_current_temperature_step = 21;
|
||||||
bool supports_current_humidity = 22;
|
bool supports_current_humidity = 22; // Deprecated: use feature_flags
|
||||||
bool supports_target_humidity = 23;
|
bool supports_target_humidity = 23; // Deprecated: use feature_flags
|
||||||
float visual_min_humidity = 24;
|
float visual_min_humidity = 24;
|
||||||
float visual_max_humidity = 25;
|
float visual_max_humidity = 25;
|
||||||
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
|
||||||
|
uint32 feature_flags = 27;
|
||||||
}
|
}
|
||||||
message ClimateStateResponse {
|
message ClimateStateResponse {
|
||||||
option (id) = 47;
|
option (id) = 47;
|
||||||
|
@@ -27,6 +27,9 @@
|
|||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_CLIMATE
|
||||||
|
#include "esphome/components/climate/climate_mode.h"
|
||||||
|
#endif
|
||||||
#ifdef USE_VOICE_ASSISTANT
|
#ifdef USE_VOICE_ASSISTANT
|
||||||
#include "esphome/components/voice_assistant/voice_assistant.h"
|
#include "esphome/components/voice_assistant/voice_assistant.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -623,9 +626,10 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
|||||||
auto traits = climate->get_traits();
|
auto traits = climate->get_traits();
|
||||||
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
|
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
|
||||||
resp.action = static_cast<enums::ClimateAction>(climate->action);
|
resp.action = static_cast<enums::ClimateAction>(climate->action);
|
||||||
if (traits.get_supports_current_temperature())
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
|
||||||
resp.current_temperature = climate->current_temperature;
|
resp.current_temperature = climate->current_temperature;
|
||||||
if (traits.get_supports_two_point_target_temperature()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
|
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
resp.target_temperature_low = climate->target_temperature_low;
|
resp.target_temperature_low = climate->target_temperature_low;
|
||||||
resp.target_temperature_high = climate->target_temperature_high;
|
resp.target_temperature_high = climate->target_temperature_high;
|
||||||
} else {
|
} else {
|
||||||
@@ -644,9 +648,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
|||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes())
|
if (traits.get_supports_swing_modes())
|
||||||
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
||||||
if (traits.get_supports_current_humidity())
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY))
|
||||||
resp.current_humidity = climate->current_humidity;
|
resp.current_humidity = climate->current_humidity;
|
||||||
if (traits.get_supports_target_humidity())
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY))
|
||||||
resp.target_humidity = climate->target_humidity;
|
resp.target_humidity = climate->target_humidity;
|
||||||
return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size,
|
return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||||
is_single);
|
is_single);
|
||||||
@@ -656,10 +660,14 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
|||||||
auto *climate = static_cast<climate::Climate *>(entity);
|
auto *climate = static_cast<climate::Climate *>(entity);
|
||||||
ListEntitiesClimateResponse msg;
|
ListEntitiesClimateResponse msg;
|
||||||
auto traits = climate->get_traits();
|
auto traits = climate->get_traits();
|
||||||
|
// Flags set for backward compatibility, deprecated in 2025.11.0
|
||||||
msg.supports_current_temperature = traits.get_supports_current_temperature();
|
msg.supports_current_temperature = traits.get_supports_current_temperature();
|
||||||
msg.supports_current_humidity = traits.get_supports_current_humidity();
|
msg.supports_current_humidity = traits.get_supports_current_humidity();
|
||||||
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
|
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
|
||||||
msg.supports_target_humidity = traits.get_supports_target_humidity();
|
msg.supports_target_humidity = traits.get_supports_target_humidity();
|
||||||
|
msg.supports_action = traits.get_supports_action();
|
||||||
|
// Current feature flags and other supported parameters
|
||||||
|
msg.feature_flags = traits.get_feature_flags();
|
||||||
msg.supported_modes = &traits.get_supported_modes_for_api_();
|
msg.supported_modes = &traits.get_supported_modes_for_api_();
|
||||||
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
||||||
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
||||||
@@ -667,7 +675,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
|||||||
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
|
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
|
||||||
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
||||||
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
||||||
msg.supports_action = traits.get_supports_action();
|
|
||||||
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
|
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
|
||||||
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
|
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
|
||||||
msg.supported_presets = &traits.get_supported_presets_for_api_();
|
msg.supported_presets = &traits.get_supported_presets_for_api_();
|
||||||
@@ -1406,7 +1413,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
|||||||
|
|
||||||
HelloResponse resp;
|
HelloResponse resp;
|
||||||
resp.api_version_major = 1;
|
resp.api_version_major = 1;
|
||||||
resp.api_version_minor = 12;
|
resp.api_version_minor = 13;
|
||||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||||
resp.set_server_info(ESPHOME_VERSION_REF);
|
resp.set_server_info(ESPHOME_VERSION_REF);
|
||||||
resp.set_name(StringRef(App.get_name()));
|
resp.set_name(StringRef(App.get_name()));
|
||||||
|
@@ -242,7 +242,6 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
const std::string &name = App.get_name();
|
const std::string &name = App.get_name();
|
||||||
const std::string &mac = get_mac_address();
|
const std::string &mac = get_mac_address();
|
||||||
|
|
||||||
std::vector<uint8_t> msg;
|
|
||||||
// Calculate positions and sizes
|
// Calculate positions and sizes
|
||||||
size_t name_len = name.size() + 1; // including null terminator
|
size_t name_len = name.size() + 1; // including null terminator
|
||||||
size_t mac_len = mac.size() + 1; // including null terminator
|
size_t mac_len = mac.size() + 1; // including null terminator
|
||||||
@@ -250,17 +249,17 @@ APIError APINoiseFrameHelper::state_action_() {
|
|||||||
size_t mac_offset = name_offset + name_len;
|
size_t mac_offset = name_offset + name_len;
|
||||||
size_t total_size = 1 + name_len + mac_len;
|
size_t total_size = 1 + name_len + mac_len;
|
||||||
|
|
||||||
msg.resize(total_size);
|
auto msg = std::make_unique<uint8_t[]>(total_size);
|
||||||
|
|
||||||
// chosen proto
|
// chosen proto
|
||||||
msg[0] = 0x01;
|
msg[0] = 0x01;
|
||||||
|
|
||||||
// node name, terminated by null byte
|
// node name, terminated by null byte
|
||||||
std::memcpy(msg.data() + name_offset, name.c_str(), name_len);
|
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
|
||||||
// node mac, terminated by null byte
|
// node mac, terminated by null byte
|
||||||
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
|
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
|
||||||
|
|
||||||
aerr = write_frame_(msg.data(), msg.size());
|
aerr = write_frame_(msg.get(), total_size);
|
||||||
if (aerr != APIError::OK)
|
if (aerr != APIError::OK)
|
||||||
return aerr;
|
return aerr;
|
||||||
|
|
||||||
@@ -339,32 +338,32 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
|
|||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||||
std::vector<uint8_t> data;
|
size_t data_size = reason_len + 1;
|
||||||
data.resize(reason_len + 1);
|
auto data = std::make_unique<uint8_t[]>(data_size);
|
||||||
data[0] = 0x01; // failure
|
data[0] = 0x01; // failure
|
||||||
|
|
||||||
// Copy error message from PROGMEM
|
// Copy error message from PROGMEM
|
||||||
if (reason_len > 0) {
|
if (reason_len > 0) {
|
||||||
memcpy_P(data.data() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Normal memory access
|
// Normal memory access
|
||||||
const char *reason_str = LOG_STR_ARG(reason);
|
const char *reason_str = LOG_STR_ARG(reason);
|
||||||
size_t reason_len = strlen(reason_str);
|
size_t reason_len = strlen(reason_str);
|
||||||
std::vector<uint8_t> data;
|
size_t data_size = reason_len + 1;
|
||||||
data.resize(reason_len + 1);
|
auto data = std::make_unique<uint8_t[]>(data_size);
|
||||||
data[0] = 0x01; // failure
|
data[0] = 0x01; // failure
|
||||||
|
|
||||||
// Copy error message in bulk
|
// Copy error message in bulk
|
||||||
if (reason_len > 0) {
|
if (reason_len > 0) {
|
||||||
std::memcpy(data.data() + 1, reason_str, reason_len);
|
std::memcpy(data.get() + 1, reason_str, reason_len);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// temporarily remove failed state
|
// temporarily remove failed state
|
||||||
auto orig_state = state_;
|
auto orig_state = state_;
|
||||||
state_ = State::EXPLICIT_REJECT;
|
state_ = State::EXPLICIT_REJECT;
|
||||||
write_frame_(data.data(), data.size());
|
write_frame_(data.get(), data_size);
|
||||||
state_ = orig_state;
|
state_ = orig_state;
|
||||||
}
|
}
|
||||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||||
|
@@ -1064,6 +1064,17 @@ bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
||||||
|
uint32_t count_bool_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 6);
|
||||||
|
this->bool_array.init(count_bool_array);
|
||||||
|
uint32_t count_int_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 7);
|
||||||
|
this->int_array.init(count_int_array);
|
||||||
|
uint32_t count_float_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 8);
|
||||||
|
this->float_array.init(count_float_array);
|
||||||
|
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
|
||||||
|
this->string_array.init(count_string_array);
|
||||||
|
ProtoDecodableMessage::decode(buffer, length);
|
||||||
|
}
|
||||||
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case 2:
|
case 2:
|
||||||
@@ -1085,6 +1096,11 @@ bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
|
||||||
|
uint32_t count_args = ProtoDecodableMessage::count_repeated_field(buffer, length, 2);
|
||||||
|
this->args.init(count_args);
|
||||||
|
ProtoDecodableMessage::decode(buffer, length);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_CAMERA
|
#ifdef USE_CAMERA
|
||||||
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
|
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
|
||||||
@@ -1185,6 +1201,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
buffer.encode_uint32(26, this->device_id);
|
buffer.encode_uint32(26, this->device_id);
|
||||||
#endif
|
#endif
|
||||||
|
buffer.encode_uint32(27, this->feature_flags);
|
||||||
}
|
}
|
||||||
void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
||||||
size.add_length(1, this->object_id_ref_.size());
|
size.add_length(1, this->object_id_ref_.size());
|
||||||
@@ -1239,6 +1256,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
|||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
size.add_uint32(2, this->device_id);
|
size.add_uint32(2, this->device_id);
|
||||||
#endif
|
#endif
|
||||||
|
size.add_uint32(2, this->feature_flags);
|
||||||
}
|
}
|
||||||
void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
|
void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_fixed32(1, this->key);
|
buffer.encode_fixed32(1, this->key);
|
||||||
|
@@ -1110,9 +1110,9 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
|||||||
#endif
|
#endif
|
||||||
StringRef service_ref_{};
|
StringRef service_ref_{};
|
||||||
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
||||||
std::vector<HomeassistantServiceMap> data{};
|
FixedVector<HomeassistantServiceMap> data{};
|
||||||
std::vector<HomeassistantServiceMap> data_template{};
|
FixedVector<HomeassistantServiceMap> data_template{};
|
||||||
std::vector<HomeassistantServiceMap> variables{};
|
FixedVector<HomeassistantServiceMap> variables{};
|
||||||
bool is_event{false};
|
bool is_event{false};
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
uint32_t call_id{0};
|
uint32_t call_id{0};
|
||||||
@@ -1263,7 +1263,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
|||||||
StringRef name_ref_{};
|
StringRef name_ref_{};
|
||||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||||
uint32_t key{0};
|
uint32_t key{0};
|
||||||
std::vector<ListEntitiesServicesArgument> args{};
|
FixedVector<ListEntitiesServicesArgument> args{};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1279,10 +1279,11 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
|
|||||||
float float_{0.0f};
|
float float_{0.0f};
|
||||||
std::string string_{};
|
std::string string_{};
|
||||||
int32_t int_{0};
|
int32_t int_{0};
|
||||||
std::vector<bool> bool_array{};
|
FixedVector<bool> bool_array{};
|
||||||
std::vector<int32_t> int_array{};
|
FixedVector<int32_t> int_array{};
|
||||||
std::vector<float> float_array{};
|
FixedVector<float> float_array{};
|
||||||
std::vector<std::string> string_array{};
|
FixedVector<std::string> string_array{};
|
||||||
|
void decode(const uint8_t *buffer, size_t length) override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -1300,7 +1301,8 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
|
|||||||
const char *message_name() const override { return "execute_service_request"; }
|
const char *message_name() const override { return "execute_service_request"; }
|
||||||
#endif
|
#endif
|
||||||
uint32_t key{0};
|
uint32_t key{0};
|
||||||
std::vector<ExecuteServiceArgument> args{};
|
FixedVector<ExecuteServiceArgument> args{};
|
||||||
|
void decode(const uint8_t *buffer, size_t length) override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
void dump_to(std::string &out) const override;
|
void dump_to(std::string &out) const override;
|
||||||
#endif
|
#endif
|
||||||
@@ -1369,7 +1371,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
|
|||||||
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 46;
|
static constexpr uint8_t MESSAGE_TYPE = 46;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 145;
|
static constexpr uint8_t ESTIMATED_SIZE = 150;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_climate_response"; }
|
const char *message_name() const override { return "list_entities_climate_response"; }
|
||||||
#endif
|
#endif
|
||||||
@@ -1390,6 +1392,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
|||||||
bool supports_target_humidity{false};
|
bool supports_target_humidity{false};
|
||||||
float visual_min_humidity{0.0f};
|
float visual_min_humidity{0.0f};
|
||||||
float visual_max_humidity{0.0f};
|
float visual_max_humidity{0.0f};
|
||||||
|
uint32_t feature_flags{0};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
@@ -1292,6 +1292,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
|
|||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
dump_field(out, "device_id", this->device_id);
|
dump_field(out, "device_id", this->device_id);
|
||||||
#endif
|
#endif
|
||||||
|
dump_field(out, "feature_flags", this->feature_flags);
|
||||||
}
|
}
|
||||||
void ClimateStateResponse::dump_to(std::string &out) const {
|
void ClimateStateResponse::dump_to(std::string &out) const {
|
||||||
MessageDumpHelper helper(out, "ClimateStateResponse");
|
MessageDumpHelper helper(out, "ClimateStateResponse");
|
||||||
|
@@ -201,9 +201,9 @@ class CustomAPIDevice {
|
|||||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||||
HomeassistantActionRequest resp;
|
HomeassistantActionRequest resp;
|
||||||
resp.set_service(StringRef(service_name));
|
resp.set_service(StringRef(service_name));
|
||||||
|
resp.data.init(data.size());
|
||||||
for (auto &it : data) {
|
for (auto &it : data) {
|
||||||
resp.data.emplace_back();
|
auto &kv = resp.data.emplace_back();
|
||||||
auto &kv = resp.data.back();
|
|
||||||
kv.set_key(StringRef(it.first));
|
kv.set_key(StringRef(it.first));
|
||||||
kv.value = it.second;
|
kv.value = it.second;
|
||||||
}
|
}
|
||||||
@@ -244,9 +244,9 @@ class CustomAPIDevice {
|
|||||||
HomeassistantActionRequest resp;
|
HomeassistantActionRequest resp;
|
||||||
resp.set_service(StringRef(service_name));
|
resp.set_service(StringRef(service_name));
|
||||||
resp.is_event = true;
|
resp.is_event = true;
|
||||||
|
resp.data.init(data.size());
|
||||||
for (auto &it : data) {
|
for (auto &it : data) {
|
||||||
resp.data.emplace_back();
|
auto &kv = resp.data.emplace_back();
|
||||||
auto &kv = resp.data.back();
|
|
||||||
kv.set_key(StringRef(it.first));
|
kv.set_key(StringRef(it.first));
|
||||||
kv.value = it.second;
|
kv.value = it.second;
|
||||||
}
|
}
|
||||||
|
@@ -41,10 +41,14 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
|||||||
|
|
||||||
template<typename... Ts> class TemplatableKeyValuePair {
|
template<typename... Ts> class TemplatableKeyValuePair {
|
||||||
public:
|
public:
|
||||||
|
// Default constructor needed for FixedVector::emplace_back()
|
||||||
|
TemplatableKeyValuePair() = default;
|
||||||
|
|
||||||
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
|
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
|
||||||
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
|
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
|
||||||
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
|
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
|
||||||
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
||||||
|
|
||||||
std::string key;
|
std::string key;
|
||||||
TemplatableStringValue<Ts...> value;
|
TemplatableStringValue<Ts...> value;
|
||||||
};
|
};
|
||||||
@@ -93,15 +97,22 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
|
|
||||||
template<typename T> void set_service(T service) { this->service_ = service; }
|
template<typename T> void set_service(T service) { this->service_ = service; }
|
||||||
|
|
||||||
|
// Initialize FixedVector members - called from Python codegen with compile-time known sizes.
|
||||||
|
// Must be called before any add_* methods; capacity must match the number of subsequent add_* calls.
|
||||||
|
void init_data(size_t count) { this->data_.init(count); }
|
||||||
|
void init_data_template(size_t count) { this->data_template_.init(count); }
|
||||||
|
void init_variables(size_t count) { this->variables_.init(count); }
|
||||||
|
|
||||||
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
|
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
|
||||||
// The value parameter can be a lambda/template, but keys are never templatable.
|
// The value parameter can be a lambda/template, but keys are never templatable.
|
||||||
// Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
|
template<typename K, typename V> void add_data(K &&key, V &&value) {
|
||||||
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
|
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
|
||||||
template<typename T> void add_data_template(std::string key, T value) {
|
|
||||||
this->data_template_.emplace_back(std::move(key), value);
|
|
||||||
}
|
}
|
||||||
template<typename T> void add_variable(std::string key, T value) {
|
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
|
||||||
this->variables_.emplace_back(std::move(key), value);
|
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
|
||||||
|
}
|
||||||
|
template<typename K, typename V> void add_variable(K &&key, V &&value) {
|
||||||
|
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
@@ -127,24 +138,9 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
std::string service_value = this->service_.value(x...);
|
std::string service_value = this->service_.value(x...);
|
||||||
resp.set_service(StringRef(service_value));
|
resp.set_service(StringRef(service_value));
|
||||||
resp.is_event = this->flags_.is_event;
|
resp.is_event = this->flags_.is_event;
|
||||||
for (auto &it : this->data_) {
|
this->populate_service_map(resp.data, this->data_, x...);
|
||||||
resp.data.emplace_back();
|
this->populate_service_map(resp.data_template, this->data_template_, x...);
|
||||||
auto &kv = resp.data.back();
|
this->populate_service_map(resp.variables, this->variables_, x...);
|
||||||
kv.set_key(StringRef(it.key));
|
|
||||||
kv.value = it.value.value(x...);
|
|
||||||
}
|
|
||||||
for (auto &it : this->data_template_) {
|
|
||||||
resp.data_template.emplace_back();
|
|
||||||
auto &kv = resp.data_template.back();
|
|
||||||
kv.set_key(StringRef(it.key));
|
|
||||||
kv.value = it.value.value(x...);
|
|
||||||
}
|
|
||||||
for (auto &it : this->variables_) {
|
|
||||||
resp.variables.emplace_back();
|
|
||||||
auto &kv = resp.variables.back();
|
|
||||||
kv.set_key(StringRef(it.key));
|
|
||||||
kv.value = it.value.value(x...);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
if (this->flags_.wants_status) {
|
if (this->flags_.wants_status) {
|
||||||
@@ -189,11 +185,28 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
|
||||||
|
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
|
||||||
|
auto &kv = vec.emplace_back();
|
||||||
|
kv.key = std::forward<K>(key);
|
||||||
|
kv.value = std::forward<V>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename VectorType, typename SourceType>
|
||||||
|
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
|
||||||
|
dest.init(source.size());
|
||||||
|
for (auto &it : source) {
|
||||||
|
auto &kv = dest.emplace_back();
|
||||||
|
kv.set_key(StringRef(it.key));
|
||||||
|
kv.value = it.value.value(x...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
APIServer *parent_;
|
APIServer *parent_;
|
||||||
TemplatableStringValue<Ts...> service_{};
|
TemplatableStringValue<Ts...> service_{};
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> data_;
|
FixedVector<TemplatableKeyValuePair<Ts...>> data_;
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
|
FixedVector<TemplatableKeyValuePair<Ts...>> data_template_;
|
||||||
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
|
FixedVector<TemplatableKeyValuePair<Ts...>> variables_;
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||||
TemplatableStringValue<Ts...> response_template_{""};
|
TemplatableStringValue<Ts...> response_template_{""};
|
||||||
|
@@ -7,6 +7,69 @@ namespace esphome::api {
|
|||||||
|
|
||||||
static const char *const TAG = "api.proto";
|
static const char *const TAG = "api.proto";
|
||||||
|
|
||||||
|
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
|
||||||
|
uint32_t count = 0;
|
||||||
|
const uint8_t *ptr = buffer;
|
||||||
|
const uint8_t *end = buffer + length;
|
||||||
|
|
||||||
|
while (ptr < end) {
|
||||||
|
uint32_t consumed;
|
||||||
|
|
||||||
|
// Parse field header (tag)
|
||||||
|
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||||
|
if (!res.has_value()) {
|
||||||
|
break; // Invalid data, stop counting
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t tag = res->as_uint32();
|
||||||
|
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
||||||
|
uint32_t field_id = tag >> 3;
|
||||||
|
ptr += consumed;
|
||||||
|
|
||||||
|
// Count if this is the target field
|
||||||
|
if (field_id == target_field_id) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip field data based on wire type
|
||||||
|
switch (field_type) {
|
||||||
|
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
|
||||||
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||||
|
if (!res.has_value()) {
|
||||||
|
return count; // Invalid data, return what we have
|
||||||
|
}
|
||||||
|
ptr += consumed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
|
||||||
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||||
|
if (!res.has_value()) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
uint32_t field_length = res->as_uint32();
|
||||||
|
ptr += consumed;
|
||||||
|
if (ptr + field_length > end) {
|
||||||
|
return count; // Out of bounds
|
||||||
|
}
|
||||||
|
ptr += field_length;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
|
||||||
|
if (ptr + 4 > end) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
ptr += 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Unknown wire type, can't continue
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||||
const uint8_t *ptr = buffer;
|
const uint8_t *ptr = buffer;
|
||||||
const uint8_t *end = buffer + length;
|
const uint8_t *end = buffer + length;
|
||||||
@@ -22,12 +85,12 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t tag = res->as_uint32();
|
uint32_t tag = res->as_uint32();
|
||||||
uint32_t field_type = tag & 0b111;
|
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
||||||
uint32_t field_id = tag >> 3;
|
uint32_t field_id = tag >> 3;
|
||||||
ptr += consumed;
|
ptr += consumed;
|
||||||
|
|
||||||
switch (field_type) {
|
switch (field_type) {
|
||||||
case 0: { // VarInt
|
case WIRE_TYPE_VARINT: { // VarInt
|
||||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||||
if (!res.has_value()) {
|
if (!res.has_value()) {
|
||||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||||
@@ -39,7 +102,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|||||||
ptr += consumed;
|
ptr += consumed;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 2: { // Length-delimited
|
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
|
||||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||||
if (!res.has_value()) {
|
if (!res.has_value()) {
|
||||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||||
@@ -57,7 +120,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|||||||
ptr += field_length;
|
ptr += field_length;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 5: { // 32-bit
|
case WIRE_TYPE_FIXED32: { // 32-bit
|
||||||
if (ptr + 4 > end) {
|
if (ptr + 4 > end) {
|
||||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||||
return;
|
return;
|
||||||
|
@@ -15,6 +15,13 @@
|
|||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
|
// Protocol Buffer wire type constants
|
||||||
|
// See https://protobuf.dev/programming-guides/encoding/#structure
|
||||||
|
constexpr uint8_t WIRE_TYPE_VARINT = 0; // int32, int64, uint32, uint64, sint32, sint64, bool, enum
|
||||||
|
constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded messages, packed repeated fields
|
||||||
|
constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float
|
||||||
|
constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag
|
||||||
|
|
||||||
// Helper functions for ZigZag encoding/decoding
|
// Helper functions for ZigZag encoding/decoding
|
||||||
inline constexpr uint32_t encode_zigzag32(int32_t value) {
|
inline constexpr uint32_t encode_zigzag32(int32_t value) {
|
||||||
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||||
@@ -241,7 +248,7 @@ class ProtoWriteBuffer {
|
|||||||
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
||||||
*/
|
*/
|
||||||
void encode_field_raw(uint32_t field_id, uint32_t type) {
|
void encode_field_raw(uint32_t field_id, uint32_t type) {
|
||||||
uint32_t val = (field_id << 3) | (type & 0b111);
|
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
|
||||||
this->encode_varint_raw(val);
|
this->encode_varint_raw(val);
|
||||||
}
|
}
|
||||||
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
|
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
|
||||||
@@ -354,7 +361,18 @@ class ProtoMessage {
|
|||||||
// Base class for messages that support decoding
|
// Base class for messages that support decoding
|
||||||
class ProtoDecodableMessage : public ProtoMessage {
|
class ProtoDecodableMessage : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
void decode(const uint8_t *buffer, size_t length);
|
virtual void decode(const uint8_t *buffer, size_t length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count occurrences of a repeated field in a protobuf buffer.
|
||||||
|
* This is a lightweight scan that only parses tags and skips field data.
|
||||||
|
*
|
||||||
|
* @param buffer Pointer to the protobuf buffer
|
||||||
|
* @param length Length of the buffer in bytes
|
||||||
|
* @param target_field_id The field ID to count
|
||||||
|
* @return Number of times the field appears in the buffer
|
||||||
|
*/
|
||||||
|
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
|
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
|
||||||
@@ -482,7 +500,7 @@ class ProtoSize {
|
|||||||
* @return The number of bytes needed to encode the field ID and wire type
|
* @return The number of bytes needed to encode the field ID and wire type
|
||||||
*/
|
*/
|
||||||
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
||||||
uint32_t tag = (field_id << 3) | (type & 0b111);
|
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
|
||||||
return varint(tag);
|
return varint(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,16 +12,16 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
|
|||||||
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
||||||
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
||||||
return arg.bool_array;
|
return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
|
||||||
}
|
}
|
||||||
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
|
||||||
return arg.int_array;
|
return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
|
||||||
}
|
}
|
||||||
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
|
||||||
return arg.float_array;
|
return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
|
||||||
}
|
}
|
||||||
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
|
||||||
return arg.string_array;
|
return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
|
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
|
||||||
|
@@ -35,9 +35,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
|||||||
msg.set_name(StringRef(this->name_));
|
msg.set_name(StringRef(this->name_));
|
||||||
msg.key = this->key_;
|
msg.key = this->key_;
|
||||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||||
|
msg.args.init(sizeof...(Ts));
|
||||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||||
msg.args.emplace_back();
|
auto &arg = msg.args.emplace_back();
|
||||||
auto &arg = msg.args.back();
|
|
||||||
arg.type = arg_types[i];
|
arg.type = arg_types[i];
|
||||||
arg.set_name(StringRef(this->arg_names_[i]));
|
arg.set_name(StringRef(this->arg_names_[i]));
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void execute(Ts... x) = 0;
|
virtual void execute(Ts... x) = 0;
|
||||||
template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) {
|
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0
esphome/components/bh1900nux/__init__.py
Normal file
0
esphome/components/bh1900nux/__init__.py
Normal file
54
esphome/components/bh1900nux/bh1900nux.cpp
Normal file
54
esphome/components/bh1900nux/bh1900nux.cpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "bh1900nux.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace bh1900nux {
|
||||||
|
|
||||||
|
static const char *const TAG = "bh1900nux.sensor";
|
||||||
|
|
||||||
|
// I2C Registers
|
||||||
|
static const uint8_t TEMPERATURE_REG = 0x00;
|
||||||
|
static const uint8_t CONFIG_REG = 0x01; // Not used and supported yet
|
||||||
|
static const uint8_t TEMPERATURE_LOW_REG = 0x02; // Not used and supported yet
|
||||||
|
static const uint8_t TEMPERATURE_HIGH_REG = 0x03; // Not used and supported yet
|
||||||
|
static const uint8_t SOFT_RESET_REG = 0x04;
|
||||||
|
|
||||||
|
// I2C Command payloads
|
||||||
|
static const uint8_t SOFT_RESET_PAYLOAD = 0x01; // Soft Reset value
|
||||||
|
|
||||||
|
static const float SENSOR_RESOLUTION = 0.0625f; // Sensor resolution per bit in degrees celsius
|
||||||
|
|
||||||
|
void BH1900NUXSensor::setup() {
|
||||||
|
// Initialize I2C device
|
||||||
|
i2c::ErrorCode result_code =
|
||||||
|
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
|
||||||
|
if (result_code != i2c::ERROR_OK) {
|
||||||
|
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BH1900NUXSensor::update() {
|
||||||
|
uint8_t temperature_raw[2];
|
||||||
|
if (this->read_register(TEMPERATURE_REG, temperature_raw, 2) != i2c::ERROR_OK) {
|
||||||
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combined raw value, unsigned and unaligned 16 bit
|
||||||
|
// Temperature is represented in just 12 bits, shift needed
|
||||||
|
int16_t raw_temperature_register_value = encode_uint16(temperature_raw[0], temperature_raw[1]);
|
||||||
|
raw_temperature_register_value >>= 4;
|
||||||
|
float temperature_value = raw_temperature_register_value * SENSOR_RESOLUTION; // Apply sensor resolution
|
||||||
|
|
||||||
|
this->publish_state(temperature_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BH1900NUXSensor::dump_config() {
|
||||||
|
LOG_SENSOR("", "BH1900NUX", this);
|
||||||
|
LOG_I2C_DEVICE(this);
|
||||||
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace bh1900nux
|
||||||
|
} // namespace esphome
|
18
esphome/components/bh1900nux/bh1900nux.h
Normal file
18
esphome/components/bh1900nux/bh1900nux.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace bh1900nux {
|
||||||
|
|
||||||
|
class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void update() override;
|
||||||
|
void dump_config() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace bh1900nux
|
||||||
|
} // namespace esphome
|
34
esphome/components/bh1900nux/sensor.py
Normal file
34
esphome/components/bh1900nux/sensor.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import i2c, sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
CODEOWNERS = ["@B48D81EFCC"]
|
||||||
|
|
||||||
|
sensor_ns = cg.esphome_ns.namespace("bh1900nux")
|
||||||
|
BH1900NUXSensor = sensor_ns.class_(
|
||||||
|
"BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
sensor.sensor_schema(
|
||||||
|
BH1900NUXSensor,
|
||||||
|
accuracy_decimals=1,
|
||||||
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
.extend(i2c.i2c_device_schema(0x48))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = await sensor.new_sensor(config)
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await i2c.register_i2c_device(var, config)
|
@@ -16,7 +16,9 @@
|
|||||||
|
|
||||||
#include "bluetooth_connection.h"
|
#include "bluetooth_connection.h"
|
||||||
|
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#include <esp_bt.h>
|
#include <esp_bt.h>
|
||||||
|
#endif
|
||||||
#include <esp_bt_device.h>
|
#include <esp_bt_device.h>
|
||||||
|
|
||||||
namespace esphome::bluetooth_proxy {
|
namespace esphome::bluetooth_proxy {
|
||||||
|
@@ -8,17 +8,30 @@ namespace cap1188 {
|
|||||||
static const char *const TAG = "cap1188";
|
static const char *const TAG = "cap1188";
|
||||||
|
|
||||||
void CAP1188Component::setup() {
|
void CAP1188Component::setup() {
|
||||||
// Reset device using the reset pin
|
this->disable_loop();
|
||||||
if (this->reset_pin_ != nullptr) {
|
|
||||||
this->reset_pin_->setup();
|
// no reset pin
|
||||||
this->reset_pin_->digital_write(false);
|
if (this->reset_pin_ == nullptr) {
|
||||||
delay(100); // NOLINT
|
this->finish_setup_();
|
||||||
this->reset_pin_->digital_write(true);
|
return;
|
||||||
delay(100); // NOLINT
|
|
||||||
this->reset_pin_->digital_write(false);
|
|
||||||
delay(100); // NOLINT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset pin configured so reset before finishing setup
|
||||||
|
this->reset_pin_->setup();
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
// delay after reset pin write
|
||||||
|
this->set_timeout(100, [this]() {
|
||||||
|
this->reset_pin_->digital_write(true);
|
||||||
|
// delay after reset pin write
|
||||||
|
this->set_timeout(100, [this]() {
|
||||||
|
this->reset_pin_->digital_write(false);
|
||||||
|
// delay after reset pin write
|
||||||
|
this->set_timeout(100, [this]() { this->finish_setup_(); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void CAP1188Component::finish_setup_() {
|
||||||
// Check if CAP1188 is actually connected
|
// Check if CAP1188 is actually connected
|
||||||
this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_);
|
this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_);
|
||||||
this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_);
|
this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_);
|
||||||
@@ -44,6 +57,9 @@ void CAP1188Component::setup() {
|
|||||||
|
|
||||||
// Speed up a bit
|
// Speed up a bit
|
||||||
this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30);
|
this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30);
|
||||||
|
|
||||||
|
// Setup successful, so enable loop
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAP1188Component::dump_config() {
|
void CAP1188Component::dump_config() {
|
||||||
|
@@ -49,6 +49,8 @@ class CAP1188Component : public Component, public i2c::I2CDevice {
|
|||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void finish_setup_();
|
||||||
|
|
||||||
std::vector<CAP1188Channel *> channels_{};
|
std::vector<CAP1188Channel *> channels_{};
|
||||||
uint8_t touch_threshold_{0x20};
|
uint8_t touch_threshold_{0x20};
|
||||||
uint8_t allow_multiple_touches_{0x80};
|
uint8_t allow_multiple_touches_{0x80};
|
||||||
|
@@ -96,7 +96,8 @@ void ClimateCall::validate_() {
|
|||||||
}
|
}
|
||||||
if (this->target_temperature_.has_value()) {
|
if (this->target_temperature_.has_value()) {
|
||||||
auto target = *this->target_temperature_;
|
auto target = *this->target_temperature_;
|
||||||
if (traits.get_supports_two_point_target_temperature()) {
|
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
|
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
|
||||||
"with two-point target temperature!");
|
"with two-point target temperature!");
|
||||||
this->target_temperature_.reset();
|
this->target_temperature_.reset();
|
||||||
@@ -106,7 +107,8 @@ void ClimateCall::validate_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
|
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
|
||||||
if (!traits.get_supports_two_point_target_temperature()) {
|
if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
|
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
|
||||||
this->target_temperature_low_.reset();
|
this->target_temperature_low_.reset();
|
||||||
this->target_temperature_high_.reset();
|
this->target_temperature_high_.reset();
|
||||||
@@ -350,13 +352,14 @@ void Climate::save_state_() {
|
|||||||
|
|
||||||
state.mode = this->mode;
|
state.mode = this->mode;
|
||||||
auto traits = this->get_traits();
|
auto traits = this->get_traits();
|
||||||
if (traits.get_supports_two_point_target_temperature()) {
|
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
state.target_temperature_low = this->target_temperature_low;
|
state.target_temperature_low = this->target_temperature_low;
|
||||||
state.target_temperature_high = this->target_temperature_high;
|
state.target_temperature_high = this->target_temperature_high;
|
||||||
} else {
|
} else {
|
||||||
state.target_temperature = this->target_temperature;
|
state.target_temperature = this->target_temperature;
|
||||||
}
|
}
|
||||||
if (traits.get_supports_target_humidity()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||||
state.target_humidity = this->target_humidity;
|
state.target_humidity = this->target_humidity;
|
||||||
}
|
}
|
||||||
if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
|
if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
|
||||||
@@ -400,7 +403,7 @@ void Climate::publish_state() {
|
|||||||
auto traits = this->get_traits();
|
auto traits = this->get_traits();
|
||||||
|
|
||||||
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));
|
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));
|
||||||
if (traits.get_supports_action()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||||
ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action)));
|
ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action)));
|
||||||
}
|
}
|
||||||
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
|
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
|
||||||
@@ -418,19 +421,20 @@ void Climate::publish_state() {
|
|||||||
if (traits.get_supports_swing_modes()) {
|
if (traits.get_supports_swing_modes()) {
|
||||||
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
|
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
|
||||||
}
|
}
|
||||||
if (traits.get_supports_current_temperature()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||||
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
|
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_two_point_target_temperature()) {
|
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
|
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
|
||||||
this->target_temperature_high);
|
this->target_temperature_high);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
|
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_current_humidity()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||||
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
|
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_target_humidity()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||||
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
|
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,13 +489,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
|||||||
auto call = climate->make_call();
|
auto call = climate->make_call();
|
||||||
auto traits = climate->get_traits();
|
auto traits = climate->get_traits();
|
||||||
call.set_mode(this->mode);
|
call.set_mode(this->mode);
|
||||||
if (traits.get_supports_two_point_target_temperature()) {
|
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
call.set_target_temperature_low(this->target_temperature_low);
|
call.set_target_temperature_low(this->target_temperature_low);
|
||||||
call.set_target_temperature_high(this->target_temperature_high);
|
call.set_target_temperature_high(this->target_temperature_high);
|
||||||
} else {
|
} else {
|
||||||
call.set_target_temperature(this->target_temperature);
|
call.set_target_temperature(this->target_temperature);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_target_humidity()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||||
call.set_target_humidity(this->target_humidity);
|
call.set_target_humidity(this->target_humidity);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
|
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
|
||||||
@@ -508,13 +513,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
|||||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
|
void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||||
auto traits = climate->get_traits();
|
auto traits = climate->get_traits();
|
||||||
climate->mode = this->mode;
|
climate->mode = this->mode;
|
||||||
if (traits.get_supports_two_point_target_temperature()) {
|
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
climate->target_temperature_low = this->target_temperature_low;
|
climate->target_temperature_low = this->target_temperature_low;
|
||||||
climate->target_temperature_high = this->target_temperature_high;
|
climate->target_temperature_high = this->target_temperature_high;
|
||||||
} else {
|
} else {
|
||||||
climate->target_temperature = this->target_temperature;
|
climate->target_temperature = this->target_temperature;
|
||||||
}
|
}
|
||||||
if (traits.get_supports_target_humidity()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||||
climate->target_humidity = this->target_humidity;
|
climate->target_humidity = this->target_humidity;
|
||||||
}
|
}
|
||||||
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
|
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
|
||||||
@@ -580,28 +586,30 @@ void Climate::dump_traits_(const char *tag) {
|
|||||||
" Target: %.1f",
|
" Target: %.1f",
|
||||||
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
|
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
|
||||||
traits.get_visual_target_temperature_step());
|
traits.get_visual_target_temperature_step());
|
||||||
if (traits.get_supports_current_temperature()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||||
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
|
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
|
||||||
}
|
}
|
||||||
if (traits.get_supports_target_humidity() || traits.get_supports_current_humidity()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
|
||||||
|
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||||
ESP_LOGCONFIG(tag,
|
ESP_LOGCONFIG(tag,
|
||||||
" - Min humidity: %.0f\n"
|
" - Min humidity: %.0f\n"
|
||||||
" - Max humidity: %.0f",
|
" - Max humidity: %.0f",
|
||||||
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
|
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
|
||||||
}
|
}
|
||||||
if (traits.get_supports_two_point_target_temperature()) {
|
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
|
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
|
||||||
}
|
}
|
||||||
if (traits.get_supports_current_temperature()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
|
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
|
||||||
}
|
}
|
||||||
if (traits.get_supports_target_humidity()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
|
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
|
||||||
}
|
}
|
||||||
if (traits.get_supports_current_humidity()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
|
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
|
||||||
}
|
}
|
||||||
if (traits.get_supports_action()) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||||
ESP_LOGCONFIG(tag, " [x] Supports action");
|
ESP_LOGCONFIG(tag, " [x] Supports action");
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_modes().empty()) {
|
if (!traits.get_supported_modes().empty()) {
|
||||||
|
@@ -98,6 +98,21 @@ enum ClimatePreset : uint8_t {
|
|||||||
CLIMATE_PRESET_ACTIVITY = 7,
|
CLIMATE_PRESET_ACTIVITY = 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum ClimateFeature : uint32_t {
|
||||||
|
// Reporting current temperature is supported
|
||||||
|
CLIMATE_SUPPORTS_CURRENT_TEMPERATURE = 1 << 0,
|
||||||
|
// Setting two target temperatures is supported (used in conjunction with CLIMATE_MODE_HEAT_COOL)
|
||||||
|
CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 1,
|
||||||
|
// Single-point mode is NOT supported (UI always displays two handles, setting 'target_temperature' is not supported)
|
||||||
|
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE = 1 << 2,
|
||||||
|
// Reporting current humidity is supported
|
||||||
|
CLIMATE_SUPPORTS_CURRENT_HUMIDITY = 1 << 3,
|
||||||
|
// Setting a target humidity is supported
|
||||||
|
CLIMATE_SUPPORTS_TARGET_HUMIDITY = 1 << 4,
|
||||||
|
// Reporting current climate action is supported
|
||||||
|
CLIMATE_SUPPORTS_ACTION = 1 << 5,
|
||||||
|
};
|
||||||
|
|
||||||
/// Convert the given ClimateMode to a human-readable string.
|
/// Convert the given ClimateMode to a human-readable string.
|
||||||
const LogString *climate_mode_to_string(ClimateMode mode);
|
const LogString *climate_mode_to_string(ClimateMode mode);
|
||||||
|
|
||||||
|
@@ -21,48 +21,92 @@ namespace climate {
|
|||||||
* - Target Temperature
|
* - Target Temperature
|
||||||
*
|
*
|
||||||
* All other properties and modes are optional and the integration must mark
|
* All other properties and modes are optional and the integration must mark
|
||||||
* each of them as supported by setting the appropriate flag here.
|
* each of them as supported by setting the appropriate flag(s) here.
|
||||||
*
|
*
|
||||||
* - supports current temperature - if the climate device supports reporting a current temperature
|
* - feature flags: see ClimateFeatures enum in climate_mode.h
|
||||||
* - supports two point target temperature - if the climate device's target temperature should be
|
|
||||||
* split in target_temperature_low and target_temperature_high instead of just the single target_temperature
|
|
||||||
* - supports modes:
|
* - supports modes:
|
||||||
* - auto mode (automatic control)
|
* - auto mode (automatic control)
|
||||||
* - cool mode (lowers current temperature)
|
* - cool mode (lowers current temperature)
|
||||||
* - heat mode (increases current temperature)
|
* - heat mode (increases current temperature)
|
||||||
* - dry mode (removes humidity from air)
|
* - dry mode (removes humidity from air)
|
||||||
* - fan mode (only turns on fan)
|
* - fan mode (only turns on fan)
|
||||||
* - supports action - if the climate device supports reporting the active
|
|
||||||
* current action of the device with the action property.
|
|
||||||
* - supports fan modes - optionally, if it has a fan which can be configured in different ways:
|
* - supports fan modes - optionally, if it has a fan which can be configured in different ways:
|
||||||
* - on, off, auto, high, medium, low, middle, focus, diffuse, quiet
|
* - on, off, auto, high, medium, low, middle, focus, diffuse, quiet
|
||||||
* - supports swing modes - optionally, if it has a swing which can be configured in different ways:
|
* - supports swing modes - optionally, if it has a swing which can be configured in different ways:
|
||||||
* - off, both, vertical, horizontal
|
* - off, both, vertical, horizontal
|
||||||
*
|
*
|
||||||
* This class also contains static data for the climate device display:
|
* This class also contains static data for the climate device display:
|
||||||
* - visual min/max temperature - tells the frontend what range of temperatures the climate device
|
* - visual min/max temperature/humidity - tells the frontend what range of temperature/humidity the
|
||||||
* should display (gauge min/max values)
|
* climate device should display (gauge min/max values)
|
||||||
* - temperature step - the step with which to increase/decrease target temperature.
|
* - temperature step - the step with which to increase/decrease target temperature.
|
||||||
* This also affects with how many decimal places the temperature is shown
|
* This also affects with how many decimal places the temperature is shown
|
||||||
*/
|
*/
|
||||||
class ClimateTraits {
|
class ClimateTraits {
|
||||||
public:
|
public:
|
||||||
bool get_supports_current_temperature() const { return this->supports_current_temperature_; }
|
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
|
||||||
|
uint32_t get_feature_flags() const { return this->feature_flags_; }
|
||||||
|
void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; }
|
||||||
|
void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; }
|
||||||
|
bool has_feature_flags(uint32_t feature_flags) const { return this->feature_flags_ & feature_flags; }
|
||||||
|
void set_feature_flags(uint32_t feature_flags) { this->feature_flags_ = feature_flags; }
|
||||||
|
|
||||||
|
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
|
||||||
|
bool get_supports_current_temperature() const {
|
||||||
|
return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
}
|
||||||
|
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
|
||||||
void set_supports_current_temperature(bool supports_current_temperature) {
|
void set_supports_current_temperature(bool supports_current_temperature) {
|
||||||
this->supports_current_temperature_ = supports_current_temperature;
|
if (supports_current_temperature) {
|
||||||
|
this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
} else {
|
||||||
|
this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bool get_supports_current_humidity() const { return this->supports_current_humidity_; }
|
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
|
||||||
|
bool get_supports_current_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); }
|
||||||
|
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
|
||||||
void set_supports_current_humidity(bool supports_current_humidity) {
|
void set_supports_current_humidity(bool supports_current_humidity) {
|
||||||
this->supports_current_humidity_ = supports_current_humidity;
|
if (supports_current_humidity) {
|
||||||
|
this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||||
|
} else {
|
||||||
|
this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bool get_supports_two_point_target_temperature() const { return this->supports_two_point_target_temperature_; }
|
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
|
||||||
|
bool get_supports_two_point_target_temperature() const {
|
||||||
|
return this->has_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
|
||||||
|
}
|
||||||
|
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
|
||||||
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
|
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
|
||||||
this->supports_two_point_target_temperature_ = supports_two_point_target_temperature;
|
if (supports_two_point_target_temperature)
|
||||||
|
// Use CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE to mimic previous behavior
|
||||||
|
{
|
||||||
|
this->add_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
|
||||||
|
} else {
|
||||||
|
this->clear_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bool get_supports_target_humidity() const { return this->supports_target_humidity_; }
|
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
|
||||||
|
bool get_supports_target_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); }
|
||||||
|
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
|
||||||
void set_supports_target_humidity(bool supports_target_humidity) {
|
void set_supports_target_humidity(bool supports_target_humidity) {
|
||||||
this->supports_target_humidity_ = supports_target_humidity;
|
if (supports_target_humidity) {
|
||||||
|
this->add_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY);
|
||||||
|
} else {
|
||||||
|
this->clear_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
|
||||||
|
bool get_supports_action() const { return this->has_feature_flags(CLIMATE_SUPPORTS_ACTION); }
|
||||||
|
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
|
||||||
|
void set_supports_action(bool supports_action) {
|
||||||
|
if (supports_action) {
|
||||||
|
this->add_feature_flags(CLIMATE_SUPPORTS_ACTION);
|
||||||
|
} else {
|
||||||
|
this->clear_feature_flags(CLIMATE_SUPPORTS_ACTION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
||||||
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
||||||
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
|
||||||
@@ -82,9 +126,6 @@ class ClimateTraits {
|
|||||||
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
||||||
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
|
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
|
||||||
|
|
||||||
void set_supports_action(bool supports_action) { this->supports_action_ = supports_action; }
|
|
||||||
bool get_supports_action() const { return this->supports_action_; }
|
|
||||||
|
|
||||||
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
|
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
|
||||||
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
||||||
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
|
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
|
||||||
@@ -219,24 +260,20 @@ class ClimateTraits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool supports_current_temperature_{false};
|
uint32_t feature_flags_{0};
|
||||||
bool supports_current_humidity_{false};
|
|
||||||
bool supports_two_point_target_temperature_{false};
|
|
||||||
bool supports_target_humidity_{false};
|
|
||||||
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
|
|
||||||
bool supports_action_{false};
|
|
||||||
std::set<climate::ClimateFanMode> supported_fan_modes_;
|
|
||||||
std::set<climate::ClimateSwingMode> supported_swing_modes_;
|
|
||||||
std::set<climate::ClimatePreset> supported_presets_;
|
|
||||||
std::set<std::string> supported_custom_fan_modes_;
|
|
||||||
std::set<std::string> supported_custom_presets_;
|
|
||||||
|
|
||||||
float visual_min_temperature_{10};
|
float visual_min_temperature_{10};
|
||||||
float visual_max_temperature_{30};
|
float visual_max_temperature_{30};
|
||||||
float visual_target_temperature_step_{0.1};
|
float visual_target_temperature_step_{0.1};
|
||||||
float visual_current_temperature_step_{0.1};
|
float visual_current_temperature_step_{0.1};
|
||||||
float visual_min_humidity_{30};
|
float visual_min_humidity_{30};
|
||||||
float visual_max_humidity_{99};
|
float visual_max_humidity_{99};
|
||||||
|
|
||||||
|
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
|
||||||
|
std::set<climate::ClimateFanMode> supported_fan_modes_;
|
||||||
|
std::set<climate::ClimateSwingMode> supported_swing_modes_;
|
||||||
|
std::set<climate::ClimatePreset> supported_presets_;
|
||||||
|
std::set<std::string> supported_custom_fan_modes_;
|
||||||
|
std::set<std::string> supported_custom_presets_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace climate
|
} // namespace climate
|
||||||
|
@@ -30,14 +30,12 @@ class DateTimeBase : public EntityBase {
|
|||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef USE_TIME
|
|
||||||
class DateTimeStateTrigger : public Trigger<ESPTime> {
|
class DateTimeStateTrigger : public Trigger<ESPTime> {
|
||||||
public:
|
public:
|
||||||
explicit DateTimeStateTrigger(DateTimeBase *parent) {
|
explicit DateTimeStateTrigger(DateTimeBase *parent) {
|
||||||
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
|
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
#endif
|
|
||||||
|
|
||||||
} // namespace datetime
|
} // namespace datetime
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@@ -11,8 +11,6 @@
|
|||||||
#include <esp_chip_info.h>
|
#include <esp_chip_info.h>
|
||||||
#include <esp_partition.h>
|
#include <esp_partition.h>
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
#include <Esp.h>
|
#include <Esp.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -125,7 +123,12 @@ void DebugComponent::log_partition_info_() {
|
|||||||
|
|
||||||
uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); }
|
uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); }
|
||||||
|
|
||||||
static const std::map<int, const char *> CHIP_FEATURES = {
|
struct ChipFeature {
|
||||||
|
int bit;
|
||||||
|
const char *name;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr ChipFeature CHIP_FEATURES[] = {
|
||||||
{CHIP_FEATURE_BLE, "BLE"},
|
{CHIP_FEATURE_BLE, "BLE"},
|
||||||
{CHIP_FEATURE_BT, "BT"},
|
{CHIP_FEATURE_BT, "BT"},
|
||||||
{CHIP_FEATURE_EMB_FLASH, "EMB Flash"},
|
{CHIP_FEATURE_EMB_FLASH, "EMB Flash"},
|
||||||
@@ -170,11 +173,13 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
|||||||
esp_chip_info(&info);
|
esp_chip_info(&info);
|
||||||
const char *model = ESPHOME_VARIANT;
|
const char *model = ESPHOME_VARIANT;
|
||||||
std::string features;
|
std::string features;
|
||||||
for (auto feature : CHIP_FEATURES) {
|
|
||||||
if (info.features & feature.first) {
|
// Check each known feature bit
|
||||||
features += feature.second;
|
for (const auto &feature : CHIP_FEATURES) {
|
||||||
|
if (info.features & feature.bit) {
|
||||||
|
features += feature.name;
|
||||||
features += ", ";
|
features += ", ";
|
||||||
info.features &= ~feature.first;
|
info.features &= ~feature.bit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (info.features != 0)
|
if (info.features != 0)
|
||||||
|
@@ -25,10 +25,37 @@ static void show_reset_reason(std::string &reset_reason, bool set, const char *r
|
|||||||
reset_reason += reason;
|
reset_reason += reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline uint32_t read_mem_u32(uintptr_t addr) {
|
static inline uint32_t read_mem_u32(uintptr_t addr) {
|
||||||
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline uint8_t read_mem_u8(uintptr_t addr) {
|
||||||
|
return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information
|
||||||
|
constexpr uint32_t SD_MAGIC_NUMBER = 0x51B1E5DB;
|
||||||
|
constexpr uintptr_t MBR_SIZE = 0x1000;
|
||||||
|
constexpr uintptr_t SOFTDEVICE_INFO_STRUCT_OFFSET = 0x2000;
|
||||||
|
constexpr uintptr_t SD_ID_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x10;
|
||||||
|
constexpr uintptr_t SD_VERSION_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x14;
|
||||||
|
|
||||||
|
static inline bool is_sd_present() {
|
||||||
|
return read_mem_u32(SOFTDEVICE_INFO_STRUCT_OFFSET + MBR_SIZE + 4) == SD_MAGIC_NUMBER;
|
||||||
|
}
|
||||||
|
static inline uint32_t sd_id_get() {
|
||||||
|
if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) {
|
||||||
|
return read_mem_u32(MBR_SIZE + SD_ID_OFFSET);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static inline uint32_t sd_version_get() {
|
||||||
|
if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) {
|
||||||
|
return read_mem_u32(MBR_SIZE + SD_VERSION_OFFSET);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
std::string DebugComponent::get_reset_reason_() {
|
std::string DebugComponent::get_reset_reason_() {
|
||||||
uint32_t cause;
|
uint32_t cause;
|
||||||
auto ret = hwinfo_get_reset_cause(&cause);
|
auto ret = hwinfo_get_reset_cause(&cause);
|
||||||
@@ -271,6 +298,29 @@ void DebugComponent::get_device_info_(std::string &device_info) {
|
|||||||
NRF_UICR->NRFFW[0]);
|
NRF_UICR->NRFFW[0]);
|
||||||
ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
|
ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
|
||||||
NRF_UICR->NRFFW[1]);
|
NRF_UICR->NRFFW[1]);
|
||||||
|
if (is_sd_present()) {
|
||||||
|
uint32_t const sd_id = sd_id_get();
|
||||||
|
uint32_t const sd_version = sd_version_get();
|
||||||
|
|
||||||
|
uint32_t ver[3];
|
||||||
|
ver[0] = sd_version / 1000000;
|
||||||
|
ver[1] = (sd_version - ver[0] * 1000000) / 1000;
|
||||||
|
ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000);
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]);
|
||||||
|
#ifdef USE_SOFTDEVICE_ID
|
||||||
|
#ifdef USE_SOFTDEVICE_VERSION
|
||||||
|
if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) {
|
||||||
|
ESP_LOGE(TAG, "Built for SoftDevice S%u %u.x.y. It may crash due to mismatch of bootloader version.",
|
||||||
|
USE_SOFTDEVICE_ID, USE_SOFTDEVICE_VERSION);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (USE_SOFTDEVICE_ID != sd_id) {
|
||||||
|
ESP_LOGE(TAG, "Built for SoftDevice S%u. It may crash due to mismatch of bootloader version.", USE_SOFTDEVICE_ID);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -775,7 +775,7 @@ void Display::test_card() {
|
|||||||
int shift_y = (h - image_h) / 2;
|
int shift_y = (h - image_h) / 2;
|
||||||
int line_w = (image_w - 6) / 6;
|
int line_w = (image_w - 6) / 6;
|
||||||
int image_c = image_w / 2;
|
int image_c = image_w / 2;
|
||||||
for (auto i = 0; i <= image_h; i++) {
|
for (auto i = 0; i != image_h; i++) {
|
||||||
int c = esp_scale(i, image_h);
|
int c = esp_scale(i, image_h);
|
||||||
this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c));
|
this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c));
|
||||||
this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); //
|
this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); //
|
||||||
@@ -809,8 +809,11 @@ void Display::test_card() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->rectangle(0, 0, w, h, Color(127, 0, 127));
|
|
||||||
this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255));
|
this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255));
|
||||||
|
this->filled_rectangle(w - 10, 0, 10, 10, Color(255, 0, 255));
|
||||||
|
this->filled_rectangle(0, h - 10, 10, 10, Color(255, 0, 255));
|
||||||
|
this->filled_rectangle(w - 10, h - 10, 10, 10, Color(255, 0, 255));
|
||||||
|
this->rectangle(0, 0, w, h, Color(255, 255, 255));
|
||||||
this->stop_poller();
|
this->stop_poller();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -324,7 +324,7 @@ def _is_framework_url(source: str) -> str:
|
|||||||
# The default/recommended arduino framework version
|
# The default/recommended arduino framework version
|
||||||
# - https://github.com/espressif/arduino-esp32/releases
|
# - https://github.com/espressif/arduino-esp32/releases
|
||||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(3, 2, 1),
|
"recommended": cv.Version(3, 3, 2),
|
||||||
"latest": cv.Version(3, 3, 2),
|
"latest": cv.Version(3, 3, 2),
|
||||||
"dev": cv.Version(3, 3, 2),
|
"dev": cv.Version(3, 3, 2),
|
||||||
}
|
}
|
||||||
@@ -343,7 +343,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# The default/recommended esp-idf framework version
|
# The default/recommended esp-idf framework version
|
||||||
# - https://github.com/espressif/esp-idf/releases
|
# - https://github.com/espressif/esp-idf/releases
|
||||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(5, 4, 2),
|
"recommended": cv.Version(5, 5, 1),
|
||||||
"latest": cv.Version(5, 5, 1),
|
"latest": cv.Version(5, 5, 1),
|
||||||
"dev": cv.Version(5, 5, 1),
|
"dev": cv.Version(5, 5, 1),
|
||||||
}
|
}
|
||||||
@@ -363,7 +363,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# The platform-espressif32 version
|
# The platform-espressif32 version
|
||||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||||
PLATFORM_VERSION_LOOKUP = {
|
PLATFORM_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(54, 3, 21, "2"),
|
"recommended": cv.Version(55, 3, 31, "1"),
|
||||||
"latest": cv.Version(55, 3, 31, "1"),
|
"latest": cv.Version(55, 3, 31, "1"),
|
||||||
"dev": cv.Version(55, 3, 31, "1"),
|
"dev": cv.Version(55, 3, 31, "1"),
|
||||||
}
|
}
|
||||||
@@ -544,6 +544,7 @@ CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
|
|||||||
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
|
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
|
||||||
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
|
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
|
||||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
|
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
|
||||||
|
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
|
||||||
|
|
||||||
|
|
||||||
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
||||||
@@ -606,6 +607,9 @@ FRAMEWORK_SCHEMA = cv.All(
|
|||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
|
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
|
||||||
): cv.boolean,
|
): cv.boolean,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
|
||||||
|
): cv.boolean,
|
||||||
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -824,6 +828,9 @@ async def to_code(config):
|
|||||||
# Disable dynamic log level control to save memory
|
# Disable dynamic log level control to save memory
|
||||||
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
|
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
|
||||||
|
|
||||||
|
# Reduce PHY TX power in the event of a brownout
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
|
||||||
|
|
||||||
# Set default CPU frequency
|
# Set default CPU frequency
|
||||||
add_idf_sdkconfig_option(
|
add_idf_sdkconfig_option(
|
||||||
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
|
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
|
||||||
@@ -864,6 +871,12 @@ async def to_code(config):
|
|||||||
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
|
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
|
||||||
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
|
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
|
||||||
|
|
||||||
|
# Disable placing libc locks in IRAM to save RAM
|
||||||
|
# This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled)
|
||||||
|
# use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM.
|
||||||
|
if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
|
||||||
|
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
|
||||||
|
|
||||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||||
if CONF_PARTITIONS in config:
|
if CONF_PARTITIONS in config:
|
||||||
add_extra_build_file(
|
add_extra_build_file(
|
||||||
|
@@ -1564,6 +1564,10 @@ BOARDS = {
|
|||||||
"name": "DFRobot Beetle ESP32-C3",
|
"name": "DFRobot Beetle ESP32-C3",
|
||||||
"variant": VARIANT_ESP32C3,
|
"variant": VARIANT_ESP32C3,
|
||||||
},
|
},
|
||||||
|
"dfrobot_firebeetle2_esp32c6": {
|
||||||
|
"name": "DFRobot FireBeetle 2 ESP32-C6",
|
||||||
|
"variant": VARIANT_ESP32C6,
|
||||||
|
},
|
||||||
"dfrobot_firebeetle2_esp32e": {
|
"dfrobot_firebeetle2_esp32e": {
|
||||||
"name": "DFRobot Firebeetle 2 ESP32-E",
|
"name": "DFRobot Firebeetle 2 ESP32-E",
|
||||||
"variant": VARIANT_ESP32,
|
"variant": VARIANT_ESP32,
|
||||||
@@ -1604,6 +1608,22 @@ BOARDS = {
|
|||||||
"name": "Ai-Thinker ESP-C3-M1-I-Kit",
|
"name": "Ai-Thinker ESP-C3-M1-I-Kit",
|
||||||
"variant": VARIANT_ESP32C3,
|
"variant": VARIANT_ESP32C3,
|
||||||
},
|
},
|
||||||
|
"esp32-c5-devkitc-1": {
|
||||||
|
"name": "Espressif ESP32-C5-DevKitC-1 4MB no PSRAM",
|
||||||
|
"variant": VARIANT_ESP32C5,
|
||||||
|
},
|
||||||
|
"esp32-c5-devkitc1-n16r4": {
|
||||||
|
"name": "Espressif ESP32-C5-DevKitC-1 N16R4 (16 MB Flash Quad, 4 MB PSRAM Quad)",
|
||||||
|
"variant": VARIANT_ESP32C5,
|
||||||
|
},
|
||||||
|
"esp32-c5-devkitc1-n4": {
|
||||||
|
"name": "Espressif ESP32-C5-DevKitC-1 N4 (4MB no PSRAM)",
|
||||||
|
"variant": VARIANT_ESP32C5,
|
||||||
|
},
|
||||||
|
"esp32-c5-devkitc1-n8r4": {
|
||||||
|
"name": "Espressif ESP32-C5-DevKitC-1 N8R4 (8 MB Flash Quad, 4 MB PSRAM Quad)",
|
||||||
|
"variant": VARIANT_ESP32C5,
|
||||||
|
},
|
||||||
"esp32-c6-devkitc-1": {
|
"esp32-c6-devkitc-1": {
|
||||||
"name": "Espressif ESP32-C6-DevKitC-1",
|
"name": "Espressif ESP32-C6-DevKitC-1",
|
||||||
"variant": VARIANT_ESP32C6,
|
"variant": VARIANT_ESP32C6,
|
||||||
@@ -2048,6 +2068,10 @@ BOARDS = {
|
|||||||
"name": "M5Stack Station",
|
"name": "M5Stack Station",
|
||||||
"variant": VARIANT_ESP32,
|
"variant": VARIANT_ESP32,
|
||||||
},
|
},
|
||||||
|
"m5stack-tab5-p4": {
|
||||||
|
"name": "M5STACK Tab5 esp32-p4 Board",
|
||||||
|
"variant": VARIANT_ESP32P4,
|
||||||
|
},
|
||||||
"m5stack-timer-cam": {
|
"m5stack-timer-cam": {
|
||||||
"name": "M5Stack Timer CAM",
|
"name": "M5Stack Timer CAM",
|
||||||
"variant": VARIANT_ESP32,
|
"variant": VARIANT_ESP32,
|
||||||
@@ -2476,6 +2500,10 @@ BOARDS = {
|
|||||||
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
|
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
|
||||||
"variant": VARIANT_ESP32S3,
|
"variant": VARIANT_ESP32S3,
|
||||||
},
|
},
|
||||||
|
"yb_esp32s3_drv": {
|
||||||
|
"name": "YelloByte YB-ESP32-S3-DRV",
|
||||||
|
"variant": VARIANT_ESP32S3,
|
||||||
|
},
|
||||||
"yb_esp32s3_eth": {
|
"yb_esp32s3_eth": {
|
||||||
"name": "YelloByte YB-ESP32-S3-ETH",
|
"name": "YelloByte YB-ESP32-S3-ETH",
|
||||||
"variant": VARIANT_ESP32S3,
|
"variant": VARIANT_ESP32S3,
|
||||||
|
@@ -108,8 +108,13 @@ class BTLoggers(Enum):
|
|||||||
"""ESP32 WiFi provisioning over Bluetooth"""
|
"""ESP32 WiFi provisioning over Bluetooth"""
|
||||||
|
|
||||||
|
|
||||||
# Set to track which loggers are needed by components
|
# Key for storing required loggers in CORE.data
|
||||||
_required_loggers: set[BTLoggers] = set()
|
ESP32_BLE_REQUIRED_LOGGERS_KEY = "esp32_ble_required_loggers"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required_loggers() -> set[BTLoggers]:
|
||||||
|
"""Get the set of required Bluetooth loggers from CORE.data."""
|
||||||
|
return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set())
|
||||||
|
|
||||||
|
|
||||||
# Dataclass for handler registration counts
|
# Dataclass for handler registration counts
|
||||||
@@ -170,12 +175,13 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
|
|||||||
Args:
|
Args:
|
||||||
*loggers: One or more BTLoggers enum members
|
*loggers: One or more BTLoggers enum members
|
||||||
"""
|
"""
|
||||||
|
required_loggers = _get_required_loggers()
|
||||||
for logger in loggers:
|
for logger in loggers:
|
||||||
if not isinstance(logger, BTLoggers):
|
if not isinstance(logger, BTLoggers):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"Logger must be a BTLoggers enum member, got {type(logger)}"
|
f"Logger must be a BTLoggers enum member, got {type(logger)}"
|
||||||
)
|
)
|
||||||
_required_loggers.add(logger)
|
required_loggers.add(logger)
|
||||||
|
|
||||||
|
|
||||||
CONF_BLE_ID = "ble_id"
|
CONF_BLE_ID = "ble_id"
|
||||||
@@ -387,6 +393,15 @@ def final_validation(config):
|
|||||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||||
validate_connection_slots(max_connections)
|
validate_connection_slots(max_connections)
|
||||||
|
|
||||||
|
# Check if hosted bluetooth is being used
|
||||||
|
if "esp32_hosted" in full_config:
|
||||||
|
add_idf_sdkconfig_option("CONFIG_BT_CLASSIC_ENABLED", False)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_BT_BLE_ENABLED", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_BT_BLUEDROID_ENABLED", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_BT_CONTROLLER_DISABLED", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
|
||||||
|
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
|
||||||
|
|
||||||
# Check if BLE Server is needed
|
# Check if BLE Server is needed
|
||||||
has_ble_server = "esp32_ble_server" in full_config
|
has_ble_server = "esp32_ble_server" in full_config
|
||||||
|
|
||||||
@@ -479,8 +494,9 @@ async def to_code(config):
|
|||||||
# Apply logger settings if log disabling is enabled
|
# Apply logger settings if log disabling is enabled
|
||||||
if config.get(CONF_DISABLE_BT_LOGS, False):
|
if config.get(CONF_DISABLE_BT_LOGS, False):
|
||||||
# Disable all Bluetooth loggers that are not required
|
# Disable all Bluetooth loggers that are not required
|
||||||
|
required_loggers = _get_required_loggers()
|
||||||
for logger in BTLoggers:
|
for logger in BTLoggers:
|
||||||
if logger not in _required_loggers:
|
if logger not in required_loggers:
|
||||||
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
|
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
|
||||||
|
|
||||||
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
|
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
|
||||||
|
@@ -6,7 +6,15 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#include <esp_bt.h>
|
#include <esp_bt.h>
|
||||||
|
#else
|
||||||
|
extern "C" {
|
||||||
|
#include <esp_hosted.h>
|
||||||
|
#include <esp_hosted_misc.h>
|
||||||
|
#include <esp_hosted_bluedroid.h>
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#include <esp_bt_device.h>
|
#include <esp_bt_device.h>
|
||||||
#include <esp_bt_main.h>
|
#include <esp_bt_main.h>
|
||||||
#include <esp_gap_ble_api.h>
|
#include <esp_gap_ble_api.h>
|
||||||
@@ -136,6 +144,7 @@ void ESP32BLE::advertising_init_() {
|
|||||||
|
|
||||||
bool ESP32BLE::ble_setup_() {
|
bool ESP32BLE::ble_setup_() {
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
if (!btStart()) {
|
if (!btStart()) {
|
||||||
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
|
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
|
||||||
@@ -169,6 +178,28 @@ bool ESP32BLE::ble_setup_() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
|
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
|
||||||
|
#else
|
||||||
|
esp_hosted_connect_to_slave(); // NOLINT
|
||||||
|
|
||||||
|
if (esp_hosted_bt_controller_init() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (esp_hosted_bt_controller_enable() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hosted_hci_bluedroid_open();
|
||||||
|
|
||||||
|
esp_bluedroid_hci_driver_operations_t operations = {
|
||||||
|
.send = hosted_hci_bluedroid_send,
|
||||||
|
.check_send_available = hosted_hci_bluedroid_check_send_available,
|
||||||
|
.register_host_callback = hosted_hci_bluedroid_register_host_callback,
|
||||||
|
};
|
||||||
|
esp_bluedroid_attach_hci_driver(&operations);
|
||||||
|
#endif
|
||||||
|
|
||||||
err = esp_bluedroid_init();
|
err = esp_bluedroid_init();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -257,6 +288,7 @@ bool ESP32BLE::ble_dismantle_() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
if (!btStop()) {
|
if (!btStop()) {
|
||||||
ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status());
|
ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status());
|
||||||
@@ -286,6 +318,19 @@ bool ESP32BLE::ble_dismantle_() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
if (esp_hosted_bt_controller_disable() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hosted_hci_bluedroid_close();
|
||||||
#endif
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,9 @@
|
|||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||||
|
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#include <esp_bt.h>
|
#include <esp_bt.h>
|
||||||
|
#endif
|
||||||
#include <esp_gap_ble_api.h>
|
#include <esp_gap_ble_api.h>
|
||||||
#include <esp_gatts_api.h>
|
#include <esp_gatts_api.h>
|
||||||
|
|
||||||
|
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#include <esp_bt.h>
|
#include <esp_bt.h>
|
||||||
|
#endif
|
||||||
#include <esp_bt_main.h>
|
#include <esp_bt_main.h>
|
||||||
#include <esp_gap_ble_api.h>
|
#include <esp_gap_ble_api.h>
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
|
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#include <esp_bt.h>
|
#include <esp_bt.h>
|
||||||
|
#endif
|
||||||
#include <esp_gap_ble_api.h>
|
#include <esp_gap_ble_api.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
@@ -10,7 +10,9 @@
|
|||||||
#include <nvs_flash.h>
|
#include <nvs_flash.h>
|
||||||
#include <freertos/FreeRTOSConfig.h>
|
#include <freertos/FreeRTOSConfig.h>
|
||||||
#include <esp_bt_main.h>
|
#include <esp_bt_main.h>
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#include <esp_bt.h>
|
#include <esp_bt.h>
|
||||||
|
#endif
|
||||||
#include <freertos/task.h>
|
#include <freertos/task.h>
|
||||||
#include <esp_gap_ble_api.h>
|
#include <esp_gap_ble_api.h>
|
||||||
|
|
||||||
|
@@ -60,11 +60,21 @@ class RegistrationCounts:
|
|||||||
clients: int = 0
|
clients: int = 0
|
||||||
|
|
||||||
|
|
||||||
# Set to track which features are needed by components
|
# CORE.data keys for state management
|
||||||
_required_features: set[BLEFeatures] = set()
|
ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY = "esp32_ble_tracker_required_features"
|
||||||
|
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY = "esp32_ble_tracker_registration_counts"
|
||||||
|
|
||||||
# Track registration counts for StaticVector sizing
|
|
||||||
_registration_counts = RegistrationCounts()
|
def _get_required_features() -> set[BLEFeatures]:
|
||||||
|
"""Get the set of required BLE features from CORE.data."""
|
||||||
|
return CORE.data.setdefault(ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY, set())
|
||||||
|
|
||||||
|
|
||||||
|
def _get_registration_counts() -> RegistrationCounts:
|
||||||
|
"""Get the registration counts from CORE.data."""
|
||||||
|
return CORE.data.setdefault(
|
||||||
|
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY, RegistrationCounts()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_ble_features(features: set[BLEFeatures]) -> None:
|
def register_ble_features(features: set[BLEFeatures]) -> None:
|
||||||
@@ -73,7 +83,7 @@ def register_ble_features(features: set[BLEFeatures]) -> None:
|
|||||||
Args:
|
Args:
|
||||||
features: Set of BLEFeatures enum members
|
features: Set of BLEFeatures enum members
|
||||||
"""
|
"""
|
||||||
_required_features.update(features)
|
_get_required_features().update(features)
|
||||||
|
|
||||||
|
|
||||||
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
|
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
|
||||||
@@ -267,15 +277,17 @@ async def to_code(config):
|
|||||||
):
|
):
|
||||||
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
||||||
|
|
||||||
|
registration_counts = _get_registration_counts()
|
||||||
|
|
||||||
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
|
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
|
||||||
_registration_counts.listeners += 1
|
registration_counts.listeners += 1
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
if CONF_MAC_ADDRESS in conf:
|
if CONF_MAC_ADDRESS in conf:
|
||||||
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
|
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
|
||||||
cg.add(trigger.set_addresses(addr_list))
|
cg.add(trigger.set_addresses(addr_list))
|
||||||
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
|
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
|
||||||
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
|
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
|
||||||
_registration_counts.listeners += 1
|
registration_counts.listeners += 1
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
|
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
|
||||||
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
|
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
|
||||||
@@ -288,7 +300,7 @@ async def to_code(config):
|
|||||||
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
|
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
|
||||||
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
|
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
|
||||||
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
|
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
|
||||||
_registration_counts.listeners += 1
|
registration_counts.listeners += 1
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
|
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
|
||||||
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
|
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
|
||||||
@@ -301,7 +313,7 @@ async def to_code(config):
|
|||||||
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
|
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
|
||||||
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
|
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
|
||||||
for conf in config.get(CONF_ON_SCAN_END, []):
|
for conf in config.get(CONF_ON_SCAN_END, []):
|
||||||
_registration_counts.listeners += 1
|
registration_counts.listeners += 1
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
await automation.build_automation(trigger, [], conf)
|
await automation.build_automation(trigger, [], conf)
|
||||||
|
|
||||||
@@ -331,19 +343,21 @@ async def to_code(config):
|
|||||||
@coroutine_with_priority(CoroPriority.FINAL)
|
@coroutine_with_priority(CoroPriority.FINAL)
|
||||||
async def _add_ble_features():
|
async def _add_ble_features():
|
||||||
# Add feature-specific defines based on what's needed
|
# Add feature-specific defines based on what's needed
|
||||||
if BLEFeatures.ESP_BT_DEVICE in _required_features:
|
required_features = _get_required_features()
|
||||||
|
if BLEFeatures.ESP_BT_DEVICE in required_features:
|
||||||
cg.add_define("USE_ESP32_BLE_DEVICE")
|
cg.add_define("USE_ESP32_BLE_DEVICE")
|
||||||
cg.add_define("USE_ESP32_BLE_UUID")
|
cg.add_define("USE_ESP32_BLE_UUID")
|
||||||
|
|
||||||
# Add defines for StaticVector sizing based on registration counts
|
# Add defines for StaticVector sizing based on registration counts
|
||||||
# Only define if count > 0 to avoid allocating unnecessary memory
|
# Only define if count > 0 to avoid allocating unnecessary memory
|
||||||
if _registration_counts.listeners > 0:
|
registration_counts = _get_registration_counts()
|
||||||
|
if registration_counts.listeners > 0:
|
||||||
cg.add_define(
|
cg.add_define(
|
||||||
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", _registration_counts.listeners
|
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", registration_counts.listeners
|
||||||
)
|
)
|
||||||
if _registration_counts.clients > 0:
|
if registration_counts.clients > 0:
|
||||||
cg.add_define(
|
cg.add_define(
|
||||||
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", _registration_counts.clients
|
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", registration_counts.clients
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -395,7 +409,7 @@ async def register_ble_device(
|
|||||||
var: cg.SafeExpType, config: ConfigType
|
var: cg.SafeExpType, config: ConfigType
|
||||||
) -> cg.SafeExpType:
|
) -> cg.SafeExpType:
|
||||||
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
||||||
_registration_counts.listeners += 1
|
_get_registration_counts().listeners += 1
|
||||||
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||||
cg.add(paren.register_listener(var))
|
cg.add(paren.register_listener(var))
|
||||||
return var
|
return var
|
||||||
@@ -403,7 +417,7 @@ async def register_ble_device(
|
|||||||
|
|
||||||
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
|
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
|
||||||
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
||||||
_registration_counts.clients += 1
|
_get_registration_counts().clients += 1
|
||||||
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||||
cg.add(paren.register_client(var))
|
cg.add(paren.register_client(var))
|
||||||
return var
|
return var
|
||||||
@@ -417,7 +431,7 @@ async def register_raw_ble_device(
|
|||||||
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
|
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
|
||||||
will not be compiled in if this is the only registration method used.
|
will not be compiled in if this is the only registration method used.
|
||||||
"""
|
"""
|
||||||
_registration_counts.listeners += 1
|
_get_registration_counts().listeners += 1
|
||||||
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||||
cg.add(paren.register_listener(var))
|
cg.add(paren.register_listener(var))
|
||||||
return var
|
return var
|
||||||
@@ -431,7 +445,7 @@ async def register_raw_client(
|
|||||||
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
|
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
|
||||||
will not be compiled in if this is the only registration method used.
|
will not be compiled in if this is the only registration method used.
|
||||||
"""
|
"""
|
||||||
_registration_counts.clients += 1
|
_get_registration_counts().clients += 1
|
||||||
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||||
cg.add(paren.register_client(var))
|
cg.add(paren.register_client(var))
|
||||||
return var
|
return var
|
||||||
|
@@ -7,7 +7,9 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
#include <esp_bt.h>
|
#include <esp_bt.h>
|
||||||
|
#endif
|
||||||
#include <esp_bt_defs.h>
|
#include <esp_bt_defs.h>
|
||||||
#include <esp_bt_main.h>
|
#include <esp_bt_main.h>
|
||||||
#include <esp_gap_ble_api.h>
|
#include <esp_gap_ble_api.h>
|
||||||
@@ -845,6 +847,7 @@ void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState
|
|||||||
|
|
||||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||||
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
|
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
|
||||||
|
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
if (force_ble && !this->coex_prefer_ble_) {
|
if (force_ble && !this->coex_prefer_ble_) {
|
||||||
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
||||||
this->coex_prefer_ble_ = true;
|
this->coex_prefer_ble_ = true;
|
||||||
@@ -854,6 +857,7 @@ void ESP32BLETracker::update_coex_preference_(bool force_ble) {
|
|||||||
this->coex_prefer_ble_ = false;
|
this->coex_prefer_ble_ = false;
|
||||||
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
|
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
|
||||||
}
|
}
|
||||||
|
#endif // CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@@ -92,9 +92,14 @@ async def to_code(config):
|
|||||||
|
|
||||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||||
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
||||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2")
|
if framework_ver >= cv.Version(5, 5, 0):
|
||||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
|
||||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
|
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
|
||||||
|
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
|
||||||
|
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")
|
||||||
|
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
|
||||||
esp32.add_extra_script(
|
esp32.add_extra_script(
|
||||||
"post",
|
"post",
|
||||||
"esp32_hosted.py",
|
"esp32_hosted.py",
|
||||||
|
@@ -42,6 +42,11 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
|
|||||||
symbols[i] = params->bit0;
|
symbols[i] = params->bit0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
||||||
|
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
|
||||||
|
*done = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return RMT_SYMBOLS_PER_BYTE;
|
return RMT_SYMBOLS_PER_BYTE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -190,7 +190,7 @@ async def to_code(config):
|
|||||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||||
cg.add_define(ThreadModel.SINGLE)
|
cg.add_define(ThreadModel.SINGLE)
|
||||||
|
|
||||||
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
|
cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
|
||||||
|
|
||||||
conf = config[CONF_FRAMEWORK]
|
conf = config[CONF_FRAMEWORK]
|
||||||
cg.add_platformio_option("framework", "arduino")
|
cg.add_platformio_option("framework", "arduino")
|
||||||
@@ -230,6 +230,12 @@ async def to_code(config):
|
|||||||
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
|
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
|
||||||
cg.add_build_flag("-DNEW_OOM_ABORT")
|
cg.add_build_flag("-DNEW_OOM_ABORT")
|
||||||
|
|
||||||
|
# In testing mode, fake a larger IRAM to allow linking grouped component tests
|
||||||
|
# Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
|
||||||
|
# This is done via a pre-build script that generates a custom linker script
|
||||||
|
if CORE.testing_mode:
|
||||||
|
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||||
|
|
||||||
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
|
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
|
||||||
|
|
||||||
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||||
@@ -265,3 +271,8 @@ def copy_files():
|
|||||||
post_build_file,
|
post_build_file,
|
||||||
CORE.relative_build_path("post_build.py"),
|
CORE.relative_build_path("post_build.py"),
|
||||||
)
|
)
|
||||||
|
iram_fix_file = dir / "iram_fix.py.script"
|
||||||
|
copy_file_if_changed(
|
||||||
|
iram_fix_file,
|
||||||
|
CORE.relative_build_path("iram_fix.py"),
|
||||||
|
)
|
||||||
|
44
esphome/components/esp8266/iram_fix.py.script
Normal file
44
esphome/components/esp8266/iram_fix.py.script
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
# pylint: disable=E0602
|
||||||
|
Import("env") # noqa
|
||||||
|
|
||||||
|
|
||||||
|
def patch_linker_script_after_preprocess(source, target, env):
|
||||||
|
"""Patch the local linker script after PlatformIO preprocesses it."""
|
||||||
|
# Check if we're in testing mode by looking for the define
|
||||||
|
build_flags = env.get("BUILD_FLAGS", [])
|
||||||
|
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||||
|
|
||||||
|
if not testing_mode:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the local linker script path
|
||||||
|
build_dir = env.subst("$BUILD_DIR")
|
||||||
|
local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
|
||||||
|
|
||||||
|
if not os.path.exists(local_ld):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Read the linker script
|
||||||
|
with open(local_ld, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
|
||||||
|
# The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
|
||||||
|
updated = re.sub(
|
||||||
|
r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
|
||||||
|
r"\g<1>0x200000",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
if updated != content:
|
||||||
|
with open(local_ld, "w") as f:
|
||||||
|
f.write(updated)
|
||||||
|
print("ESPHome: Patched IRAM size to 2MB for testing mode")
|
||||||
|
|
||||||
|
|
||||||
|
# Hook into the build process right before linking
|
||||||
|
# This runs after PlatformIO has already preprocessed the linker scripts
|
||||||
|
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)
|
@@ -19,6 +19,7 @@ from esphome.const import (
|
|||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.coroutine import CoroPriority
|
from esphome.coroutine import CoroPriority
|
||||||
import esphome.final_validate as fv
|
import esphome.final_validate as fv
|
||||||
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -136,11 +137,12 @@ FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
|||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
|
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
|
||||||
async def to_code(config):
|
async def to_code(config: ConfigType) -> None:
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
cg.add(var.set_port(config[CONF_PORT]))
|
cg.add(var.set_port(config[CONF_PORT]))
|
||||||
|
|
||||||
if CONF_PASSWORD in config:
|
# Password could be set to an empty string and we can assume that means no password
|
||||||
|
if config.get(CONF_PASSWORD):
|
||||||
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
|
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
|
||||||
cg.add_define("USE_OTA_PASSWORD")
|
cg.add_define("USE_OTA_PASSWORD")
|
||||||
# Only include hash algorithms when password is configured
|
# Only include hash algorithms when password is configured
|
||||||
|
@@ -90,13 +90,12 @@ void HomeassistantNumber::control(float value) {
|
|||||||
api::HomeassistantActionRequest resp;
|
api::HomeassistantActionRequest resp;
|
||||||
resp.set_service(SERVICE_NAME);
|
resp.set_service(SERVICE_NAME);
|
||||||
|
|
||||||
resp.data.emplace_back();
|
resp.data.init(2);
|
||||||
auto &entity_id = resp.data.back();
|
auto &entity_id = resp.data.emplace_back();
|
||||||
entity_id.set_key(ENTITY_ID_KEY);
|
entity_id.set_key(ENTITY_ID_KEY);
|
||||||
entity_id.value = this->entity_id_;
|
entity_id.value = this->entity_id_;
|
||||||
|
|
||||||
resp.data.emplace_back();
|
auto &entity_value = resp.data.emplace_back();
|
||||||
auto &entity_value = resp.data.back();
|
|
||||||
entity_value.set_key(VALUE_KEY);
|
entity_value.set_key(VALUE_KEY);
|
||||||
entity_value.value = to_string(value);
|
entity_value.value = to_string(value);
|
||||||
|
|
||||||
|
@@ -51,8 +51,8 @@ void HomeassistantSwitch::write_state(bool state) {
|
|||||||
resp.set_service(SERVICE_OFF);
|
resp.set_service(SERVICE_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.data.emplace_back();
|
resp.data.init(1);
|
||||||
auto &entity_id_kv = resp.data.back();
|
auto &entity_id_kv = resp.data.emplace_back();
|
||||||
entity_id_kv.set_key(ENTITY_ID_KEY);
|
entity_id_kv.set_key(ENTITY_ID_KEY);
|
||||||
entity_id_kv.value = this->entity_id_;
|
entity_id_kv.value = this->entity_id_;
|
||||||
|
|
||||||
|
@@ -9,8 +9,8 @@ static const char *const TAG = "htu21d";
|
|||||||
|
|
||||||
static const uint8_t HTU21D_ADDRESS = 0x40;
|
static const uint8_t HTU21D_ADDRESS = 0x40;
|
||||||
static const uint8_t HTU21D_REGISTER_RESET = 0xFE;
|
static const uint8_t HTU21D_REGISTER_RESET = 0xFE;
|
||||||
static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xE3;
|
static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3;
|
||||||
static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xE5;
|
static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5;
|
||||||
static const uint8_t HTU21D_WRITERHT_REG_CMD = 0xE6; /**< Write RH/T User Register 1 */
|
static const uint8_t HTU21D_WRITERHT_REG_CMD = 0xE6; /**< Write RH/T User Register 1 */
|
||||||
static const uint8_t HTU21D_REGISTER_STATUS = 0xE7;
|
static const uint8_t HTU21D_REGISTER_STATUS = 0xE7;
|
||||||
static const uint8_t HTU21D_WRITEHEATER_REG_CMD = 0x51; /**< Write Heater Control Register */
|
static const uint8_t HTU21D_WRITEHEATER_REG_CMD = 0x51; /**< Write Heater Control Register */
|
||||||
|
@@ -143,7 +143,18 @@ def validate_mclk_divisible_by_3(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
_use_legacy_driver = None
|
# Key for storing legacy driver setting in CORE.data
|
||||||
|
I2S_USE_LEGACY_DRIVER_KEY = "i2s_use_legacy_driver"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_use_legacy_driver():
|
||||||
|
"""Get the legacy driver setting from CORE.data."""
|
||||||
|
return CORE.data.get(I2S_USE_LEGACY_DRIVER_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_use_legacy_driver(value: bool) -> None:
|
||||||
|
"""Set the legacy driver setting in CORE.data."""
|
||||||
|
CORE.data[I2S_USE_LEGACY_DRIVER_KEY] = value
|
||||||
|
|
||||||
|
|
||||||
def i2s_audio_component_schema(
|
def i2s_audio_component_schema(
|
||||||
@@ -209,17 +220,15 @@ async def register_i2s_audio_component(var, config):
|
|||||||
|
|
||||||
|
|
||||||
def validate_use_legacy(value):
|
def validate_use_legacy(value):
|
||||||
global _use_legacy_driver # noqa: PLW0603
|
|
||||||
if CONF_USE_LEGACY in value:
|
if CONF_USE_LEGACY in value:
|
||||||
if (_use_legacy_driver is not None) and (
|
existing_value = _get_use_legacy_driver()
|
||||||
_use_legacy_driver != value[CONF_USE_LEGACY]
|
if (existing_value is not None) and (existing_value != value[CONF_USE_LEGACY]):
|
||||||
):
|
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value."
|
f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value."
|
||||||
)
|
)
|
||||||
if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino):
|
if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino):
|
||||||
raise cv.Invalid("Arduino supports only the legacy i2s driver")
|
raise cv.Invalid("Arduino supports only the legacy i2s driver")
|
||||||
_use_legacy_driver = value[CONF_USE_LEGACY]
|
_set_use_legacy_driver(value[CONF_USE_LEGACY])
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@@ -249,7 +258,8 @@ def _final_validate(_):
|
|||||||
|
|
||||||
|
|
||||||
def use_legacy():
|
def use_legacy():
|
||||||
return not (CORE.using_esp_idf and not _use_legacy_driver)
|
legacy_driver = _get_use_legacy_driver()
|
||||||
|
return not (CORE.using_esp_idf and not legacy_driver)
|
||||||
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
@@ -218,7 +218,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
|||||||
}
|
}
|
||||||
case improv::GET_WIFI_NETWORKS: {
|
case improv::GET_WIFI_NETWORKS: {
|
||||||
std::vector<std::string> networks;
|
std::vector<std::string> networks;
|
||||||
auto results = wifi::global_wifi_component->get_scan_result();
|
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||||
for (auto &scan : results) {
|
for (auto &scan : results) {
|
||||||
if (scan.get_is_hidden())
|
if (scan.get_is_hidden())
|
||||||
continue;
|
continue;
|
||||||
|
@@ -35,6 +35,7 @@ CONF_CHARGE = "charge"
|
|||||||
CONF_CHARGE_COULOMBS = "charge_coulombs"
|
CONF_CHARGE_COULOMBS = "charge_coulombs"
|
||||||
CONF_ENERGY_JOULES = "energy_joules"
|
CONF_ENERGY_JOULES = "energy_joules"
|
||||||
CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient"
|
CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient"
|
||||||
|
CONF_RESET_ON_BOOT = "reset_on_boot"
|
||||||
UNIT_AMPERE_HOURS = "Ah"
|
UNIT_AMPERE_HOURS = "Ah"
|
||||||
UNIT_COULOMB = "C"
|
UNIT_COULOMB = "C"
|
||||||
UNIT_JOULE = "J"
|
UNIT_JOULE = "J"
|
||||||
@@ -113,6 +114,7 @@ INA2XX_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_TEMPERATURE_COEFFICIENT, default=0): cv.int_range(
|
cv.Optional(CONF_TEMPERATURE_COEFFICIENT, default=0): cv.int_range(
|
||||||
min=0, max=16383
|
min=0, max=16383
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_RESET_ON_BOOT, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_SHUNT_VOLTAGE): cv.maybe_simple_value(
|
cv.Optional(CONF_SHUNT_VOLTAGE): cv.maybe_simple_value(
|
||||||
sensor.sensor_schema(
|
sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_MILLIVOLT,
|
unit_of_measurement=UNIT_MILLIVOLT,
|
||||||
@@ -206,6 +208,7 @@ async def setup_ina2xx(var, config):
|
|||||||
cg.add(var.set_adc_range(config[CONF_ADC_RANGE]))
|
cg.add(var.set_adc_range(config[CONF_ADC_RANGE]))
|
||||||
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
|
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
|
||||||
cg.add(var.set_shunt_tempco(config[CONF_TEMPERATURE_COEFFICIENT]))
|
cg.add(var.set_shunt_tempco(config[CONF_TEMPERATURE_COEFFICIENT]))
|
||||||
|
cg.add(var.set_reset_on_boot(config[CONF_RESET_ON_BOOT]))
|
||||||
|
|
||||||
adc_time_config = config[CONF_ADC_TIME]
|
adc_time_config = config[CONF_ADC_TIME]
|
||||||
if isinstance(adc_time_config, dict):
|
if isinstance(adc_time_config, dict):
|
||||||
|
@@ -257,7 +257,12 @@ bool INA2XX::reset_energy_counters() {
|
|||||||
bool INA2XX::reset_config_() {
|
bool INA2XX::reset_config_() {
|
||||||
ESP_LOGV(TAG, "Reset");
|
ESP_LOGV(TAG, "Reset");
|
||||||
ConfigurationRegister cfg{0};
|
ConfigurationRegister cfg{0};
|
||||||
cfg.RST = true;
|
if (!this->reset_on_boot_) {
|
||||||
|
ESP_LOGI(TAG, "Skipping on-boot device reset");
|
||||||
|
cfg.RST = false;
|
||||||
|
} else {
|
||||||
|
cfg.RST = true;
|
||||||
|
}
|
||||||
return this->write_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16);
|
return this->write_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -127,6 +127,7 @@ class INA2XX : public PollingComponent {
|
|||||||
void set_adc_time_die_temperature(AdcTime time) { this->adc_time_die_temperature_ = time; }
|
void set_adc_time_die_temperature(AdcTime time) { this->adc_time_die_temperature_ = time; }
|
||||||
void set_adc_avg_samples(AdcAvgSamples samples) { this->adc_avg_samples_ = samples; }
|
void set_adc_avg_samples(AdcAvgSamples samples) { this->adc_avg_samples_ = samples; }
|
||||||
void set_shunt_tempco(uint16_t coeff) { this->shunt_tempco_ppm_c_ = coeff; }
|
void set_shunt_tempco(uint16_t coeff) { this->shunt_tempco_ppm_c_ = coeff; }
|
||||||
|
void set_reset_on_boot(bool reset) { this->reset_on_boot_ = reset; }
|
||||||
|
|
||||||
void set_shunt_voltage_sensor(sensor::Sensor *sensor) { this->shunt_voltage_sensor_ = sensor; }
|
void set_shunt_voltage_sensor(sensor::Sensor *sensor) { this->shunt_voltage_sensor_ = sensor; }
|
||||||
void set_bus_voltage_sensor(sensor::Sensor *sensor) { this->bus_voltage_sensor_ = sensor; }
|
void set_bus_voltage_sensor(sensor::Sensor *sensor) { this->bus_voltage_sensor_ = sensor; }
|
||||||
@@ -172,6 +173,7 @@ class INA2XX : public PollingComponent {
|
|||||||
AdcTime adc_time_die_temperature_{AdcTime::ADC_TIME_4120US};
|
AdcTime adc_time_die_temperature_{AdcTime::ADC_TIME_4120US};
|
||||||
AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_128};
|
AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_128};
|
||||||
uint16_t shunt_tempco_ppm_c_{0};
|
uint16_t shunt_tempco_ppm_c_{0};
|
||||||
|
bool reset_on_boot_{true};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Calculated coefficients
|
// Calculated coefficients
|
||||||
|
@@ -177,9 +177,10 @@ void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ =
|
|||||||
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||||
void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; }
|
void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; }
|
||||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
|
bool LightState::supports_effects() { return !this->effects_.empty(); }
|
||||||
const std::vector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||||
void LightState::add_effects(const std::vector<LightEffect *> &effects) {
|
void LightState::add_effects(const std::vector<LightEffect *> &effects) {
|
||||||
this->effects_.reserve(this->effects_.size() + effects.size());
|
// Called once from Python codegen during setup with all effects from YAML config
|
||||||
|
this->effects_.init(effects.size());
|
||||||
for (auto *effect : effects) {
|
for (auto *effect : effects) {
|
||||||
this->effects_.push_back(effect);
|
this->effects_.push_back(effect);
|
||||||
}
|
}
|
||||||
|
@@ -11,8 +11,9 @@
|
|||||||
#include "light_traits.h"
|
#include "light_traits.h"
|
||||||
#include "light_transformer.h"
|
#include "light_transformer.h"
|
||||||
|
|
||||||
#include <vector>
|
#include "esphome/core/helpers.h"
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace light {
|
namespace light {
|
||||||
@@ -159,7 +160,7 @@ class LightState : public EntityBase, public Component {
|
|||||||
bool supports_effects();
|
bool supports_effects();
|
||||||
|
|
||||||
/// Get all effects for this light state.
|
/// Get all effects for this light state.
|
||||||
const std::vector<LightEffect *> &get_effects() const;
|
const FixedVector<LightEffect *> &get_effects() const;
|
||||||
|
|
||||||
/// Add effects for this light state.
|
/// Add effects for this light state.
|
||||||
void add_effects(const std::vector<LightEffect *> &effects);
|
void add_effects(const std::vector<LightEffect *> &effects);
|
||||||
@@ -260,7 +261,7 @@ class LightState : public EntityBase, public Component {
|
|||||||
/// The currently active transformer for this light (transition/flash).
|
/// The currently active transformer for this light (transition/flash).
|
||||||
std::unique_ptr<LightTransformer> transformer_{nullptr};
|
std::unique_ptr<LightTransformer> transformer_{nullptr};
|
||||||
/// List of effects for this light.
|
/// List of effects for this light.
|
||||||
std::vector<LightEffect *> effects_;
|
FixedVector<LightEffect *> effects_;
|
||||||
/// Object used to store the persisted values of the light.
|
/// Object used to store the persisted values of the light.
|
||||||
ESPPreferenceObject rtc_;
|
ESPPreferenceObject rtc_;
|
||||||
/// Value for storing the index of the currently active effect. 0 if no effect is active
|
/// Value for storing the index of the currently active effect. 0 if no effect is active
|
||||||
|
@@ -486,7 +486,6 @@ CONF_RESUME_ON_INPUT = "resume_on_input"
|
|||||||
CONF_RIGHT_BUTTON = "right_button"
|
CONF_RIGHT_BUTTON = "right_button"
|
||||||
CONF_ROLLOVER = "rollover"
|
CONF_ROLLOVER = "rollover"
|
||||||
CONF_ROOT_BACK_BTN = "root_back_btn"
|
CONF_ROOT_BACK_BTN = "root_back_btn"
|
||||||
CONF_ROWS = "rows"
|
|
||||||
CONF_SCALE_LINES = "scale_lines"
|
CONF_SCALE_LINES = "scale_lines"
|
||||||
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
||||||
CONF_SELECTED_INDEX = "selected_index"
|
CONF_SELECTED_INDEX = "selected_index"
|
||||||
|
@@ -2,7 +2,7 @@ from esphome import automation
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.key_provider import KeyProvider
|
from esphome.components.key_provider import KeyProvider
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
|
from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH
|
||||||
from esphome.cpp_generator import MockObj
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
from ..automation import action_to_code
|
from ..automation import action_to_code
|
||||||
@@ -15,7 +15,6 @@ from ..defines import (
|
|||||||
CONF_ONE_CHECKED,
|
CONF_ONE_CHECKED,
|
||||||
CONF_PAD_COLUMN,
|
CONF_PAD_COLUMN,
|
||||||
CONF_PAD_ROW,
|
CONF_PAD_ROW,
|
||||||
CONF_ROWS,
|
|
||||||
CONF_SELECTED,
|
CONF_SELECTED,
|
||||||
)
|
)
|
||||||
from ..helpers import lvgl_components_required
|
from ..helpers import lvgl_components_required
|
||||||
|
@@ -2,7 +2,7 @@ from esphome import automation, pins
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import key_provider
|
from esphome.components import key_provider
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
|
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID
|
||||||
|
|
||||||
CODEOWNERS = ["@ssieb"]
|
CODEOWNERS = ["@ssieb"]
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_(
|
|||||||
)
|
)
|
||||||
|
|
||||||
CONF_KEYPAD_ID = "keypad_id"
|
CONF_KEYPAD_ID = "keypad_id"
|
||||||
CONF_ROWS = "rows"
|
|
||||||
CONF_COLUMNS = "columns"
|
CONF_COLUMNS = "columns"
|
||||||
CONF_KEYS = "keys"
|
CONF_KEYS = "keys"
|
||||||
CONF_DEBOUNCE_TIME = "debounce_time"
|
CONF_DEBOUNCE_TIME = "debounce_time"
|
||||||
|
@@ -83,7 +83,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
auto &txt_records = service.txt_records;
|
auto &txt_records = service.txt_records;
|
||||||
txt_records.reserve(txt_count);
|
txt_records.init(txt_count);
|
||||||
|
|
||||||
if (!friendly_name_empty) {
|
if (!friendly_name_empty) {
|
||||||
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
|
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
|
||||||
@@ -171,12 +171,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
|||||||
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
|
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
|
||||||
fallback_service.proto = MDNS_STR(SERVICE_TCP);
|
fallback_service.proto = MDNS_STR(SERVICE_TCP);
|
||||||
fallback_service.port = USE_WEBSERVER_PORT;
|
fallback_service.port = USE_WEBSERVER_PORT;
|
||||||
fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
|
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_MDNS_STORE_SERVICES
|
|
||||||
// Copy to member variable if storage is enabled (verbose logging, OpenThread, or extra services)
|
|
||||||
this->services_ = services;
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -38,7 +38,7 @@ struct MDNSService {
|
|||||||
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
|
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
|
||||||
const MDNSString *proto;
|
const MDNSString *proto;
|
||||||
TemplatableValue<uint16_t> port;
|
TemplatableValue<uint16_t> port;
|
||||||
std::vector<MDNSTXTRecord> txt_records;
|
FixedVector<MDNSTXTRecord> txt_records;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MDNSComponent : public Component {
|
class MDNSComponent : public Component {
|
||||||
|
@@ -12,8 +12,13 @@ namespace mdns {
|
|||||||
static const char *const TAG = "mdns";
|
static const char *const TAG = "mdns";
|
||||||
|
|
||||||
void MDNSComponent::setup() {
|
void MDNSComponent::setup() {
|
||||||
|
#ifdef USE_MDNS_STORE_SERVICES
|
||||||
|
this->compile_records_(this->services_);
|
||||||
|
const auto &services = this->services_;
|
||||||
|
#else
|
||||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||||
this->compile_records_(services);
|
this->compile_records_(services);
|
||||||
|
#endif
|
||||||
|
|
||||||
esp_err_t err = mdns_init();
|
esp_err_t err = mdns_init();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
|
@@ -12,8 +12,13 @@ namespace esphome {
|
|||||||
namespace mdns {
|
namespace mdns {
|
||||||
|
|
||||||
void MDNSComponent::setup() {
|
void MDNSComponent::setup() {
|
||||||
|
#ifdef USE_MDNS_STORE_SERVICES
|
||||||
|
this->compile_records_(this->services_);
|
||||||
|
const auto &services = this->services_;
|
||||||
|
#else
|
||||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||||
this->compile_records_(services);
|
this->compile_records_(services);
|
||||||
|
#endif
|
||||||
|
|
||||||
MDNS.begin(this->hostname_.c_str());
|
MDNS.begin(this->hostname_.c_str());
|
||||||
|
|
||||||
|
@@ -12,8 +12,13 @@ namespace esphome {
|
|||||||
namespace mdns {
|
namespace mdns {
|
||||||
|
|
||||||
void MDNSComponent::setup() {
|
void MDNSComponent::setup() {
|
||||||
|
#ifdef USE_MDNS_STORE_SERVICES
|
||||||
|
this->compile_records_(this->services_);
|
||||||
|
const auto &services = this->services_;
|
||||||
|
#else
|
||||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||||
this->compile_records_(services);
|
this->compile_records_(services);
|
||||||
|
#endif
|
||||||
|
|
||||||
MDNS.begin(this->hostname_.c_str());
|
MDNS.begin(this->hostname_.c_str());
|
||||||
|
|
||||||
|
@@ -12,8 +12,13 @@ namespace esphome {
|
|||||||
namespace mdns {
|
namespace mdns {
|
||||||
|
|
||||||
void MDNSComponent::setup() {
|
void MDNSComponent::setup() {
|
||||||
|
#ifdef USE_MDNS_STORE_SERVICES
|
||||||
|
this->compile_records_(this->services_);
|
||||||
|
const auto &services = this->services_;
|
||||||
|
#else
|
||||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||||
this->compile_records_(services);
|
this->compile_records_(services);
|
||||||
|
#endif
|
||||||
|
|
||||||
MDNS.begin(this->hostname_.c_str());
|
MDNS.begin(this->hostname_.c_str());
|
||||||
|
|
||||||
|
@@ -11,6 +11,7 @@ from esphome.const import (
|
|||||||
CONF_BRIGHTNESS,
|
CONF_BRIGHTNESS,
|
||||||
CONF_COLOR_ORDER,
|
CONF_COLOR_ORDER,
|
||||||
CONF_DIMENSIONS,
|
CONF_DIMENSIONS,
|
||||||
|
CONF_DISABLED,
|
||||||
CONF_HEIGHT,
|
CONF_HEIGHT,
|
||||||
CONF_INIT_SEQUENCE,
|
CONF_INIT_SEQUENCE,
|
||||||
CONF_INVERT_COLORS,
|
CONF_INVERT_COLORS,
|
||||||
@@ -301,6 +302,8 @@ class DriverChip:
|
|||||||
Check if a rotation can be implemented in hardware using the MADCTL register.
|
Check if a rotation can be implemented in hardware using the MADCTL register.
|
||||||
A rotation of 180 is always possible if x and y mirroring are supported, 90 and 270 are possible if the model supports swapping X and Y.
|
A rotation of 180 is always possible if x and y mirroring are supported, 90 and 270 are possible if the model supports swapping X and Y.
|
||||||
"""
|
"""
|
||||||
|
if config.get(CONF_TRANSFORM) == CONF_DISABLED:
|
||||||
|
return False
|
||||||
transforms = self.transforms
|
transforms = self.transforms
|
||||||
rotation = config.get(CONF_ROTATION, 0)
|
rotation = config.get(CONF_ROTATION, 0)
|
||||||
if rotation == 0 or not transforms:
|
if rotation == 0 or not transforms:
|
||||||
@@ -358,26 +361,26 @@ class DriverChip:
|
|||||||
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
|
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
# fill in defaults if not provided
|
if not isinstance(transform, dict):
|
||||||
mirror_x = transform.get(CONF_MIRROR_X, self.get_default(CONF_MIRROR_X))
|
# Presumably disabled
|
||||||
mirror_y = transform.get(CONF_MIRROR_Y, self.get_default(CONF_MIRROR_Y))
|
return {
|
||||||
swap_xy = transform.get(CONF_SWAP_XY, self.get_default(CONF_SWAP_XY))
|
CONF_MIRROR_X: False,
|
||||||
transform[CONF_MIRROR_X] = mirror_x
|
CONF_MIRROR_Y: False,
|
||||||
transform[CONF_MIRROR_Y] = mirror_y
|
CONF_SWAP_XY: False,
|
||||||
transform[CONF_SWAP_XY] = swap_xy
|
CONF_TRANSFORM: False,
|
||||||
|
}
|
||||||
# Can we use the MADCTL register to set the rotation?
|
# Can we use the MADCTL register to set the rotation?
|
||||||
if can_transform and CONF_TRANSFORM not in config:
|
if can_transform and CONF_TRANSFORM not in config:
|
||||||
rotation = config[CONF_ROTATION]
|
rotation = config[CONF_ROTATION]
|
||||||
if rotation == 180:
|
if rotation == 180:
|
||||||
transform[CONF_MIRROR_X] = not mirror_x
|
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||||
transform[CONF_MIRROR_Y] = not mirror_y
|
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||||
elif rotation == 90:
|
elif rotation == 90:
|
||||||
transform[CONF_SWAP_XY] = not swap_xy
|
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||||
transform[CONF_MIRROR_X] = not mirror_x
|
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||||
else:
|
else:
|
||||||
transform[CONF_SWAP_XY] = not swap_xy
|
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||||
transform[CONF_MIRROR_Y] = not mirror_y
|
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||||
transform[CONF_TRANSFORM] = True
|
transform[CONF_TRANSFORM] = True
|
||||||
return transform
|
return transform
|
||||||
|
|
||||||
|
@@ -37,6 +37,7 @@ from esphome.const import (
|
|||||||
CONF_DATA_RATE,
|
CONF_DATA_RATE,
|
||||||
CONF_DC_PIN,
|
CONF_DC_PIN,
|
||||||
CONF_DIMENSIONS,
|
CONF_DIMENSIONS,
|
||||||
|
CONF_DISABLED,
|
||||||
CONF_ENABLE_PIN,
|
CONF_ENABLE_PIN,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_INIT_SEQUENCE,
|
CONF_INIT_SEQUENCE,
|
||||||
@@ -146,12 +147,15 @@ def swap_xy_schema(model):
|
|||||||
def model_schema(config):
|
def model_schema(config):
|
||||||
model = MODELS[config[CONF_MODEL]]
|
model = MODELS[config[CONF_MODEL]]
|
||||||
bus_mode = config[CONF_BUS_MODE]
|
bus_mode = config[CONF_BUS_MODE]
|
||||||
transform = cv.Schema(
|
transform = cv.Any(
|
||||||
{
|
cv.Schema(
|
||||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
{
|
||||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||||
**swap_xy_schema(model),
|
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||||
}
|
**swap_xy_schema(model),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.one_of(CONF_DISABLED, lower=True),
|
||||||
)
|
)
|
||||||
# CUSTOM model will need to provide a custom init sequence
|
# CUSTOM model will need to provide a custom init sequence
|
||||||
iseqconf = (
|
iseqconf = (
|
||||||
@@ -160,7 +164,11 @@ def model_schema(config):
|
|||||||
else cv.Optional(CONF_INIT_SEQUENCE)
|
else cv.Optional(CONF_INIT_SEQUENCE)
|
||||||
)
|
)
|
||||||
# Dimensions are optional if the model has a default width and the x-y transform is not overridden
|
# Dimensions are optional if the model has a default width and the x-y transform is not overridden
|
||||||
is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
|
transform_config = config.get(CONF_TRANSFORM, {})
|
||||||
|
is_swapped = (
|
||||||
|
isinstance(transform_config, dict)
|
||||||
|
and transform_config.get(CONF_SWAP_XY, False) is True
|
||||||
|
)
|
||||||
cv_dimensions = (
|
cv_dimensions = (
|
||||||
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
|
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
|
||||||
)
|
)
|
||||||
@@ -192,9 +200,7 @@ def model_schema(config):
|
|||||||
.extend(
|
.extend(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(MipiSpi),
|
cv.GenerateID(): cv.declare_id(MipiSpi),
|
||||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
|
cv_dimensions(CONF_DIMENSIONS): dimension_schema(1),
|
||||||
model.get_default(CONF_DRAW_ROUNDING, 1)
|
|
||||||
),
|
|
||||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||||
pins.gpio_output_pin_schema
|
pins.gpio_output_pin_schema
|
||||||
),
|
),
|
||||||
@@ -400,6 +406,7 @@ def get_instance(config):
|
|||||||
offset_height,
|
offset_height,
|
||||||
DISPLAY_ROTATIONS[rotation],
|
DISPLAY_ROTATIONS[rotation],
|
||||||
frac,
|
frac,
|
||||||
|
config[CONF_DRAW_ROUNDING],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return MipiSpiBuffer, templateargs
|
return MipiSpiBuffer, templateargs
|
||||||
@@ -431,7 +438,6 @@ async def to_code(config):
|
|||||||
else:
|
else:
|
||||||
config[CONF_ROTATION] = 0
|
config[CONF_ROTATION] = 0
|
||||||
cg.add(var.set_model(config[CONF_MODEL]))
|
cg.add(var.set_model(config[CONF_MODEL]))
|
||||||
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
|
|
||||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||||
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
||||||
cg.add(var.set_enable_pins(enable))
|
cg.add(var.set_enable_pins(enable))
|
||||||
|
@@ -38,7 +38,7 @@ static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel ord
|
|||||||
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
||||||
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||||
|
|
||||||
static const uint8_t DELAY_FLAG = 0xFF;
|
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||||
// store a 16 bit value in a buffer, big endian.
|
// store a 16 bit value in a buffer, big endian.
|
||||||
static inline void put16_be(uint8_t *buf, uint16_t value) {
|
static inline void put16_be(uint8_t *buf, uint16_t value) {
|
||||||
buf[0] = value >> 8;
|
buf[0] = value >> 8;
|
||||||
@@ -79,7 +79,7 @@ class MipiSpi : public display::Display,
|
|||||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||||
spi::DATA_RATE_1MHZ> {
|
spi::DATA_RATE_1MHZ> {
|
||||||
public:
|
public:
|
||||||
MipiSpi() {}
|
MipiSpi() = default;
|
||||||
void update() override { this->stop_poller(); }
|
void update() override { this->stop_poller(); }
|
||||||
void draw_pixel_at(int x, int y, Color color) override {}
|
void draw_pixel_at(int x, int y, Color color) override {}
|
||||||
void set_model(const char *model) { this->model_ = model; }
|
void set_model(const char *model) { this->model_ = model; }
|
||||||
@@ -99,7 +99,6 @@ class MipiSpi : public display::Display,
|
|||||||
int get_width_internal() override { return WIDTH; }
|
int get_width_internal() override { return WIDTH; }
|
||||||
int get_height_internal() override { return HEIGHT; }
|
int get_height_internal() override { return HEIGHT; }
|
||||||
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||||
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
|
|
||||||
|
|
||||||
// reset the display, and write the init sequence
|
// reset the display, and write the init sequence
|
||||||
void setup() override {
|
void setup() override {
|
||||||
@@ -326,6 +325,7 @@ class MipiSpi : public display::Display,
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a buffer to the display.
|
* Writes a buffer to the display.
|
||||||
|
* @param ptr The pointer to the pixel data
|
||||||
* @param w Width of each line in bytes
|
* @param w Width of each line in bytes
|
||||||
* @param h Height of the buffer in rows
|
* @param h Height of the buffer in rows
|
||||||
* @param pad Padding in bytes after each line
|
* @param pad Padding in bytes after each line
|
||||||
@@ -424,7 +424,6 @@ class MipiSpi : public display::Display,
|
|||||||
|
|
||||||
// other properties set by configuration
|
// other properties set by configuration
|
||||||
bool invert_colors_{};
|
bool invert_colors_{};
|
||||||
unsigned draw_rounding_{2};
|
|
||||||
optional<uint8_t> brightness_{};
|
optional<uint8_t> brightness_{};
|
||||||
const char *model_{"Unknown"};
|
const char *model_{"Unknown"};
|
||||||
std::vector<uint8_t> init_sequence_{};
|
std::vector<uint8_t> init_sequence_{};
|
||||||
@@ -444,12 +443,20 @@ class MipiSpi : public display::Display,
|
|||||||
* @tparam OFFSET_WIDTH The x-offset 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 OFFSET_HEIGHT The y-offset of the display in pixels
|
||||||
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
|
* @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,
|
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||||
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION>
|
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION,
|
||||||
|
int FRACTION, unsigned ROUNDING>
|
||||||
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
||||||
OFFSET_WIDTH, OFFSET_HEIGHT> {
|
OFFSET_WIDTH, OFFSET_HEIGHT> {
|
||||||
public:
|
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
|
||||||
|
// ignore the extra columns and rows when drawing, but use them to write to the display.
|
||||||
|
static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING;
|
||||||
|
static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING;
|
||||||
|
|
||||||
MipiSpiBuffer() { this->rotation_ = ROTATION; }
|
MipiSpiBuffer() { this->rotation_ = ROTATION; }
|
||||||
|
|
||||||
void dump_config() override {
|
void dump_config() override {
|
||||||
@@ -461,15 +468,15 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
" Buffer fraction: 1/%d\n"
|
" Buffer fraction: 1/%d\n"
|
||||||
" Buffer bytes: %zu\n"
|
" Buffer bytes: %zu\n"
|
||||||
" Draw rounding: %u",
|
" Draw rounding: %u",
|
||||||
this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION,
|
this->rotation_, BUFFERPIXEL * 8, FRACTION,
|
||||||
this->draw_rounding_);
|
sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() override {
|
void setup() override {
|
||||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||||
OFFSET_HEIGHT>::setup();
|
OFFSET_HEIGHT>::setup();
|
||||||
RAMAllocator<BUFFERTYPE> allocator{};
|
RAMAllocator<BUFFERTYPE> allocator{};
|
||||||
this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION);
|
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
|
||||||
if (this->buffer_ == nullptr) {
|
if (this->buffer_ == nullptr) {
|
||||||
this->mark_failed("Buffer allocation failed");
|
this->mark_failed("Buffer allocation failed");
|
||||||
}
|
}
|
||||||
@@ -508,15 +515,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
|
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
|
||||||
this->y_high_);
|
this->y_high_);
|
||||||
// Some chips require that the drawing window be aligned on certain boundaries
|
// Some chips require that the drawing window be aligned on certain boundaries
|
||||||
auto dr = this->draw_rounding_;
|
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
|
||||||
this->x_low_ = this->x_low_ / dr * dr;
|
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
|
||||||
this->y_low_ = this->y_low_ / dr * dr;
|
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||||
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
|
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||||
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
|
|
||||||
int w = this->x_high_ - this->x_low_ + 1;
|
int w = this->x_high_ - this->x_low_ + 1;
|
||||||
int h = this->y_high_ - this->y_low_ + 1;
|
int h = this->y_high_ - this->y_low_ + 1;
|
||||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||||
this->y_low_ - this->start_line_, WIDTH - w);
|
this->y_low_ - this->start_line_, BUFFER_WIDTH - w);
|
||||||
// invalidate watermarks
|
// invalidate watermarks
|
||||||
this->x_low_ = WIDTH;
|
this->x_low_ = WIDTH;
|
||||||
this->y_low_ = HEIGHT;
|
this->y_low_ = HEIGHT;
|
||||||
@@ -536,10 +542,10 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
void draw_pixel_at(int x, int y, Color color) override {
|
void draw_pixel_at(int x, int y, Color color) override {
|
||||||
if (!this->get_clipping().inside(x, y))
|
if (!this->get_clipping().inside(x, y))
|
||||||
return;
|
return;
|
||||||
rotate_coordinates_(x, y);
|
rotate_coordinates(x, y);
|
||||||
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
||||||
return;
|
return;
|
||||||
this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color);
|
this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color);
|
||||||
if (x < this->x_low_) {
|
if (x < this->x_low_) {
|
||||||
this->x_low_ = x;
|
this->x_low_ = x;
|
||||||
}
|
}
|
||||||
@@ -560,7 +566,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
this->y_low_ = this->start_line_;
|
this->y_low_ = this->start_line_;
|
||||||
this->x_high_ = WIDTH - 1;
|
this->x_high_ = WIDTH - 1;
|
||||||
this->y_high_ = this->end_line_ - 1;
|
this->y_high_ = this->end_line_ - 1;
|
||||||
std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color));
|
std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color));
|
||||||
}
|
}
|
||||||
|
|
||||||
int get_width() override {
|
int get_width() override {
|
||||||
@@ -577,7 +583,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Rotate the coordinates to match the display orientation.
|
// Rotate the coordinates to match the display orientation.
|
||||||
void rotate_coordinates_(int &x, int &y) const {
|
static void rotate_coordinates(int &x, int &y) {
|
||||||
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
|
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||||
x = WIDTH - x - 1;
|
x = WIDTH - x - 1;
|
||||||
y = HEIGHT - y - 1;
|
y = HEIGHT - y - 1;
|
||||||
@@ -593,7 +599,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert a color to the buffer pixel format.
|
// Convert a color to the buffer pixel format.
|
||||||
BUFFERTYPE convert_color_(Color &color) const {
|
static BUFFERTYPE convert_color(const Color &color) {
|
||||||
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
|
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
|
||||||
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
|
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
|
||||||
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {
|
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {
|
||||||
|
@@ -3,6 +3,7 @@ import esphome.config_validation as cv
|
|||||||
|
|
||||||
from .amoled import CO5300
|
from .amoled import CO5300
|
||||||
from .ili import ILI9488_A
|
from .ili import ILI9488_A
|
||||||
|
from .jc import AXS15231
|
||||||
|
|
||||||
DriverChip(
|
DriverChip(
|
||||||
"WAVESHARE-4-TFT",
|
"WAVESHARE-4-TFT",
|
||||||
@@ -152,3 +153,12 @@ CO5300.extend(
|
|||||||
cs_pin=12,
|
cs_pin=12,
|
||||||
reset_pin=39,
|
reset_pin=39,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AXS15231.extend(
|
||||||
|
"WAVESHARE-ESP32-S3-TOUCH-LCD-3.49",
|
||||||
|
width=172,
|
||||||
|
height=640,
|
||||||
|
data_rate="80MHz",
|
||||||
|
cs_pin=9,
|
||||||
|
reset_pin=21,
|
||||||
|
)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
@@ -48,6 +49,7 @@ from .gpio import nrf52_pin_to_code # noqa
|
|||||||
CODEOWNERS = ["@tomaszduda23"]
|
CODEOWNERS = ["@tomaszduda23"]
|
||||||
AUTO_LOAD = ["zephyr"]
|
AUTO_LOAD = ["zephyr"]
|
||||||
IS_TARGET_PLATFORM = True
|
IS_TARGET_PLATFORM = True
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def set_core_data(config: ConfigType) -> ConfigType:
|
def set_core_data(config: ConfigType) -> ConfigType:
|
||||||
@@ -127,6 +129,10 @@ def _validate_mcumgr(config):
|
|||||||
def _final_validate(config):
|
def _final_validate(config):
|
||||||
if CONF_DFU in config:
|
if CONF_DFU in config:
|
||||||
_validate_mcumgr(config)
|
_validate_mcumgr(config)
|
||||||
|
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Selected generic Adafruit bootloader. The board might crash. Consider settings `bootloader:`"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
@@ -157,6 +163,13 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
|
if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
|
||||||
cg.add_define("USE_BOOTLOADER_MCUBOOT")
|
cg.add_define("USE_BOOTLOADER_MCUBOOT")
|
||||||
else:
|
else:
|
||||||
|
if "_sd" in config[KEY_BOOTLOADER]:
|
||||||
|
bootloader = config[KEY_BOOTLOADER].split("_")
|
||||||
|
sd_id = bootloader[2][2:]
|
||||||
|
cg.add_define("USE_SOFTDEVICE_ID", int(sd_id))
|
||||||
|
if (len(bootloader)) > 3:
|
||||||
|
sd_version = bootloader[3][1:]
|
||||||
|
cg.add_define("USE_SOFTDEVICE_VERSION", int(sd_version))
|
||||||
# make sure that firmware.zip is created
|
# make sure that firmware.zip is created
|
||||||
# for Adafruit_nRF52_Bootloader
|
# for Adafruit_nRF52_Bootloader
|
||||||
cg.add_platformio_option("board_upload.protocol", "nrfutil")
|
cg.add_platformio_option("board_upload.protocol", "nrfutil")
|
||||||
|
@@ -11,10 +11,18 @@ from .const import (
|
|||||||
BOARDS_ZEPHYR = {
|
BOARDS_ZEPHYR = {
|
||||||
"adafruit_itsybitsy_nrf52840": {
|
"adafruit_itsybitsy_nrf52840": {
|
||||||
KEY_BOOTLOADER: [
|
KEY_BOOTLOADER: [
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||||
|
BOOTLOADER_ADAFRUIT,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"xiao_ble": {
|
||||||
|
KEY_BOOTLOADER: [
|
||||||
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||||
BOOTLOADER_ADAFRUIT,
|
BOOTLOADER_ADAFRUIT,
|
||||||
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
BOOTLOADER_ADAFRUIT_NRF52_SD132,
|
||||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -66,6 +66,7 @@ from esphome.const import (
|
|||||||
DEVICE_CLASS_SPEED,
|
DEVICE_CLASS_SPEED,
|
||||||
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE_DELTA,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
DEVICE_CLASS_VOLTAGE,
|
DEVICE_CLASS_VOLTAGE,
|
||||||
@@ -130,6 +131,7 @@ DEVICE_CLASSES = [
|
|||||||
DEVICE_CLASS_SPEED,
|
DEVICE_CLASS_SPEED,
|
||||||
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE_DELTA,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
DEVICE_CLASS_VOLTAGE,
|
DEVICE_CLASS_VOLTAGE,
|
||||||
|
@@ -89,6 +89,7 @@ from esphome.const import (
|
|||||||
DEVICE_CLASS_SPEED,
|
DEVICE_CLASS_SPEED,
|
||||||
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE_DELTA,
|
||||||
DEVICE_CLASS_TIMESTAMP,
|
DEVICE_CLASS_TIMESTAMP,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
@@ -157,6 +158,7 @@ DEVICE_CLASSES = [
|
|||||||
DEVICE_CLASS_SPEED,
|
DEVICE_CLASS_SPEED,
|
||||||
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
DEVICE_CLASS_SULPHUR_DIOXIDE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_TEMPERATURE_DELTA,
|
||||||
DEVICE_CLASS_TIMESTAMP,
|
DEVICE_CLASS_TIMESTAMP,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||||
|
@@ -145,7 +145,7 @@ class BSDSocketImpl : public Socket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
|
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
|
||||||
return ::sendto(fd_, buf, len, flags, to, tolen);
|
return ::sendto(fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
|
||||||
}
|
}
|
||||||
|
|
||||||
int setblocking(bool blocking) override {
|
int setblocking(bool blocking) override {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from esphome import core
|
from esphome import core
|
||||||
from esphome.config_helpers import Extend, Remove, merge_config
|
from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
|
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
|
||||||
from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base
|
from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base
|
||||||
@@ -170,10 +170,10 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Merge substitutions in config, overriding with substitutions coming from command line:
|
# Merge substitutions in config, overriding with substitutions coming from command line:
|
||||||
substitutions = {
|
# Use merge_dicts_ordered to preserve OrderedDict type for move_to_end()
|
||||||
**config.get(CONF_SUBSTITUTIONS, {}),
|
substitutions = merge_dicts_ordered(
|
||||||
**(command_line_substitutions or {}),
|
config.get(CONF_SUBSTITUTIONS, {}), command_line_substitutions or {}
|
||||||
}
|
)
|
||||||
with cv.prepend_path("substitutions"):
|
with cv.prepend_path("substitutions"):
|
||||||
if not isinstance(substitutions, dict):
|
if not isinstance(substitutions, dict):
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
|
@@ -241,9 +241,14 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
|
|||||||
|
|
||||||
climate::ClimateTraits ThermostatClimate::traits() {
|
climate::ClimateTraits ThermostatClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(true);
|
|
||||||
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
|
||||||
|
if (this->supports_two_points_)
|
||||||
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
|
||||||
|
|
||||||
if (this->humidity_sensor_ != nullptr)
|
if (this->humidity_sensor_ != nullptr)
|
||||||
traits.set_supports_current_humidity(true);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||||
|
|
||||||
if (this->supports_auto_)
|
if (this->supports_auto_)
|
||||||
traits.add_supported_mode(climate::CLIMATE_MODE_AUTO);
|
traits.add_supported_mode(climate::CLIMATE_MODE_AUTO);
|
||||||
@@ -294,9 +299,6 @@ climate::ClimateTraits ThermostatClimate::traits() {
|
|||||||
for (auto &it : this->custom_preset_config_) {
|
for (auto &it : this->custom_preset_config_) {
|
||||||
traits.add_supported_custom_preset(it.first);
|
traits.add_supported_custom_preset(it.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
traits.set_supports_two_point_target_temperature(this->supports_two_points_);
|
|
||||||
traits.set_supports_action(true);
|
|
||||||
return traits;
|
return traits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -14,6 +14,7 @@ MODELS = {
|
|||||||
"GENERIC": Model.MODEL_GENERIC,
|
"GENERIC": Model.MODEL_GENERIC,
|
||||||
"RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C,
|
"RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C,
|
||||||
"RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F,
|
"RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F,
|
||||||
|
"RAS-2819T": Model.MODEL_RAS_2819T,
|
||||||
}
|
}
|
||||||
|
|
||||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend(
|
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend(
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
#include "toshiba.h"
|
#include "toshiba.h"
|
||||||
|
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -97,6 +98,282 @@ const std::vector<uint8_t> RAC_PT1411HWRU_TEMPERATURE_F{0x10, 0x30, 0x00, 0x20,
|
|||||||
0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C,
|
0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C,
|
||||||
0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
|
0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
|
||||||
|
|
||||||
|
// RAS-2819T protocol constants
|
||||||
|
const uint16_t RAS_2819T_HEADER1 = 0xC23D;
|
||||||
|
const uint8_t RAS_2819T_HEADER2 = 0xD5;
|
||||||
|
const uint8_t RAS_2819T_MESSAGE_LENGTH = 6;
|
||||||
|
|
||||||
|
// RAS-2819T fan speed codes for rc_code_1 (bytes 2-3)
|
||||||
|
const uint16_t RAS_2819T_FAN_AUTO = 0xBF40;
|
||||||
|
const uint16_t RAS_2819T_FAN_QUIET = 0xFF00;
|
||||||
|
const uint16_t RAS_2819T_FAN_LOW = 0x9F60;
|
||||||
|
const uint16_t RAS_2819T_FAN_MEDIUM = 0x5FA0;
|
||||||
|
const uint16_t RAS_2819T_FAN_HIGH = 0x3FC0;
|
||||||
|
|
||||||
|
// RAS-2819T fan speed codes for rc_code_2 (byte 1)
|
||||||
|
const uint8_t RAS_2819T_FAN2_AUTO = 0x66;
|
||||||
|
const uint8_t RAS_2819T_FAN2_QUIET = 0x01;
|
||||||
|
const uint8_t RAS_2819T_FAN2_LOW = 0x28;
|
||||||
|
const uint8_t RAS_2819T_FAN2_MEDIUM = 0x3C;
|
||||||
|
const uint8_t RAS_2819T_FAN2_HIGH = 0x50;
|
||||||
|
|
||||||
|
// RAS-2819T second packet suffix bytes for rc_code_2 (bytes 3-5)
|
||||||
|
// These are fixed patterns, not actual checksums
|
||||||
|
struct Ras2819tPacketSuffix {
|
||||||
|
uint8_t byte3;
|
||||||
|
uint8_t byte4;
|
||||||
|
uint8_t byte5;
|
||||||
|
};
|
||||||
|
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_AUTO{0x00, 0x02, 0x3D};
|
||||||
|
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_QUIET{0x00, 0x02, 0xD8};
|
||||||
|
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_LOW{0x00, 0x02, 0xFF};
|
||||||
|
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_MEDIUM{0x00, 0x02, 0x13};
|
||||||
|
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_HIGH{0x00, 0x02, 0x27};
|
||||||
|
|
||||||
|
// RAS-2819T swing toggle command
|
||||||
|
const uint64_t RAS_2819T_SWING_TOGGLE = 0xC23D6B94E01F;
|
||||||
|
|
||||||
|
// RAS-2819T single-packet commands
|
||||||
|
const uint64_t RAS_2819T_POWER_OFF_COMMAND = 0xC23D7B84E01F;
|
||||||
|
|
||||||
|
// RAS-2819T known valid command patterns for validation
|
||||||
|
const std::array<uint64_t, 2> RAS_2819T_VALID_SINGLE_COMMANDS = {
|
||||||
|
RAS_2819T_POWER_OFF_COMMAND, // Power off
|
||||||
|
RAS_2819T_SWING_TOGGLE, // Swing toggle
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint16_t RAS_2819T_VALID_HEADER1 = 0xC23D;
|
||||||
|
const uint8_t RAS_2819T_VALID_HEADER2 = 0xD5;
|
||||||
|
|
||||||
|
const uint8_t RAS_2819T_DRY_BYTE2 = 0x1F;
|
||||||
|
const uint8_t RAS_2819T_DRY_BYTE3 = 0xE0;
|
||||||
|
const uint8_t RAS_2819T_DRY_TEMP_OFFSET = 0x24;
|
||||||
|
|
||||||
|
const uint8_t RAS_2819T_AUTO_BYTE2 = 0x1F;
|
||||||
|
const uint8_t RAS_2819T_AUTO_BYTE3 = 0xE0;
|
||||||
|
const uint8_t RAS_2819T_AUTO_TEMP_OFFSET = 0x08;
|
||||||
|
|
||||||
|
const uint8_t RAS_2819T_FAN_ONLY_TEMP = 0xE4;
|
||||||
|
const uint8_t RAS_2819T_FAN_ONLY_TEMP_INV = 0x1B;
|
||||||
|
|
||||||
|
const uint8_t RAS_2819T_HEAT_TEMP_OFFSET = 0x0C;
|
||||||
|
|
||||||
|
// RAS-2819T second packet fixed values
|
||||||
|
const uint8_t RAS_2819T_AUTO_DRY_FAN_BYTE = 0x65;
|
||||||
|
const uint8_t RAS_2819T_AUTO_DRY_SUFFIX = 0x3A;
|
||||||
|
const uint8_t RAS_2819T_HEAT_SUFFIX = 0x3B;
|
||||||
|
|
||||||
|
// RAS-2819T temperature codes for 18-30°C
|
||||||
|
static const uint8_t RAS_2819T_TEMP_CODES[] = {
|
||||||
|
0x10, // 18°C
|
||||||
|
0x30, // 19°C
|
||||||
|
0x20, // 20°C
|
||||||
|
0x60, // 21°C
|
||||||
|
0x70, // 22°C
|
||||||
|
0x50, // 23°C
|
||||||
|
0x40, // 24°C
|
||||||
|
0xC0, // 25°C
|
||||||
|
0xD0, // 26°C
|
||||||
|
0x90, // 27°C
|
||||||
|
0x80, // 28°C
|
||||||
|
0xA0, // 29°C
|
||||||
|
0xB0 // 30°C
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions for RAS-2819T protocol
|
||||||
|
//
|
||||||
|
// ===== RAS-2819T PROTOCOL DOCUMENTATION =====
|
||||||
|
//
|
||||||
|
// The RAS-2819T uses a two-packet IR protocol with some exceptions for simple commands.
|
||||||
|
//
|
||||||
|
// PACKET STRUCTURE:
|
||||||
|
// All packets are 6 bytes (48 bits) transmitted with standard Toshiba timing.
|
||||||
|
//
|
||||||
|
// TWO-PACKET COMMANDS (Mode/Temperature/Fan changes):
|
||||||
|
//
|
||||||
|
// First Packet (rc_code_1): [C2 3D] [FAN_HI FAN_LO] [TEMP] [~TEMP]
|
||||||
|
// Byte 0-1: Header (always 0xC23D)
|
||||||
|
// Byte 2-3: Fan speed encoding (varies by mode, see fan tables below)
|
||||||
|
// Byte 4: Temperature + mode encoding
|
||||||
|
// Byte 5: Bitwise complement of temperature byte
|
||||||
|
//
|
||||||
|
// Second Packet (rc_code_2): [D5] [FAN2] [00] [SUF1] [SUF2] [SUF3]
|
||||||
|
// Byte 0: Header (always 0xD5)
|
||||||
|
// Byte 1: Fan speed secondary encoding
|
||||||
|
// Byte 2: Always 0x00
|
||||||
|
// Byte 3-5: Fixed suffix pattern (depends on fan speed and mode)
|
||||||
|
//
|
||||||
|
// TEMPERATURE ENCODING:
|
||||||
|
// Base temp codes: 18°C=0x10, 19°C=0x30, 20°C=0x20, 21°C=0x60, 22°C=0x70,
|
||||||
|
// 23°C=0x50, 24°C=0x40, 25°C=0xC0, 26°C=0xD0, 27°C=0x90,
|
||||||
|
// 28°C=0x80, 29°C=0xA0, 30°C=0xB0
|
||||||
|
// Mode offsets added to base temp:
|
||||||
|
// COOL: No offset
|
||||||
|
// HEAT: +0x0C (e.g., 24°C heat = 0x40 | 0x0C = 0x4C)
|
||||||
|
// AUTO: +0x08 (e.g., 24°C auto = 0x40 | 0x08 = 0x48)
|
||||||
|
// DRY: +0x24 (e.g., 24°C dry = 0x40 | 0x24 = 0x64)
|
||||||
|
//
|
||||||
|
// FAN SPEED ENCODING (First packet bytes 2-3):
|
||||||
|
// AUTO: 0xBF40, QUIET: 0xFF00, LOW: 0x9F60, MEDIUM: 0x5FA0, HIGH: 0x3FC0
|
||||||
|
// Special cases: AUTO/DRY modes use 0x1FE0 instead
|
||||||
|
//
|
||||||
|
// SINGLE-PACKET COMMANDS:
|
||||||
|
// Power Off: 0xC23D7B84E01F (6 bytes, no second packet)
|
||||||
|
// Swing Toggle: 0xC23D6B94E01F (6 bytes, no second packet)
|
||||||
|
//
|
||||||
|
// MODE DETECTION (from first packet):
|
||||||
|
// - Check bytes 2-3: if 0x7B84 → OFF mode
|
||||||
|
// - Check bytes 2-3: if 0x1FE0 → AUTO/DRY/low-temp-COOL (distinguish by temp code)
|
||||||
|
// - Otherwise: COOL/HEAT/FAN_ONLY (distinguish by temp code and byte 5)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fan speed encoding for RAS-2819T first packet (rc_code_1, bytes 2-3)
|
||||||
|
*/
|
||||||
|
static uint16_t get_ras_2819t_fan_code(climate::ClimateFanMode fan_mode) {
|
||||||
|
switch (fan_mode) {
|
||||||
|
case climate::CLIMATE_FAN_QUIET:
|
||||||
|
return RAS_2819T_FAN_QUIET;
|
||||||
|
case climate::CLIMATE_FAN_LOW:
|
||||||
|
return RAS_2819T_FAN_LOW;
|
||||||
|
case climate::CLIMATE_FAN_MEDIUM:
|
||||||
|
return RAS_2819T_FAN_MEDIUM;
|
||||||
|
case climate::CLIMATE_FAN_HIGH:
|
||||||
|
return RAS_2819T_FAN_HIGH;
|
||||||
|
case climate::CLIMATE_FAN_AUTO:
|
||||||
|
default:
|
||||||
|
return RAS_2819T_FAN_AUTO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fan speed encoding for RAS-2819T rc_code_2 packet (second packet)
|
||||||
|
*/
|
||||||
|
struct Ras2819tSecondPacketCodes {
|
||||||
|
uint8_t fan_byte;
|
||||||
|
Ras2819tPacketSuffix suffix;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Ras2819tSecondPacketCodes get_ras_2819t_second_packet_codes(climate::ClimateFanMode fan_mode) {
|
||||||
|
switch (fan_mode) {
|
||||||
|
case climate::CLIMATE_FAN_QUIET:
|
||||||
|
return {RAS_2819T_FAN2_QUIET, RAS_2819T_SUFFIX_QUIET};
|
||||||
|
case climate::CLIMATE_FAN_LOW:
|
||||||
|
return {RAS_2819T_FAN2_LOW, RAS_2819T_SUFFIX_LOW};
|
||||||
|
case climate::CLIMATE_FAN_MEDIUM:
|
||||||
|
return {RAS_2819T_FAN2_MEDIUM, RAS_2819T_SUFFIX_MEDIUM};
|
||||||
|
case climate::CLIMATE_FAN_HIGH:
|
||||||
|
return {RAS_2819T_FAN2_HIGH, RAS_2819T_SUFFIX_HIGH};
|
||||||
|
case climate::CLIMATE_FAN_AUTO:
|
||||||
|
default:
|
||||||
|
return {RAS_2819T_FAN2_AUTO, RAS_2819T_SUFFIX_AUTO};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get temperature code for RAS-2819T protocol
|
||||||
|
*/
|
||||||
|
static uint8_t get_ras_2819t_temp_code(float temperature) {
|
||||||
|
int temp_index = static_cast<int>(temperature) - 18;
|
||||||
|
if (temp_index < 0 || temp_index >= static_cast<int>(sizeof(RAS_2819T_TEMP_CODES))) {
|
||||||
|
ESP_LOGW(TAG, "Temperature %.1f°C out of range [18-30°C], defaulting to 24°C", temperature);
|
||||||
|
return 0x40; // Default to 24°C
|
||||||
|
}
|
||||||
|
|
||||||
|
return RAS_2819T_TEMP_CODES[temp_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode temperature from RAS-2819T temp code
|
||||||
|
*/
|
||||||
|
static float decode_ras_2819t_temperature(uint8_t temp_code) {
|
||||||
|
uint8_t base_temp_code = temp_code & 0xF0;
|
||||||
|
|
||||||
|
// Find the code in the temperature array
|
||||||
|
for (size_t temp_index = 0; temp_index < sizeof(RAS_2819T_TEMP_CODES); temp_index++) {
|
||||||
|
if (RAS_2819T_TEMP_CODES[temp_index] == base_temp_code) {
|
||||||
|
return static_cast<float>(temp_index + 18); // 18°C is the minimum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGW(TAG, "Unknown temp code: 0x%02X, defaulting to 24°C", base_temp_code);
|
||||||
|
return 24.0f; // Default to 24°C
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode fan speed from RAS-2819T IR codes
|
||||||
|
*/
|
||||||
|
static climate::ClimateFanMode decode_ras_2819t_fan_mode(uint16_t fan_code) {
|
||||||
|
switch (fan_code) {
|
||||||
|
case RAS_2819T_FAN_QUIET:
|
||||||
|
return climate::CLIMATE_FAN_QUIET;
|
||||||
|
case RAS_2819T_FAN_LOW:
|
||||||
|
return climate::CLIMATE_FAN_LOW;
|
||||||
|
case RAS_2819T_FAN_MEDIUM:
|
||||||
|
return climate::CLIMATE_FAN_MEDIUM;
|
||||||
|
case RAS_2819T_FAN_HIGH:
|
||||||
|
return climate::CLIMATE_FAN_HIGH;
|
||||||
|
case RAS_2819T_FAN_AUTO:
|
||||||
|
default:
|
||||||
|
return climate::CLIMATE_FAN_AUTO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate RAS-2819T IR command structure and content
|
||||||
|
*/
|
||||||
|
static bool is_valid_ras_2819t_command(uint64_t rc_code_1, uint64_t rc_code_2 = 0) {
|
||||||
|
// Check header of first packet
|
||||||
|
uint16_t header1 = (rc_code_1 >> 32) & 0xFFFF;
|
||||||
|
if (header1 != RAS_2819T_VALID_HEADER1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single packet commands
|
||||||
|
if (rc_code_2 == 0) {
|
||||||
|
for (uint64_t valid_cmd : RAS_2819T_VALID_SINGLE_COMMANDS) {
|
||||||
|
if (rc_code_1 == valid_cmd) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Additional validation for unknown single packets
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two-packet commands - validate second packet header
|
||||||
|
uint8_t header2 = (rc_code_2 >> 40) & 0xFF;
|
||||||
|
if (header2 != RAS_2819T_VALID_HEADER2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate temperature complement in first packet (byte 4 should be ~byte 5)
|
||||||
|
uint8_t temp_byte = (rc_code_1 >> 8) & 0xFF;
|
||||||
|
uint8_t temp_complement = rc_code_1 & 0xFF;
|
||||||
|
if (temp_byte != static_cast<uint8_t>(~temp_complement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate fan speed combinations make sense
|
||||||
|
uint16_t fan_code = (rc_code_1 >> 16) & 0xFFFF;
|
||||||
|
uint8_t fan2_byte = (rc_code_2 >> 32) & 0xFF;
|
||||||
|
|
||||||
|
// Check if fan codes are from known valid patterns
|
||||||
|
bool valid_fan_combo = false;
|
||||||
|
if (fan_code == RAS_2819T_FAN_AUTO && fan2_byte == RAS_2819T_FAN2_AUTO)
|
||||||
|
valid_fan_combo = true;
|
||||||
|
if (fan_code == RAS_2819T_FAN_QUIET && fan2_byte == RAS_2819T_FAN2_QUIET)
|
||||||
|
valid_fan_combo = true;
|
||||||
|
if (fan_code == RAS_2819T_FAN_LOW && fan2_byte == RAS_2819T_FAN2_LOW)
|
||||||
|
valid_fan_combo = true;
|
||||||
|
if (fan_code == RAS_2819T_FAN_MEDIUM && fan2_byte == RAS_2819T_FAN2_MEDIUM)
|
||||||
|
valid_fan_combo = true;
|
||||||
|
if (fan_code == RAS_2819T_FAN_HIGH && fan2_byte == RAS_2819T_FAN2_HIGH)
|
||||||
|
valid_fan_combo = true;
|
||||||
|
if (fan_code == 0x1FE0 && fan2_byte == RAS_2819T_AUTO_DRY_FAN_BYTE)
|
||||||
|
valid_fan_combo = true; // AUTO/DRY
|
||||||
|
|
||||||
|
return valid_fan_combo;
|
||||||
|
}
|
||||||
|
|
||||||
void ToshibaClimate::setup() {
|
void ToshibaClimate::setup() {
|
||||||
if (this->sensor_) {
|
if (this->sensor_) {
|
||||||
this->sensor_->add_on_state_callback([this](float state) {
|
this->sensor_->add_on_state_callback([this](float state) {
|
||||||
@@ -126,16 +403,43 @@ void ToshibaClimate::setup() {
|
|||||||
this->minimum_temperature_ = this->temperature_min_();
|
this->minimum_temperature_ = this->temperature_min_();
|
||||||
this->maximum_temperature_ = this->temperature_max_();
|
this->maximum_temperature_ = this->temperature_max_();
|
||||||
this->swing_modes_ = this->toshiba_swing_modes_();
|
this->swing_modes_ = this->toshiba_swing_modes_();
|
||||||
|
|
||||||
|
// Ensure swing mode is always initialized to a valid value
|
||||||
|
if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) {
|
||||||
|
// No swing support for this model or current swing mode not supported, reset to OFF
|
||||||
|
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure mode is valid - ESPHome should only use standard climate modes
|
||||||
|
if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_HEAT &&
|
||||||
|
this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT_COOL &&
|
||||||
|
this->mode != climate::CLIMATE_MODE_DRY && this->mode != climate::CLIMATE_MODE_FAN_ONLY) {
|
||||||
|
ESP_LOGW(TAG, "Invalid mode detected during setup, resetting to OFF");
|
||||||
|
this->mode = climate::CLIMATE_MODE_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure fan mode is valid
|
||||||
|
if (!this->fan_mode.has_value()) {
|
||||||
|
ESP_LOGW(TAG, "Fan mode not set during setup, defaulting to AUTO");
|
||||||
|
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||||
|
}
|
||||||
|
|
||||||
// Never send nan to HA
|
// Never send nan to HA
|
||||||
if (std::isnan(this->target_temperature))
|
if (std::isnan(this->target_temperature))
|
||||||
this->target_temperature = 24;
|
this->target_temperature = 24;
|
||||||
|
// Log final state for debugging HA errors
|
||||||
|
ESP_LOGV(TAG, "Setup complete - Mode: %d, Fan: %s, Swing: %d, Temp: %.1f", static_cast<int>(this->mode),
|
||||||
|
this->fan_mode.has_value() ? std::to_string(static_cast<int>(this->fan_mode.value())).c_str() : "NONE",
|
||||||
|
static_cast<int>(this->swing_mode), this->target_temperature);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToshibaClimate::transmit_state() {
|
void ToshibaClimate::transmit_state() {
|
||||||
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) {
|
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) {
|
||||||
transmit_rac_pt1411hwru_();
|
this->transmit_rac_pt1411hwru_();
|
||||||
|
} else if (this->model_ == MODEL_RAS_2819T) {
|
||||||
|
this->transmit_ras_2819t_();
|
||||||
} else {
|
} else {
|
||||||
transmit_generic_();
|
this->transmit_generic_();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +534,7 @@ void ToshibaClimate::transmit_generic_() {
|
|||||||
auto transmit = this->transmitter_->transmit();
|
auto transmit = this->transmitter_->transmit();
|
||||||
auto *data = transmit.get_data();
|
auto *data = transmit.get_data();
|
||||||
|
|
||||||
encode_(data, message, message_length, 1);
|
this->encode_(data, message, message_length, 1);
|
||||||
|
|
||||||
transmit.perform();
|
transmit.perform();
|
||||||
}
|
}
|
||||||
@@ -348,15 +652,12 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
|
|||||||
message[11] += message[index];
|
message[11] += message[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ESP_LOGV(TAG, "*** Generated codes: 0x%.2X%.2X%.2X%.2X%.2X%.2X 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1],
|
|
||||||
message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9], message[10],
|
|
||||||
message[11]);
|
|
||||||
|
|
||||||
// load first block of IR code and repeat it once
|
// load first block of IR code and repeat it once
|
||||||
encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
this->encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
||||||
// load second block of IR code, if present
|
// load second block of IR code, if present
|
||||||
if (message[6] != 0) {
|
if (message[6] != 0) {
|
||||||
encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0);
|
this->encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
transmit.perform();
|
transmit.perform();
|
||||||
@@ -366,19 +667,19 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
|
|||||||
data->space(TOSHIBA_PACKET_SPACE);
|
data->space(TOSHIBA_PACKET_SPACE);
|
||||||
switch (this->swing_mode) {
|
switch (this->swing_mode) {
|
||||||
case climate::CLIMATE_SWING_VERTICAL:
|
case climate::CLIMATE_SWING_VERTICAL:
|
||||||
encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
this->encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case climate::CLIMATE_SWING_OFF:
|
case climate::CLIMATE_SWING_OFF:
|
||||||
default:
|
default:
|
||||||
encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
this->encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
data->space(TOSHIBA_PACKET_SPACE);
|
data->space(TOSHIBA_PACKET_SPACE);
|
||||||
transmit.perform();
|
transmit.perform();
|
||||||
|
|
||||||
if (this->sensor_) {
|
if (this->sensor_) {
|
||||||
transmit_rac_pt1411hwru_temp_(true, false);
|
this->transmit_rac_pt1411hwru_temp_(true, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,15 +731,217 @@ void ToshibaClimate::transmit_rac_pt1411hwru_temp_(const bool cs_state, const bo
|
|||||||
// Byte 5: Footer lower/bitwise complement of byte 4
|
// Byte 5: Footer lower/bitwise complement of byte 4
|
||||||
message[5] = ~message[4];
|
message[5] = ~message[4];
|
||||||
|
|
||||||
ESP_LOGV(TAG, "*** Generated code: 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], message[2], message[3],
|
|
||||||
message[4], message[5]);
|
|
||||||
// load IR code and repeat it once
|
// load IR code and repeat it once
|
||||||
encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
this->encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
||||||
|
|
||||||
transmit.perform();
|
transmit.perform();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ToshibaClimate::transmit_ras_2819t_() {
|
||||||
|
// Handle swing mode transmission for RAS-2819T
|
||||||
|
// Note: RAS-2819T uses a toggle command, so we need to track state changes
|
||||||
|
|
||||||
|
// Check if ONLY swing mode changed (and no other climate parameters)
|
||||||
|
bool swing_changed = (this->swing_mode != this->last_swing_mode_);
|
||||||
|
bool mode_changed = (this->mode != this->last_mode_);
|
||||||
|
bool fan_changed = (this->fan_mode != this->last_fan_mode_);
|
||||||
|
bool temp_changed = (abs(this->target_temperature - this->last_target_temperature_) > 0.1f);
|
||||||
|
|
||||||
|
bool only_swing_changed = swing_changed && !mode_changed && !fan_changed && !temp_changed;
|
||||||
|
|
||||||
|
if (only_swing_changed) {
|
||||||
|
// Send ONLY swing toggle command (like the physical remote does)
|
||||||
|
auto swing_transmit = this->transmitter_->transmit();
|
||||||
|
auto *swing_data = swing_transmit.get_data();
|
||||||
|
|
||||||
|
// Convert toggle command to bytes for transmission
|
||||||
|
uint8_t swing_message[RAS_2819T_MESSAGE_LENGTH];
|
||||||
|
swing_message[0] = (RAS_2819T_SWING_TOGGLE >> 40) & 0xFF;
|
||||||
|
swing_message[1] = (RAS_2819T_SWING_TOGGLE >> 32) & 0xFF;
|
||||||
|
swing_message[2] = (RAS_2819T_SWING_TOGGLE >> 24) & 0xFF;
|
||||||
|
swing_message[3] = (RAS_2819T_SWING_TOGGLE >> 16) & 0xFF;
|
||||||
|
swing_message[4] = (RAS_2819T_SWING_TOGGLE >> 8) & 0xFF;
|
||||||
|
swing_message[5] = RAS_2819T_SWING_TOGGLE & 0xFF;
|
||||||
|
|
||||||
|
// Use single packet transmission WITH repeat (like regular commands)
|
||||||
|
this->encode_(swing_data, swing_message, RAS_2819T_MESSAGE_LENGTH, 1);
|
||||||
|
swing_transmit.perform();
|
||||||
|
|
||||||
|
// Update all state tracking
|
||||||
|
this->last_swing_mode_ = this->swing_mode;
|
||||||
|
this->last_mode_ = this->mode;
|
||||||
|
this->last_fan_mode_ = this->fan_mode;
|
||||||
|
this->last_target_temperature_ = this->target_temperature;
|
||||||
|
|
||||||
|
// Immediately publish the state change to Home Assistant
|
||||||
|
this->publish_state();
|
||||||
|
|
||||||
|
return; // Exit early - don't send climate command
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, send the regular climate command (temperature/mode/fan)
|
||||||
|
uint8_t message1[RAS_2819T_MESSAGE_LENGTH] = {0};
|
||||||
|
uint8_t message2[RAS_2819T_MESSAGE_LENGTH] = {0};
|
||||||
|
float temperature =
|
||||||
|
clamp<float>(this->target_temperature, TOSHIBA_RAS_2819T_TEMP_C_MIN, TOSHIBA_RAS_2819T_TEMP_C_MAX);
|
||||||
|
|
||||||
|
// Build first packet (RAS_2819T_HEADER1 + 4 bytes)
|
||||||
|
message1[0] = (RAS_2819T_HEADER1 >> 8) & 0xFF;
|
||||||
|
message1[1] = RAS_2819T_HEADER1 & 0xFF;
|
||||||
|
|
||||||
|
// Handle OFF mode
|
||||||
|
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||||
|
// Extract bytes from power off command constant
|
||||||
|
message1[2] = (RAS_2819T_POWER_OFF_COMMAND >> 24) & 0xFF;
|
||||||
|
message1[3] = (RAS_2819T_POWER_OFF_COMMAND >> 16) & 0xFF;
|
||||||
|
message1[4] = (RAS_2819T_POWER_OFF_COMMAND >> 8) & 0xFF;
|
||||||
|
message1[5] = RAS_2819T_POWER_OFF_COMMAND & 0xFF;
|
||||||
|
// No second packet for OFF
|
||||||
|
} else {
|
||||||
|
// Get temperature and fan encoding
|
||||||
|
uint8_t temp_code = get_ras_2819t_temp_code(temperature);
|
||||||
|
|
||||||
|
// Get fan speed encoding for rc_code_1
|
||||||
|
climate::ClimateFanMode effective_fan_mode = this->fan_mode.value();
|
||||||
|
|
||||||
|
// Dry mode only supports AUTO fan speed
|
||||||
|
if (this->mode == climate::CLIMATE_MODE_DRY) {
|
||||||
|
effective_fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||||
|
if (this->fan_mode.value() != climate::CLIMATE_FAN_AUTO) {
|
||||||
|
ESP_LOGW(TAG, "Dry mode only supports AUTO fan speed, forcing AUTO");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t fan_code = get_ras_2819t_fan_code(effective_fan_mode);
|
||||||
|
|
||||||
|
// Mode and temperature encoding
|
||||||
|
switch (this->mode) {
|
||||||
|
case climate::CLIMATE_MODE_COOL:
|
||||||
|
// All cooling temperatures support fan speed control
|
||||||
|
message1[2] = (fan_code >> 8) & 0xFF;
|
||||||
|
message1[3] = fan_code & 0xFF;
|
||||||
|
message1[4] = temp_code;
|
||||||
|
message1[5] = ~temp_code;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case climate::CLIMATE_MODE_HEAT:
|
||||||
|
// Heating supports fan speed control
|
||||||
|
message1[2] = (fan_code >> 8) & 0xFF;
|
||||||
|
message1[3] = fan_code & 0xFF;
|
||||||
|
// Heat mode adds offset to temperature code
|
||||||
|
message1[4] = temp_code | RAS_2819T_HEAT_TEMP_OFFSET;
|
||||||
|
message1[5] = ~(temp_code | RAS_2819T_HEAT_TEMP_OFFSET);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||||
|
// Auto mode uses fixed encoding
|
||||||
|
message1[2] = RAS_2819T_AUTO_BYTE2;
|
||||||
|
message1[3] = RAS_2819T_AUTO_BYTE3;
|
||||||
|
message1[4] = temp_code | RAS_2819T_AUTO_TEMP_OFFSET;
|
||||||
|
message1[5] = ~(temp_code | RAS_2819T_AUTO_TEMP_OFFSET);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case climate::CLIMATE_MODE_DRY:
|
||||||
|
// Dry mode uses fixed encoding and forces AUTO fan
|
||||||
|
message1[2] = RAS_2819T_DRY_BYTE2;
|
||||||
|
message1[3] = RAS_2819T_DRY_BYTE3;
|
||||||
|
message1[4] = temp_code | RAS_2819T_DRY_TEMP_OFFSET;
|
||||||
|
message1[5] = ~message1[4];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||||
|
// Fan only mode supports fan speed control
|
||||||
|
message1[2] = (fan_code >> 8) & 0xFF;
|
||||||
|
message1[3] = fan_code & 0xFF;
|
||||||
|
message1[4] = RAS_2819T_FAN_ONLY_TEMP;
|
||||||
|
message1[5] = RAS_2819T_FAN_ONLY_TEMP_INV;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Default case supports fan speed control
|
||||||
|
message1[2] = (fan_code >> 8) & 0xFF;
|
||||||
|
message1[3] = fan_code & 0xFF;
|
||||||
|
message1[4] = temp_code;
|
||||||
|
message1[5] = ~temp_code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build second packet (RAS_2819T_HEADER2 + 4 bytes)
|
||||||
|
message2[0] = RAS_2819T_HEADER2;
|
||||||
|
|
||||||
|
// Get fan speed encoding for rc_code_2
|
||||||
|
Ras2819tSecondPacketCodes second_packet_codes = get_ras_2819t_second_packet_codes(effective_fan_mode);
|
||||||
|
|
||||||
|
// Determine header byte 2 and fan encoding based on mode
|
||||||
|
switch (this->mode) {
|
||||||
|
case climate::CLIMATE_MODE_COOL:
|
||||||
|
message2[1] = second_packet_codes.fan_byte;
|
||||||
|
message2[2] = 0x00;
|
||||||
|
message2[3] = second_packet_codes.suffix.byte3;
|
||||||
|
message2[4] = second_packet_codes.suffix.byte4;
|
||||||
|
message2[5] = second_packet_codes.suffix.byte5;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case climate::CLIMATE_MODE_HEAT:
|
||||||
|
message2[1] = second_packet_codes.fan_byte;
|
||||||
|
message2[2] = 0x00;
|
||||||
|
message2[3] = second_packet_codes.suffix.byte3;
|
||||||
|
message2[4] = 0x00;
|
||||||
|
message2[5] = RAS_2819T_HEAT_SUFFIX;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||||
|
case climate::CLIMATE_MODE_DRY:
|
||||||
|
// Auto/Dry modes use fixed values regardless of fan setting
|
||||||
|
message2[1] = RAS_2819T_AUTO_DRY_FAN_BYTE;
|
||||||
|
message2[2] = 0x00;
|
||||||
|
message2[3] = 0x00;
|
||||||
|
message2[4] = 0x00;
|
||||||
|
message2[5] = RAS_2819T_AUTO_DRY_SUFFIX;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||||
|
message2[1] = second_packet_codes.fan_byte;
|
||||||
|
message2[2] = 0x00;
|
||||||
|
message2[3] = second_packet_codes.suffix.byte3;
|
||||||
|
message2[4] = 0x00;
|
||||||
|
message2[5] = RAS_2819T_HEAT_SUFFIX;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
message2[1] = second_packet_codes.fan_byte;
|
||||||
|
message2[2] = 0x00;
|
||||||
|
message2[3] = second_packet_codes.suffix.byte3;
|
||||||
|
message2[4] = second_packet_codes.suffix.byte4;
|
||||||
|
message2[5] = second_packet_codes.suffix.byte5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log final messages being transmitted
|
||||||
|
|
||||||
|
// Transmit using proper Toshiba protocol timing
|
||||||
|
auto transmit = this->transmitter_->transmit();
|
||||||
|
auto *data = transmit.get_data();
|
||||||
|
|
||||||
|
// Use existing Toshiba encode function for proper timing
|
||||||
|
this->encode_(data, message1, RAS_2819T_MESSAGE_LENGTH, 1);
|
||||||
|
|
||||||
|
if (this->mode != climate::CLIMATE_MODE_OFF) {
|
||||||
|
// Send second packet with gap
|
||||||
|
this->encode_(data, message2, RAS_2819T_MESSAGE_LENGTH, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
transmit.perform();
|
||||||
|
|
||||||
|
// Update all state tracking after successful transmission
|
||||||
|
this->last_swing_mode_ = this->swing_mode;
|
||||||
|
this->last_mode_ = this->mode;
|
||||||
|
this->last_fan_mode_ = this->fan_mode;
|
||||||
|
this->last_target_temperature_ = this->target_temperature;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) {
|
uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) {
|
||||||
const std::vector<uint8_t> header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER,
|
const std::vector<uint8_t> header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER,
|
||||||
RAC_PT1411HWRU_SWING_HEADER};
|
RAC_PT1411HWRU_SWING_HEADER};
|
||||||
@@ -464,11 +967,11 @@ bool ToshibaClimate::compare_rac_pt1411hwru_packets_(const uint8_t *message1, co
|
|||||||
bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) {
|
bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) {
|
||||||
uint8_t checksum = 0;
|
uint8_t checksum = 0;
|
||||||
|
|
||||||
switch (is_valid_rac_pt1411hwru_header_(message)) {
|
switch (this->is_valid_rac_pt1411hwru_header_(message)) {
|
||||||
case RAC_PT1411HWRU_MESSAGE_HEADER0:
|
case RAC_PT1411HWRU_MESSAGE_HEADER0:
|
||||||
case RAC_PT1411HWRU_CS_HEADER:
|
case RAC_PT1411HWRU_CS_HEADER:
|
||||||
case RAC_PT1411HWRU_SWING_HEADER:
|
case RAC_PT1411HWRU_SWING_HEADER:
|
||||||
if (is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast<uint8_t>(~message[3])) &&
|
if (this->is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast<uint8_t>(~message[3])) &&
|
||||||
(message[4] == static_cast<uint8_t>(~message[5]))) {
|
(message[4] == static_cast<uint8_t>(~message[5]))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -490,7 +993,103 @@ bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ToshibaClimate::process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data) {
|
||||||
|
// Check for power-off command (single packet)
|
||||||
|
if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_POWER_OFF_COMMAND) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_OFF;
|
||||||
|
ESP_LOGI(TAG, "Mode: OFF");
|
||||||
|
this->publish_state();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for swing toggle command (single packet)
|
||||||
|
if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_SWING_TOGGLE) {
|
||||||
|
// Toggle swing mode
|
||||||
|
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) {
|
||||||
|
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||||
|
ESP_LOGI(TAG, "Swing: OFF");
|
||||||
|
} else {
|
||||||
|
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||||
|
ESP_LOGI(TAG, "Swing: VERTICAL");
|
||||||
|
}
|
||||||
|
this->publish_state();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular two-packet commands (mode/temperature/fan changes)
|
||||||
|
if (toshiba_data.rc_code_2 != 0) {
|
||||||
|
// Convert to byte array for easier processing
|
||||||
|
uint8_t message1[6], message2[6];
|
||||||
|
for (uint8_t i = 0; i < 6; i++) {
|
||||||
|
message1[i] = (toshiba_data.rc_code_1 >> (40 - i * 8)) & 0xFF;
|
||||||
|
message2[i] = (toshiba_data.rc_code_2 >> (40 - i * 8)) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the protocol using message1 (rc_code_1)
|
||||||
|
uint8_t temp_code = message1[4];
|
||||||
|
|
||||||
|
// Decode mode - check bytes 2-3 pattern and temperature code
|
||||||
|
if ((message1[2] == 0x7B) && (message1[3] == 0x84)) {
|
||||||
|
// OFF mode has specific pattern
|
||||||
|
this->mode = climate::CLIMATE_MODE_OFF;
|
||||||
|
ESP_LOGI(TAG, "Mode: OFF");
|
||||||
|
} else if ((message1[2] == 0x1F) && (message1[3] == 0xE0)) {
|
||||||
|
// 0x1FE0 pattern is used for AUTO, DRY, and low-temp COOL
|
||||||
|
if ((temp_code & 0x0F) == 0x08) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||||
|
ESP_LOGI(TAG, "Mode: AUTO");
|
||||||
|
} else if ((temp_code & 0x0F) == 0x04) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_DRY;
|
||||||
|
ESP_LOGI(TAG, "Mode: DRY");
|
||||||
|
} else {
|
||||||
|
this->mode = climate::CLIMATE_MODE_COOL;
|
||||||
|
ESP_LOGI(TAG, "Mode: COOL (low temp)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Variable fan speed patterns - decode by temperature code
|
||||||
|
if ((temp_code & 0x0F) == 0x0C) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||||
|
ESP_LOGI(TAG, "Mode: HEAT");
|
||||||
|
} else if (message1[5] == 0x1B) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||||
|
ESP_LOGI(TAG, "Mode: FAN_ONLY");
|
||||||
|
} else {
|
||||||
|
this->mode = climate::CLIMATE_MODE_COOL;
|
||||||
|
ESP_LOGI(TAG, "Mode: COOL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode fan speed from rc_code_1
|
||||||
|
uint16_t fan_code = (message1[2] << 8) | message1[3];
|
||||||
|
this->fan_mode = decode_ras_2819t_fan_mode(fan_code);
|
||||||
|
|
||||||
|
// Decode temperature
|
||||||
|
if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_FAN_ONLY) {
|
||||||
|
this->target_temperature = decode_ras_2819t_temperature(temp_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->publish_state();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "Unknown single-packet RAS-2819T command: 0x%" PRIX64, toshiba_data.rc_code_1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||||
|
// Try modern ToshibaAcProtocol decoder first (handles RAS-2819T and potentially others)
|
||||||
|
remote_base::ToshibaAcProtocol toshiba_protocol;
|
||||||
|
auto decode_result = toshiba_protocol.decode(data);
|
||||||
|
|
||||||
|
if (decode_result.has_value()) {
|
||||||
|
auto toshiba_data = decode_result.value();
|
||||||
|
// Validate and process RAS-2819T commands
|
||||||
|
if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) {
|
||||||
|
return this->process_ras_2819t_command_(toshiba_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to generic processing for older protocols
|
||||||
uint8_t message[18] = {0};
|
uint8_t message[18] = {0};
|
||||||
uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0;
|
uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0;
|
||||||
|
|
||||||
@@ -499,11 +1098,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Read incoming bits into buffer
|
// Read incoming bits into buffer
|
||||||
if (!decode_(&data, message, message_length)) {
|
if (!this->decode_(&data, message, message_length)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Determine incoming message protocol version and/or length
|
// Determine incoming message protocol version and/or length
|
||||||
if (is_valid_rac_pt1411hwru_header_(message)) {
|
if (this->is_valid_rac_pt1411hwru_header_(message)) {
|
||||||
// We already received four bytes
|
// We already received four bytes
|
||||||
message_length = RAC_PT1411HWRU_MESSAGE_LENGTH - 4;
|
message_length = RAC_PT1411HWRU_MESSAGE_LENGTH - 4;
|
||||||
} else if ((message[0] ^ message[1] ^ message[2]) != message[3]) {
|
} else if ((message[0] ^ message[1] ^ message[2]) != message[3]) {
|
||||||
@@ -514,11 +1113,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
message_length = message[2] + 2;
|
message_length = message[2] + 2;
|
||||||
}
|
}
|
||||||
// Decode the remaining bytes
|
// Decode the remaining bytes
|
||||||
if (!decode_(&data, &message[4], message_length)) {
|
if (!this->decode_(&data, &message[4], message_length)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// If this is a RAC-PT1411HWRU message, we expect the first packet a second time and also possibly a third packet
|
// If this is a RAC-PT1411HWRU message, we expect the first packet a second time and also possibly a third packet
|
||||||
if (is_valid_rac_pt1411hwru_header_(message)) {
|
if (this->is_valid_rac_pt1411hwru_header_(message)) {
|
||||||
// There is always a space between packets
|
// There is always a space between packets
|
||||||
if (!data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
|
if (!data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -527,7 +1126,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) {
|
if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
|
if (!this->decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// If this is a RAC-PT1411HWRU message, there may also be a third packet.
|
// If this is a RAC-PT1411HWRU message, there may also be a third packet.
|
||||||
@@ -535,25 +1134,25 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
|
if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
|
||||||
// Validate header 3
|
// Validate header 3
|
||||||
data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE);
|
data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE);
|
||||||
if (decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
|
if (this->decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
|
||||||
if (!is_valid_rac_pt1411hwru_message_(&message[12])) {
|
if (!this->is_valid_rac_pt1411hwru_message_(&message[12])) {
|
||||||
// If a third packet was received but the checksum is not valid, fail
|
// If a third packet was received but the checksum is not valid, fail
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!compare_rac_pt1411hwru_packets_(&message[0], &message[6])) {
|
if (!this->compare_rac_pt1411hwru_packets_(&message[0], &message[6])) {
|
||||||
// If the first two packets don't match each other, fail
|
// If the first two packets don't match each other, fail
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!is_valid_rac_pt1411hwru_message_(&message[0])) {
|
if (!this->is_valid_rac_pt1411hwru_message_(&message[0])) {
|
||||||
// If the first packet isn't valid, fail
|
// If the first packet isn't valid, fail
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header has been verified, now determine protocol version and set the climate component properties
|
// Header has been verified, now determine protocol version and set the climate component properties
|
||||||
switch (is_valid_rac_pt1411hwru_header_(message)) {
|
switch (this->is_valid_rac_pt1411hwru_header_(message)) {
|
||||||
// Power, temperature, mode, fan speed
|
// Power, temperature, mode, fan speed
|
||||||
case RAC_PT1411HWRU_MESSAGE_HEADER0:
|
case RAC_PT1411HWRU_MESSAGE_HEADER0:
|
||||||
// Get the mode
|
// Get the mode
|
||||||
@@ -608,7 +1207,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Get the target temperature
|
// Get the target temperature
|
||||||
if (is_valid_rac_pt1411hwru_message_(&message[12])) {
|
if (this->is_valid_rac_pt1411hwru_message_(&message[12])) {
|
||||||
temperature_code =
|
temperature_code =
|
||||||
(message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG);
|
(message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG);
|
||||||
if (message[15] & RAC_PT1411HWRU_FLAG_FAH) {
|
if (message[15] & RAC_PT1411HWRU_FLAG_FAH) {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/components/climate_ir/climate_ir.h"
|
#include "esphome/components/climate_ir/climate_ir.h"
|
||||||
|
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace toshiba {
|
namespace toshiba {
|
||||||
@@ -10,6 +11,7 @@ enum Model {
|
|||||||
MODEL_GENERIC = 0, // Temperature range is from 17 to 30
|
MODEL_GENERIC = 0, // Temperature range is from 17 to 30
|
||||||
MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30
|
MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30
|
||||||
MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30
|
MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30
|
||||||
|
MODEL_RAS_2819T = 3, // RAS-2819T protocol variant, temperature range 18 to 30
|
||||||
};
|
};
|
||||||
|
|
||||||
// Supported temperature ranges
|
// Supported temperature ranges
|
||||||
@@ -19,6 +21,8 @@ const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN = 16.0;
|
|||||||
const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX = 30.0;
|
const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX = 30.0;
|
||||||
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN = 60.0;
|
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN = 60.0;
|
||||||
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0;
|
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0;
|
||||||
|
const float TOSHIBA_RAS_2819T_TEMP_C_MIN = 18.0;
|
||||||
|
const float TOSHIBA_RAS_2819T_TEMP_C_MAX = 30.0;
|
||||||
|
|
||||||
class ToshibaClimate : public climate_ir::ClimateIR {
|
class ToshibaClimate : public climate_ir::ClimateIR {
|
||||||
public:
|
public:
|
||||||
@@ -35,6 +39,9 @@ class ToshibaClimate : public climate_ir::ClimateIR {
|
|||||||
void transmit_generic_();
|
void transmit_generic_();
|
||||||
void transmit_rac_pt1411hwru_();
|
void transmit_rac_pt1411hwru_();
|
||||||
void transmit_rac_pt1411hwru_temp_(bool cs_state = true, bool cs_send_update = true);
|
void transmit_rac_pt1411hwru_temp_(bool cs_state = true, bool cs_send_update = true);
|
||||||
|
void transmit_ras_2819t_();
|
||||||
|
// Process RAS-2819T IR command data
|
||||||
|
bool process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data);
|
||||||
// Returns the header if valid, else returns zero
|
// Returns the header if valid, else returns zero
|
||||||
uint8_t is_valid_rac_pt1411hwru_header_(const uint8_t *message);
|
uint8_t is_valid_rac_pt1411hwru_header_(const uint8_t *message);
|
||||||
// Returns true if message is a valid RAC-PT1411HWRU IR message, regardless if first or second packet
|
// Returns true if message is a valid RAC-PT1411HWRU IR message, regardless if first or second packet
|
||||||
@@ -43,11 +50,26 @@ class ToshibaClimate : public climate_ir::ClimateIR {
|
|||||||
bool compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2);
|
bool compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2);
|
||||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// RAS-2819T state tracking for swing mode optimization
|
||||||
|
climate::ClimateSwingMode last_swing_mode_{climate::CLIMATE_SWING_OFF};
|
||||||
|
climate::ClimateMode last_mode_{climate::CLIMATE_MODE_OFF};
|
||||||
|
optional<climate::ClimateFanMode> last_fan_mode_{};
|
||||||
|
float last_target_temperature_{24.0f};
|
||||||
|
|
||||||
float temperature_min_() {
|
float temperature_min_() {
|
||||||
return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MIN : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN;
|
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F)
|
||||||
|
return TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN;
|
||||||
|
if (this->model_ == MODEL_RAS_2819T)
|
||||||
|
return TOSHIBA_RAS_2819T_TEMP_C_MIN;
|
||||||
|
return TOSHIBA_GENERIC_TEMP_C_MIN; // Default to GENERIC for unknown models
|
||||||
}
|
}
|
||||||
float temperature_max_() {
|
float temperature_max_() {
|
||||||
return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX;
|
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F)
|
||||||
|
return TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX;
|
||||||
|
if (this->model_ == MODEL_RAS_2819T)
|
||||||
|
return TOSHIBA_RAS_2819T_TEMP_C_MAX;
|
||||||
|
return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models
|
||||||
}
|
}
|
||||||
std::set<climate::ClimateSwingMode> toshiba_swing_modes_() {
|
std::set<climate::ClimateSwingMode> toshiba_swing_modes_() {
|
||||||
return (this->model_ == MODEL_GENERIC)
|
return (this->model_ == MODEL_GENERIC)
|
||||||
|
@@ -380,24 +380,25 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
|||||||
if (this->on_connect_) {
|
if (this->on_connect_) {
|
||||||
this->on_connect_(rsp);
|
this->on_connect_(rsp);
|
||||||
}
|
}
|
||||||
this->sessions_.insert(rsp);
|
this->sessions_.push_back(rsp);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AsyncEventSource::loop() {
|
void AsyncEventSource::loop() {
|
||||||
// Clean up dead sessions safely
|
// Clean up dead sessions safely
|
||||||
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
||||||
// and the main loop handles the actual cleanup to avoid race conditions
|
// and the main loop handles the actual cleanup to avoid race conditions
|
||||||
auto it = this->sessions_.begin();
|
for (size_t i = 0; i < this->sessions_.size();) {
|
||||||
while (it != this->sessions_.end()) {
|
auto *ses = this->sessions_[i];
|
||||||
auto *ses = *it;
|
|
||||||
// If the session has a dead socket (marked by destroy callback)
|
// If the session has a dead socket (marked by destroy callback)
|
||||||
if (ses->fd_.load() == 0) {
|
if (ses->fd_.load() == 0) {
|
||||||
ESP_LOGD(TAG, "Removing dead event source session");
|
ESP_LOGD(TAG, "Removing dead event source session");
|
||||||
it = this->sessions_.erase(it);
|
|
||||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
|
// Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
|
||||||
|
this->sessions_[i] = this->sessions_.back();
|
||||||
|
this->sessions_.pop_back();
|
||||||
} else {
|
} else {
|
||||||
ses->loop();
|
ses->loop();
|
||||||
++it;
|
++i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,6 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -315,7 +314,10 @@ class AsyncEventSource : public AsyncWebHandler {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::string url_;
|
std::string url_;
|
||||||
std::set<AsyncEventSourceResponse *> sessions_;
|
// Use vector instead of set: SSE sessions are typically 1-5 connections (browsers, dashboards).
|
||||||
|
// Linear search is faster than red-black tree overhead for this small dataset.
|
||||||
|
// Only operations needed: add session, remove session, iterate sessions - no need for sorted order.
|
||||||
|
std::vector<AsyncEventSourceResponse *> sessions_;
|
||||||
connect_handler_t on_connect_{};
|
connect_handler_t on_connect_{};
|
||||||
esphome::web_server::WebServer *web_server_;
|
esphome::web_server::WebServer *web_server_;
|
||||||
};
|
};
|
||||||
|
@@ -402,8 +402,8 @@ async def to_code(config):
|
|||||||
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
|
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
|
||||||
|
|
||||||
# Disable Enterprise WiFi support if no EAP is configured
|
# Disable Enterprise WiFi support if no EAP is configured
|
||||||
if CORE.is_esp32 and not has_eap:
|
if CORE.is_esp32:
|
||||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
|
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
|
||||||
|
|
||||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||||
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
||||||
@@ -447,6 +447,8 @@ async def to_code(config):
|
|||||||
var.get_disconnect_trigger(), [], on_disconnect_config
|
var.get_disconnect_trigger(), [], on_disconnect_config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CORE.add_job(final_step)
|
||||||
|
|
||||||
|
|
||||||
@automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({}))
|
@automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({}))
|
||||||
async def wifi_connected_to_code(config, condition_id, template_arg, args):
|
async def wifi_connected_to_code(config, condition_id, template_arg, args):
|
||||||
@@ -468,6 +470,28 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
|
|||||||
return cg.new_Pvariable(action_id, template_arg)
|
return cg.new_Pvariable(action_id, template_arg)
|
||||||
|
|
||||||
|
|
||||||
|
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
|
||||||
|
|
||||||
|
|
||||||
|
def request_wifi_scan_results():
|
||||||
|
"""Request that WiFi scan results be kept in memory after connection.
|
||||||
|
|
||||||
|
Components that need access to scan results after WiFi is connected should
|
||||||
|
call this function during their code generation. This prevents the WiFi component from
|
||||||
|
freeing scan result memory after successful connection.
|
||||||
|
"""
|
||||||
|
CORE.data[KEEP_SCAN_RESULTS_KEY] = True
|
||||||
|
|
||||||
|
|
||||||
|
@coroutine_with_priority(CoroPriority.FINAL)
|
||||||
|
async def final_step():
|
||||||
|
"""Final code generation step to configure scan result retention."""
|
||||||
|
if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False):
|
||||||
|
cg.add(
|
||||||
|
cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
"wifi.configure",
|
"wifi.configure",
|
||||||
WiFiConfigureAction,
|
WiFiConfigureAction,
|
||||||
|
@@ -549,7 +549,7 @@ void WiFiComponent::start_scanning() {
|
|||||||
// Using insertion sort instead of std::stable_sort saves flash memory
|
// Using insertion sort instead of std::stable_sort saves flash memory
|
||||||
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
|
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
|
||||||
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
|
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
|
||||||
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
|
template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
|
||||||
const size_t size = results.size();
|
const size_t size = results.size();
|
||||||
for (size_t i = 1; i < size; i++) {
|
for (size_t i = 1; i < size; i++) {
|
||||||
// Make a copy to avoid issues with move semantics during comparison
|
// Make a copy to avoid issues with move semantics during comparison
|
||||||
@@ -713,6 +713,12 @@ void WiFiComponent::check_connecting_finished() {
|
|||||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
|
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
|
||||||
this->num_retried_ = 0;
|
this->num_retried_ = 0;
|
||||||
|
|
||||||
|
// Free scan results memory unless a component needs them
|
||||||
|
if (!this->keep_scan_results_) {
|
||||||
|
this->scan_result_.clear();
|
||||||
|
this->scan_result_.shrink_to_fit();
|
||||||
|
}
|
||||||
|
|
||||||
if (this->fast_connect_) {
|
if (this->fast_connect_) {
|
||||||
this->save_fast_connect_settings_();
|
this->save_fast_connect_settings_();
|
||||||
}
|
}
|
||||||
|
@@ -121,6 +121,14 @@ struct EAPAuth {
|
|||||||
|
|
||||||
using bssid_t = std::array<uint8_t, 6>;
|
using bssid_t = std::array<uint8_t, 6>;
|
||||||
|
|
||||||
|
// Use std::vector for RP2040 since scan count is unknown (callback-based)
|
||||||
|
// Use FixedVector for other platforms where count is queried first
|
||||||
|
#ifdef USE_RP2040
|
||||||
|
template<typename T> using wifi_scan_vector_t = std::vector<T>;
|
||||||
|
#else
|
||||||
|
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||||
|
#endif
|
||||||
|
|
||||||
class WiFiAP {
|
class WiFiAP {
|
||||||
public:
|
public:
|
||||||
void set_ssid(const std::string &ssid);
|
void set_ssid(const std::string &ssid);
|
||||||
@@ -278,7 +286,7 @@ class WiFiComponent : public Component {
|
|||||||
const std::string &get_use_address() const;
|
const std::string &get_use_address() const;
|
||||||
void set_use_address(const std::string &use_address);
|
void set_use_address(const std::string &use_address);
|
||||||
|
|
||||||
const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
|
const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return scan_result_; }
|
||||||
|
|
||||||
network::IPAddress wifi_soft_ap_ip();
|
network::IPAddress wifi_soft_ap_ip();
|
||||||
|
|
||||||
@@ -316,6 +324,7 @@ class WiFiComponent : public Component {
|
|||||||
int8_t wifi_rssi();
|
int8_t wifi_rssi();
|
||||||
|
|
||||||
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||||
|
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
|
||||||
|
|
||||||
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; };
|
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; };
|
||||||
Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; };
|
Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; };
|
||||||
@@ -385,7 +394,7 @@ class WiFiComponent : public Component {
|
|||||||
std::string use_address_;
|
std::string use_address_;
|
||||||
std::vector<WiFiAP> sta_;
|
std::vector<WiFiAP> sta_;
|
||||||
std::vector<WiFiSTAPriority> sta_priorities_;
|
std::vector<WiFiSTAPriority> sta_priorities_;
|
||||||
std::vector<WiFiScanResult> scan_result_;
|
wifi_scan_vector_t<WiFiScanResult> scan_result_;
|
||||||
WiFiAP selected_ap_;
|
WiFiAP selected_ap_;
|
||||||
WiFiAP ap_;
|
WiFiAP ap_;
|
||||||
optional<float> output_power_;
|
optional<float> output_power_;
|
||||||
@@ -424,6 +433,7 @@ class WiFiComponent : public Component {
|
|||||||
#endif
|
#endif
|
||||||
bool enable_on_boot_;
|
bool enable_on_boot_;
|
||||||
bool got_ipv4_address_{false};
|
bool got_ipv4_address_{false};
|
||||||
|
bool keep_scan_results_{false};
|
||||||
|
|
||||||
// Pointers at the end (naturally aligned)
|
// Pointers at the end (naturally aligned)
|
||||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||||
|
@@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
|||||||
this->retry_connect();
|
this->retry_connect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count the number of results first
|
||||||
auto *head = reinterpret_cast<bss_info *>(arg);
|
auto *head = reinterpret_cast<bss_info *>(arg);
|
||||||
|
size_t count = 0;
|
||||||
|
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->scan_result_.init(count);
|
||||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||||
WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||||
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,
|
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,
|
||||||
|
@@ -784,7 +784,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
|||||||
}
|
}
|
||||||
records.resize(number);
|
records.resize(number);
|
||||||
|
|
||||||
scan_result_.reserve(number);
|
scan_result_.init(number);
|
||||||
for (int i = 0; i < number; i++) {
|
for (int i = 0; i < number; i++) {
|
||||||
auto &record = records[i];
|
auto &record = records[i];
|
||||||
bssid_t bssid;
|
bssid_t bssid;
|
||||||
|
@@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
|||||||
if (num < 0)
|
if (num < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this->scan_result_.reserve(static_cast<unsigned int>(num));
|
this->scan_result_.init(static_cast<unsigned int>(num));
|
||||||
for (int i = 0; i < num; i++) {
|
for (int i = 0; i < num; i++) {
|
||||||
String ssid = WiFi.SSID(i);
|
String ssid = WiFi.SSID(i);
|
||||||
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import text_sensor
|
from esphome.components import text_sensor, wifi
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BSSID,
|
CONF_BSSID,
|
||||||
@@ -77,7 +77,9 @@ async def to_code(config):
|
|||||||
await setup_conf(config, CONF_SSID)
|
await setup_conf(config, CONF_SSID)
|
||||||
await setup_conf(config, CONF_BSSID)
|
await setup_conf(config, CONF_BSSID)
|
||||||
await setup_conf(config, CONF_MAC_ADDRESS)
|
await setup_conf(config, CONF_MAC_ADDRESS)
|
||||||
await setup_conf(config, CONF_SCAN_RESULTS)
|
if CONF_SCAN_RESULTS in config:
|
||||||
|
await setup_conf(config, CONF_SCAN_RESULTS)
|
||||||
|
wifi.request_wifi_scan_results()
|
||||||
await setup_conf(config, CONF_DNS_ADDRESS)
|
await setup_conf(config, CONF_DNS_ADDRESS)
|
||||||
if conf := config.get(CONF_IP_ADDRESS):
|
if conf := config.get(CONF_IP_ADDRESS):
|
||||||
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
|
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
|
||||||
|
@@ -222,18 +222,25 @@ def copy_files():
|
|||||||
] in ["xiao_ble"]:
|
] in ["xiao_ble"]:
|
||||||
fake_board_manifest = """
|
fake_board_manifest = """
|
||||||
{
|
{
|
||||||
"frameworks": [
|
"frameworks": [
|
||||||
"zephyr"
|
"zephyr"
|
||||||
],
|
],
|
||||||
"name": "esphome nrf52",
|
"name": "esphome nrf52",
|
||||||
"upload": {
|
"upload": {
|
||||||
"maximum_ram_size": 248832,
|
"maximum_ram_size": 248832,
|
||||||
"maximum_size": 815104
|
"maximum_size": 815104,
|
||||||
},
|
"speed": 115200
|
||||||
"url": "https://esphome.io/",
|
},
|
||||||
"vendor": "esphome"
|
"url": "https://esphome.io/",
|
||||||
|
"vendor": "esphome",
|
||||||
|
"build": {
|
||||||
|
"softdevice": {
|
||||||
|
"sd_fwid": "0x00B6"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
write_file_if_changed(
|
write_file_if_changed(
|
||||||
CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"),
|
CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"),
|
||||||
fake_board_manifest,
|
fake_board_manifest,
|
||||||
|
@@ -12,7 +12,7 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from esphome import core, loader, pins, yaml_util
|
from esphome import core, loader, pins, yaml_util
|
||||||
from esphome.config_helpers import Extend, Remove
|
from esphome.config_helpers import Extend, Remove, merge_dicts_ordered
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ESPHOME,
|
CONF_ESPHOME,
|
||||||
@@ -922,10 +922,9 @@ def validate_config(
|
|||||||
if CONF_SUBSTITUTIONS in config or command_line_substitutions:
|
if CONF_SUBSTITUTIONS in config or command_line_substitutions:
|
||||||
from esphome.components import substitutions
|
from esphome.components import substitutions
|
||||||
|
|
||||||
result[CONF_SUBSTITUTIONS] = {
|
result[CONF_SUBSTITUTIONS] = merge_dicts_ordered(
|
||||||
**(config.get(CONF_SUBSTITUTIONS) or {}),
|
config.get(CONF_SUBSTITUTIONS) or {}, command_line_substitutions
|
||||||
**command_line_substitutions,
|
)
|
||||||
}
|
|
||||||
result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
|
result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
|
||||||
try:
|
try:
|
||||||
substitutions.do_substitution_pass(config, command_line_substitutions)
|
substitutions.do_substitution_pass(config, command_line_substitutions)
|
||||||
|
@@ -10,6 +10,7 @@ from esphome.const import (
|
|||||||
PlatformFramework,
|
PlatformFramework,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
|
from esphome.util import OrderedDict
|
||||||
|
|
||||||
# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum
|
# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum
|
||||||
_PLATFORM_FRAMEWORK_LOOKUP = {
|
_PLATFORM_FRAMEWORK_LOOKUP = {
|
||||||
@@ -17,6 +18,25 @@ _PLATFORM_FRAMEWORK_LOOKUP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def merge_dicts_ordered(*dicts: dict) -> OrderedDict:
|
||||||
|
"""Merge multiple dicts into an OrderedDict, preserving key order.
|
||||||
|
|
||||||
|
This is a helper to ensure that dictionary merging preserves OrderedDict type,
|
||||||
|
which is important for operations like move_to_end().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*dicts: Variable number of dictionaries to merge (later dicts override earlier ones)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OrderedDict with merged contents
|
||||||
|
"""
|
||||||
|
result = OrderedDict()
|
||||||
|
for d in dicts:
|
||||||
|
if d:
|
||||||
|
result.update(d)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class Extend:
|
class Extend:
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -60,7 +80,11 @@ def merge_config(full_old, full_new):
|
|||||||
if isinstance(new, dict):
|
if isinstance(new, dict):
|
||||||
if not isinstance(old, dict):
|
if not isinstance(old, dict):
|
||||||
return new
|
return new
|
||||||
res = old.copy()
|
# Preserve OrderedDict type by copying to OrderedDict if either input is OrderedDict
|
||||||
|
if isinstance(old, OrderedDict) or isinstance(new, OrderedDict):
|
||||||
|
res = OrderedDict(old)
|
||||||
|
else:
|
||||||
|
res = old.copy()
|
||||||
for k, v in new.items():
|
for k, v in new.items():
|
||||||
if isinstance(v, Remove) and k in old:
|
if isinstance(v, Remove) and k in old:
|
||||||
del res[k]
|
del res[k]
|
||||||
|
@@ -244,6 +244,20 @@ RESERVED_IDS = [
|
|||||||
"uart0",
|
"uart0",
|
||||||
"uart1",
|
"uart1",
|
||||||
"uart2",
|
"uart2",
|
||||||
|
# ESP32 ROM functions
|
||||||
|
"crc16_be",
|
||||||
|
"crc16_le",
|
||||||
|
"crc32_be",
|
||||||
|
"crc32_le",
|
||||||
|
"crc8_be",
|
||||||
|
"crc8_le",
|
||||||
|
"dbg_state",
|
||||||
|
"debug_timer",
|
||||||
|
"one_bits",
|
||||||
|
"recv_packet",
|
||||||
|
"send_packet",
|
||||||
|
"check_pos",
|
||||||
|
"software_reset",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@@ -696,6 +696,7 @@ CONF_OPEN_DRAIN = "open_drain"
|
|||||||
CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt"
|
CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt"
|
||||||
CONF_OPEN_DURATION = "open_duration"
|
CONF_OPEN_DURATION = "open_duration"
|
||||||
CONF_OPEN_ENDSTOP = "open_endstop"
|
CONF_OPEN_ENDSTOP = "open_endstop"
|
||||||
|
CONF_OPENTHREAD = "openthread"
|
||||||
CONF_OPERATION = "operation"
|
CONF_OPERATION = "operation"
|
||||||
CONF_OPTIMISTIC = "optimistic"
|
CONF_OPTIMISTIC = "optimistic"
|
||||||
CONF_OPTION = "option"
|
CONF_OPTION = "option"
|
||||||
@@ -836,6 +837,7 @@ CONF_RMT_CHANNEL = "rmt_channel"
|
|||||||
CONF_RMT_SYMBOLS = "rmt_symbols"
|
CONF_RMT_SYMBOLS = "rmt_symbols"
|
||||||
CONF_ROTATION = "rotation"
|
CONF_ROTATION = "rotation"
|
||||||
CONF_ROW = "row"
|
CONF_ROW = "row"
|
||||||
|
CONF_ROWS = "rows"
|
||||||
CONF_RS_PIN = "rs_pin"
|
CONF_RS_PIN = "rs_pin"
|
||||||
CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance"
|
CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance"
|
||||||
CONF_RTD_WIRES = "rtd_wires"
|
CONF_RTD_WIRES = "rtd_wires"
|
||||||
@@ -1298,6 +1300,7 @@ DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide"
|
|||||||
DEVICE_CLASS_SWITCH = "switch"
|
DEVICE_CLASS_SWITCH = "switch"
|
||||||
DEVICE_CLASS_TAMPER = "tamper"
|
DEVICE_CLASS_TAMPER = "tamper"
|
||||||
DEVICE_CLASS_TEMPERATURE = "temperature"
|
DEVICE_CLASS_TEMPERATURE = "temperature"
|
||||||
|
DEVICE_CLASS_TEMPERATURE_DELTA = "temperature_delta"
|
||||||
DEVICE_CLASS_TIMESTAMP = "timestamp"
|
DEVICE_CLASS_TIMESTAMP = "timestamp"
|
||||||
DEVICE_CLASS_UPDATE = "update"
|
DEVICE_CLASS_UPDATE = "update"
|
||||||
DEVICE_CLASS_VIBRATION = "vibration"
|
DEVICE_CLASS_VIBRATION = "vibration"
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
#include "esphome/core/preferences.h"
|
#include "esphome/core/preferences.h"
|
||||||
#include "esphome/core/scheduler.h"
|
#include "esphome/core/scheduler.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ namespace esphome {
|
|||||||
|
|
||||||
template<typename... Ts> class AndCondition : public Condition<Ts...> {
|
template<typename... Ts> class AndCondition : public Condition<Ts...> {
|
||||||
public:
|
public:
|
||||||
explicit AndCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||||
bool check(Ts... x) override {
|
bool check(Ts... x) override {
|
||||||
for (auto *condition : this->conditions_) {
|
for (auto *condition : this->conditions_) {
|
||||||
if (!condition->check(x...))
|
if (!condition->check(x...))
|
||||||
@@ -25,12 +26,12 @@ template<typename... Ts> class AndCondition : public Condition<Ts...> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<Condition<Ts...> *> conditions_;
|
FixedVector<Condition<Ts...> *> conditions_;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class OrCondition : public Condition<Ts...> {
|
template<typename... Ts> class OrCondition : public Condition<Ts...> {
|
||||||
public:
|
public:
|
||||||
explicit OrCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||||
bool check(Ts... x) override {
|
bool check(Ts... x) override {
|
||||||
for (auto *condition : this->conditions_) {
|
for (auto *condition : this->conditions_) {
|
||||||
if (condition->check(x...))
|
if (condition->check(x...))
|
||||||
@@ -41,7 +42,7 @@ template<typename... Ts> class OrCondition : public Condition<Ts...> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<Condition<Ts...> *> conditions_;
|
FixedVector<Condition<Ts...> *> conditions_;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class NotCondition : public Condition<Ts...> {
|
template<typename... Ts> class NotCondition : public Condition<Ts...> {
|
||||||
@@ -55,7 +56,7 @@ template<typename... Ts> class NotCondition : public Condition<Ts...> {
|
|||||||
|
|
||||||
template<typename... Ts> class XorCondition : public Condition<Ts...> {
|
template<typename... Ts> class XorCondition : public Condition<Ts...> {
|
||||||
public:
|
public:
|
||||||
explicit XorCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||||
bool check(Ts... x) override {
|
bool check(Ts... x) override {
|
||||||
size_t result = 0;
|
size_t result = 0;
|
||||||
for (auto *condition : this->conditions_) {
|
for (auto *condition : this->conditions_) {
|
||||||
@@ -66,7 +67,7 @@ template<typename... Ts> class XorCondition : public Condition<Ts...> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::vector<Condition<Ts...> *> conditions_;
|
FixedVector<Condition<Ts...> *> conditions_;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
|
template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
|
||||||
|
@@ -202,7 +202,7 @@
|
|||||||
#define USB_HOST_MAX_REQUESTS 16
|
#define USB_HOST_MAX_REQUESTS 16
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1)
|
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
|
||||||
#define USE_ETHERNET
|
#define USE_ETHERNET
|
||||||
#define USE_ETHERNET_KSZ8081
|
#define USE_ETHERNET_KSZ8081
|
||||||
#endif
|
#endif
|
||||||
|
@@ -197,6 +197,18 @@ template<typename T> class FixedVector {
|
|||||||
public:
|
public:
|
||||||
FixedVector() = default;
|
FixedVector() = default;
|
||||||
|
|
||||||
|
/// Constructor from initializer list - allocates exact size needed
|
||||||
|
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
|
||||||
|
FixedVector(std::initializer_list<T> init_list) {
|
||||||
|
init(init_list.size());
|
||||||
|
size_t idx = 0;
|
||||||
|
for (const auto &item : init_list) {
|
||||||
|
new (data_ + idx) T(item);
|
||||||
|
++idx;
|
||||||
|
}
|
||||||
|
size_ = init_list.size();
|
||||||
|
}
|
||||||
|
|
||||||
~FixedVector() { cleanup_(); }
|
~FixedVector() { cleanup_(); }
|
||||||
|
|
||||||
// Disable copy operations (avoid accidental expensive copies)
|
// Disable copy operations (avoid accidental expensive copies)
|
||||||
|
@@ -10,6 +10,10 @@ from esphome.helpers import get_bool_env
|
|||||||
|
|
||||||
from .util.password import password_hash
|
from .util.password import password_hash
|
||||||
|
|
||||||
|
# Sentinel file name used for CORE.config_path when dashboard initializes.
|
||||||
|
# This ensures .parent returns the config directory instead of root.
|
||||||
|
_DASHBOARD_SENTINEL_FILE = "___DASHBOARD_SENTINEL___.yaml"
|
||||||
|
|
||||||
|
|
||||||
class DashboardSettings:
|
class DashboardSettings:
|
||||||
"""Settings for the dashboard."""
|
"""Settings for the dashboard."""
|
||||||
@@ -48,7 +52,12 @@ class DashboardSettings:
|
|||||||
self.config_dir = Path(args.configuration)
|
self.config_dir = Path(args.configuration)
|
||||||
self.absolute_config_dir = self.config_dir.resolve()
|
self.absolute_config_dir = self.config_dir.resolve()
|
||||||
self.verbose = args.verbose
|
self.verbose = args.verbose
|
||||||
CORE.config_path = self.config_dir / "."
|
# Set to a sentinel file so .parent gives us the config directory.
|
||||||
|
# Previously this was `os.path.join(self.config_dir, ".")` which worked because
|
||||||
|
# os.path.dirname("/config/.") returns "/config", but Path("/config/.").parent
|
||||||
|
# normalizes to Path("/config") first, then .parent returns Path("/"), breaking
|
||||||
|
# secret resolution. Using a sentinel file ensures .parent gives the correct directory.
|
||||||
|
CORE.config_path = self.config_dir / _DASHBOARD_SENTINEL_FILE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def relative_url(self) -> str:
|
def relative_url(self) -> str:
|
||||||
|
@@ -242,7 +242,7 @@ def send_check(
|
|||||||
|
|
||||||
|
|
||||||
def perform_ota(
|
def perform_ota(
|
||||||
sock: socket.socket, password: str, file_handle: io.IOBase, filename: Path
|
sock: socket.socket, password: str | None, file_handle: io.IOBase, filename: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
file_contents = file_handle.read()
|
file_contents = file_handle.read()
|
||||||
file_size = len(file_contents)
|
file_size = len(file_contents)
|
||||||
@@ -278,13 +278,13 @@ def perform_ota(
|
|||||||
|
|
||||||
def perform_auth(
|
def perform_auth(
|
||||||
sock: socket.socket,
|
sock: socket.socket,
|
||||||
password: str,
|
password: str | None,
|
||||||
hash_func: Callable[..., Any],
|
hash_func: Callable[..., Any],
|
||||||
nonce_size: int,
|
nonce_size: int,
|
||||||
hash_name: str,
|
hash_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Perform challenge-response authentication using specified hash algorithm."""
|
"""Perform challenge-response authentication using specified hash algorithm."""
|
||||||
if not password:
|
if password is None:
|
||||||
raise OTAError("ESP requests password, but no password given!")
|
raise OTAError("ESP requests password, but no password given!")
|
||||||
|
|
||||||
nonce_bytes = receive_exactly(
|
nonce_bytes = receive_exactly(
|
||||||
@@ -385,7 +385,7 @@ def perform_ota(
|
|||||||
|
|
||||||
|
|
||||||
def run_ota_impl_(
|
def run_ota_impl_(
|
||||||
remote_host: str | list[str], remote_port: int, password: str, filename: Path
|
remote_host: str | list[str], remote_port: int, password: str | None, filename: Path
|
||||||
) -> tuple[int, str | None]:
|
) -> tuple[int, str | None]:
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
|
|
||||||
@@ -436,7 +436,7 @@ def run_ota_impl_(
|
|||||||
|
|
||||||
|
|
||||||
def run_ota(
|
def run_ota(
|
||||||
remote_host: str | list[str], remote_port: int, password: str, filename: Path
|
remote_host: str | list[str], remote_port: int, password: str | None, filename: Path
|
||||||
) -> tuple[int, str | None]:
|
) -> tuple[int, str | None]:
|
||||||
try:
|
try:
|
||||||
return run_ota_impl_(remote_host, remote_port, password, filename)
|
return run_ota_impl_(remote_host, remote_port, password, filename)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user