1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-04 00:51:49 +00:00

Compare commits

..

87 Commits

Author SHA1 Message Date
Jesse Hills
970680b1b2 Add has_buffered_data() function to ESPADFSpeaker class 2023-11-24 21:19:36 +13:00
Jesse Hills
f500bd5e6f Extend timeout to 500ms 2023-11-24 21:19:36 +13:00
Jesse Hills
e2bb81e233 Update esphome/components/esp_adf/microphone/esp_adf_microphone.cpp
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2023-11-24 12:01:28 +13:00
Jesse Hills
26a1d14ee0 Add s3-box-3 board 2023-11-16 15:04:49 +13:00
Jesse Hills
97f07f8d13 Merge branch 'dev' into jesserockz-2023-284 2023-11-16 13:10:34 +13:00
dependabot[bot]
4e3170dc95 Bump zeroconf from 0.126.0 to 0.127.0 (#5768)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-15 21:48:59 +00:00
J. Nick Koston
c795dbde26 dashboard: split dashboard web server code into its own module (#5770) 2023-11-15 21:34:09 +00:00
dependabot[bot]
4ce627b4ee Bump aioesphomeapi from 18.4.0 to 18.4.1 (#5767)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-15 15:28:04 -06:00
J. Nick Koston
86b4fdc139 dashboard: Break apart dashboard into separate modules (#5764)
* Break apart dashboard into seperate modules

* reduce code change

* late imports

* late imports

* preen

* remove accidental changes

* save the file
2023-11-15 13:00:28 -05:00
J. Nick Koston
20ea8bf06e dashboard: convert ping thread to use asyncio (#5749) 2023-11-14 22:55:33 -06:00
J. Nick Koston
642db6d92b Speed up OTAs (#5720) 2023-11-14 22:14:37 -06:00
Jesse Hills
4aac5a23cd Merge branch 'release' into dev 2023-11-15 16:13:43 +13:00
Jesse Hills
c536c976b7 Merge pull request #5758 from esphome/bump-2023.11.0
2023.11.0
2023-11-15 16:11:18 +13:00
J. Nick Koston
214b419db2 dashboard: Use mdns cache when available if device connection is OTA (#5724)
* Use mdns or freshen cache when device connection is OTA

Since we already have a service browser running, we likely
already know the IP of the deivce we want to connect to so
we can replace OTA with the address to avoid the esphome
app having to look it up again

* isort

* Fix zeroconf name resolution refactoring error

HostResolver should get the type as the first arg instead
of the name

* no i/o

* tornado support native coros

* lint

* use new tornado start methods

* use new tornado start methods

* use new tornado start methods

* break

* lint

* lint

* typing, missing awaits

* io in executor

* missed one

* fix: missing if

* stale comment

* rename run_command to build_device_command since it does not actually run anything
2023-11-14 21:21:44 -05:00
Jesse Hills
0c18872888 Bump version to 2023.11.0 2023-11-15 14:13:39 +13:00
Jesse Hills
197b6b4275 Merge pull request #5756 from esphome/bump-2023.11.0b7
2023.11.0b7
2023-11-15 13:19:48 +13:00
Jesse Hills
4e8bdc2155 Bump version to 2023.11.0b7 2023-11-15 12:45:03 +13:00
Jesse Hills
f1e8622187 Dont dump wifi info when disabled (#5755) 2023-11-15 12:45:02 +13:00
Jesse Hills
e0c7a02fbc Allow setup to continue past mqtt if network/wifi is disabled (#5754) 2023-11-15 12:45:02 +13:00
Jimmy Hedman
cdcb25be8e Make precommit checks happy (#5751) 2023-11-15 12:38:36 +13:00
Jesse Hills
aecc6655db Dont dump wifi info when disabled (#5755) 2023-11-14 21:57:25 +00:00
Jesse Hills
2754ddec1b Allow setup to continue past mqtt if network/wifi is disabled (#5754) 2023-11-15 10:51:45 +13:00
Jesse Hills
2a20a5fc11 Merge pull request #5750 from esphome/bump-2023.11.0b6
2023.11.0b6
2023-11-14 16:16:11 +13:00
Jesse Hills
7100d073f8 Bump version to 2023.11.0b6 2023-11-14 14:32:41 +13:00
Keith Burzinski
1ac6cf2ff9 Generate partitions.csv based on flash size (#5697) 2023-11-14 14:32:41 +13:00
J. Nick Koston
2ee089c9d5 dashboard: Run get_serial_ports in the executor (#5740) 2023-11-14 14:32:41 +13:00
J. Nick Koston
bd568eecf5 dashboard: remove usage of codecs module (#5741) 2023-11-14 14:32:40 +13:00
Keith Burzinski
ae0e481cff Generate partitions.csv based on flash size (#5697) 2023-11-14 13:47:29 +13:00
J. Nick Koston
f198be39d7 dashboard: Run get_serial_ports in the executor (#5740) 2023-11-14 13:46:51 +13:00
J. Nick Koston
08fc96b890 dashboard: remove usage of codecs module (#5741) 2023-11-14 13:44:49 +13:00
dependabot[bot]
8c28bea5b1 Bump zeroconf from 0.123.0 to 0.126.0 (#5748)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-13 16:19:15 -06:00
Sergey Dudanov
00eedeb8b3 remote_base: added helper class and schemas (#5169)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-11-14 06:55:36 +13:00
Jimmy Hedman
0a4853ba7b Correct url for Arduino platform (#5744) 2023-11-14 06:38:08 +13:00
Jesse Hills
3e2b83acb0 Merge pull request #5742 from esphome/bump-2023.11.0b5
2023.11.0b5
2023-11-13 16:37:28 +13:00
Jesse Hills
c1eb5bd675 Bump version to 2023.11.0b5 2023-11-13 15:26:04 +13:00
Jesse Hills
a9772ebf3f Handle wake word not set up internally (#5738) 2023-11-13 15:26:04 +13:00
Jesse Hills
45276cc244 Handle wake word not set up internally (#5738) 2023-11-13 11:48:26 +13:00
Jesse Hills
a9a17ee89d Merge pull request #5737 from esphome/bump-2023.11.0b4
2023.11.0b4
2023-11-13 11:25:42 +13:00
Jesse Hills
f094702a16 Bump version to 2023.11.0b4 2023-11-13 10:23:28 +13:00
J. Nick Koston
908f56ff46 Bump zeroconf to 0.123.0 (#5736) 2023-11-13 10:23:28 +13:00
J. Nick Koston
bd5905c59a Migrate to using aioesphomeapi for the log runner to fix multiple issues (#5733) 2023-11-13 10:23:28 +13:00
dependabot[bot]
91299f05f7 Bump aioesphomeapi from 18.2.7 to 18.4.0 (#5735)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-13 10:23:27 +13:00
Mike La Spina
30e5ff9fff Missed ifdefs (#5727) 2023-11-13 10:23:27 +13:00
J. Nick Koston
163b38e153 Fix zeroconf name resolution refactoring error (#5725) 2023-11-13 10:23:27 +13:00
Jesse Hills
3b486084c8 Add resistance_sampler interface for config validation (#5718) 2023-11-13 10:23:27 +13:00
Jimmy Hedman
684cf10230 Bump Arduino Pico Framework to 3.6.0 and Platform to 1.10.0 (#5731) 2023-11-13 07:28:02 +13:00
J. Nick Koston
63a277ba80 Bump zeroconf to 0.123.0 (#5736) 2023-11-13 07:20:34 +13:00
J. Nick Koston
53f3385c49 Migrate to using aioesphomeapi for the log runner to fix multiple issues (#5733) 2023-11-12 14:36:56 +13:00
dependabot[bot]
51930a0243 Bump aioesphomeapi from 18.2.7 to 18.4.0 (#5735)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-11 21:59:36 +00:00
Mike La Spina
6a5cea171e Missed ifdefs (#5727) 2023-11-10 18:37:39 -06:00
J. Nick Koston
3363c8f434 Fix zeroconf name resolution refactoring error (#5725) 2023-11-10 22:55:21 +13:00
J. Nick Koston
3b891bc146 Speed up YAML by using YAML C loader when available (#5721) 2023-11-10 22:17:40 +13:00
dependabot[bot]
0f19450ab4 Bump black from 23.10.1 to 23.11.0 (#5702)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-11-09 21:18:22 -06:00
Samuel Sieb
98ec798bfc fix pin range for xl9535 (#5722)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2023-11-10 01:53:35 +00:00
Jesse Hills
01d28ce3fc Add resistance_sampler interface for config validation (#5718) 2023-11-10 11:40:07 +13:00
Jesse Hills
9d453f0ba2 Merge pull request #5714 from esphome/bump-2023.11.0b3
2023.11.0b3
2023-11-10 08:16:33 +13:00
dependabot[bot]
799851a83a Bump zeroconf from 0.120.0 to 0.122.3 (#5715) 2023-11-10 07:29:44 +13:00
dependabot[bot]
bc7519f645 Bump zeroconf from 0.120.0 to 0.122.3 (#5715) 2023-11-09 12:26:05 -06:00
Jesse Hills
7a9866f1b6 Bump version to 2023.11.0b3 2023-11-09 22:14:22 +13:00
J. Nick Koston
3d30f1f733 Update Dockerfile to use piwheels for armv7 (#5709) 2023-11-09 22:14:22 +13:00
J. Nick Koston
1e55764d52 Bump aioesphomeapi to 18.2.7 (#5706) 2023-11-09 22:14:22 +13:00
J. Nick Koston
28513a0502 Update Dockerfile to use piwheels for armv7 (#5709) 2023-11-09 21:04:39 +13:00
J. Nick Koston
3e3266fa74 Bump aioesphomeapi to 18.2.7 (#5706) 2023-11-09 15:52:08 +13:00
Rodrigo Martín
ce020b1f9f fix: Fix broken bluetooth_proxy and ble_clients after BLE enable/disable (#5704) 2023-11-08 23:35:37 +00:00
J. Nick Koston
d394b957d1 Use piwheels for armv7 docker image builds (#5703) 2023-11-09 11:50:08 +13:00
J. Nick Koston
cf22c55430 Fix static assets cache logic (#5700) 2023-11-08 21:04:01 +00:00
Jesse Hills
511348974e Fix esp32_rmt_led_strip custom timing units (#5696) 2023-11-08 09:01:26 +00:00
Jesse Hills
972598a698 Handle nanoseconds in config (#5695) 2023-11-08 21:34:44 +13:00
Edward Firmo
d81bec860b Nextion support to esp-idf (#5667)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2023-11-07 19:50:45 -06:00
Jesse Hills
fde7a04ee7 Bump version to 2023.12.0-dev 2023-11-08 13:13:56 +13:00
Jesse Hills
ff9bffc363 Dont include when not using a supported board 2023-10-11 17:33:16 +13:00
Jesse Hills
89b3af8be4 Merge remote-tracking branch 'origin/dev' into jesserockz-2023-284 2023-10-11 16:32:26 +13:00
Jesse Hills
c9b2e54c1a Fix esp-adf init 2023-10-10 11:43:17 +13:00
Jesse Hills
6dd92053b5 Fix adf version 2023-10-05 12:18:46 +13:00
Jesse Hills
33346c0b6a Add back esp-dsp component.
Without this, esp-idf will auto download a version and may be incompatible
2023-10-04 13:37:21 +13:00
Jesse Hills
161fbecfe1 Allow esp-adf libraries to be used by any board 2023-09-29 09:57:05 +13:00
Jesse Hills
fce2eafda0 Allow manual overriding of esp-adf board 2023-09-13 11:49:27 +12:00
Jesse Hills
c19f0cf6bc Remove unused esp-dsp component 2023-09-11 16:07:19 +12:00
Jesse Hills
b05e7bfe0a Update flags 2023-09-11 16:07:09 +12:00
Jesse Hills
3e58ee2130 Allow patching esp-idf with esp-adf files 2023-09-01 10:13:00 +12:00
Jesse Hills
bab9c7c70e Remove afe and try to use algorithm stream 2023-08-31 17:38:49 +12:00
Jesse Hills
0b60a1d9eb Move microphone to use rtos task to read
Use afe_fr for noise suppression (not working)
2023-08-29 11:19:09 +12:00
Jesse Hills
f7455ad76a Fix ifdef 2023-08-24 17:18:52 +12:00
Jesse Hills
3190e86ba8 Dont warn when buffer is just empty 2023-08-24 17:02:50 +12:00
Jesse Hills
a34569d314 Merge branch 'dev' into jesserockz-2023-284 2023-08-23 14:31:19 +12:00
Jesse Hills
6c1c200cf9 Codeowners 2023-08-23 14:31:04 +12:00
Jesse Hills
3635179564 ESP-ADF microphone and speaker board support for s3-box 2023-08-11 07:53:34 +12:00
67 changed files with 3439 additions and 1740 deletions

View File

@@ -3,7 +3,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.1
rev: 23.11.0
hooks:
- id: black
args:

View File

@@ -100,6 +100,7 @@ esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core
esphome/components/esp_adf/* @jesserockz
esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb
@@ -246,6 +247,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet
esphome/components/resistance_sampler/* @jesserockz
esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz

View File

@@ -5,6 +5,7 @@
# One of "docker", "hassio"
ARG BASEIMGTYPE=docker
# https://github.com/hassio-addons/addon-debian-base/releases
FROM ghcr.io/hassio-addons/debian-base:7.2.0 AS base-hassio
# https://hub.docker.com/_/debian?tab=tags&page=1&name=bookworm
@@ -12,9 +13,10 @@ FROM debian:12.2-slim AS base-docker
FROM base-${BASEIMGTYPE} AS base
ARG TARGETARCH
ARG TARGETVARIANT
ARG PIP_EXTRA_INDEX_URL
# Note that --break-system-packages is used below because
# https://peps.python.org/pep-0668/ added a safety check that prevents
@@ -59,8 +61,7 @@ ENV \
# Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/
LANG=C.UTF-8 LC_ALL=C.UTF-8 \
# Store globally installed pio libs in /piolibs
PLATFORMIO_GLOBALLIB_DIR=/piolibs \
PIP_EXTRA_INDEX_URL=${PIP_EXTRA_INDEX_URL}
PLATFORMIO_GLOBALLIB_DIR=/piolibs
# Support legacy binaries on Debian multiarch system. There is no "correct" way
# to do this, other than using properly built toolchains...
@@ -72,8 +73,12 @@ RUN \
RUN \
# Ubuntu python3-pip is missing wheel
pip3 install --break-system-packages --no-cache-dir \
platformio==6.1.11 \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir \
platformio==6.1.11 \
# Change some platformio settings
&& platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000 \
@@ -84,8 +89,12 @@ RUN \
# tmpfs is for https://github.com/rust-lang/cargo/issues/8719
COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini /
RUN --mount=type=tmpfs,target=/root/.cargo CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \
pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \
pip3 install \
--break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
&& /platformio_install_deps.py /platformio.ini --libraries
@@ -94,7 +103,11 @@ FROM base AS docker
# Copy esphome and install
COPY . /esphome
RUN pip3 install --break-system-packages --no-cache-dir --no-use-pep517 -e /esphome
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir --no-use-pep517 -e /esphome
# Settings for dashboard
ENV USERNAME="" PASSWORD=""
@@ -140,7 +153,11 @@ COPY docker/ha-addon-rootfs/ /
# Copy esphome and install
COPY . /esphome
RUN pip3 install --break-system-packages --no-cache-dir --no-use-pep517 -e /esphome
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir --no-use-pep517 -e /esphome
# Labels
LABEL \
@@ -176,7 +193,11 @@ RUN \
/var/lib/apt/lists/*
COPY requirements_test.txt /
RUN pip3 install --break-system-packages --no-cache-dir -r /requirements_test.txt
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
fi; \
pip3 install \
--break-system-packages --no-cache-dir -r /requirements_test.txt
VOLUME ["/esphome"]
WORKDIR /esphome

View File

@@ -143,25 +143,15 @@ def main():
imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push]
imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
build_args = [
"--build-arg",
f"BASEIMGTYPE={params.baseimgtype}",
"--build-arg",
f"BUILD_VERSION={args.tag}",
]
if args.arch == ARCH_ARMV7:
build_args += [
"--build-arg",
"PIP_EXTRA_INDEX_URL=https://www.piwheels.org/simple",
]
# 3. build
cmd = [
"docker",
"buildx",
"build",
*build_args,
"--build-arg",
f"BASEIMGTYPE={params.baseimgtype}",
"--build-arg",
f"BUILD_VERSION={args.tag}",
"--cache-from",
f"type=registry,ref={cache_img}",
"--file",

View File

@@ -514,7 +514,7 @@ def command_clean(args, config):
def command_dashboard(args):
from esphome.dashboard import dashboard
return dashboard.start_web_server(args)
return dashboard.start_dashboard(args)
def command_update_all(args):

View File

@@ -1,71 +1,65 @@
from __future__ import annotations
import asyncio
import logging
from datetime import datetime
from typing import Optional
from typing import Any
from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel
import zeroconf
from aioesphomeapi import APIClient
from aioesphomeapi.api_pb2 import SubscribeLogsResponse
from aioesphomeapi.log_runner import async_run
from zeroconf.asyncio import AsyncZeroconf
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE
from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__
from esphome.util import safe_print
from . import CONF_ENCRYPTION
_LOGGER = logging.getLogger(__name__)
async def async_run_logs(config, address):
"""Run the logs command in the event loop."""
conf = config["api"]
port: int = int(conf[CONF_PORT])
password: str = conf[CONF_PASSWORD]
noise_psk: Optional[str] = None
noise_psk: str | None = None
if CONF_ENCRYPTION in conf:
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
_LOGGER.info("Starting log output from %s using esphome API", address)
aiozc = AsyncZeroconf()
cli = APIClient(
address,
port,
password,
client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk,
zeroconf_instance=aiozc.zeroconf,
)
first_connect = True
dashboard = CORE.dashboard
def on_log(msg):
time_ = datetime.now().time().strftime("[%H:%M:%S]")
text = msg.message.decode("utf8", "backslashreplace")
safe_print(time_ + text)
async def on_connect():
nonlocal first_connect
try:
await cli.subscribe_logs(
on_log,
log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE,
dump_config=first_connect,
)
first_connect = False
except APIConnectionError:
cli.disconnect()
async def on_disconnect(expected_disconnect: bool) -> None:
_LOGGER.warning("Disconnected from API")
zc = zeroconf.Zeroconf()
reconnect = ReconnectLogic(
client=cli,
on_connect=on_connect,
on_disconnect=on_disconnect,
zeroconf_instance=zc,
)
await reconnect.start()
def on_log(msg: SubscribeLogsResponse) -> None:
"""Handle a new log message."""
time_ = datetime.now()
message: bytes = msg.message
text = message.decode("utf8", "backslashreplace")
if dashboard:
text = text.replace("\033", "\\033")
print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}")
stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc)
try:
while True:
await asyncio.sleep(60)
finally:
await aiozc.async_close()
await stop()
def run_logs(config: dict[str, Any], address: str) -> None:
"""Run the logs command."""
try:
asyncio.run(async_run_logs(config, address))
except KeyboardInterrupt:
await reconnect.stop()
zc.close()
def run_logs(config, address):
asyncio.run(async_run_logs(config, address))
pass

View File

@@ -1,38 +1,37 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import (
climate,
remote_transmitter,
remote_receiver,
sensor,
remote_base,
)
from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID
from esphome.components import climate, sensor, remote_base
from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR
DEPENDENCIES = ["remote_transmitter"]
AUTO_LOAD = ["sensor", "remote_base"]
CODEOWNERS = ["@glmnet"]
climate_ir_ns = cg.esphome_ns.namespace("climate_ir")
ClimateIR = climate_ir_ns.class_(
"ClimateIR", climate.Climate, cg.Component, remote_base.RemoteReceiverListener
"ClimateIR",
climate.Climate,
cg.Component,
remote_base.RemoteReceiverListener,
remote_base.RemoteTransmittable,
)
CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
}
).extend(cv.COMPONENT_SCHEMA)
CLIMATE_IR_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
{
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
)
CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
{
cv.Optional(CONF_RECEIVER_ID): cv.use_id(
remote_receiver.RemoteReceiverComponent
cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
remote_base.RemoteReceiverBase
),
}
)
@@ -41,15 +40,11 @@ CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
async def register_climate_ir(var, config):
await cg.register_component(var, config)
await climate.register_climate(var, config)
await remote_base.register_transmittable(var, config)
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
if remote_base.CONF_RECEIVER_ID in config:
await remote_base.register_listener(var, config)
if sensor_id := config.get(CONF_SENSOR):
sens = await cg.get_variable(sensor_id)
cg.add(var.set_sensor(sens))
if receiver_id := config.get(CONF_RECEIVER_ID):
receiver = await cg.get_variable(receiver_id)
cg.add(receiver.register_listener(var))
transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter))

View File

@@ -18,7 +18,10 @@ namespace climate_ir {
Likewise to decode a IR into the AC state, implement
bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true
*/
class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener {
class ClimateIR : public Component,
public climate::Climate,
public remote_base::RemoteReceiverListener,
public remote_base::RemoteTransmittable {
public:
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {},
@@ -35,9 +38,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base:
void setup() override;
void dump_config() override;
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
this->transmitter_ = transmitter;
}
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
@@ -64,7 +64,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base:
std::set<climate::ClimateSwingMode> swing_modes_ = {};
std::set<climate::ClimatePreset> presets_ = {};
remote_transmitter::RemoteTransmitterComponent *transmitter_;
sensor::Sensor *sensor_{nullptr};
};

View File

@@ -102,11 +102,7 @@ void CoolixClimate::transmit_state() {
}
}
ESP_LOGV(TAG, "Sending coolix code: 0x%06" PRIX32, remote_state);
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
remote_base::CoolixProtocol().encode(data, remote_state);
transmit.perform();
this->transmit_<remote_base::CoolixProtocol>(remote_state);
}
bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) {

View File

@@ -386,10 +386,21 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
)
FLASH_SIZES = [
"4MB",
"8MB",
"16MB",
"32MB",
]
CONF_FLASH_SIZE = "flash_size"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
*FLASH_SIZES, upper=True
),
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
}
@@ -401,6 +412,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
@@ -505,24 +517,46 @@ async def to_code(config):
)
ARDUINO_PARTITIONS_CSV = """\
nvs, data, nvs, 0x009000, 0x005000,
otadata, data, ota, 0x00e000, 0x002000,
app0, app, ota_0, 0x010000, 0x1C0000,
app1, app, ota_1, 0x1D0000, 0x1C0000,
eeprom, data, 0x99, 0x390000, 0x001000,
spiffs, data, spiffs, 0x391000, 0x00F000
APP_PARTITION_SIZES = {
"4MB": 0x1C0000, # 1792 KB
"8MB": 0x3C0000, # 3840 KB
"16MB": 0x7C0000, # 7936 KB
"32MB": 0xFC0000, # 16128 KB
}
def get_arduino_partition_csv(flash_size):
app_partition_size = APP_PARTITION_SIZES[flash_size]
eeprom_partition_size = 0x1000 # 4 KB
spiffs_partition_size = 0xF000 # 60 KB
app0_partition_start = 0x010000 # 64 KB
app1_partition_start = app0_partition_start + app_partition_size
eeprom_partition_start = app1_partition_start + app_partition_size
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
partition_csv = f"""\
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
"""
return partition_csv
IDF_PARTITIONS_CSV = """\
# Name, Type, SubType, Offset, Size, Flags
def get_idf_partition_csv(flash_size):
app_partition_size = APP_PARTITION_SIZES[flash_size]
partition_csv = f"""\
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
app0, app, ota_0, , 0x1C0000,
app1, app, ota_1, , 0x1C0000,
nvs, data, nvs, , 0x6d000,
app0, app, ota_0, , 0x{app_partition_size:X},
app1, app, ota_1, , 0x{app_partition_size:X},
nvs, data, nvs, , 0x6D000,
"""
return partition_csv
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
@@ -565,13 +599,17 @@ def copy_files():
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
ARDUINO_PARTITIONS_CSV,
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
if CORE.using_esp_idf:
_write_sdkconfig()
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
IDF_PARTITIONS_CSV,
get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,

View File

@@ -0,0 +1,102 @@
import os
import esphome.config_validation as cv
import esphome.codegen as cg
import esphome.final_validate as fv
from esphome.components import esp32
from esphome.const import CONF_ID, CONF_BOARD
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
CONF_ESP_ADF_ID = "esp_adf_id"
CONF_ESP_ADF = "esp_adf"
esp_adf_ns = cg.esphome_ns.namespace("esp_adf")
ESPADF = esp_adf_ns.class_("ESPADF", cg.Component)
ESPADFPipeline = esp_adf_ns.class_("ESPADFPipeline", cg.Parented.template(ESPADF))
SUPPORTED_BOARDS = {
"esp32s3box": "CONFIG_ESP32_S3_BOX_BOARD",
"esp32s3boxlite": "CONFIG_ESP32_S3_BOX_LITE_BOARD",
"esp32s3box3": "CONFIG_ESP32_S3_BOX_3_BOARD",
}
def _default_board(config):
config = config.copy()
if board := config.get(CONF_BOARD) is None:
board = esp32.get_board()
if board in SUPPORTED_BOARDS:
config[CONF_BOARD] = board
return config
def final_validate_usable_board(platform: str):
def _validate(adf_config):
board = adf_config.get(CONF_BOARD)
if board not in SUPPORTED_BOARDS:
raise cv.Invalid(f"Board {board} is not supported by esp-adf {platform}")
return adf_config
return cv.Schema(
{cv.Required(CONF_ESP_ADF_ID): fv.id_declaration_match_schema(_validate)},
extra=cv.ALLOW_EXTRA,
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPADF),
cv.Optional(CONF_BOARD): cv.string_strict,
}
),
_default_board,
cv.only_with_esp_idf,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add_define("USE_ESP_ADF")
cg.add_platformio_option("build_unflags", "-Wl,--end-group")
esp32.add_idf_component(
name="esp-adf",
repo="https://github.com/espressif/esp-adf",
path="components",
ref="v2.5",
components=["*"],
submodules=["components/esp-sr", "components/esp-adf-libs"],
)
esp32.add_idf_component(
name="esp-dsp",
repo="https://github.com/espressif/esp-dsp",
ref="v1.2.0",
)
cg.add_platformio_option(
"board_build.embed_txtfiles", "components/dueros_service/duer_profile"
)
if board := config.get(CONF_BOARD):
cg.add_define("USE_ESP_ADF_BOARD")
esp32.add_idf_sdkconfig_option(SUPPORTED_BOARDS[board], True)
esp32.add_extra_script(
"pre",
"apply_adf_patches.py",
os.path.join(os.path.dirname(__file__), "apply_adf_patches.py.script"),
)
esp32.add_extra_build_file(
"esp_adf_patches/idf_v4.4_freertos.patch",
"https://github.com/espressif/esp-adf/raw/v2.5/idf_patches/idf_v4.4_freertos.patch",
)

View File

@@ -0,0 +1,23 @@
from os.path import join, isfile
Import("env")
FRAMEWORK_DIR = env.PioPlatform().get_package_dir("framework-espidf")
patchflag_path = join(FRAMEWORK_DIR, ".adf-patching-done")
PROJECT_DIR = env.get('PROJECT_DIR')
PATCH_FILE = join(PROJECT_DIR, "esp_adf_patches", "idf_v4.4_freertos.patch")
# patch file only if we didn't do it before
if not isfile(patchflag_path):
print(PATCH_FILE)
assert isfile(PATCH_FILE)
env.Execute("patch -p1 -d %s -i %s" % (FRAMEWORK_DIR, PATCH_FILE))
def _touch(path):
with open(path, "w") as fp:
fp.write("")
env.Execute(lambda *args, **kwargs: _touch(patchflag_path))

View File

@@ -0,0 +1,30 @@
#include "esp_adf.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP_IDF
#ifdef USE_ESP_ADF_BOARD
#include <board.h>
#endif
#include "esphome/core/log.h"
namespace esphome {
namespace esp_adf {
static const char *const TAG = "esp_adf";
void ESPADF::setup() {
#ifdef USE_ESP_ADF_BOARD
ESP_LOGI(TAG, "Start codec chip");
audio_board_handle_t board_handle = audio_board_init();
audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_BOTH, AUDIO_HAL_CTRL_START);
#endif
}
float ESPADF::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace esp_adf
} // namespace esphome
#endif // USE_ESP_IDF

View File

@@ -0,0 +1,58 @@
#pragma once
#ifdef USE_ESP_IDF
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace esp_adf {
static const size_t BUFFER_SIZE = 1024;
enum class TaskEventType : uint8_t {
STARTING = 0,
STARTED,
RUNNING,
STOPPING,
STOPPED,
WARNING = 255,
};
struct TaskEvent {
TaskEventType type;
esp_err_t err;
};
struct CommandEvent {
bool stop;
};
struct DataEvent {
bool stop;
size_t len;
uint8_t data[BUFFER_SIZE];
};
class ESPADF;
class ESPADFPipeline : public Parented<ESPADF> {};
class ESPADF : public Component {
public:
void setup() override;
float get_setup_priority() const override;
void lock() { this->lock_.lock(); }
bool try_lock() { return this->lock_.try_lock(); }
void unlock() { this->lock_.unlock(); }
protected:
Mutex lock_;
};
} // namespace esp_adf
} // namespace esphome
#endif // USE_ESP_IDF

View File

@@ -0,0 +1,41 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import microphone
from esphome.const import CONF_ID
from .. import (
CONF_ESP_ADF_ID,
ESPADF,
ESPADFPipeline,
esp_adf_ns,
final_validate_usable_board,
)
AUTO_LOAD = ["esp_adf"]
CONFLICTS_WITH = ["i2s_audio"]
DEPENDENCIES = ["esp32"]
ESPADFMicrophone = esp_adf_ns.class_(
"ESPADFMicrophone", ESPADFPipeline, microphone.Microphone, cg.Component
)
CONFIG_SCHEMA = cv.All(
microphone.MICROPHONE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(ESPADFMicrophone),
cv.GenerateID(CONF_ESP_ADF_ID): cv.use_id(ESPADF),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_esp_idf,
)
FINAL_VALIDATE_SCHEMA = final_validate_usable_board("microphone")
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_ESP_ADF_ID])
await microphone.register_microphone(var, config)

View File

@@ -0,0 +1,336 @@
#include "esp_adf_microphone.h"
#ifdef USE_ESP_IDF
#include <driver/i2s.h>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <algorithm_stream.h>
#include <audio_element.h>
#include <audio_hal.h>
#include <audio_pipeline.h>
#include <filter_resample.h>
#include <i2s_stream.h>
#include <raw_stream.h>
#include <recorder_sr.h>
#include <board.h>
namespace esphome {
namespace esp_adf {
static const char *const TAG = "esp_adf.microphone";
void ESPADFMicrophone::setup() {
this->ring_buffer_ = rb_create(8000, sizeof(int16_t));
if (this->ring_buffer_ == nullptr) {
ESP_LOGW(TAG, "Could not allocate ring buffer.");
this->mark_failed();
return;
}
this->read_event_queue_ = xQueueCreate(20, sizeof(TaskEvent));
this->read_command_queue_ = xQueueCreate(20, sizeof(CommandEvent));
}
void ESPADFMicrophone::start() {
if (this->is_failed())
return;
if (this->state_ == microphone::STATE_STOPPING) {
ESP_LOGW(TAG, "Microphone is stopping, cannot start.");
return;
}
this->state_ = microphone::STATE_STARTING;
}
void ESPADFMicrophone::start_() {
if (!this->parent_->try_lock()) {
return;
}
xTaskCreate(ESPADFMicrophone::read_task, "read_task", 8192, (void *) this, 0, &this->read_task_handle_);
}
void ESPADFMicrophone::read_task(void *params) {
ESPADFMicrophone *this_mic = (ESPADFMicrophone *) params;
TaskEvent event;
ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
int16_t *buffer = allocator.allocate(BUFFER_SIZE / sizeof(int16_t));
if (buffer == nullptr) {
event.type = TaskEventType::WARNING;
event.err = ESP_ERR_NO_MEM;
xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
event.type = TaskEventType::STOPPED;
event.err = ESP_OK;
xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
while (true) {
delay(10);
}
return;
}
event.type = TaskEventType::STARTING;
xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
audio_pipeline_cfg_t pipeline_cfg = {
.rb_size = 8 * 1024,
};
audio_pipeline_handle_t pipeline = audio_pipeline_init(&pipeline_cfg);
i2s_driver_config_t i2s_config = {
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_IRAM,
.dma_buf_count = 8,
.dma_buf_len = 128,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
};
i2s_stream_cfg_t i2s_cfg = {
.type = AUDIO_STREAM_READER,
.i2s_config = i2s_config,
.i2s_port = static_cast<i2s_port_t>(CODEC_ADC_I2S_PORT),
.use_alc = false,
.volume = 0,
.out_rb_size = I2S_STREAM_RINGBUFFER_SIZE,
.task_stack = I2S_STREAM_TASK_STACK,
.task_core = I2S_STREAM_TASK_CORE,
.task_prio = I2S_STREAM_TASK_PRIO,
.stack_in_ext = false,
.multi_out_num = 0,
.uninstall_drv = true,
.need_expand = false,
.expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT,
};
audio_element_handle_t i2s_stream_reader = i2s_stream_init(&i2s_cfg);
rsp_filter_cfg_t rsp_cfg = {
.src_rate = 16000,
.src_ch = 2,
.dest_rate = 16000,
.dest_bits = 16,
.dest_ch = 1,
.src_bits = I2S_BITS_PER_SAMPLE_16BIT,
.mode = RESAMPLE_DECODE_MODE,
.max_indata_bytes = RSP_FILTER_BUFFER_BYTE,
.out_len_bytes = RSP_FILTER_BUFFER_BYTE,
.type = ESP_RESAMPLE_TYPE_AUTO,
.complexity = 2,
.down_ch_idx = 0,
.prefer_flag = ESP_RSP_PREFER_TYPE_SPEED,
.out_rb_size = RSP_FILTER_RINGBUFFER_SIZE,
.task_stack = RSP_FILTER_TASK_STACK,
.task_core = RSP_FILTER_TASK_CORE,
.task_prio = RSP_FILTER_TASK_PRIO,
.stack_in_ext = true,
};
audio_element_handle_t filter = rsp_filter_init(&rsp_cfg);
algorithm_stream_cfg_t algo_cfg = {
.input_type = ALGORITHM_STREAM_INPUT_TYPE1,
.task_stack = 10 * 1024,
.task_prio = ALGORITHM_STREAM_TASK_PERIOD,
.task_core = ALGORITHM_STREAM_PINNED_TO_CORE,
.out_rb_size = ALGORITHM_STREAM_RINGBUFFER_SIZE,
.stack_in_ext = true,
.rec_linear_factor = 1,
.ref_linear_factor = 1,
.debug_input = false,
.swap_ch = false,
// .algo_mask = ALGORITHM_STREAM_USE_AGC,
// .algo_mask = (ALGORITHM_STREAM_USE_AEC | ALGORITHM_STREAM_USE_AGC | ALGORITHM_STREAM_USE_NS),
// .algo_mask = (ALGORITHM_STREAM_USE_AGC | ALGORITHM_STREAM_USE_NS),
.algo_mask = (ALGORITHM_STREAM_USE_AEC | ALGORITHM_STREAM_USE_NS),
// .algo_mask = (ALGORITHM_STREAM_USE_NS),
.sample_rate = 16000,
.mic_ch = 1,
.agc_gain = 10,
.aec_low_cost = false,
};
// audio_element_handle_t algo_stream = algo_stream_init(&algo_cfg);
raw_stream_cfg_t raw_cfg = {
.type = AUDIO_STREAM_READER,
.out_rb_size = 8 * 1024,
};
audio_element_handle_t raw_read = raw_stream_init(&raw_cfg);
audio_pipeline_register(pipeline, i2s_stream_reader, "i2s");
audio_pipeline_register(pipeline, filter, "filter");
// audio_pipeline_register(pipeline, algo_stream, "algo");
audio_pipeline_register(pipeline, raw_read, "raw");
const char *link_tag[4] = {
"i2s",
"filter",
// "algo",
"raw",
};
audio_pipeline_link(pipeline, &link_tag[0], 3);
audio_pipeline_run(pipeline);
event.type = TaskEventType::STARTED;
xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
CommandEvent command_event;
while (true) {
if (xQueueReceive(this_mic->read_command_queue_, &command_event, 0) == pdTRUE) {
if (command_event.stop) {
// Stop signal from main thread
break;
}
}
int bytes_read = raw_stream_read(raw_read, (char *) buffer, BUFFER_SIZE);
if (bytes_read == -2 || bytes_read == 0) {
// No data in buffers to read.
continue;
} else if (bytes_read < 0) {
event.type = TaskEventType::WARNING;
event.err = bytes_read;
xQueueSend(this_mic->read_event_queue_, &event, 0);
continue;
}
event.type = TaskEventType::RUNNING;
event.err = bytes_read;
xQueueSend(this_mic->read_event_queue_, &event, 0);
int available = rb_bytes_available(this_mic->ring_buffer_);
if (available < bytes_read) {
rb_read(this_mic->ring_buffer_, nullptr, bytes_read - available, 0);
}
rb_write(this_mic->ring_buffer_, (char *) buffer, bytes_read, 0);
}
allocator.deallocate(buffer, BUFFER_SIZE / sizeof(int16_t));
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
audio_pipeline_terminate(pipeline);
event.type = TaskEventType::STOPPING;
xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
audio_pipeline_unregister(pipeline, i2s_stream_reader);
audio_pipeline_unregister(pipeline, filter);
// audio_pipeline_unregister(pipeline, algo_stream);
audio_pipeline_unregister(pipeline, raw_read);
audio_pipeline_deinit(pipeline);
audio_element_deinit(i2s_stream_reader);
audio_element_deinit(filter);
// audio_element_deinit(algo_stream);
audio_element_deinit(raw_read);
event.type = TaskEventType::STOPPED;
xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
while (true) {
delay(10);
}
}
void ESPADFMicrophone::stop() {
if (this->state_ == microphone::STATE_STOPPED || this->state_ == microphone::STATE_STOPPING || this->is_failed())
return;
this->state_ = microphone::STATE_STOPPING;
CommandEvent command_event;
command_event.stop = true;
xQueueSendToFront(this->read_command_queue_, &command_event, portMAX_DELAY);
ESP_LOGD(TAG, "Stopping microphone");
}
size_t ESPADFMicrophone::read(int16_t *buf, size_t len) {
if (rb_bytes_available(this->ring_buffer_) == 0) {
return 0; // No data
}
int bytes_read = rb_read(this->ring_buffer_, (char *) buf, len, 0);
if (bytes_read == -4 || bytes_read == -2 || bytes_read == 0) {
// No data in buffers to read.
return 0;
} else if (bytes_read < 0) {
ESP_LOGW(TAG, "Error reading from I2S microphone %s (%d)", esp_err_to_name(bytes_read), bytes_read);
this->status_set_warning();
return 0;
}
this->status_clear_warning();
return bytes_read;
}
void ESPADFMicrophone::read_() {
std::vector<int16_t> samples;
samples.resize(BUFFER_SIZE);
this->read(samples.data(), samples.size());
this->data_callbacks_.call(samples);
}
void ESPADFMicrophone::watch_() {
TaskEvent event;
if (xQueueReceive(this->read_event_queue_, &event, 0) == pdTRUE) {
switch (event.type) {
case TaskEventType::STARTING:
case TaskEventType::STOPPING:
break;
case TaskEventType::STARTED:
ESP_LOGD(TAG, "Microphone started");
this->state_ = microphone::STATE_RUNNING;
break;
case TaskEventType::RUNNING:
this->status_clear_warning();
// ESP_LOGD(TAG, "Putting %d bytes into ring buffer", event.err);
break;
case TaskEventType::STOPPED:
this->parent_->unlock();
this->state_ = microphone::STATE_STOPPED;
vTaskDelete(this->read_task_handle_);
this->read_task_handle_ = nullptr;
ESP_LOGD(TAG, "Microphone stopped");
break;
case TaskEventType::WARNING:
ESP_LOGW(TAG, "Error writing to pipeline: %s", esp_err_to_name(event.err));
this->status_set_warning();
break;
}
}
}
void ESPADFMicrophone::loop() {
this->watch_();
switch (this->state_) {
case microphone::STATE_STOPPED:
case microphone::STATE_STOPPING:
break;
case microphone::STATE_STARTING:
this->start_();
break;
case microphone::STATE_RUNNING:
if (this->data_callbacks_.size() > 0) {
this->read_();
}
break;
}
}
} // namespace esp_adf
} // namespace esphome
#endif // USE_ESP_IDF

View File

@@ -0,0 +1,42 @@
#pragma once
#ifdef USE_ESP_IDF
#include "../esp_adf.h"
#include "esphome/components/microphone/microphone.h"
#include "esphome/core/component.h"
#include <ringbuf.h>
namespace esphome {
namespace esp_adf {
class ESPADFMicrophone : public ESPADFPipeline, public microphone::Microphone, public Component {
public:
void setup() override;
void start() override;
void stop() override;
void loop() override;
size_t read(int16_t *buf, size_t len) override;
protected:
void start_();
void read_();
void watch_();
static void read_task(void *params);
ringbuf_handle_t ring_buffer_;
TaskHandle_t read_task_handle_{nullptr};
QueueHandle_t read_event_queue_;
QueueHandle_t read_command_queue_;
};
} // namespace esp_adf
} // namespace esphome
#endif // USE_ESP_IDF

View File

@@ -0,0 +1,41 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import speaker
from esphome.const import CONF_ID
from .. import (
CONF_ESP_ADF_ID,
ESPADF,
ESPADFPipeline,
esp_adf_ns,
final_validate_usable_board,
)
AUTO_LOAD = ["esp_adf"]
CONFLICTS_WITH = ["i2s_audio"]
DEPENDENCIES = ["esp32"]
ESPADFSpeaker = esp_adf_ns.class_(
"ESPADFSpeaker", ESPADFPipeline, speaker.Speaker, cg.Component
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPADFSpeaker),
cv.GenerateID(CONF_ESP_ADF_ID): cv.use_id(ESPADF),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_esp_idf,
)
FINAL_VALIDATE_SCHEMA = final_validate_usable_board("speaker")
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_ESP_ADF_ID])
await speaker.register_speaker(var, config)

View File

@@ -0,0 +1,274 @@
#include "esp_adf_speaker.h"
#ifdef USE_ESP_IDF
#include <driver/i2s.h>
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <audio_hal.h>
#include <filter_resample.h>
#include <i2s_stream.h>
#include <raw_stream.h>
namespace esphome {
namespace esp_adf {
static const size_t BUFFER_COUNT = 50;
static const char *const TAG = "esp_adf.speaker";
void ESPADFSpeaker::setup() {
ESP_LOGCONFIG(TAG, "Setting up ESP ADF Speaker...");
this->buffer_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(DataEvent));
this->event_queue_ = xQueueCreate(20, sizeof(TaskEvent));
}
void ESPADFSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
void ESPADFSpeaker::start_() {
if (!this->parent_->try_lock()) {
return; // Waiting for another i2s component to return lock
}
xTaskCreate(ESPADFSpeaker::player_task, "speaker_task", 8192, (void *) this, 0, &this->player_task_handle_);
}
void ESPADFSpeaker::player_task(void *params) {
ESPADFSpeaker *this_speaker = (ESPADFSpeaker *) params;
TaskEvent event;
event.type = TaskEventType::STARTING;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
i2s_driver_config_t i2s_config = {
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_IRAM,
.dma_buf_count = 8,
.dma_buf_len = 1024,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0,
.mclk_multiple = I2S_MCLK_MULTIPLE_256,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
};
audio_pipeline_cfg_t pipeline_cfg = {
.rb_size = 8 * 1024,
};
audio_pipeline_handle_t pipeline = audio_pipeline_init(&pipeline_cfg);
i2s_stream_cfg_t i2s_cfg = {
.type = AUDIO_STREAM_WRITER,
.i2s_config = i2s_config,
.i2s_port = I2S_NUM_0,
.use_alc = false,
.volume = 0,
.out_rb_size = I2S_STREAM_RINGBUFFER_SIZE,
.task_stack = I2S_STREAM_TASK_STACK,
.task_core = I2S_STREAM_TASK_CORE,
.task_prio = I2S_STREAM_TASK_PRIO,
.stack_in_ext = false,
.multi_out_num = 0,
.uninstall_drv = true,
.need_expand = false,
.expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT,
};
audio_element_handle_t i2s_stream_writer = i2s_stream_init(&i2s_cfg);
rsp_filter_cfg_t rsp_cfg = {
.src_rate = 16000,
.src_ch = 1,
.dest_rate = 16000,
.dest_bits = 16,
.dest_ch = 2,
.src_bits = 16,
.mode = RESAMPLE_DECODE_MODE,
.max_indata_bytes = RSP_FILTER_BUFFER_BYTE,
.out_len_bytes = RSP_FILTER_BUFFER_BYTE,
.type = ESP_RESAMPLE_TYPE_AUTO,
.complexity = 2,
.down_ch_idx = 0,
.prefer_flag = ESP_RSP_PREFER_TYPE_SPEED,
.out_rb_size = RSP_FILTER_RINGBUFFER_SIZE,
.task_stack = RSP_FILTER_TASK_STACK,
.task_core = RSP_FILTER_TASK_CORE,
.task_prio = RSP_FILTER_TASK_PRIO,
.stack_in_ext = true,
};
audio_element_handle_t filter = rsp_filter_init(&rsp_cfg);
raw_stream_cfg_t raw_cfg = {
.type = AUDIO_STREAM_WRITER,
.out_rb_size = 8 * 1024,
};
audio_element_handle_t raw_write = raw_stream_init(&raw_cfg);
audio_pipeline_register(pipeline, raw_write, "raw");
audio_pipeline_register(pipeline, filter, "filter");
audio_pipeline_register(pipeline, i2s_stream_writer, "i2s");
const char *link_tag[3] = {
"raw",
// "filter",
"i2s",
};
audio_pipeline_link(pipeline, &link_tag[0], 2);
audio_pipeline_run(pipeline);
DataEvent data_event;
event.type = TaskEventType::STARTED;
xQueueSend(this_speaker->event_queue_, &event, 0);
uint32_t last_received = millis();
while (true) {
if (xQueueReceive(this_speaker->buffer_queue_, &data_event, 0) != pdTRUE) {
if (millis() - last_received > 500) {
// No audio for 500ms, stop
break;
} else {
continue;
}
}
if (data_event.stop) {
// Stop signal from main thread
while (xQueueReceive(this_speaker->buffer_queue_, &data_event, 0) == pdTRUE) {
// Flush queue
}
break;
}
size_t remaining = data_event.len;
size_t current = 0;
if (remaining > 0)
last_received = millis();
while (remaining > 0) {
int bytes_written = raw_stream_write(raw_write, (char *) data_event.data + current, remaining);
if (bytes_written == ESP_FAIL) {
event = {.type = TaskEventType::WARNING, .err = ESP_FAIL};
xQueueSend(this_speaker->event_queue_, &event, 0);
continue;
}
remaining -= bytes_written;
current += bytes_written;
}
event.type = TaskEventType::RUNNING;
xQueueSend(this_speaker->event_queue_, &event, 0);
}
audio_pipeline_stop(pipeline);
audio_pipeline_wait_for_stop(pipeline);
audio_pipeline_terminate(pipeline);
event.type = TaskEventType::STOPPING;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
audio_pipeline_unregister(pipeline, i2s_stream_writer);
audio_pipeline_unregister(pipeline, filter);
audio_pipeline_unregister(pipeline, raw_write);
audio_pipeline_deinit(pipeline);
audio_element_deinit(i2s_stream_writer);
audio_element_deinit(filter);
audio_element_deinit(raw_write);
event.type = TaskEventType::STOPPED;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
while (true) {
delay(10);
}
}
void ESPADFSpeaker::stop() {
if (this->state_ == speaker::STATE_STOPPED)
return;
if (this->state_ == speaker::STATE_STARTING) {
this->state_ = speaker::STATE_STOPPED;
return;
}
this->state_ = speaker::STATE_STOPPING;
DataEvent data;
data.stop = true;
xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY);
}
void ESPADFSpeaker::watch_() {
TaskEvent event;
if (xQueueReceive(this->event_queue_, &event, 0) == pdTRUE) {
switch (event.type) {
case TaskEventType::STARTING:
case TaskEventType::STOPPING:
break;
case TaskEventType::STARTED:
this->state_ = speaker::STATE_RUNNING;
break;
case TaskEventType::RUNNING:
this->status_clear_warning();
break;
case TaskEventType::STOPPED:
this->parent_->unlock();
this->state_ = speaker::STATE_STOPPED;
vTaskDelete(this->player_task_handle_);
this->player_task_handle_ = nullptr;
break;
case TaskEventType::WARNING:
ESP_LOGW(TAG, "Error writing to pipeline: %s", esp_err_to_name(event.err));
this->status_set_warning();
break;
}
}
}
void ESPADFSpeaker::loop() {
this->watch_();
switch (this->state_) {
case speaker::STATE_STARTING:
this->start_();
break;
case speaker::STATE_RUNNING:
case speaker::STATE_STOPPING:
case speaker::STATE_STOPPED:
break;
}
}
size_t ESPADFSpeaker::play(const uint8_t *data, size_t length) {
if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) {
this->start();
}
size_t remaining = length;
size_t index = 0;
while (remaining > 0) {
DataEvent event;
event.stop = false;
size_t to_send_length = std::min(remaining, BUFFER_SIZE);
event.len = to_send_length;
memcpy(event.data, data + index, to_send_length);
if (xQueueSend(this->buffer_queue_, &event, 0) != pdTRUE) {
return index; // Queue full
}
remaining -= to_send_length;
index += to_send_length;
}
return index;
}
bool ESPADFSpeaker::has_buffered_data() const { return uxQueueMessagesWaiting(this->buffer_queue_) > 0; }
} // namespace esp_adf
} // namespace esphome
#endif // USE_ESP_IDF

View File

@@ -0,0 +1,48 @@
#pragma once
#ifdef USE_ESP_IDF
#include "../esp_adf.h"
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "esphome/components/speaker/speaker.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <audio_element.h>
#include <audio_pipeline.h>
namespace esphome {
namespace esp_adf {
class ESPADFSpeaker : public ESPADFPipeline, public speaker::Speaker, public Component {
public:
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
void setup() override;
void loop() override;
void start() override;
void stop() override;
size_t play(const uint8_t *data, size_t length) override;
bool has_buffered_data() const override;
protected:
void start_();
void watch_();
static void player_task(void *params);
TaskHandle_t player_task_handle_{nullptr};
QueueHandle_t buffer_queue_;
QueueHandle_t event_queue_;
};
} // namespace esp_adf
} // namespace esphome
#endif // USE_ESP_IDF

View File

@@ -3,7 +3,6 @@
#ifdef USE_ARDUINO
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#include <IRSender.h> // arduino-heatpump library
namespace esphome {
@@ -11,14 +10,13 @@ namespace heatpumpir {
class IRSenderESPHome : public IRSender {
public:
IRSenderESPHome(remote_transmitter::RemoteTransmitterComponent *transmitter)
: IRSender(0), transmit_(transmitter->transmit()){};
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){};
void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming)
void space(int space_length) override;
void mark(int mark_length) override;
protected:
remote_transmitter::RemoteTransmitterComponent::TransmitCall transmit_;
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
};
} // namespace heatpumpir

View File

@@ -68,6 +68,7 @@ void LD2420Component::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420:");
ESP_LOGCONFIG(TAG, " Firmware Version : %7s", this->ld2420_firmware_ver_);
ESP_LOGCONFIG(TAG, "LD2420 Number:");
#ifdef USE_NUMBER
LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_);
LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_);
LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_);
@@ -76,10 +77,13 @@ void LD2420Component::dump_config() {
LOG_NUMBER(TAG, " Gate Move Threshold:", this->gate_move_threshold_numbers_[gate]);
LOG_NUMBER(TAG, " Gate Still Threshold::", this->gate_still_threshold_numbers_[gate]);
}
#endif
#ifdef USE_BUTTON
LOG_BUTTON(TAG, " Apply Config:", this->apply_config_button_);
LOG_BUTTON(TAG, " Revert Edits:", this->revert_config_button_);
LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_);
LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_);
#endif
ESP_LOGCONFIG(TAG, "LD2420 Select:");
LOG_SELECT(TAG, " Operating Mode", this->operating_selector_);
if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
@@ -183,9 +187,11 @@ void LD2420Component::factory_reset_action() {
return;
}
this->set_min_max_distances_timeout(FACTORY_MAX_GATE, FACTORY_MIN_GATE, FACTORY_TIMEOUT);
#ifdef USE_NUMBER
this->gate_timeout_number_->state = FACTORY_TIMEOUT;
this->min_gate_distance_number_->state = FACTORY_MIN_GATE;
this->max_gate_distance_number_->state = FACTORY_MAX_GATE;
#endif
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) {
this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate];
this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate];

View File

@@ -147,7 +147,7 @@ void MQTTClientComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str());
}
}
bool MQTTClientComponent::can_proceed() { return this->is_connected(); }
bool MQTTClientComponent::can_proceed() { return network::is_disabled() || this->is_connected(); }
void MQTTClientComponent::start_dnslookup_() {
for (auto &subscription : this->subscriptions_) {

View File

@@ -29,6 +29,14 @@ bool is_connected() {
return false;
}
bool is_disabled() {
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->is_disabled();
#endif
return false;
}
network::IPAddress get_ip_address() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr)

View File

@@ -8,6 +8,8 @@ namespace network {
/// Return whether the node is connected to the network (through wifi, eth, ...)
bool is_connected();
/// Return whether the network is disabled (only wifi for now)
bool is_disabled();
/// Get the active network hostname
std::string get_use_address();
IPAddress get_ip_address();

View File

@@ -36,7 +36,7 @@ CONFIG_SCHEMA = (
display.BASIC_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(Nextion),
cv.Optional(CONF_TFT_URL): cv.All(cv.string, cv.only_with_arduino),
cv.Optional(CONF_TFT_URL): cv.url,
cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
cv.Optional(CONF_ON_SETUP): automation.validate_automation(
{
@@ -85,10 +85,10 @@ async def to_code(config):
if CONF_TFT_URL in config:
cg.add_define("USE_NEXTION_TFT_UPLOAD")
cg.add(var.set_tft_url(config[CONF_TFT_URL]))
if CORE.is_esp32:
if CORE.is_esp32 and CORE.using_arduino:
cg.add_library("WiFiClientSecure", None)
cg.add_library("HTTPClient", None)
if CORE.is_esp8266:
elif CORE.is_esp8266 and CORE.using_arduino:
cg.add_library("ESP8266HTTPClient", None)
if CONF_TOUCH_SLEEP_TIMEOUT in config:

View File

@@ -128,7 +128,7 @@ void Nextion::dump_config() {
ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False");
if (this->touch_sleep_timeout_ != 0) {
ESP_LOGCONFIG(TAG, " Touch Timeout: %d", this->touch_sleep_timeout_);
ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_);
}
if (this->wake_up_page_ != -1) {
@@ -868,6 +868,12 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool
start = millis();
while ((timeout == 0 && this->available()) || millis() - start <= timeout) {
if (!this->available()) {
App.feed_wdt();
delay(1);
continue;
}
this->read_byte(&c);
if (c == 0xFF) {
nr_of_ff_bytes++;
@@ -886,7 +892,7 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool
}
}
App.feed_wdt();
delay(1);
delay(2);
if (exit_flag || ff_flag) {
break;

View File

@@ -12,14 +12,18 @@
#include "esphome/components/display/display_color_utils.h"
#ifdef USE_NEXTION_TFT_UPLOAD
#ifdef ARDUINO
#ifdef USE_ESP32
#include <HTTPClient.h>
#endif
#endif // USE_ESP32
#ifdef USE_ESP8266
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#endif
#endif
#endif // USE_ESP8266
#elif defined(USE_ESP_IDF)
#include <esp_http_client.h>
#endif // ARDUINO vs ESP-IDF
#endif // USE_NEXTION_TFT_UPLOAD
namespace esphome {
namespace nextion {
@@ -685,16 +689,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
#ifdef USE_NEXTION_TFT_UPLOAD
/**
* Set the tft file URL. https seems problamtic with arduino..
* Set the tft file URL. https seems problematic with arduino..
*/
void set_tft_url(const std::string &tft_url) { this->tft_url_ = tft_url; }
#endif
/**
* Upload the tft file and softreset the Nextion
* Upload the tft file and soft reset Nextion
* @return bool True: Transfer completed successfuly, False: Transfer failed.
*/
void upload_tft();
bool upload_tft();
void dump_config() override;
/**
@@ -817,16 +823,16 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr};
WiFiClient *get_wifi_client_();
#endif
int content_length_ = 0;
int tft_size_ = 0;
#ifdef ARDUINO
/**
* will request chunk_size chunks from the web server
* and send each to the nextion
* @param int contentLength Total size of the file
* @param uint32_t chunk_size
* @return true if success, false for failure.
* @param HTTPClient http HTTP client handler.
* @param int range_start Position of next byte to transfer.
* @return position of last byte transferred, -1 for failure.
*/
int content_length_ = 0;
int tft_size_ = 0;
int upload_by_chunks_(HTTPClient *http, int range_start);
bool upload_with_range_(uint32_t range_start, uint32_t range_end);
@@ -839,7 +845,30 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
* @return true if success, false for failure.
*/
bool upload_from_buffer_(const uint8_t *file_buf, size_t buf_size);
void upload_end_();
/**
* Ends the upload process, restart Nextion and, if successful,
* restarts ESP
* @param bool url successful True: Transfer completed successfuly, False: Transfer failed.
* @return bool True: Transfer completed successfuly, False: Transfer failed.
*/
bool upload_end_(bool successful);
#elif defined(USE_ESP_IDF)
/**
* will request 4096 bytes chunks from the web server
* and send each to Nextion
* @param std::string url Full url for download.
* @param int range_start Position of next byte to transfer.
* @return position of last byte transferred, -1 for failure.
*/
int upload_range(const std::string &url, int range_start);
/**
* Ends the upload process, restart Nextion and, if successful,
* restarts ESP
* @param bool url successful True: Transfer completed successfuly, False: Transfer failed.
* @return bool True: Transfer completed successfuly, False: Transfer failed.
*/
bool upload_end(bool successful);
#endif // ARDUINO vs ESP-IDF
#endif // USE_NEXTION_TFT_UPLOAD

View File

@@ -55,7 +55,7 @@ void Nextion::set_protocol_reparse_mode(bool active_mode) {
// Set Colors
void Nextion::set_component_background_color(const char *component, uint32_t color) {
this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, color);
this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu32, component, color);
}
void Nextion::set_component_background_color(const char *component, const char *color) {
@@ -68,7 +68,8 @@ void Nextion::set_component_background_color(const char *component, Color color)
}
void Nextion::set_component_pressed_background_color(const char *component, uint32_t color) {
this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, color);
this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu32, component,
color);
}
void Nextion::set_component_pressed_background_color(const char *component, const char *color) {
@@ -89,7 +90,7 @@ void Nextion::set_component_picc(const char *component, uint8_t pic_id) {
}
void Nextion::set_component_font_color(const char *component, uint32_t color) {
this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, color);
this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu32, component, color);
}
void Nextion::set_component_font_color(const char *component, const char *color) {
@@ -102,7 +103,7 @@ void Nextion::set_component_font_color(const char *component, Color color) {
}
void Nextion::set_component_pressed_font_color(const char *component, uint32_t color) {
this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, color);
this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu32, component, color);
}
void Nextion::set_component_pressed_font_color(const char *component, const char *color) {

View File

@@ -1,5 +1,6 @@
#include "nextion.h"
#ifdef ARDUINO
#ifdef USE_NEXTION_TFT_UPLOAD
#include "esphome/core/application.h"
@@ -128,15 +129,15 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) {
return range_end + 1;
}
void Nextion::upload_tft() {
bool Nextion::upload_tft() {
if (this->is_updating_) {
ESP_LOGD(TAG, "Currently updating");
return;
return false;
}
if (!network::is_connected()) {
ESP_LOGD(TAG, "network is not connected");
return;
return false;
}
this->is_updating_ = true;
@@ -164,7 +165,7 @@ void Nextion::upload_tft() {
ESP_LOGD(TAG, "connection failed");
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_);
return;
return false;
} else {
ESP_LOGD(TAG, "Connected");
}
@@ -192,7 +193,7 @@ void Nextion::upload_tft() {
}
if ((code != 200 && code != 206) || tries > 5) {
this->upload_end_();
return this->upload_end_(false);
}
String content_range_string = http.header("Content-Range");
@@ -203,7 +204,7 @@ void Nextion::upload_tft() {
if (this->content_length_ < 4096) {
ESP_LOGE(TAG, "Failed to get file size");
this->upload_end_();
return this->upload_end_(false);
}
ESP_LOGD(TAG, "Updating Nextion %s...", this->device_model_.c_str());
@@ -246,7 +247,7 @@ void Nextion::upload_tft() {
ESP_LOGD(TAG, "preparation for tft update done");
} else {
ESP_LOGD(TAG, "preparation for tft update failed %d \"%s\"", response[0], response.c_str());
this->upload_end_();
return this->upload_end_(false);
}
// Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096
@@ -280,7 +281,7 @@ void Nextion::upload_tft() {
this->transfer_buffer_ = allocator.allocate(chunk_size);
if (!this->transfer_buffer_)
this->upload_end_();
return this->upload_end_(false);
}
this->transfer_buffer_size_ = chunk_size;
@@ -295,7 +296,7 @@ void Nextion::upload_tft() {
result = this->upload_by_chunks_(&http, result);
if (result < 0) {
ESP_LOGD(TAG, "Error updating Nextion!");
this->upload_end_();
return this->upload_end_(false);
}
App.feed_wdt();
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
@@ -303,15 +304,19 @@ void Nextion::upload_tft() {
}
ESP_LOGD(TAG, "Successfully updated Nextion!");
this->upload_end_();
return this->upload_end_(true);
}
void Nextion::upload_end_() {
bool Nextion::upload_end_(bool successful) {
this->is_updating_ = false;
ESP_LOGD(TAG, "Restarting Nextion");
this->soft_reset();
delay(1500); // NOLINT
ESP_LOGD(TAG, "Restarting esphome");
ESP.restart(); // NOLINT(readability-static-accessed-through-instance)
if (successful) {
delay(1500); // NOLINT
ESP_LOGD(TAG, "Restarting esphome");
ESP.restart(); // NOLINT(readability-static-accessed-through-instance)
}
return successful;
}
#ifdef USE_ESP8266
@@ -337,3 +342,4 @@ WiFiClient *Nextion::get_wifi_client_() {
} // namespace esphome
#endif // USE_NEXTION_TFT_UPLOAD
#endif // ARDUINO

View File

@@ -0,0 +1,268 @@
#include "nextion.h"
#ifdef USE_ESP_IDF
#ifdef USE_NEXTION_TFT_UPLOAD
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/util.h"
#include "esphome/core/log.h"
#include "esphome/components/network/util.h"
#include <esp_heap_caps.h>
#include <esp_http_client.h>
namespace esphome {
namespace nextion {
static const char *const TAG = "nextion_upload";
// Followed guide
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
int Nextion::upload_range(const std::string &url, int range_start) {
ESP_LOGVV(TAG, "url: %s", url.c_str());
uint range_size = this->tft_size_ - range_start;
ESP_LOGVV(TAG, "tft_size_: %i", this->tft_size_);
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
int range_end = (range_start == 0) ? std::min(this->tft_size_, 16383) : this->tft_size_;
if (range_size <= 0 or range_end <= range_start) {
ESP_LOGE(TAG, "Invalid range");
ESP_LOGD(TAG, "Range start: %i", range_start);
ESP_LOGD(TAG, "Range end: %i", range_end);
ESP_LOGD(TAG, "Range size: %i", range_size);
return -1;
}
esp_http_client_config_t config = {
.url = url.c_str(),
.cert_pem = nullptr,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
char range_header[64];
sprintf(range_header, "bytes=%d-%d", range_start, range_end);
ESP_LOGV(TAG, "Requesting range: %s", range_header);
esp_http_client_set_header(client, "Range", range_header);
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
ESP_LOGV(TAG, "Opening http connetion");
esp_err_t err;
if ((err = esp_http_client_open(client, 0)) != ESP_OK) {
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return -1;
}
ESP_LOGV(TAG, "Fetch content length");
int content_length = esp_http_client_fetch_headers(client);
ESP_LOGV(TAG, "content_length = %d", content_length);
if (content_length <= 0) {
ESP_LOGE(TAG, "Failed to get content length: %d", content_length);
esp_http_client_cleanup(client);
return -1;
}
int total_read_len = 0, read_len;
ESP_LOGV(TAG, "Allocate buffer");
uint8_t *buffer = new uint8_t[4096];
std::string recv_string;
if (buffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for buffer");
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
} else {
ESP_LOGV(TAG, "Memory for buffer allocated successfully");
while (true) {
App.feed_wdt();
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
int read_len = esp_http_client_read(client, reinterpret_cast<char *>(buffer), 4096);
ESP_LOGVV(TAG, "Read %d bytes from HTTP client, writing to UART", read_len);
if (read_len > 0) {
this->write_array(buffer, read_len);
ESP_LOGVV(TAG, "Write to UART successful");
this->recv_ret_string_(recv_string, 5000, true);
this->content_length_ -= read_len;
ESP_LOGD(TAG, "Uploaded %0.2f %%, remaining %d bytes",
100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_);
if (recv_string[0] != 0x05) { // 0x05 == "ok"
ESP_LOGD(
TAG, "recv_string [%s]",
format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
}
// handle partial upload request
if (recv_string[0] == 0x08 && recv_string.size() == 5) {
uint32_t result = 0;
for (int j = 0; j < 4; ++j) {
result += static_cast<uint8_t>(recv_string[j + 1]) << (8 * j);
}
if (result > 0) {
ESP_LOGI(TAG, "Nextion reported new range %" PRIu32, result);
this->content_length_ = this->tft_size_ - result;
// Deallocate the buffer when done
delete[] buffer;
ESP_LOGVV(TAG, "Memory for buffer deallocated");
esp_http_client_cleanup(client);
esp_http_client_close(client);
return result;
}
}
recv_string.clear();
} else if (read_len == 0) {
ESP_LOGV(TAG, "End of HTTP response reached");
break; // Exit the loop if there is no more data to read
} else {
ESP_LOGE(TAG, "Failed to read from HTTP client, error code: %d", read_len);
break; // Exit the loop on error
}
}
// Deallocate the buffer when done
delete[] buffer;
ESP_LOGVV(TAG, "Memory for buffer deallocated");
}
esp_http_client_cleanup(client);
esp_http_client_close(client);
return range_end + 1;
}
bool Nextion::upload_tft() {
ESP_LOGD(TAG, "Nextion TFT upload requested");
ESP_LOGD(TAG, "url: %s", this->tft_url_.c_str());
if (this->is_updating_) {
ESP_LOGW(TAG, "Currently updating");
return false;
}
if (!network::is_connected()) {
ESP_LOGE(TAG, "Network is not connected");
return false;
}
this->is_updating_ = true;
// Define the configuration for the HTTP client
ESP_LOGV(TAG, "Establishing connection to HTTP server");
ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size());
esp_http_client_config_t config = {
.url = this->tft_url_.c_str(),
.cert_pem = nullptr,
.method = HTTP_METHOD_HEAD,
.timeout_ms = 15000,
};
// Initialize the HTTP client with the configuration
ESP_LOGV(TAG, "Initializing HTTP client");
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
esp_http_client_handle_t http = esp_http_client_init(&config);
if (!http) {
ESP_LOGE(TAG, "Failed to initialize HTTP client.");
return this->upload_end(false);
}
// Perform the HTTP request
ESP_LOGV(TAG, "Check if the client could connect");
ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size());
esp_err_t err = esp_http_client_perform(http);
if (err != ESP_OK) {
ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(http);
return this->upload_end(false);
}
// Check the HTTP Status Code
int status_code = esp_http_client_get_status_code(http);
ESP_LOGV(TAG, "HTTP Status Code: %d", status_code);
size_t tft_file_size = esp_http_client_get_content_length(http);
ESP_LOGD(TAG, "TFT file size: %zu", tft_file_size);
if (tft_file_size < 4096) {
ESP_LOGE(TAG, "File size check failed. Size: %zu", tft_file_size);
esp_http_client_cleanup(http);
return this->upload_end(false);
} else {
ESP_LOGV(TAG, "File size check passed. Proceeding...");
}
this->content_length_ = tft_file_size;
this->tft_size_ = tft_file_size;
ESP_LOGD(TAG, "Updating Nextion");
// The Nextion will ignore the update command if it is sleeping
this->send_command_("sleep=0");
this->set_backlight_brightness(1.0);
vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT
App.feed_wdt();
char command[128];
// Tells the Nextion the content length of the tft file and baud rate it will be sent at
// Once the Nextion accepts the command it will wait until the file is successfully uploaded
// If it fails for any reason a power cycle of the display will be needed
sprintf(command, "whmi-wris %d,%" PRIu32 ",1", this->content_length_, this->parent_->get_baud_rate());
// Clear serial receive buffer
uint8_t d;
while (this->available()) {
this->read_byte(&d);
};
this->send_command_(command);
std::string response;
ESP_LOGV(TAG, "Waiting for upgrade response");
this->recv_ret_string_(response, 2048, true); // This can take some time to return
// The Nextion display will, if it's ready to accept data, send a 0x05 byte.
ESP_LOGD(TAG, "Upgrade response is [%s]",
format_hex_pretty(reinterpret_cast<const uint8_t *>(response.data()), response.size()).c_str());
if (response.find(0x05) != std::string::npos) {
ESP_LOGV(TAG, "Preparation for tft update done");
} else {
ESP_LOGE(TAG, "Preparation for tft update failed %d \"%s\"", response[0], response.c_str());
esp_http_client_cleanup(http);
return this->upload_end(false);
}
ESP_LOGD(TAG, "Updating tft from \"%s\" with a file size of %d, Heap Size %" PRIu32, this->tft_url_.c_str(),
content_length_, esp_get_free_heap_size());
ESP_LOGV(TAG, "Starting transfer by chunks loop");
int result = 0;
while (content_length_ > 0) {
result = upload_range(this->tft_url_.c_str(), result);
if (result < 0) {
ESP_LOGE(TAG, "Error updating Nextion!");
esp_http_client_cleanup(http);
return this->upload_end(false);
}
App.feed_wdt();
ESP_LOGV(TAG, "Heap Size %" PRIu32 ", Bytes left %d", esp_get_free_heap_size(), content_length_);
}
ESP_LOGD(TAG, "Successfully updated Nextion!");
ESP_LOGD(TAG, "Close HTTP connection");
esp_http_client_close(http);
esp_http_client_cleanup(http);
return upload_end(true);
}
bool Nextion::upload_end(bool successful) {
this->is_updating_ = false;
ESP_LOGD(TAG, "Restarting Nextion");
this->soft_reset();
vTaskDelay(pdMS_TO_TICKS(1500)); // NOLINT
if (successful) {
ESP_LOGD(TAG, "Restarting esphome");
esp_restart(); // NOLINT(readability-static-accessed-through-instance)
}
return successful;
}
} // namespace nextion
} // namespace esphome
#endif // USE_NEXTION_TFT_UPLOAD
#endif // USE_ESP_IDF

View File

@@ -2,7 +2,7 @@ from math import log
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components import sensor, resistance_sampler
from esphome.const import (
CONF_CALIBRATION,
CONF_REFERENCE_RESISTANCE,
@@ -15,6 +15,8 @@ from esphome.const import (
UNIT_CELSIUS,
)
AUTO_LOAD = ["resistance_sampler"]
ntc_ns = cg.esphome_ns.namespace("ntc")
NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor)
@@ -124,7 +126,7 @@ CONFIG_SCHEMA = (
)
.extend(
{
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_SENSOR): cv.use_id(resistance_sampler.ResistanceSampler),
cv.Required(CONF_CALIBRATION): process_calibration,
}
)

View File

@@ -52,8 +52,9 @@ RemoteReceiverTrigger = ns.class_(
"RemoteReceiverTrigger", automation.Trigger, RemoteReceiverListener
)
RemoteTransmitterDumper = ns.class_("RemoteTransmitterDumper")
RemoteTransmittable = ns.class_("RemoteTransmittable")
RemoteTransmitterActionBase = ns.class_(
"RemoteTransmitterActionBase", automation.Action
"RemoteTransmitterActionBase", RemoteTransmittable, automation.Action
)
RemoteReceiverBase = ns.class_("RemoteReceiverBase")
RemoteTransmitterBase = ns.class_("RemoteTransmitterBase")
@@ -68,11 +69,30 @@ def templatize(value):
return cv.Schema(ret)
REMOTE_LISTENER_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase),
}
)
REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase),
}
)
async def register_listener(var, config):
receiver = await cg.get_variable(config[CONF_RECEIVER_ID])
cg.add(receiver.register_listener(var))
async def register_transmittable(var, config):
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter_))
def register_binary_sensor(name, type, schema):
return BINARY_SENSOR_REGISTRY.register(name, type, schema)
@@ -129,10 +149,9 @@ def validate_repeat(value):
BASE_REMOTE_TRANSMITTER_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase),
cv.Optional(CONF_REPEAT): validate_repeat,
}
)
).extend(REMOTE_TRANSMITTABLE_SCHEMA)
def register_action(name, type_, schema):
@@ -143,9 +162,8 @@ def register_action(name, type_, schema):
def decorator(func):
async def new_func(config, action_id, template_arg, args):
transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID])
var = cg.new_Pvariable(action_id, template_arg)
cg.add(var.set_parent(transmitter))
await register_transmittable(var, config)
if CONF_REPEAT in config:
conf = config[CONF_REPEAT]
template_ = await cg.templatable(conf[CONF_TIMES], args, cg.uint32)
@@ -1539,7 +1557,7 @@ MIDEA_SCHEMA = cv.Schema(
@register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA)
def midea_binary_sensor(var, config):
cg.add(var.set_code(config[CONF_CODE]))
cg.add(var.set_data(config[CONF_CODE]))
@register_trigger("midea", MideaTrigger, MideaData)

View File

@@ -67,20 +67,7 @@ class MideaProtocol : public RemoteProtocol<MideaData> {
void dump(const MideaData &data) override;
};
class MideaBinarySensor : public RemoteReceiverBinarySensorBase {
public:
bool matches(RemoteReceiveData src) override {
auto data = MideaProtocol().decode(src);
return data.has_value() && data.value() == this->data_;
}
void set_code(const std::vector<uint8_t> &code) { this->data_ = code; }
protected:
MideaData data_;
};
using MideaTrigger = RemoteReceiverTrigger<MideaProtocol, MideaData>;
using MideaDumper = RemoteReceiverDumper<MideaProtocol, MideaData>;
DECLARE_REMOTE_PROTOCOL(Midea)
template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
TEMPLATABLE_VALUE(std::vector<uint8_t>, code)

View File

@@ -15,6 +15,8 @@ struct RCSwitchData {
class RCSwitchBase {
public:
using ProtocolData = RCSwitchData;
RCSwitchBase() = default;
RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high,
uint32_t one_low, bool inverted);
@@ -213,7 +215,7 @@ class RCSwitchDumper : public RemoteReceiverDumperBase {
bool dump(RemoteReceiveData src) override;
};
using RCSwitchTrigger = RemoteReceiverTrigger<RCSwitchBase, RCSwitchData>;
using RCSwitchTrigger = RemoteReceiverTrigger<RCSwitchBase>;
} // namespace remote_base
} // namespace esphome

View File

@@ -127,6 +127,14 @@ class RemoteTransmitterBase : public RemoteComponentBase {
this->temp_.reset();
return TransmitCall(this);
}
template<typename Protocol>
void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
auto call = this->transmit();
Protocol().encode(call.get_data(), data);
call.set_send_times(send_times);
call.set_send_wait(send_wait);
call.perform();
}
protected:
void send_(uint32_t send_times, uint32_t send_wait);
@@ -184,12 +192,13 @@ class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitial
template<typename T> class RemoteProtocol {
public:
virtual void encode(RemoteTransmitData *dst, const T &data) = 0;
virtual optional<T> decode(RemoteReceiveData src) = 0;
virtual void dump(const T &data) = 0;
using ProtocolData = T;
virtual void encode(RemoteTransmitData *dst, const ProtocolData &data) = 0;
virtual optional<ProtocolData> decode(RemoteReceiveData src) = 0;
virtual void dump(const ProtocolData &data) = 0;
};
template<typename T, typename D> class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase {
template<typename T> class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase {
public:
RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {}
@@ -201,13 +210,14 @@ template<typename T, typename D> class RemoteReceiverBinarySensor : public Remot
}
public:
void set_data(D data) { data_ = data; }
void set_data(typename T::ProtocolData data) { data_ = data; }
protected:
D data_;
typename T::ProtocolData data_;
};
template<typename T, typename D> class RemoteReceiverTrigger : public Trigger<D>, public RemoteReceiverListener {
template<typename T>
class RemoteReceiverTrigger : public Trigger<typename T::ProtocolData>, public RemoteReceiverListener {
protected:
bool on_receive(RemoteReceiveData src) override {
auto proto = T();
@@ -220,28 +230,36 @@ template<typename T, typename D> class RemoteReceiverTrigger : public Trigger<D>
}
};
template<typename... Ts> class RemoteTransmitterActionBase : public Action<Ts...> {
class RemoteTransmittable {
public:
void set_parent(RemoteTransmitterBase *parent) { this->parent_ = parent; }
RemoteTransmittable() {}
RemoteTransmittable(RemoteTransmitterBase *transmitter) : transmitter_(transmitter) {}
void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
TEMPLATABLE_VALUE(uint32_t, send_times);
TEMPLATABLE_VALUE(uint32_t, send_wait);
protected:
template<typename Protocol>
void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
this->transmitter_->transmit<Protocol>(data, send_times, send_wait);
}
RemoteTransmitterBase *transmitter_;
};
template<typename... Ts> class RemoteTransmitterActionBase : public RemoteTransmittable, public Action<Ts...> {
TEMPLATABLE_VALUE(uint32_t, send_times)
TEMPLATABLE_VALUE(uint32_t, send_wait)
protected:
void play(Ts... x) override {
auto call = this->parent_->transmit();
auto call = this->transmitter_->transmit();
this->encode(call.get_data(), x...);
call.set_send_times(this->send_times_.value_or(x..., 1));
call.set_send_wait(this->send_wait_.value_or(x..., 0));
call.perform();
}
protected:
virtual void encode(RemoteTransmitData *dst, Ts... x) = 0;
RemoteTransmitterBase *parent_{};
};
template<typename T, typename D> class RemoteReceiverDumper : public RemoteReceiverDumperBase {
template<typename T> class RemoteReceiverDumper : public RemoteReceiverDumperBase {
public:
bool dump(RemoteReceiveData src) override {
auto proto = T();
@@ -254,9 +272,9 @@ template<typename T, typename D> class RemoteReceiverDumper : public RemoteRecei
};
#define DECLARE_REMOTE_PROTOCOL_(prefix) \
using prefix##BinarySensor = RemoteReceiverBinarySensor<prefix##Protocol, prefix##Data>; \
using prefix##Trigger = RemoteReceiverTrigger<prefix##Protocol, prefix##Data>; \
using prefix##Dumper = RemoteReceiverDumper<prefix##Protocol, prefix##Data>;
using prefix##BinarySensor = RemoteReceiverBinarySensor<prefix##Protocol>; \
using prefix##Trigger = RemoteReceiverTrigger<prefix##Protocol>; \
using prefix##Dumper = RemoteReceiverDumper<prefix##Protocol>;
#define DECLARE_REMOTE_PROTOCOL(prefix) DECLARE_REMOTE_PROTOCOL_(prefix)
} // namespace remote_base

View File

@@ -1,7 +1,8 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/resistance_sampler/resistance_sampler.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome {
namespace resistance {
@@ -11,7 +12,7 @@ enum ResistanceConfiguration {
DOWNSTREAM,
};
class ResistanceSensor : public Component, public sensor::Sensor {
class ResistanceSensor : public Component, public sensor::Sensor, resistance_sampler::ResistanceSampler {
public:
void set_sensor(Sensor *sensor) { sensor_ = sensor; }
void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; }

View File

@@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.components import sensor, resistance_sampler
from esphome.const import (
CONF_SENSOR,
STATE_CLASS_MEASUREMENT,
@@ -8,8 +8,15 @@ from esphome.const import (
ICON_FLASH,
)
AUTO_LOAD = ["resistance_sampler"]
resistance_ns = cg.esphome_ns.namespace("resistance")
ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor)
ResistanceSensor = resistance_ns.class_(
"ResistanceSensor",
cg.Component,
sensor.Sensor,
resistance_sampler.ResistanceSampler,
)
CONF_REFERENCE_VOLTAGE = "reference_voltage"
CONF_CONFIGURATION = "configuration"

View File

@@ -0,0 +1,6 @@
import esphome.codegen as cg
resistance_sampler_ns = cg.esphome_ns.namespace("resistance_sampler")
ResistanceSampler = resistance_sampler_ns.class_("ResistanceSampler")
CODEOWNERS = ["@jesserockz"]

View File

@@ -0,0 +1,10 @@
#pragma once
namespace esphome {
namespace resistance_sampler {
/// Abstract interface to mark components that provide resistance values.
class ResistanceSampler {};
} // namespace resistance_sampler
} // namespace esphome

View File

@@ -74,12 +74,12 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# The default/recommended arduino framework version
# - https://github.com/earlephilhower/arduino-pico/releases
# - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 4, 0)
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 6, 0)
# The platformio/raspberrypi version to use for arduino frameworks
# - https://github.com/platformio/platform-raspberrypi/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/raspberrypi
ARDUINO_PLATFORM_VERSION = cv.Version(1, 9, 0)
ARDUINO_PLATFORM_VERSION = cv.Version(1, 10, 0)
def _arduino_check_versions(value):

View File

@@ -610,6 +610,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
if (code == "wake-word-timeout" || code == "wake_word_detection_aborted") {
// Don't change state here since either the "tts-end" or "run-end" events will do it.
return;
} else if (code == "wake-provider-missing" || code == "wake-engine-missing") {
// Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again.
this->request_stop();
this->error_trigger_->trigger(code, message);
return;
}
ESP_LOGE(TAG, "Error: %s - %s", code.c_str(), message.c_str());
if (this->state_ != State::IDLE) {

View File

@@ -389,6 +389,10 @@ void WiFiComponent::print_connect_params_() {
bssid_t bssid = wifi_bssid();
ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str());
if (this->is_disabled()) {
ESP_LOGCONFIG(TAG, " WiFi is disabled!");
return;
}
ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str());
ESP_LOGCONFIG(TAG, " IP Address: %s", wifi_sta_ip().str().c_str());
ESP_LOGCONFIG(TAG, " BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X"), bssid[0], bssid[1], bssid[2], bssid[3],

View File

@@ -43,11 +43,17 @@ def validate_mode(mode):
return mode
def validate_pin(pin):
if pin in (8, 9):
raise cv.Invalid(f"pin {pin} doesn't exist")
return pin
XL9535_PIN_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(XL9535GPIOPin),
cv.Required(CONF_XL9535): cv.use_id(XL9535Component),
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15),
cv.Required(CONF_NUMBER): cv.All(cv.int_range(min=0, max=17), validate_pin),
cv.Optional(CONF_MODE, default={}): cv.All(
{
cv.Optional(CONF_INPUT, default=False): cv.boolean,

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2023.11.0b2"
__version__ = "2023.12.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -64,6 +64,7 @@
// IDF-specific feature flags
#ifdef USE_ESP_IDF
#define USE_MQTT_IDF_ENQUEUE
#define USE_ESP_ADF
#endif
// ESP32-specific feature flags

95
esphome/dashboard/core.py Normal file
View File

@@ -0,0 +1,95 @@
from __future__ import annotations
import asyncio
import logging
import threading
from typing import TYPE_CHECKING
from ..zeroconf import DiscoveredImport
from .entries import DashboardEntry
from .settings import DashboardSettings
if TYPE_CHECKING:
from .status.mdns import MDNSStatus
_LOGGER = logging.getLogger(__name__)
def list_dashboard_entries() -> list[DashboardEntry]:
"""List all dashboard entries."""
return DASHBOARD.settings.entries()
class ESPHomeDashboard:
"""Class that represents the dashboard."""
__slots__ = (
"loop",
"ping_result",
"import_result",
"stop_event",
"ping_request",
"mqtt_ping_request",
"mdns_status",
"settings",
)
def __init__(self) -> None:
"""Initialize the ESPHomeDashboard."""
self.loop: asyncio.AbstractEventLoop | None = None
self.ping_result: dict[str, bool | None] = {}
self.import_result: dict[str, DiscoveredImport] = {}
self.stop_event = threading.Event()
self.ping_request: asyncio.Event | None = None
self.mqtt_ping_request = threading.Event()
self.mdns_status: MDNSStatus | None = None
self.settings: DashboardSettings = DashboardSettings()
async def async_setup(self) -> None:
"""Setup the dashboard."""
self.loop = asyncio.get_running_loop()
self.ping_request = asyncio.Event()
async def async_run(self) -> None:
"""Run the dashboard."""
settings = self.settings
mdns_task: asyncio.Task | None = None
ping_status_task: asyncio.Task | None = None
if settings.status_use_ping:
from .status.ping import PingStatus
ping_status = PingStatus()
ping_status_task = asyncio.create_task(ping_status.async_run())
else:
from .status.mdns import MDNSStatus
mdns_status = MDNSStatus()
await mdns_status.async_refresh_hosts()
self.mdns_status = mdns_status
mdns_task = asyncio.create_task(mdns_status.async_run())
if settings.status_use_mqtt:
from .status.mqtt import MqttStatusThread
status_thread_mqtt = MqttStatusThread()
status_thread_mqtt.start()
shutdown_event = asyncio.Event()
try:
await shutdown_event.wait()
finally:
_LOGGER.info("Shutting down...")
self.stop_event.set()
self.ping_request.set()
if ping_status_task:
ping_status_task.cancel()
if mdns_task:
mdns_task.cancel()
if settings.status_use_mqtt:
status_thread_mqtt.join()
self.mqtt_ping_request.set()
await asyncio.sleep(0)
DASHBOARD = ESPHomeDashboard()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
from __future__ import annotations
import os
from esphome import const
from esphome.storage_json import StorageJSON, ext_storage_path
class DashboardEntry:
"""Represents a single dashboard entry.
This class is thread-safe and read-only.
"""
__slots__ = ("path", "_storage", "_loaded_storage")
def __init__(self, path: str) -> None:
"""Initialize the DashboardEntry."""
self.path = path
self._storage = None
self._loaded_storage = False
def __repr__(self):
"""Return the representation of this entry."""
return (
f"DashboardEntry({self.path} "
f"address={self.address} "
f"web_port={self.web_port} "
f"name={self.name} "
f"no_mdns={self.no_mdns})"
)
@property
def filename(self):
"""Return the filename of this entry."""
return os.path.basename(self.path)
@property
def storage(self) -> StorageJSON | None:
"""Return the StorageJSON object for this entry."""
if not self._loaded_storage:
self._storage = StorageJSON.load(ext_storage_path(self.filename))
self._loaded_storage = True
return self._storage
@property
def address(self):
"""Return the address of this entry."""
if self.storage is None:
return None
return self.storage.address
@property
def no_mdns(self):
"""Return the no_mdns of this entry."""
if self.storage is None:
return None
return self.storage.no_mdns
@property
def web_port(self):
"""Return the web port of this entry."""
if self.storage is None:
return None
return self.storage.web_port
@property
def name(self):
"""Return the name of this entry."""
if self.storage is None:
return self.filename.replace(".yml", "").replace(".yaml", "")
return self.storage.name
@property
def friendly_name(self):
"""Return the friendly name of this entry."""
if self.storage is None:
return self.name
return self.storage.friendly_name
@property
def comment(self):
"""Return the comment of this entry."""
if self.storage is None:
return None
return self.storage.comment
@property
def target_platform(self):
"""Return the target platform of this entry."""
if self.storage is None:
return None
return self.storage.target_platform
@property
def update_available(self):
"""Return if an update is available for this entry."""
if self.storage is None:
return True
return self.update_old != self.update_new
@property
def update_old(self):
if self.storage is None:
return ""
return self.storage.esphome_version or ""
@property
def update_new(self):
return const.__version__
@property
def loaded_integrations(self):
if self.storage is None:
return []
return self.storage.loaded_integrations

View File

@@ -0,0 +1,146 @@
from __future__ import annotations
import hmac
import os
from pathlib import Path
from esphome import util
from esphome.core import CORE
from esphome.helpers import get_bool_env
from esphome.storage_json import ext_storage_path
from .entries import DashboardEntry
from .util import password_hash
class DashboardSettings:
"""Settings for the dashboard."""
def __init__(self):
self.config_dir = ""
self.password_hash = ""
self.username = ""
self.using_password = False
self.on_ha_addon = False
self.cookie_secret = None
self.absolute_config_dir = None
self._entry_cache: dict[
str, tuple[tuple[int, int, float, int], DashboardEntry]
] = {}
def parse_args(self, args):
self.on_ha_addon: bool = args.ha_addon
password: str = args.password or os.getenv("PASSWORD", "")
if not self.on_ha_addon:
self.username: str = args.username or os.getenv("USERNAME", "")
self.using_password = bool(password)
if self.using_password:
self.password_hash = password_hash(password)
self.config_dir: str = args.configuration
self.absolute_config_dir: Path = Path(self.config_dir).resolve()
CORE.config_path = os.path.join(self.config_dir, ".")
@property
def relative_url(self):
return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL", "/")
@property
def status_use_ping(self):
return get_bool_env("ESPHOME_DASHBOARD_USE_PING")
@property
def status_use_mqtt(self):
return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT")
@property
def using_ha_addon_auth(self):
if not self.on_ha_addon:
return False
return not get_bool_env("DISABLE_HA_AUTHENTICATION")
@property
def using_auth(self):
return self.using_password or self.using_ha_addon_auth
@property
def streamer_mode(self):
return get_bool_env("ESPHOME_STREAMER_MODE")
def check_password(self, username, password):
if not self.using_auth:
return True
if username != self.username:
return False
# Compare password in constant running time (to prevent timing attacks)
return hmac.compare_digest(self.password_hash, password_hash(password))
def rel_path(self, *args):
joined_path = os.path.join(self.config_dir, *args)
# Raises ValueError if not relative to ESPHome config folder
Path(joined_path).resolve().relative_to(self.absolute_config_dir)
return joined_path
def list_yaml_files(self) -> list[str]:
return util.list_yaml_files([self.config_dir])
def entries(self) -> list[DashboardEntry]:
"""Fetch all dashboard entries, thread-safe."""
path_to_cache_key: dict[str, tuple[int, int, float, int]] = {}
#
# The cache key is (inode, device, mtime, size)
# which allows us to avoid locking since it ensures
# every iteration of this call will always return the newest
# items from disk at the cost of a stat() call on each
# file which is much faster than reading the file
# for the cache hit case which is the common case.
#
# Because there is no lock the cache may
# get built more than once but that's fine as its still
# thread-safe and results in orders of magnitude less
# reads from disk than if we did not cache at all and
# does not have a lock contention issue.
#
for file in self.list_yaml_files():
try:
# Prefer the json storage path if it exists
stat = os.stat(ext_storage_path(os.path.basename(file)))
except OSError:
try:
# Fallback to the yaml file if the storage
# file does not exist or could not be generated
stat = os.stat(file)
except OSError:
# File was deleted, ignore
continue
path_to_cache_key[file] = (
stat.st_ino,
stat.st_dev,
stat.st_mtime,
stat.st_size,
)
entry_cache = self._entry_cache
# Remove entries that no longer exist
removed: list[str] = []
for file in entry_cache:
if file not in path_to_cache_key:
removed.append(file)
for file in removed:
entry_cache.pop(file)
dashboard_entries: list[DashboardEntry] = []
for file, cache_key in path_to_cache_key.items():
if cached_entry := entry_cache.get(file):
entry_key, dashboard_entry = cached_entry
if entry_key == cache_key:
dashboard_entries.append(dashboard_entry)
continue
dashboard_entry = DashboardEntry(file)
dashboard_entries.append(dashboard_entry)
entry_cache[file] = (cache_key, dashboard_entry)
return dashboard_entries

View File

View File

@@ -0,0 +1,109 @@
from __future__ import annotations
import asyncio
from esphome.zeroconf import (
ESPHOME_SERVICE_TYPE,
AsyncEsphomeZeroconf,
DashboardBrowser,
DashboardImportDiscovery,
DashboardStatus,
)
from ..core import DASHBOARD, list_dashboard_entries
class MDNSStatus:
"""Class that updates the mdns status."""
def __init__(self) -> None:
"""Initialize the MDNSStatus class."""
super().__init__()
self.aiozc: AsyncEsphomeZeroconf | None = None
# This is the current mdns state for each host (True, False, None)
self.host_mdns_state: dict[str, bool | None] = {}
# This is the hostnames to filenames mapping
self.host_name_to_filename: dict[str, str] = {}
self.filename_to_host_name: dict[str, str] = {}
# This is a set of host names to track (i.e no_mdns = false)
self.host_name_with_mdns_enabled: set[set] = set()
self._loop = asyncio.get_running_loop()
def filename_to_host_name_thread_safe(self, filename: str) -> str | None:
"""Resolve a filename to an address in a thread-safe manner."""
return self.filename_to_host_name.get(filename)
async def async_resolve_host(self, host_name: str) -> str | None:
"""Resolve a host name to an address in a thread-safe manner."""
if aiozc := self.aiozc:
return await aiozc.async_resolve_host(host_name)
return None
async def async_refresh_hosts(self):
"""Refresh the hosts to track."""
entries = await self._loop.run_in_executor(None, list_dashboard_entries)
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
host_mdns_state = self.host_mdns_state
host_name_to_filename = self.host_name_to_filename
filename_to_host_name = self.filename_to_host_name
ping_result = DASHBOARD.ping_result
for entry in entries:
name = entry.name
# If no_mdns is set, remove it from the set
if entry.no_mdns:
host_name_with_mdns_enabled.discard(name)
continue
# We are tracking this host
host_name_with_mdns_enabled.add(name)
filename = entry.filename
# If we just adopted/imported this host, we likely
# already have a state for it, so we should make sure
# to set it so the dashboard shows it as online
if name in host_mdns_state:
ping_result[filename] = host_mdns_state[name]
# Make sure the mapping is up to date
# so when we get an mdns update we can map it back
# to the filename
host_name_to_filename[name] = filename
filename_to_host_name[filename] = name
async def async_run(self) -> None:
dashboard = DASHBOARD
aiozc = AsyncEsphomeZeroconf()
self.aiozc = aiozc
host_mdns_state = self.host_mdns_state
host_name_to_filename = self.host_name_to_filename
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
ping_result = dashboard.ping_result
def on_update(dat: dict[str, bool | None]) -> None:
"""Update the global PING_RESULT dict."""
for name, result in dat.items():
host_mdns_state[name] = result
if name in host_name_with_mdns_enabled:
filename = host_name_to_filename[name]
ping_result[filename] = result
stat = DashboardStatus(on_update)
imports = DashboardImportDiscovery()
dashboard.import_result = imports.import_state
browser = DashboardBrowser(
aiozc.zeroconf,
ESPHOME_SERVICE_TYPE,
[stat.browser_callback, imports.browser_callback],
)
while not dashboard.stop_event.is_set():
await self.async_refresh_hosts()
await dashboard.ping_request.wait()
dashboard.ping_request.clear()
await browser.async_cancel()
await aiozc.async_close()
self.aiozc = None

View File

@@ -0,0 +1,67 @@
from __future__ import annotations
import binascii
import json
import os
import threading
from esphome import mqtt
from ..core import DASHBOARD, list_dashboard_entries
class MqttStatusThread(threading.Thread):
"""Status thread to get the status of the devices via MQTT."""
def run(self) -> None:
"""Run the status thread."""
dashboard = DASHBOARD
entries = list_dashboard_entries()
config = mqtt.config_from_env()
topic = "esphome/discover/#"
def on_message(client, userdata, msg):
nonlocal entries
payload = msg.payload.decode(errors="backslashreplace")
if len(payload) > 0:
data = json.loads(payload)
if "name" not in data:
return
for entry in entries:
if entry.name == data["name"]:
dashboard.ping_result[entry.filename] = True
return
def on_connect(client, userdata, flags, return_code):
client.publish("esphome/discover", None, retain=False)
mqttid = str(binascii.hexlify(os.urandom(6)).decode())
client = mqtt.prepare(
config,
[topic],
on_message,
on_connect,
None,
None,
f"esphome-dashboard-{mqttid}",
)
client.loop_start()
while not dashboard.stop_event.wait(2):
# update entries
entries = list_dashboard_entries()
# will be set to true on on_message
for entry in entries:
if entry.no_mdns:
dashboard.ping_result[entry.filename] = False
client.publish("esphome/discover", None, retain=False)
dashboard.mqtt_ping_request.wait()
dashboard.mqtt_ping_request.clear()
client.disconnect()
client.loop_stop()

View File

@@ -0,0 +1,57 @@
from __future__ import annotations
import asyncio
import os
from typing import cast
from ..core import DASHBOARD
from ..entries import DashboardEntry
from ..core import list_dashboard_entries
from ..util import chunked
async def _async_ping_host(host: str) -> bool:
"""Ping a host."""
ping_command = ["ping", "-n" if os.name == "nt" else "-c", "1"]
process = await asyncio.create_subprocess_exec(
*ping_command,
host,
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
close_fds=False,
)
await process.wait()
return process.returncode == 0
class PingStatus:
def __init__(self) -> None:
"""Initialize the PingStatus class."""
super().__init__()
self._loop = asyncio.get_running_loop()
async def async_run(self) -> None:
"""Run the ping status."""
dashboard = DASHBOARD
while not dashboard.stop_event.is_set():
# Only ping if the dashboard is open
await dashboard.ping_request.wait()
dashboard.ping_result.clear()
entries = await self._loop.run_in_executor(None, list_dashboard_entries)
to_ping: list[DashboardEntry] = [
entry for entry in entries if entry.address is not None
]
for ping_group in chunked(to_ping, 16):
ping_group = cast(list[DashboardEntry], ping_group)
results = await asyncio.gather(
*(_async_ping_host(entry.address) for entry in ping_group),
return_exceptions=True,
)
for entry, result in zip(ping_group, results):
if isinstance(result, Exception):
result = False
elif isinstance(result, BaseException):
raise result
dashboard.ping_result[entry.filename] = result

View File

@@ -1,5 +1,9 @@
import hashlib
import unicodedata
from collections.abc import Iterable
from functools import partial
from itertools import islice
from typing import Any
from esphome.const import ALLOWED_NAME_CHARS
@@ -30,3 +34,19 @@ def friendly_name_slugify(value):
.strip("-")
)
return "".join(c for c in value if c in ALLOWED_NAME_CHARS)
def take(take_num: int, iterable: Iterable) -> list[Any]:
"""Return first n items of the iterable as a list.
From itertools recipes
"""
return list(islice(iterable, take_num))
def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]:
"""Break *iterable* into lists of length *n*.
From more-itertools
"""
return iter(partial(take, chunked_num, iter(iterable)), [])

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,13 @@
from __future__ import annotations
import gzip
import hashlib
import io
import logging
import random
import socket
import sys
import time
import gzip
from esphome.core import EsphomeError
from esphome.helpers import is_ip_address, resolve_ip_address
@@ -40,6 +43,10 @@ MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45]
FEATURE_SUPPORTS_COMPRESSION = 0x01
UPLOAD_BLOCK_SIZE = 8192
UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8
_LOGGER = logging.getLogger(__name__)
@@ -184,7 +191,9 @@ def send_check(sock, data, msg):
raise OTAError(f"Error sending {msg}: {err}") from err
def perform_ota(sock, password, file_handle, filename):
def perform_ota(
sock: socket.socket, password: str, file_handle: io.IOBase, filename: str
) -> None:
file_contents = file_handle.read()
file_size = len(file_contents)
_LOGGER.info("Uploading %s (%s bytes)", filename, file_size)
@@ -254,14 +263,16 @@ def perform_ota(sock, password, file_handle, filename):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 0)
# Limit send buffer (usually around 100kB) in order to have progress bar
# show the actual progress
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 8192)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, UPLOAD_BUFFER_SIZE)
# Set higher timeout during upload
sock.settimeout(20.0)
sock.settimeout(30.0)
start_time = time.perf_counter()
offset = 0
progress = ProgressBar()
while True:
chunk = upload_contents[offset : offset + 1024]
chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE]
if not chunk:
break
offset += len(chunk)
@@ -277,8 +288,9 @@ def perform_ota(sock, password, file_handle, filename):
# Enable nodelay for last checks
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
duration = time.perf_counter() - start_time
_LOGGER.info("Waiting for result...")
_LOGGER.info("Upload took %.2f seconds, waiting for result...", duration)
receive_exactly(sock, 1, "receive OK", RESPONSE_RECEIVE_OK)
receive_exactly(sock, 1, "Update end", RESPONSE_UPDATE_END_OK)

View File

@@ -23,6 +23,14 @@ from esphome.core import (
from esphome.helpers import add_class_to_obj
from esphome.util import OrderedDict, filter_yaml_files
try:
from yaml import CSafeLoader as FastestAvailableSafeLoader
except ImportError:
from yaml import ( # type: ignore[assignment]
SafeLoader as FastestAvailableSafeLoader,
)
_LOGGER = logging.getLogger(__name__)
# Mostly copied from Home Assistant because that code works fine and
@@ -89,7 +97,7 @@ def _add_data_ref(fn):
return wrapped
class ESPHomeLoader(yaml.SafeLoader):
class ESPHomeLoader(FastestAvailableSafeLoader):
"""Loader class that keeps track of line numbers."""
@_add_data_ref

View File

@@ -1,22 +1,21 @@
from __future__ import annotations
import asyncio
import logging
from dataclasses import dataclass
from typing import Callable
from zeroconf import (
IPVersion,
ServiceBrowser,
ServiceInfo,
ServiceStateChange,
Zeroconf,
)
from zeroconf import IPVersion, ServiceInfo, ServiceStateChange, Zeroconf
from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf
from esphome.storage_json import StorageJSON, ext_storage_path
_LOGGER = logging.getLogger(__name__)
_BACKGROUND_TASKS: set[asyncio.Task] = set()
class HostResolver(ServiceInfo):
"""Resolve a host name to an IP address."""
@@ -65,7 +64,7 @@ class DiscoveredImport:
network: str
class DashboardBrowser(ServiceBrowser):
class DashboardBrowser(AsyncServiceBrowser):
"""A class to browse for ESPHome nodes."""
@@ -94,7 +93,28 @@ class DashboardImportDiscovery:
# Ignore updates for devices that are not in the import state
return
info = zeroconf.get_service_info(service_type, name)
info = AsyncServiceInfo(
service_type,
name,
)
if info.load_from_cache(zeroconf):
self._process_service_info(name, info)
return
task = asyncio.create_task(
self._async_process_service_info(zeroconf, info, service_type, name)
)
_BACKGROUND_TASKS.add(task)
task.add_done_callback(_BACKGROUND_TASKS.discard)
async def _async_process_service_info(
self, zeroconf: Zeroconf, info: AsyncServiceInfo, service_type: str, name: str
) -> None:
"""Process a service info."""
if await info.async_request(zeroconf):
self._process_service_info(name, info)
def _process_service_info(self, name: str, info: ServiceInfo) -> None:
"""Process a service info."""
_LOGGER.debug("-> resolved info: %s", info)
if info is None:
return
@@ -146,13 +166,32 @@ class DashboardImportDiscovery:
)
def _make_host_resolver(host: str) -> HostResolver:
"""Create a new HostResolver for the given host name."""
name = host.partition(".")[0]
info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}")
return info
class EsphomeZeroconf(Zeroconf):
def resolve_host(self, host: str, timeout=3.0):
def resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
"""Resolve a host name to an IP address."""
name = host.partition(".")[0]
info = HostResolver(f"{name}.{ESPHOME_SERVICE_TYPE}", ESPHOME_SERVICE_TYPE)
if (info.load_from_cache(self) or info.request(self, timeout * 1000)) and (
addresses := info.ip_addresses_by_version(IPVersion.V4Only)
):
info = _make_host_resolver(host)
if (
info.load_from_cache(self)
or (timeout and info.request(self, timeout * 1000))
) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
return str(addresses[0])
return None
class AsyncEsphomeZeroconf(AsyncZeroconf):
async def async_resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
"""Resolve a host name to an IP address."""
info = _make_host_resolver(host)
if (
info.load_from_cache(self.zeroconf)
or (timeout and await info.async_request(self.zeroconf, timeout * 1000))
) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
return str(addresses[0])
return None

View File

@@ -159,7 +159,7 @@ board_build.filesystem_size = 0.5m
platform = https://github.com/maxgerhardt/platform-raspberrypi.git
platform_packages =
; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted
earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.4.0/rp2040-3.4.0.zip
earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/3.6.0/rp2040-3.6.0.zip
framework = arduino
lib_deps =

View File

@@ -10,8 +10,8 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile
esptool==4.6.2
click==8.1.7
esphome-dashboard==20231107.0
aioesphomeapi==18.2.4
zeroconf==0.120.0
aioesphomeapi==18.4.1
zeroconf==0.127.0
# esp-idf requires this, but doesn't bundle it by default
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24

View File

@@ -1,6 +1,6 @@
pylint==2.17.6
flake8==6.1.0 # also change in .pre-commit-config.yaml when updating
black==23.10.1 # also change in .pre-commit-config.yaml when updating
black==23.11.0 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -45,7 +45,7 @@ def sub(path, pattern, repl, expected_count=1):
content, count = re.subn(pattern, repl, content, flags=re.MULTILINE)
if expected_count is not None:
assert count == expected_count, f"Pattern {pattern} replacement failed!"
with open(path, "wt") as fh:
with open(path, "w") as fh:
fh.write(content)

View File

@@ -3050,6 +3050,9 @@ remote_receiver:
on_coolix:
then:
delay: !lambda "return x.first + x.second;"
on_rc_switch:
then:
delay: !lambda "return uint32_t(x.code) + x.protocol;"
status_led:
pin: GPIO2

View File

@@ -425,6 +425,15 @@ binary_sensor:
input: true
inverted: false
- platform: gpio
name: XL9535 Pin 17
pin:
xl9535: xl9535_hub
number: 17
mode:
input: true
inverted: false
climate:
- platform: tuya
id: tuya_climate

View File

@@ -1,4 +1,4 @@
from typing import Iterator
from collections.abc import Iterator
import math

View File

@@ -1,5 +1,5 @@
import pytest
from mock import Mock
from unittest.mock import Mock
from esphome import cpp_helpers as ch
from esphome import const