mirror of
https://github.com/esphome/esphome.git
synced 2025-11-04 00:51:49 +00:00
Compare commits
60 Commits
2023.11.2
...
jesserockz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
970680b1b2 | ||
|
|
f500bd5e6f | ||
|
|
e2bb81e233 | ||
|
|
26a1d14ee0 | ||
|
|
97f07f8d13 | ||
|
|
4e3170dc95 | ||
|
|
c795dbde26 | ||
|
|
4ce627b4ee | ||
|
|
86b4fdc139 | ||
|
|
20ea8bf06e | ||
|
|
642db6d92b | ||
|
|
4aac5a23cd | ||
|
|
214b419db2 | ||
|
|
cdcb25be8e | ||
|
|
aecc6655db | ||
|
|
2754ddec1b | ||
|
|
ae0e481cff | ||
|
|
f198be39d7 | ||
|
|
08fc96b890 | ||
|
|
8c28bea5b1 | ||
|
|
00eedeb8b3 | ||
|
|
0a4853ba7b | ||
|
|
45276cc244 | ||
|
|
684cf10230 | ||
|
|
63a277ba80 | ||
|
|
53f3385c49 | ||
|
|
51930a0243 | ||
|
|
6a5cea171e | ||
|
|
3363c8f434 | ||
|
|
3b891bc146 | ||
|
|
0f19450ab4 | ||
|
|
98ec798bfc | ||
|
|
01d28ce3fc | ||
|
|
bc7519f645 | ||
|
|
28513a0502 | ||
|
|
3e3266fa74 | ||
|
|
ce020b1f9f | ||
|
|
d394b957d1 | ||
|
|
cf22c55430 | ||
|
|
511348974e | ||
|
|
972598a698 | ||
|
|
d81bec860b | ||
|
|
fde7a04ee7 | ||
|
|
ff9bffc363 | ||
|
|
89b3af8be4 | ||
|
|
c9b2e54c1a | ||
|
|
6dd92053b5 | ||
|
|
33346c0b6a | ||
|
|
161fbecfe1 | ||
|
|
fce2eafda0 | ||
|
|
c19f0cf6bc | ||
|
|
b05e7bfe0a | ||
|
|
3e58ee2130 | ||
|
|
bab9c7c70e | ||
|
|
0b60a1d9eb | ||
|
|
f7455ad76a | ||
|
|
3190e86ba8 | ||
|
|
a34569d314 | ||
|
|
6c1c200cf9 | ||
|
|
3635179564 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -68,7 +68,7 @@ ENV \
|
||||
# See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian
|
||||
RUN \
|
||||
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
|
||||
ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \
|
||||
ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \
|
||||
fi
|
||||
|
||||
RUN \
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -3,26 +3,23 @@ from typing import Union, Optional
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import os
|
||||
import esphome.final_validate as fv
|
||||
|
||||
from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p
|
||||
from esphome.const import (
|
||||
CONF_ADVANCED,
|
||||
CONF_BOARD,
|
||||
CONF_COMPONENTS,
|
||||
CONF_ESPHOME,
|
||||
CONF_FRAMEWORK,
|
||||
CONF_IGNORE_EFUSE_MAC_CRC,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
CONF_REF,
|
||||
CONF_REFRESH,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
CONF_VARIANT,
|
||||
CONF_VERSION,
|
||||
CONF_ADVANCED,
|
||||
CONF_REFRESH,
|
||||
CONF_PATH,
|
||||
CONF_URL,
|
||||
CONF_REF,
|
||||
CONF_IGNORE_EFUSE_MAC_CRC,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_NAME,
|
||||
@@ -330,32 +327,6 @@ def _detect_variant(value):
|
||||
return value
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]:
|
||||
return config
|
||||
|
||||
pio_flash_size_key = "board_upload.flash_size"
|
||||
pio_partitions_key = "board_build.partitions"
|
||||
if (
|
||||
CONF_PARTITIONS in config
|
||||
and pio_partitions_key
|
||||
in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
||||
)
|
||||
|
||||
if (
|
||||
pio_flash_size_key
|
||||
in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONF_PLATFORM_VERSION = "platform_version"
|
||||
|
||||
ARDUINO_FRAMEWORK_SCHEMA = cv.All(
|
||||
@@ -416,7 +387,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
|
||||
|
||||
|
||||
FLASH_SIZES = [
|
||||
"2MB",
|
||||
"4MB",
|
||||
"8MB",
|
||||
"16MB",
|
||||
@@ -424,7 +394,6 @@ FLASH_SIZES = [
|
||||
]
|
||||
|
||||
CONF_FLASH_SIZE = "flash_size"
|
||||
CONF_PARTITIONS = "partitions"
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -432,7 +401,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
|
||||
*FLASH_SIZES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_PARTITIONS): cv.file_,
|
||||
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
|
||||
cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
|
||||
}
|
||||
@@ -442,9 +410,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
|
||||
|
||||
|
||||
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])
|
||||
@@ -497,10 +462,7 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
|
||||
|
||||
if CONF_PARTITIONS in config:
|
||||
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
|
||||
else:
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
|
||||
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
|
||||
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
|
||||
@@ -545,10 +507,7 @@ async def to_code(config):
|
||||
[f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"],
|
||||
)
|
||||
|
||||
if CONF_PARTITIONS in config:
|
||||
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
|
||||
else:
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
|
||||
cg.add_define(
|
||||
"USE_ARDUINO_VERSION_CODE",
|
||||
@@ -559,7 +518,6 @@ async def to_code(config):
|
||||
|
||||
|
||||
APP_PARTITION_SIZES = {
|
||||
"2MB": 0x0C0000, # 768 KB
|
||||
"4MB": 0x1C0000, # 1792 KB
|
||||
"8MB": 0x3C0000, # 3840 KB
|
||||
"16MB": 0x7C0000, # 7936 KB
|
||||
|
||||
102
esphome/components/esp_adf/__init__.py
Normal file
102
esphome/components/esp_adf/__init__.py
Normal 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",
|
||||
)
|
||||
23
esphome/components/esp_adf/apply_adf_patches.py.script
Normal file
23
esphome/components/esp_adf/apply_adf_patches.py.script
Normal 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))
|
||||
30
esphome/components/esp_adf/esp_adf.cpp
Normal file
30
esphome/components/esp_adf/esp_adf.cpp
Normal 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
|
||||
58
esphome/components/esp_adf/esp_adf.h
Normal file
58
esphome/components/esp_adf/esp_adf.h
Normal 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
|
||||
41
esphome/components/esp_adf/microphone/__init__.py
Normal file
41
esphome/components/esp_adf/microphone/__init__.py
Normal 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)
|
||||
336
esphome/components/esp_adf/microphone/esp_adf_microphone.cpp
Normal file
336
esphome/components/esp_adf/microphone/esp_adf_microphone.cpp
Normal 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
|
||||
42
esphome/components/esp_adf/microphone/esp_adf_microphone.h
Normal file
42
esphome/components/esp_adf/microphone/esp_adf_microphone.h
Normal 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
|
||||
41
esphome/components/esp_adf/speaker/__init__.py
Normal file
41
esphome/components/esp_adf/speaker/__init__.py
Normal 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)
|
||||
274
esphome/components/esp_adf/speaker/esp_adf_speaker.cpp
Normal file
274
esphome/components/esp_adf/speaker/esp_adf_speaker.cpp
Normal 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
|
||||
48
esphome/components/esp_adf/speaker/esp_adf_speaker.h
Normal file
48
esphome/components/esp_adf/speaker/esp_adf_speaker.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "my9231.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace my9231 {
|
||||
@@ -52,11 +51,7 @@ void MY9231OutputComponent::setup() {
|
||||
MY9231_CMD_SCATTER_APDM | MY9231_CMD_FREQUENCY_DIVIDE_1 | MY9231_CMD_REACTION_FAST | MY9231_CMD_ONE_SHOT_DISABLE;
|
||||
ESP_LOGV(TAG, " Command: 0x%02X", command);
|
||||
|
||||
{
|
||||
InterruptLock lock;
|
||||
this->send_dcki_pulses_(32 * this->num_chips_);
|
||||
this->init_chips_(command);
|
||||
}
|
||||
this->init_chips_(command);
|
||||
ESP_LOGV(TAG, " Chips initialized.");
|
||||
}
|
||||
void MY9231OutputComponent::dump_config() {
|
||||
@@ -71,14 +66,11 @@ void MY9231OutputComponent::loop() {
|
||||
if (!this->update_)
|
||||
return;
|
||||
|
||||
{
|
||||
InterruptLock lock;
|
||||
for (auto pwm_amount : this->pwm_amounts_) {
|
||||
this->write_word_(pwm_amount, this->bit_depth_);
|
||||
}
|
||||
// Send 8 DI pulses. After 8 falling edges, the duty data are store.
|
||||
this->send_di_pulses_(8);
|
||||
for (auto pwm_amount : this->pwm_amounts_) {
|
||||
this->write_word_(pwm_amount, this->bit_depth_);
|
||||
}
|
||||
// Send 8 DI pulses. After 8 falling edges, the duty data are store.
|
||||
this->send_di_pulses_(8);
|
||||
this->update_ = false;
|
||||
}
|
||||
void MY9231OutputComponent::set_channel_value_(uint8_t channel, uint16_t value) {
|
||||
@@ -100,7 +92,6 @@ void MY9231OutputComponent::init_chips_(uint8_t command) {
|
||||
// Send 16 DI pulse. After 14 falling edges, the command data are
|
||||
// stored and after 16 falling edges the duty mode is activated.
|
||||
this->send_di_pulses_(16);
|
||||
delayMicroseconds(12);
|
||||
}
|
||||
void MY9231OutputComponent::write_word_(uint16_t value, uint8_t bits) {
|
||||
for (uint8_t i = bits; i > 0; i--) {
|
||||
@@ -115,13 +106,6 @@ void MY9231OutputComponent::send_di_pulses_(uint8_t count) {
|
||||
this->pin_di_->digital_write(false);
|
||||
}
|
||||
}
|
||||
void MY9231OutputComponent::send_dcki_pulses_(uint8_t count) {
|
||||
delayMicroseconds(12);
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
this->pin_dcki_->digital_write(true);
|
||||
this->pin_dcki_->digital_write(false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace my9231
|
||||
} // namespace esphome
|
||||
|
||||
@@ -49,7 +49,6 @@ class MY9231OutputComponent : public Component {
|
||||
void init_chips_(uint8_t command);
|
||||
void write_word_(uint16_t value, uint8_t bits);
|
||||
void send_di_pulses_(uint8_t count);
|
||||
void send_dcki_pulses_(uint8_t count);
|
||||
|
||||
GPIOPin *pin_di_;
|
||||
GPIOPin *pin_dcki_;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
268
esphome/components/nextion/nextion_upload_idf.cpp
Normal file
268
esphome/components/nextion/nextion_upload_idf.cpp
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -18,25 +18,20 @@ DEPENDENCIES = ["api", "microphone"]
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
|
||||
CONF_ON_END = "on_end"
|
||||
CONF_ON_ERROR = "on_error"
|
||||
CONF_ON_INTENT_END = "on_intent_end"
|
||||
CONF_ON_INTENT_START = "on_intent_start"
|
||||
CONF_SILENCE_DETECTION = "silence_detection"
|
||||
CONF_ON_LISTENING = "on_listening"
|
||||
CONF_ON_START = "on_start"
|
||||
CONF_ON_STT_END = "on_stt_end"
|
||||
CONF_ON_STT_VAD_END = "on_stt_vad_end"
|
||||
CONF_ON_STT_VAD_START = "on_stt_vad_start"
|
||||
CONF_ON_TTS_END = "on_tts_end"
|
||||
CONF_ON_TTS_START = "on_tts_start"
|
||||
CONF_ON_WAKE_WORD_DETECTED = "on_wake_word_detected"
|
||||
|
||||
CONF_SILENCE_DETECTION = "silence_detection"
|
||||
CONF_ON_STT_END = "on_stt_end"
|
||||
CONF_ON_TTS_START = "on_tts_start"
|
||||
CONF_ON_TTS_END = "on_tts_end"
|
||||
CONF_ON_END = "on_end"
|
||||
CONF_ON_ERROR = "on_error"
|
||||
CONF_USE_WAKE_WORD = "use_wake_word"
|
||||
CONF_VAD_THRESHOLD = "vad_threshold"
|
||||
|
||||
CONF_AUTO_GAIN = "auto_gain"
|
||||
CONF_NOISE_SUPPRESSION_LEVEL = "noise_suppression_level"
|
||||
CONF_AUTO_GAIN = "auto_gain"
|
||||
CONF_VOLUME_MULTIPLIER = "volume_multiplier"
|
||||
|
||||
|
||||
@@ -93,18 +88,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_INTENT_START): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_INTENT_END): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_STT_VAD_START): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
cv.Optional(CONF_ON_STT_VAD_END): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
)
|
||||
@@ -194,34 +177,6 @@ async def to_code(config):
|
||||
config[CONF_ON_CLIENT_DISCONNECTED],
|
||||
)
|
||||
|
||||
if CONF_ON_INTENT_START in config:
|
||||
await automation.build_automation(
|
||||
var.get_intent_start_trigger(),
|
||||
[],
|
||||
config[CONF_ON_INTENT_START],
|
||||
)
|
||||
|
||||
if CONF_ON_INTENT_END in config:
|
||||
await automation.build_automation(
|
||||
var.get_intent_end_trigger(),
|
||||
[],
|
||||
config[CONF_ON_INTENT_END],
|
||||
)
|
||||
|
||||
if CONF_ON_STT_VAD_START in config:
|
||||
await automation.build_automation(
|
||||
var.get_stt_vad_start_trigger(),
|
||||
[],
|
||||
config[CONF_ON_STT_VAD_START],
|
||||
)
|
||||
|
||||
if CONF_ON_STT_VAD_END in config:
|
||||
await automation.build_automation(
|
||||
var.get_stt_vad_end_trigger(),
|
||||
[],
|
||||
config[CONF_ON_STT_VAD_END],
|
||||
)
|
||||
|
||||
cg.add_define("USE_VOICE_ASSISTANT")
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ void VoiceAssistant::setup() {
|
||||
|
||||
this->socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
if (socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket");
|
||||
ESP_LOGW(TAG, "Could not create socket.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -69,7 +69,7 @@ void VoiceAssistant::setup() {
|
||||
ExternalRAMAllocator<uint8_t> speaker_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->speaker_buffer_ = speaker_allocator.allocate(SPEAKER_BUFFER_SIZE);
|
||||
if (this->speaker_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate speaker buffer");
|
||||
ESP_LOGW(TAG, "Could not allocate speaker buffer.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -79,7 +79,7 @@ void VoiceAssistant::setup() {
|
||||
ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
|
||||
this->input_buffer_ = allocator.allocate(INPUT_BUFFER_SIZE);
|
||||
if (this->input_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate input buffer");
|
||||
ESP_LOGW(TAG, "Could not allocate input buffer.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -89,7 +89,7 @@ void VoiceAssistant::setup() {
|
||||
|
||||
this->ring_buffer_ = rb_create(BUFFER_SIZE, sizeof(int16_t));
|
||||
if (this->ring_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate ring buffer");
|
||||
ESP_LOGW(TAG, "Could not allocate ring buffer.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -98,7 +98,7 @@ void VoiceAssistant::setup() {
|
||||
ExternalRAMAllocator<uint8_t> send_allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
|
||||
this->send_buffer_ = send_allocator.allocate(SEND_BUFFER_SIZE);
|
||||
if (send_buffer_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not allocate send buffer");
|
||||
ESP_LOGW(TAG, "Could not allocate send buffer.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -221,8 +221,8 @@ void VoiceAssistant::loop() {
|
||||
msg.audio_settings = audio_settings;
|
||||
|
||||
if (this->api_client_ == nullptr || !this->api_client_->send_voice_assistant_request(msg)) {
|
||||
ESP_LOGW(TAG, "Could not request start");
|
||||
this->error_trigger_->trigger("not-connected", "Could not request start");
|
||||
ESP_LOGW(TAG, "Could not request start.");
|
||||
this->error_trigger_->trigger("not-connected", "Could not request start.");
|
||||
this->continuous_ = false;
|
||||
this->set_state_(State::IDLE, State::IDLE);
|
||||
break;
|
||||
@@ -280,7 +280,7 @@ void VoiceAssistant::loop() {
|
||||
this->speaker_buffer_size_ += len;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Receive buffer full");
|
||||
ESP_LOGW(TAG, "Receive buffer full.");
|
||||
}
|
||||
if (this->speaker_buffer_size_ > 0) {
|
||||
size_t written = this->speaker_->play(this->speaker_buffer_, this->speaker_buffer_size_);
|
||||
@@ -290,7 +290,7 @@ void VoiceAssistant::loop() {
|
||||
this->speaker_buffer_index_ -= written;
|
||||
this->set_timeout("speaker-timeout", 2000, [this]() { this->speaker_->stop(); });
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Speaker buffer full");
|
||||
ESP_LOGW(TAG, "Speaker buffer full.");
|
||||
}
|
||||
}
|
||||
if (this->wait_for_stream_end_) {
|
||||
@@ -513,7 +513,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_STT_START:
|
||||
ESP_LOGD(TAG, "STT started");
|
||||
ESP_LOGD(TAG, "STT Started");
|
||||
this->listening_trigger_->trigger();
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_STT_END: {
|
||||
@@ -525,24 +525,19 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
}
|
||||
}
|
||||
if (text.empty()) {
|
||||
ESP_LOGW(TAG, "No text in STT_END event");
|
||||
ESP_LOGW(TAG, "No text in STT_END event.");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str());
|
||||
this->stt_end_trigger_->trigger(text);
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_INTENT_START:
|
||||
ESP_LOGD(TAG, "Intent started");
|
||||
this->intent_start_trigger_->trigger();
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_INTENT_END: {
|
||||
for (auto arg : msg.data) {
|
||||
if (arg.name == "conversation_id") {
|
||||
this->conversation_id_ = std::move(arg.value);
|
||||
}
|
||||
}
|
||||
this->intent_end_trigger_->trigger();
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_TTS_START: {
|
||||
@@ -553,7 +548,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
}
|
||||
}
|
||||
if (text.empty()) {
|
||||
ESP_LOGW(TAG, "No text in TTS_START event");
|
||||
ESP_LOGW(TAG, "No text in TTS_START event.");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Response: \"%s\"", text.c_str());
|
||||
@@ -571,7 +566,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
}
|
||||
}
|
||||
if (url.empty()) {
|
||||
ESP_LOGW(TAG, "No url in TTS_END event");
|
||||
ESP_LOGW(TAG, "No url in TTS_END event.");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str());
|
||||
@@ -639,14 +634,6 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
this->set_state_(State::RESPONSE_FINISHED, State::IDLE);
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_STT_VAD_START:
|
||||
ESP_LOGD(TAG, "Starting STT by VAD");
|
||||
this->stt_vad_start_trigger_->trigger();
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_STT_VAD_END:
|
||||
ESP_LOGD(TAG, "STT by VAD end");
|
||||
this->stt_vad_end_trigger_->trigger();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGD(TAG, "Unhandled event type: %d", msg.event_type);
|
||||
break;
|
||||
|
||||
@@ -100,17 +100,13 @@ class VoiceAssistant : public Component {
|
||||
void set_auto_gain(uint8_t auto_gain) { this->auto_gain_ = auto_gain; }
|
||||
void set_volume_multiplier(float volume_multiplier) { this->volume_multiplier_ = volume_multiplier; }
|
||||
|
||||
Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; }
|
||||
Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; }
|
||||
Trigger<> *get_listening_trigger() const { return this->listening_trigger_; }
|
||||
Trigger<> *get_end_trigger() const { return this->end_trigger_; }
|
||||
Trigger<> *get_start_trigger() const { return this->start_trigger_; }
|
||||
Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; }
|
||||
Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; }
|
||||
Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; }
|
||||
Trigger<std::string> *get_stt_end_trigger() const { return this->stt_end_trigger_; }
|
||||
Trigger<std::string> *get_tts_end_trigger() const { return this->tts_end_trigger_; }
|
||||
Trigger<std::string> *get_tts_start_trigger() const { return this->tts_start_trigger_; }
|
||||
Trigger<std::string> *get_tts_end_trigger() const { return this->tts_end_trigger_; }
|
||||
Trigger<> *get_end_trigger() const { return this->end_trigger_; }
|
||||
Trigger<std::string, std::string> *get_error_trigger() const { return this->error_trigger_; }
|
||||
|
||||
Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
|
||||
@@ -128,17 +124,13 @@ class VoiceAssistant : public Component {
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
struct sockaddr_storage dest_addr_;
|
||||
|
||||
Trigger<> *intent_end_trigger_ = new Trigger<>();
|
||||
Trigger<> *intent_start_trigger_ = new Trigger<>();
|
||||
Trigger<> *listening_trigger_ = new Trigger<>();
|
||||
Trigger<> *end_trigger_ = new Trigger<>();
|
||||
Trigger<> *start_trigger_ = new Trigger<>();
|
||||
Trigger<> *stt_vad_start_trigger_ = new Trigger<>();
|
||||
Trigger<> *stt_vad_end_trigger_ = new Trigger<>();
|
||||
Trigger<> *wake_word_detected_trigger_ = new Trigger<>();
|
||||
Trigger<std::string> *stt_end_trigger_ = new Trigger<std::string>();
|
||||
Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>();
|
||||
Trigger<std::string> *tts_start_trigger_ = new Trigger<std::string>();
|
||||
Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>();
|
||||
Trigger<> *end_trigger_ = new Trigger<>();
|
||||
Trigger<std::string, std::string> *error_trigger_ = new Trigger<std::string, std::string>();
|
||||
|
||||
Trigger<> *client_connected_trigger_ = new Trigger<>();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Constants used by esphome."""
|
||||
|
||||
__version__ = "2023.11.2"
|
||||
__version__ = "2023.12.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -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
95
esphome/dashboard/core.py
Normal 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
116
esphome/dashboard/entries.py
Normal file
116
esphome/dashboard/entries.py
Normal 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
|
||||
146
esphome/dashboard/settings.py
Normal file
146
esphome/dashboard/settings.py
Normal 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
|
||||
0
esphome/dashboard/status/__init__.py
Normal file
0
esphome/dashboard/status/__init__.py
Normal file
109
esphome/dashboard/status/mdns.py
Normal file
109
esphome/dashboard/status/mdns.py
Normal 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
|
||||
67
esphome/dashboard/status/mqtt.py
Normal file
67
esphome/dashboard/status/mqtt.py
Normal 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()
|
||||
57
esphome/dashboard/status/ping.py
Normal file
57
esphome/dashboard/status/ping.py
Normal 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
|
||||
@@ -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)), [])
|
||||
|
||||
1069
esphome/dashboard/web_server.py
Normal file
1069
esphome/dashboard/web_server.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,14 +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: float = 3.0) -> str | None:
|
||||
"""Resolve a host name to an IP address."""
|
||||
name = host.partition(".")[0]
|
||||
info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}")
|
||||
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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.5.2
|
||||
zeroconf==0.123.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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Iterator
|
||||
from collections.abc import Iterator
|
||||
|
||||
import math
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user