mirror of
https://github.com/esphome/esphome.git
synced 2026-09-04 03:56:04 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
@@ -112,7 +112,9 @@ AGS10_SET_ZERO_POINT_ACTION_MODE = {
|
||||
AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(AGS10Component),
|
||||
cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True),
|
||||
cv.Required(CONF_MODE): cv.templatable(
|
||||
cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True)
|
||||
),
|
||||
cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t),
|
||||
},
|
||||
)
|
||||
@@ -127,8 +129,10 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
mode = await cg.templatable(config.get(CONF_MODE), args, enumerate)
|
||||
mode = await cg.templatable(
|
||||
config.get(CONF_MODE), args, AGS10SetZeroPointActionMode
|
||||
)
|
||||
cg.add(var.set_mode(mode))
|
||||
value = await cg.templatable(config[CONF_VALUE], args, int)
|
||||
value = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
|
||||
cg.add(var.set_value(value))
|
||||
return var
|
||||
|
||||
@@ -173,8 +173,10 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
|
||||
cg.add(var.set_hw_frontend_reset(template_))
|
||||
|
||||
if freq := config.get(CONF_FREQUENCY):
|
||||
template_ = await cg.templatable(freq, args, float)
|
||||
template_ = int(template_ / 1000000)
|
||||
if cg.is_template(freq):
|
||||
template_ = await cg.templatable(freq, args, cg.int32)
|
||||
else:
|
||||
template_ = int(freq / 1000000)
|
||||
cg.add(var.set_frequency(template_))
|
||||
|
||||
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
|
||||
@@ -182,31 +184,19 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
|
||||
cg.add(var.set_sensing_distance(template_))
|
||||
|
||||
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
|
||||
template_ = await cg.templatable(selfcheck, args, float)
|
||||
if isinstance(template_, cv.TimePeriod):
|
||||
template_ = template_.total_milliseconds
|
||||
template_ = int(template_)
|
||||
template_ = await cg.templatable(selfcheck, args, cg.int32)
|
||||
cg.add(var.set_poweron_selfcheck_time(template_))
|
||||
|
||||
if protect := config.get(CONF_PROTECT_TIME):
|
||||
template_ = await cg.templatable(protect, args, float)
|
||||
if isinstance(template_, cv.TimePeriod):
|
||||
template_ = template_.total_milliseconds
|
||||
template_ = int(template_)
|
||||
template_ = await cg.templatable(protect, args, cg.int32)
|
||||
cg.add(var.set_protect_time(template_))
|
||||
|
||||
if trig_base := config.get(CONF_TRIGGER_BASE):
|
||||
template_ = await cg.templatable(trig_base, args, float)
|
||||
if isinstance(template_, cv.TimePeriod):
|
||||
template_ = template_.total_milliseconds
|
||||
template_ = int(template_)
|
||||
template_ = await cg.templatable(trig_base, args, cg.int32)
|
||||
cg.add(var.set_trigger_base(template_))
|
||||
|
||||
if trig_keep := config.get(CONF_TRIGGER_KEEP):
|
||||
template_ = await cg.templatable(trig_keep, args, float)
|
||||
if isinstance(template_, cv.TimePeriod):
|
||||
template_ = template_.total_milliseconds
|
||||
template_ = int(template_)
|
||||
template_ = await cg.templatable(trig_keep, args, cg.int32)
|
||||
cg.add(var.set_trigger_keep(template_))
|
||||
|
||||
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
|
||||
@@ -214,8 +204,10 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
|
||||
cg.add(var.set_stage_gain(template_))
|
||||
|
||||
if power := config.get(CONF_POWER_CONSUMPTION):
|
||||
template_ = await cg.templatable(power, args, float)
|
||||
template_ = int(template_ * 1000000)
|
||||
if cg.is_template(power):
|
||||
template_ = await cg.templatable(power, args, cg.int32)
|
||||
else:
|
||||
template_ = int(power * 1000000)
|
||||
cg.add(var.set_power_consumption(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -97,8 +97,12 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_RELEASE = "release"
|
||||
CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification"
|
||||
CONF_SIGNING_KEY = "signing_key"
|
||||
CONF_SIGNING_SCHEME = "signing_scheme"
|
||||
CONF_SRAM1_AS_IRAM = "sram1_as_iram"
|
||||
CONF_SUBTYPE = "subtype"
|
||||
CONF_VERIFICATION_KEY = "verification_key"
|
||||
|
||||
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
|
||||
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
|
||||
@@ -120,6 +124,27 @@ ASSERTION_LEVELS = {
|
||||
"SILENT": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT",
|
||||
}
|
||||
|
||||
SIGNING_SCHEMES = {
|
||||
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
|
||||
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
|
||||
}
|
||||
|
||||
# Chip variants that only support one signing scheme for Secure Boot V2.
|
||||
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
|
||||
# Variants not listed in either set support both RSA and ECDSA
|
||||
# (e.g. C5, C6, H2, P4). New variants should be added to the
|
||||
# appropriate set if they only support one scheme.
|
||||
SIGNED_OTA_RSA_ONLY_VARIANTS = {
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32C3,
|
||||
}
|
||||
SIGNED_OTA_ECC_ONLY_VARIANTS = {
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C61,
|
||||
}
|
||||
|
||||
COMPILER_OPTIMIZATIONS = {
|
||||
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
|
||||
"NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE",
|
||||
@@ -962,6 +987,47 @@ def final_validate(config):
|
||||
)
|
||||
# disable the rollback feature anyway since it can't be used.
|
||||
advanced[CONF_ENABLE_OTA_ROLLBACK] = False
|
||||
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
|
||||
scheme = signed_ota[CONF_SIGNING_SCHEME]
|
||||
variant = config[CONF_VARIANT]
|
||||
scheme_variant_conflicts = {
|
||||
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
|
||||
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
|
||||
}
|
||||
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
|
||||
0
|
||||
]:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Signing scheme '{scheme}' is not supported on "
|
||||
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
|
||||
path=[
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ADVANCED,
|
||||
CONF_SIGNED_OTA_VERIFICATION,
|
||||
CONF_SIGNING_SCHEME,
|
||||
],
|
||||
)
|
||||
)
|
||||
if CONF_OTA not in full_config:
|
||||
_LOGGER.warning(
|
||||
"Signed OTA verification is enabled but no OTA component is configured. "
|
||||
"The initial firmware will be signed but OTA updates won't be possible "
|
||||
"until an OTA component is added."
|
||||
)
|
||||
if CONF_SIGNING_KEY in signed_ota:
|
||||
_LOGGER.info(
|
||||
"Signed OTA verification is enabled. Keep your signing key safe! "
|
||||
"If you lose the signing key, you will NOT be able to OTA update "
|
||||
"devices running firmware signed with this key. "
|
||||
"Without the key, you'll need to reflash via serial."
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Signed OTA verification is configured with a public verification key. "
|
||||
"Binaries will NOT be signed automatically during build. "
|
||||
"You must sign them externally before flashing."
|
||||
)
|
||||
if errs:
|
||||
raise cv.MultipleInvalid(errs)
|
||||
|
||||
@@ -1173,6 +1239,18 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
min=8192, max=32768
|
||||
),
|
||||
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
|
||||
cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SIGNING_KEY): cv.file_,
|
||||
cv.Optional(CONF_VERIFICATION_KEY): cv.file_,
|
||||
cv.Optional(
|
||||
CONF_SIGNING_SCHEME, default="rsa3072"
|
||||
): cv.one_of(*SIGNING_SCHEMES, lower=True),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
|
||||
): cv.boolean,
|
||||
@@ -1878,6 +1956,32 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True)
|
||||
cg.add_define("USE_OTA_ROLLBACK")
|
||||
|
||||
# Enable signed app verification without hardware secure boot
|
||||
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT", True)
|
||||
|
||||
scheme = signed_ota[CONF_SIGNING_SCHEME]
|
||||
for key, flag in SIGNING_SCHEMES.items():
|
||||
add_idf_sdkconfig_option(flag, scheme == key)
|
||||
|
||||
if CONF_SIGNING_KEY in signed_ota:
|
||||
# Private key mode — auto-sign binaries during build
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", True)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_SIGNING_KEY",
|
||||
str(signed_ota[CONF_SIGNING_KEY].resolve()),
|
||||
)
|
||||
else:
|
||||
# Public key mode — verification only, external signing required
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_VERIFICATION_KEY",
|
||||
str(signed_ota[CONF_VERIFICATION_KEY].resolve()),
|
||||
)
|
||||
|
||||
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
|
||||
|
||||
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
|
||||
|
||||
cg.add_define(
|
||||
|
||||
@@ -8,6 +8,99 @@ import shutil # noqa: E402
|
||||
from glob import glob # noqa: E402
|
||||
|
||||
|
||||
def _parse_sdkconfig(sdkconfig_path):
|
||||
"""Parse sdkconfig file and return a dict of CONFIG_ options."""
|
||||
options = {}
|
||||
try:
|
||||
for line in sdkconfig_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
key, _, value = line.partition("=")
|
||||
# Strip surrounding quotes from string values
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
options[key] = value
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return options
|
||||
|
||||
|
||||
def sign_firmware(source, target, env):
|
||||
"""
|
||||
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
|
||||
Reads signing configuration from sdkconfig.
|
||||
"""
|
||||
build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
|
||||
project_dir = pathlib.Path(env.subst("$PROJECT_DIR"))
|
||||
pioenv = env.subst("$PIOENV")
|
||||
sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}")
|
||||
|
||||
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT") != "y":
|
||||
return
|
||||
|
||||
if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") != "y":
|
||||
print("Signed OTA verification enabled but build-time signing disabled.")
|
||||
print("You must sign the firmware externally before flashing.")
|
||||
return
|
||||
|
||||
signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY")
|
||||
if not signing_key:
|
||||
print("Error: CONFIG_SECURE_BOOT_SIGNING_KEY not set in sdkconfig")
|
||||
env.Exit(1)
|
||||
return
|
||||
|
||||
signing_key_path = pathlib.Path(signing_key)
|
||||
if not signing_key_path.exists():
|
||||
print(f"Error: Signing key not found: {signing_key_path}")
|
||||
env.Exit(1)
|
||||
return
|
||||
|
||||
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
|
||||
# so the espsecure signature version is always 2.
|
||||
sign_version = "2"
|
||||
|
||||
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
|
||||
firmware_path = build_dir / firmware_name
|
||||
|
||||
if not firmware_path.exists():
|
||||
print(f"Error: Firmware binary not found: {firmware_path}")
|
||||
env.Exit(1)
|
||||
return
|
||||
|
||||
python_exe = f'"{env.subst("$PYTHONEXE")}"'
|
||||
unsigned_path = firmware_path.with_suffix(".unsigned.bin")
|
||||
|
||||
# Keep a copy of the unsigned binary
|
||||
shutil.copyfile(str(firmware_path), str(unsigned_path))
|
||||
|
||||
cmd = [
|
||||
python_exe,
|
||||
"-m",
|
||||
"espsecure",
|
||||
"sign-data",
|
||||
"--version",
|
||||
sign_version,
|
||||
"--keyfile",
|
||||
str(signing_key_path),
|
||||
"--output",
|
||||
str(firmware_path),
|
||||
str(unsigned_path),
|
||||
]
|
||||
|
||||
print(f"Signing firmware with key: {signing_key_path.name}")
|
||||
result = env.Execute(
|
||||
env.VerboseAction(" ".join(cmd), "Signing firmware with espsecure")
|
||||
)
|
||||
|
||||
if result == 0:
|
||||
print("Successfully signed firmware")
|
||||
else:
|
||||
print(f"Error: espsecure sign_data failed with code {result}")
|
||||
# Restore unsigned binary on failure
|
||||
shutil.copyfile(str(unsigned_path), str(firmware_path))
|
||||
env.Exit(1)
|
||||
|
||||
|
||||
def merge_factory_bin(source, target, env):
|
||||
"""
|
||||
Merges all flash sections into a single .factory.bin using esptool.
|
||||
@@ -124,7 +217,8 @@ def esp32_copy_ota_bin(source, target, env):
|
||||
print(f"Copied firmware to {new_file_name}")
|
||||
|
||||
|
||||
# Run merge first, then ota copy second
|
||||
# Run signing first, then merge, then ota copy
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ DEPENDENCIES = ["uart"]
|
||||
AUTO_LOAD = ["climate"]
|
||||
CODEOWNERS = ["@crnjan"]
|
||||
|
||||
CONF_CURRENT_TEMPERATURE_MIN_INTERVAL = "current_temperature_min_interval"
|
||||
|
||||
mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi_cn105")
|
||||
|
||||
MitsubishiCN105Climate = mitsubishi_ns.class_(
|
||||
@@ -20,7 +22,14 @@ MitsubishiCN105Climate = mitsubishi_ns.class_(
|
||||
CONFIG_SCHEMA = (
|
||||
climate.climate_schema(MitsubishiCN105Climate)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend({cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval})
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval,
|
||||
cv.Optional(
|
||||
CONF_CURRENT_TEMPERATURE_MIN_INTERVAL, default="60s"
|
||||
): cv.update_interval,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
@@ -39,3 +48,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = await climate.new_climate(config)
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
cg.add(
|
||||
var.set_current_temperature_min_interval(
|
||||
config[CONF_CURRENT_TEMPERATURE_MIN_INTERVAL]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -22,7 +22,33 @@ static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42;
|
||||
static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62;
|
||||
static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02;
|
||||
static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03;
|
||||
static constexpr std::array<uint8_t, 2> STATUS_MSG_TYPES = {STATUS_MSG_SETTINGS, STATUS_MSG_ROOM_TEMP};
|
||||
|
||||
static constexpr std::array<std::optional<MitsubishiCN105::Mode>, 9> PROTOCOL_MODE_MAP = {
|
||||
std::nullopt, // 0x00
|
||||
MitsubishiCN105::Mode::HEAT, // 0x01
|
||||
MitsubishiCN105::Mode::DRY, // 0x02
|
||||
MitsubishiCN105::Mode::COOL, // 0x03
|
||||
std::nullopt, // 0x04
|
||||
std::nullopt, // 0x05
|
||||
std::nullopt, // 0x06
|
||||
MitsubishiCN105::Mode::FAN_ONLY, // 0x07
|
||||
MitsubishiCN105::Mode::AUTO // 0x08
|
||||
};
|
||||
|
||||
static constexpr std::array<std::optional<MitsubishiCN105::FanMode>, 7> PROTOCOL_FAN_MODE_MAP = {
|
||||
MitsubishiCN105::FanMode::AUTO, // 0x00
|
||||
MitsubishiCN105::FanMode::QUIET, // 0x01
|
||||
MitsubishiCN105::FanMode::SPEED_1, // 0x02
|
||||
MitsubishiCN105::FanMode::SPEED_2, // 0x03
|
||||
std::nullopt, // 0x04
|
||||
MitsubishiCN105::FanMode::SPEED_3, // 0x05
|
||||
MitsubishiCN105::FanMode::SPEED_4 // 0x06
|
||||
};
|
||||
|
||||
template<typename T, size_t N>
|
||||
static constexpr std::optional<T> lookup(const std::array<std::optional<T>, N> &table, uint8_t value) {
|
||||
return (value < N) ? table[value] : std::nullopt;
|
||||
}
|
||||
|
||||
static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
|
||||
return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
|
||||
@@ -54,12 +80,14 @@ bool MitsubishiCN105::update() {
|
||||
|
||||
if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) {
|
||||
this->write_timeout_start_ms_.reset();
|
||||
this->read_pos_ = 0;
|
||||
this->frame_parser_.reset();
|
||||
this->set_state_(State::READ_TIMEOUT);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this->read_incoming_bytes_();
|
||||
return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) {
|
||||
return this->process_rx_packet_(type, payload, len);
|
||||
});
|
||||
}
|
||||
|
||||
void MitsubishiCN105::set_state_(State new_state) {
|
||||
@@ -111,7 +139,7 @@ void MitsubishiCN105::did_transition_(State to) {
|
||||
|
||||
case State::CONNECTED:
|
||||
this->write_timeout_start_ms_.reset();
|
||||
this->status_msg_index_ = 0;
|
||||
this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
break;
|
||||
|
||||
@@ -121,10 +149,8 @@ void MitsubishiCN105::did_transition_(State to) {
|
||||
|
||||
case State::STATUS_UPDATED: {
|
||||
this->write_timeout_start_ms_.reset();
|
||||
if (++this->status_msg_index_ >= STATUS_MSG_TYPES.size()) {
|
||||
this->status_msg_index_ = 0;
|
||||
}
|
||||
if (this->status_msg_index_ != 0) {
|
||||
if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) {
|
||||
this->current_status_msg_type_ = STATUS_MSG_ROOM_TEMP;
|
||||
this->set_state_(State::UPDATING_STATUS);
|
||||
} else {
|
||||
this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||
@@ -134,6 +160,7 @@ void MitsubishiCN105::did_transition_(State to) {
|
||||
|
||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
||||
this->status_update_start_ms_ = get_loop_time_ms();
|
||||
this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
|
||||
this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
break;
|
||||
|
||||
@@ -146,15 +173,26 @@ void MitsubishiCN105::did_transition_(State to) {
|
||||
}
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::should_request_room_temperature_() const {
|
||||
if (!this->is_room_temperature_enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->last_room_temperature_update_ms_.has_value()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (get_loop_time_ms() - *this->last_room_temperature_update_ms_) >= this->room_temperature_min_interval_ms_;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
|
||||
dump_buffer_vv("TX", packet, len);
|
||||
FrameParser::dump_buffer_vv("TX", packet, len);
|
||||
this->device_.write_array(packet, len);
|
||||
this->write_timeout_start_ms_ = get_loop_time_ms();
|
||||
}
|
||||
|
||||
void MitsubishiCN105::update_status_() {
|
||||
ESP_LOGV(TAG, "Requesting status update, index=%u", this->status_msg_index_);
|
||||
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {STATUS_MSG_TYPES[this->status_msg_index_]};
|
||||
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {this->current_status_msg_type_};
|
||||
this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
|
||||
}
|
||||
|
||||
@@ -163,67 +201,6 @@ void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) {
|
||||
this->set_state_(state);
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::read_incoming_bytes_() {
|
||||
uint8_t watchdog = 64;
|
||||
while (this->device_.available() > 0 && watchdog-- > 0) {
|
||||
uint8_t &value = this->read_buffer_[this->read_pos_];
|
||||
if (!this->device_.read_byte(&value)) {
|
||||
ESP_LOGW(TAG, "UART read failed while data available");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (++this->read_pos_) {
|
||||
case 1:
|
||||
if (value != PREAMBLE) {
|
||||
this->reset_read_position_and_dump_buffer_("RX ignoring preamble");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 2:
|
||||
continue;
|
||||
|
||||
case 3:
|
||||
if (value != HEADER_BYTE_1) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: header 1 mismatch");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 4:
|
||||
if (value != HEADER_BYTE_2) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: header 2 mismatch");
|
||||
}
|
||||
continue;
|
||||
|
||||
case HEADER_LEN:
|
||||
static_assert(READ_BUFFER_SIZE > HEADER_LEN);
|
||||
if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: payload too large");
|
||||
}
|
||||
continue;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
|
||||
if (this->read_pos_ <= len_without_checksum) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checksum(this->read_buffer_, len_without_checksum) != value) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: checksum mismatch");
|
||||
continue;
|
||||
}
|
||||
|
||||
bool processed = this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN,
|
||||
len_without_checksum - HEADER_LEN);
|
||||
this->reset_read_position_and_dump_buffer_("RX");
|
||||
return processed;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
|
||||
switch (type) {
|
||||
case PACKET_TYPE_CONNECT_RESPONSE:
|
||||
@@ -251,11 +228,19 @@ bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg_type == STATUS_MSG_TYPES[this->status_msg_index_]) {
|
||||
if (msg_type == this->current_status_msg_type_) {
|
||||
this->set_state_(State::STATUS_UPDATED);
|
||||
}
|
||||
|
||||
return previous != this->status_ && this->is_status_initialized();
|
||||
bool changed = previous.power_on != this->status_.power_on || previous.mode != this->status_.mode ||
|
||||
previous.fan_mode != this->status_.fan_mode ||
|
||||
previous.target_temperature != this->status_.target_temperature;
|
||||
|
||||
if (this->is_room_temperature_enabled()) {
|
||||
changed |= previous.room_temperature != this->status_.room_temperature;
|
||||
}
|
||||
|
||||
return changed && this->is_status_initialized();
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
|
||||
@@ -278,6 +263,9 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len)
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool i_see = payload[3] > 0x08;
|
||||
this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN);
|
||||
this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN);
|
||||
this->status_.power_on = payload[2] != 0;
|
||||
this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31);
|
||||
|
||||
@@ -291,21 +279,11 @@ bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, siz
|
||||
}
|
||||
|
||||
this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10);
|
||||
this->last_room_temperature_update_ms_ = get_loop_time_ms();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) {
|
||||
dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
|
||||
this->read_pos_ = 0;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
|
||||
ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *MitsubishiCN105::state_to_string(State state) {
|
||||
switch (state) {
|
||||
case State::NOT_CONNECTED:
|
||||
@@ -328,4 +306,79 @@ const LogString *MitsubishiCN105::state_to_string(State state) {
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
|
||||
template<typename Callback>
|
||||
bool MitsubishiCN105::FrameParser::read_and_parse(uart::UARTDevice &device, Callback &&callback) {
|
||||
uint8_t watchdog = 64;
|
||||
while (device.available() > 0 && watchdog-- > 0) {
|
||||
uint8_t &value = this->read_buffer_[this->read_pos_];
|
||||
if (!device.read_byte(&value)) {
|
||||
ESP_LOGW(TAG, "UART read failed while data available");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (++this->read_pos_) {
|
||||
case 1:
|
||||
if (value != PREAMBLE) {
|
||||
this->reset_and_dump_buffer_("RX ignoring preamble");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 2:
|
||||
continue;
|
||||
|
||||
case 3:
|
||||
if (value != HEADER_BYTE_1) {
|
||||
this->reset_and_dump_buffer_("RX invalid: header 1 mismatch");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 4:
|
||||
if (value != HEADER_BYTE_2) {
|
||||
this->reset_and_dump_buffer_("RX invalid: header 2 mismatch");
|
||||
}
|
||||
continue;
|
||||
|
||||
case HEADER_LEN:
|
||||
static_assert(READ_BUFFER_SIZE > HEADER_LEN);
|
||||
if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
|
||||
this->reset_and_dump_buffer_("RX invalid: payload too large");
|
||||
}
|
||||
continue;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
|
||||
if (this->read_pos_ <= len_without_checksum) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checksum(this->read_buffer_, len_without_checksum) != value) {
|
||||
this->reset_and_dump_buffer_("RX invalid: checksum mismatch");
|
||||
continue;
|
||||
}
|
||||
|
||||
dump_buffer_vv("RX", this->read_buffer_, this->read_pos_);
|
||||
const bool processed =
|
||||
callback(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
|
||||
this->read_pos_ = 0;
|
||||
return processed;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::FrameParser::reset_and_dump_buffer_(const char *prefix) {
|
||||
dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
|
||||
this->read_pos_ = 0;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::FrameParser::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
|
||||
ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -9,11 +9,30 @@ uint32_t get_loop_time_ms();
|
||||
|
||||
class MitsubishiCN105 {
|
||||
public:
|
||||
struct Status {
|
||||
bool operator==(const Status &) const = default;
|
||||
enum class Mode : uint8_t {
|
||||
HEAT,
|
||||
DRY,
|
||||
COOL,
|
||||
FAN_ONLY,
|
||||
AUTO,
|
||||
UNKNOWN,
|
||||
};
|
||||
|
||||
enum class FanMode : uint8_t {
|
||||
AUTO,
|
||||
QUIET,
|
||||
SPEED_1,
|
||||
SPEED_2,
|
||||
SPEED_3,
|
||||
SPEED_4,
|
||||
UNKNOWN,
|
||||
};
|
||||
|
||||
struct Status {
|
||||
bool power_on{false};
|
||||
float target_temperature{NAN};
|
||||
Mode mode{Mode::UNKNOWN};
|
||||
FanMode fan_mode{FanMode::UNKNOWN};
|
||||
float room_temperature{NAN};
|
||||
};
|
||||
|
||||
@@ -25,8 +44,17 @@ class MitsubishiCN105 {
|
||||
uint32_t get_update_interval() const { return this->update_interval_ms_; }
|
||||
void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; }
|
||||
|
||||
uint32_t get_room_temperature_min_interval() const { return this->room_temperature_min_interval_ms_; }
|
||||
bool is_room_temperature_enabled() const { return this->room_temperature_min_interval_ms_ != SCHEDULER_DONT_RUN; }
|
||||
void set_room_temperature_min_interval(uint32_t interval_ms) {
|
||||
this->room_temperature_min_interval_ms_ = interval_ms;
|
||||
}
|
||||
|
||||
const Status &status() const { return this->status_; }
|
||||
bool is_status_initialized() const { return !std::isnan(status_.room_temperature); }
|
||||
bool is_status_initialized() const {
|
||||
return this->is_room_temperature_enabled() ? !std::isnan(this->status_.room_temperature)
|
||||
: !std::isnan(this->status_.target_temperature);
|
||||
}
|
||||
|
||||
protected:
|
||||
enum class State : uint8_t {
|
||||
@@ -40,35 +68,46 @@ class MitsubishiCN105 {
|
||||
READ_TIMEOUT
|
||||
};
|
||||
|
||||
class FrameParser {
|
||||
public:
|
||||
template<typename Callback> bool read_and_parse(uart::UARTDevice &device, Callback &&callback);
|
||||
void reset() { read_pos_ = 0; }
|
||||
static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len);
|
||||
|
||||
protected:
|
||||
void reset_and_dump_buffer_(const char *prefix);
|
||||
|
||||
private:
|
||||
static constexpr size_t READ_BUFFER_SIZE = 32;
|
||||
uint8_t read_buffer_[READ_BUFFER_SIZE];
|
||||
uint8_t read_pos_{0};
|
||||
};
|
||||
|
||||
void set_state_(State new_state);
|
||||
void did_transition_(State to);
|
||||
bool read_incoming_bytes_();
|
||||
bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len);
|
||||
bool process_status_packet_(const uint8_t *payload, size_t len);
|
||||
bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len);
|
||||
bool parse_status_settings_(const uint8_t *payload, size_t len);
|
||||
bool parse_status_room_temperature_(const uint8_t *payload, size_t len);
|
||||
void reset_read_position_and_dump_buffer_(const char *prefix);
|
||||
void send_packet_(const uint8_t *packet, size_t len);
|
||||
void update_status_();
|
||||
void cancel_waiting_and_transition_to_(State state);
|
||||
bool should_request_room_temperature_() const;
|
||||
template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); }
|
||||
static bool should_transition(State from, State to);
|
||||
static const LogString *state_to_string(State state);
|
||||
static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len);
|
||||
|
||||
uart::UARTDevice &device_;
|
||||
uint32_t update_interval_ms_{1000};
|
||||
uint32_t room_temperature_min_interval_ms_{60000};
|
||||
std::optional<uint32_t> write_timeout_start_ms_;
|
||||
std::optional<uint32_t> status_update_start_ms_;
|
||||
std::optional<uint32_t> last_room_temperature_update_ms_;
|
||||
Status status_{};
|
||||
State state_{State::NOT_CONNECTED};
|
||||
uint8_t status_msg_index_{0};
|
||||
|
||||
private:
|
||||
static constexpr size_t READ_BUFFER_SIZE = 32;
|
||||
uint8_t read_buffer_[READ_BUFFER_SIZE];
|
||||
uint8_t read_pos_{0};
|
||||
uint8_t current_status_msg_type_{0};
|
||||
FrameParser frame_parser_;
|
||||
};
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -6,8 +6,42 @@ namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
static const char *const TAG = "mitsubishi_cn105.climate";
|
||||
|
||||
static constexpr std::array MODE_MAP{
|
||||
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
|
||||
std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT},
|
||||
std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY},
|
||||
std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL},
|
||||
std::pair{MitsubishiCN105::Mode::FAN_ONLY, climate::CLIMATE_MODE_FAN_ONLY},
|
||||
};
|
||||
|
||||
static constexpr std::array FAN_MODE_MAP{
|
||||
std::pair{MitsubishiCN105::FanMode::AUTO, climate::CLIMATE_FAN_AUTO},
|
||||
std::pair{MitsubishiCN105::FanMode::QUIET, climate::CLIMATE_FAN_QUIET},
|
||||
std::pair{MitsubishiCN105::FanMode::SPEED_1, climate::CLIMATE_FAN_LOW},
|
||||
std::pair{MitsubishiCN105::FanMode::SPEED_2, climate::CLIMATE_FAN_MEDIUM},
|
||||
std::pair{MitsubishiCN105::FanMode::SPEED_3, climate::CLIMATE_FAN_MIDDLE},
|
||||
std::pair{MitsubishiCN105::FanMode::SPEED_4, climate::CLIMATE_FAN_HIGH},
|
||||
};
|
||||
|
||||
template<typename A, typename B, std::size_t N>
|
||||
static bool map_lookup(const std::array<std::pair<A, B>, N> &map, A key, B &out) {
|
||||
for (const auto &[from, to] : map) {
|
||||
if (from == key) {
|
||||
out = to;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::dump_config() {
|
||||
LOG_CLIMATE("", "Mitsubishi CN105 Climate", this);
|
||||
if (this->hp_.is_room_temperature_enabled()) {
|
||||
ESP_LOGCONFIG(TAG, " Current temperature min interval: %" PRIu32 " ms",
|
||||
this->hp_.get_room_temperature_min_interval());
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Current temperature: disabled");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Update interval: %" PRIu32 " ms\n"
|
||||
" UART: baud_rate=%" PRIu32 " data_bits=%u parity=%s stop_bits=%u",
|
||||
@@ -26,12 +60,32 @@ void MitsubishiCN105Climate::loop() {
|
||||
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
climate::ClimateTraits traits;
|
||||
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_COOL,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
climate::CLIMATE_MODE_FAN_ONLY,
|
||||
climate::CLIMATE_MODE_AUTO,
|
||||
});
|
||||
|
||||
traits.set_supported_fan_modes({
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_FAN_QUIET,
|
||||
climate::CLIMATE_FAN_LOW,
|
||||
climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_MIDDLE,
|
||||
climate::CLIMATE_FAN_HIGH,
|
||||
});
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
traits.set_visual_temperature_step(1.0f);
|
||||
traits.set_visual_current_temperature_step(0.5f);
|
||||
|
||||
if (this->hp_.is_room_temperature_enabled()) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_visual_current_temperature_step(0.5f);
|
||||
}
|
||||
|
||||
return traits;
|
||||
}
|
||||
@@ -42,7 +96,25 @@ void MitsubishiCN105Climate::apply_values_() {
|
||||
const auto &status = this->hp_.status();
|
||||
|
||||
this->target_temperature = status.target_temperature;
|
||||
this->current_temperature = status.room_temperature;
|
||||
|
||||
if (this->hp_.is_room_temperature_enabled()) {
|
||||
this->current_temperature = status.room_temperature;
|
||||
}
|
||||
|
||||
if (status.power_on) {
|
||||
if (!map_lookup(MODE_MAP, status.mode, this->mode)) {
|
||||
ESP_LOGD(TAG, "Unable to map mode");
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
|
||||
climate::ClimateFanMode fan_mode;
|
||||
if (map_lookup(FAN_MODE_MAP, status.fan_mode, fan_mode)) {
|
||||
this->fan_mode = fan_mode;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Unable to map fan mode");
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
|
||||
void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); }
|
||||
void set_current_temperature_min_interval(uint32_t ms) { hp_.set_room_temperature_min_interval(ms); }
|
||||
|
||||
protected:
|
||||
void apply_values_();
|
||||
|
||||
@@ -37,6 +37,7 @@ enum OTAResponseTypes {
|
||||
OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A,
|
||||
OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B,
|
||||
OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C,
|
||||
OTA_RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D,
|
||||
OTA_RESPONSE_ERROR_UNKNOWN = 0xFF,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "esphome/components/md5/md5.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_task_wdt.h>
|
||||
@@ -10,6 +11,8 @@
|
||||
|
||||
namespace esphome::ota {
|
||||
|
||||
static const char *const TAG = "ota.idf";
|
||||
|
||||
std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
|
||||
|
||||
OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
|
||||
@@ -98,7 +101,12 @@ OTAResponseTypes IDFOTABackend::end() {
|
||||
}
|
||||
}
|
||||
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
|
||||
#ifdef USE_OTA_SIGNED_VERIFICATION
|
||||
ESP_LOGE(TAG, "OTA validation failed (err=0x%X) - possible signature verification failure", err);
|
||||
return OTA_RESPONSE_ERROR_SIGNATURE_INVALID;
|
||||
#else
|
||||
return OTA_RESPONSE_ERROR_UPDATE_END;
|
||||
#endif
|
||||
}
|
||||
if (err == ESP_ERR_FLASH_OP_TIMEOUT || err == ESP_ERR_FLASH_OP_FAIL) {
|
||||
return OTA_RESPONSE_ERROR_WRITING_FLASH;
|
||||
|
||||
@@ -211,6 +211,7 @@
|
||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#define ESPHOME_TASK_LOG_BUFFER_SIZE 768
|
||||
#define USE_OTA_ROLLBACK
|
||||
#define USE_OTA_SIGNED_VERIFICATION
|
||||
#define USE_ESP32_MIN_CHIP_REVISION_SET
|
||||
#define USE_ESP32_SRAM1_AS_IRAM
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88
|
||||
RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89
|
||||
RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A
|
||||
RESPONSE_ERROR_MD5_MISMATCH = 0x8B
|
||||
RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C
|
||||
RESPONSE_ERROR_SIGNATURE_INVALID = 0x8D
|
||||
RESPONSE_ERROR_UNKNOWN = 0xFF
|
||||
|
||||
OTA_VERSION_1_0 = 1
|
||||
@@ -192,6 +194,12 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None
|
||||
"Error: Application MD5 code mismatch. Please try again "
|
||||
"or flash over USB with a good quality cable."
|
||||
)
|
||||
if dat == RESPONSE_ERROR_SIGNATURE_INVALID:
|
||||
raise OTAError(
|
||||
"Error: Firmware signature verification failed. The firmware was not signed "
|
||||
"with the correct key. Ensure the signing key matches the one used to build "
|
||||
"the firmware currently running on the device."
|
||||
)
|
||||
if dat == RESPONSE_ERROR_UNKNOWN:
|
||||
raise OTAError("Unknown error from ESP")
|
||||
if not isinstance(expect, (list, tuple)):
|
||||
|
||||
@@ -4,3 +4,14 @@ sensor:
|
||||
tvoc:
|
||||
name: AGS10 TVOC
|
||||
update_interval: 60s
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test AGS10 Actions"
|
||||
on_press:
|
||||
- ags10.set_zero_point:
|
||||
id: ags10_1
|
||||
mode: CURRENT_VALUE
|
||||
- ags10.new_i2c_address:
|
||||
id: ags10_1
|
||||
address: 0x1A
|
||||
|
||||
@@ -12,6 +12,11 @@ esphome:
|
||||
trigger_keep: 10s
|
||||
stage_gain: 3
|
||||
power_consumption: 70uA
|
||||
- at581x.settings:
|
||||
id: waveradar
|
||||
frequency: !lambda "return 5800;"
|
||||
poweron_selfcheck_time: !lambda "return 2000;"
|
||||
power_consumption: !lambda "return 70;"
|
||||
- at581x.reset:
|
||||
id: waveradar
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
*** DO NOT USE THIS KEY...EVER ***
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
*** DO NOT USE THIS KEY...EVER ***
|
||||
@@ -0,0 +1,10 @@
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
signed_ota_verification:
|
||||
signing_key: ../../components/esp32/dummy_signing_key.pem
|
||||
signing_scheme: rsa3072
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -60,6 +60,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
||||
// Settings should still have initial values
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan());
|
||||
EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::UNKNOWN);
|
||||
EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::UNKNOWN);
|
||||
|
||||
ctx.sut.set_current_time(300);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
@@ -68,6 +70,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
||||
// Check settings that we just read from received package
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f);
|
||||
EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::AUTO);
|
||||
EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::AUTO);
|
||||
|
||||
// Now fetch room temperature (0x03)
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||
@@ -260,24 +264,28 @@ TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx(
|
||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56});
|
||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_TRUE(ctx.sut.status().power_on);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f);
|
||||
EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::COOL);
|
||||
EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::QUIET);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx(
|
||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xB7});
|
||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xAD});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f);
|
||||
EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::FAN_ONLY);
|
||||
EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::SPEED_4);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) {
|
||||
|
||||
Reference in New Issue
Block a user