mirror of
https://github.com/esphome/esphome.git
synced 2026-10-03 09:46:03 +00:00
Merge remote-tracking branch 'origin/esp8266-arduino-toolchain' into esp8266-native-pch
This commit is contained in:
@@ -4,7 +4,6 @@ import re
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from esphome import git
|
||||
@@ -13,7 +12,7 @@ from esphome.components.packages import validate_source_shorthand
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ESPHOME, CONF_PROJECT, CONF_REF, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
from esphome.happy_eyeballs import ensure_happy_eyeballs
|
||||
from esphome.net_retry import fetch_with_retry, http_request
|
||||
from esphome.types import ConfigType
|
||||
from esphome.yaml_util import dump
|
||||
|
||||
@@ -111,14 +110,20 @@ def import_config(
|
||||
|
||||
if git_file.query and "full_config" in git_file.query:
|
||||
url = git_file.raw_url
|
||||
try:
|
||||
ensure_happy_eyeballs()
|
||||
req = requests.get(url, timeout=30)
|
||||
|
||||
# Deferred so config-time imports of this component stay light;
|
||||
# http_request does the lazy import for the request itself.
|
||||
import requests
|
||||
|
||||
def _fetch() -> str:
|
||||
req = http_request("GET", url, timeout=30)
|
||||
req.raise_for_status()
|
||||
return req.text
|
||||
|
||||
try:
|
||||
contents = fetch_with_retry(url, _fetch, what="Import")
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise ValueError(f"Error while fetching {url}: {e}") from e
|
||||
|
||||
contents = req.text
|
||||
yaml = YAML()
|
||||
loaded_yaml = yaml.load(contents)
|
||||
if (
|
||||
|
||||
@@ -7,8 +7,10 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -18,8 +20,10 @@ from esphome.const import (
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_EMPTY,
|
||||
UNIT_HERTZ,
|
||||
UNIT_PULSES,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
@@ -29,6 +33,32 @@ from .. import CONF_EMONTX_ID, CONF_TAG_NAME, EmonTx, emontx_ns
|
||||
|
||||
EmonTxSensor = emontx_ns.class_("EmonTxSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
# Known emonTx/avrdb JSON tag conventions, gathered from real firmware
|
||||
# (see https://github.com/openenergymonitor/avrdb_firmware), used to decide
|
||||
# whether each tag below requires a numeric index or may also appear bare:
|
||||
#
|
||||
# Tag family Bare (no index) Numeric-indexed
|
||||
# ----------- ----------------------- ----------------------------------
|
||||
# P (power) no P1, P2, ... (multi-channel boards)
|
||||
# E (energy) no E1, E2, ...
|
||||
# V (voltage) Vrms (NOT matched here, V1, V2, V3 (per-phase boards)
|
||||
# doesn't fit "V"+digits)
|
||||
# I (current) no I1, I2, ...
|
||||
# T (temp.) no T1, T2, ...
|
||||
# F (frequency) F (single mains freq.) not seen indexed
|
||||
# PULSE pulse (single-CT boards) PULSE1, PULSE2, ... (other variants)
|
||||
# PF (power not seen bare PF1, PF2, ... (currently unused/
|
||||
# factor) commented out in avrdb firmware)
|
||||
# AP (apparent not seen bare AP1, AP2, ... (not an avrdb tag at
|
||||
# power) all; avrdb uses "VA"+index instead,
|
||||
# itself currently unused/commented
|
||||
# out; "AP" is kept here for other
|
||||
# firmware/integrations using it)
|
||||
#
|
||||
# This is why a bare "PULSE" resolves to proper defaults below, but bare
|
||||
# "PF"/"AP" fall back to generic defaults instead: only PULSE has a
|
||||
# confirmed bare-tag use in real, currently-shipping firmware.
|
||||
|
||||
# Define sensor type configurations by prefix
|
||||
SENSOR_CONFIGS = {
|
||||
"P": {
|
||||
@@ -63,7 +93,25 @@ SENSOR_CONFIGS = {
|
||||
},
|
||||
}
|
||||
|
||||
# Pattern-based configurations
|
||||
# Tags reported once, without a numeric index (e.g. "F"), matched exactly
|
||||
# rather than by prefix.
|
||||
EXACT_TAG_CONFIGS = {
|
||||
"F": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_HERTZ,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_FREQUENCY,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
}
|
||||
|
||||
# Pattern-based configurations. The remainder after the prefix must be a
|
||||
# non-empty numeric index (like V1/I1/E1), so e.g. "APPLE" doesn't collide
|
||||
# with the "AP" prefix and a bare "PF"/"AP" (no index) doesn't match.
|
||||
# "PULSE" is the exception: some emonTx firmware (e.g. avrdb-based single-CT
|
||||
# variants) reports a single pulse counter as a bare "pulse" tag with no
|
||||
# numeric index at all, so that pattern also accepts an empty suffix.
|
||||
PATTERNS_ALLOWING_BARE_TAG = {"PULSE"}
|
||||
|
||||
PATTERN_CONFIGS = {
|
||||
"PULSE": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_PULSES,
|
||||
@@ -77,14 +125,21 @@ PATTERN_CONFIGS = {
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
"AP": {
|
||||
CONF_UNIT_OF_MEASUREMENT: UNIT_VOLT_AMPS,
|
||||
CONF_DEVICE_CLASS: DEVICE_CLASS_APPARENT_POWER,
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS: 2,
|
||||
},
|
||||
}
|
||||
|
||||
# BASE_SCHEMA intentionally omits state_class and accuracy_decimals defaults.
|
||||
# Passing them to sensor_schema() would register them via cv.Optional(key, default=...),
|
||||
# making them always present in the validated config dict and preventing
|
||||
# apply_tag_defaults from overriding them with the correct per-prefix values.
|
||||
# They are injected by apply_tag_defaults below, after running through
|
||||
# sensor.validate_state_class() so the value is code-generation-ready.
|
||||
# They are injected by apply_tag_defaults below, after running through the
|
||||
# same validators sensor_schema() would use (see _DEFAULT_VALIDATORS) so the
|
||||
# values are code-generation-ready.
|
||||
BASE_SCHEMA = sensor.sensor_schema(EmonTxSensor).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_EMONTX_ID): cv.use_id(EmonTx),
|
||||
@@ -93,30 +148,43 @@ BASE_SCHEMA = sensor.sensor_schema(EmonTxSensor).extend(
|
||||
)
|
||||
|
||||
|
||||
_DEFAULT_VALIDATORS = {
|
||||
CONF_STATE_CLASS: sensor.validate_state_class,
|
||||
CONF_DEVICE_CLASS: sensor.validate_device_class,
|
||||
CONF_UNIT_OF_MEASUREMENT: sensor.validate_unit_of_measurement,
|
||||
}
|
||||
|
||||
|
||||
def _apply_defaults(config: ConfigType, defaults: dict) -> None:
|
||||
"""Inject defaults into config, skipping keys already set by the user.
|
||||
state_class values are run through validate_state_class so they are
|
||||
code-generation-ready, matching what sensor_schema() would normally do."""
|
||||
Values are run through the same validators sensor_schema() would use, so
|
||||
they are code-generation-ready and a typo'd constant fails validation
|
||||
instead of shipping silently."""
|
||||
for key, value in defaults.items():
|
||||
if key not in config:
|
||||
if key == CONF_STATE_CLASS:
|
||||
value = sensor.validate_state_class(value)
|
||||
if key in _DEFAULT_VALIDATORS:
|
||||
value = _DEFAULT_VALIDATORS[key](value)
|
||||
config[key] = value
|
||||
|
||||
|
||||
def apply_tag_defaults(config: ConfigType) -> ConfigType:
|
||||
"""Apply defaults based on tag prefix if applicable, but don't restrict any tags."""
|
||||
tag = config[CONF_TAG_NAME]
|
||||
tag_upper = tag.upper()
|
||||
|
||||
if (exact_config := EXACT_TAG_CONFIGS.get(tag_upper)) is not None:
|
||||
_apply_defaults(config, exact_config)
|
||||
return config
|
||||
|
||||
for pattern, pattern_config in PATTERN_CONFIGS.items():
|
||||
suffix = tag_upper[len(pattern) :]
|
||||
bare_ok = not suffix and pattern in PATTERNS_ALLOWING_BARE_TAG
|
||||
if tag_upper.startswith(pattern) and (suffix.isdigit() or bare_ok):
|
||||
_apply_defaults(config, pattern_config)
|
||||
return config
|
||||
|
||||
# Only apply defaults for known prefixes with numeric indices (e.g. E1, V2, T3)
|
||||
if len(tag) >= 2:
|
||||
tag_upper = tag.upper()
|
||||
|
||||
for pattern, pattern_config in PATTERN_CONFIGS.items():
|
||||
if tag_upper.startswith(pattern):
|
||||
_apply_defaults(config, pattern_config)
|
||||
return config
|
||||
|
||||
# Only apply defaults for known prefixes with numeric indices (e.g. E1, V2, T3)
|
||||
prefix = tag_upper[0]
|
||||
if prefix in SENSOR_CONFIGS and tag[1:].isdigit():
|
||||
_apply_defaults(config, SENSOR_CONFIGS[prefix])
|
||||
|
||||
@@ -661,8 +661,10 @@ def _decode_pc(config: ConfigType, addr: str, *, bulk: bool = False) -> None:
|
||||
_warn_decode_problem(
|
||||
str(path), "Cannot decode crash addresses: %s missing", path
|
||||
)
|
||||
# The detailed warning names no address, so mark each one
|
||||
_LOGGER.warning("Not decoded %s (toolchain file missing)", addr)
|
||||
# The detailed warning names no address; mark named
|
||||
# registers, but bulk stack words at debug (~150 per dump)
|
||||
log = _LOGGER.debug if bulk else _LOGGER.warning
|
||||
log("Not decoded %s (toolchain file missing)", addr)
|
||||
return
|
||||
addr2line, elf = str(addr2line), str(elf)
|
||||
else:
|
||||
@@ -674,7 +676,8 @@ def _decode_pc(config: ConfigType, addr: str, *, bulk: bool = False) -> None:
|
||||
"no-addr2line",
|
||||
"Cannot decode crash addresses: no addr2line or ELF in idedata",
|
||||
)
|
||||
_LOGGER.warning("Not decoded %s (no addr2line or ELF)", addr)
|
||||
log = _LOGGER.debug if bulk else _LOGGER.warning
|
||||
log("Not decoded %s (no addr2line or ELF)", addr)
|
||||
return
|
||||
addr2line, elf = idedata.addr2line_path, idedata.firmware_elf_path
|
||||
command = [addr2line, "-pfiaC", "-e", elf, addr]
|
||||
@@ -688,8 +691,9 @@ def _decode_pc(config: ConfigType, addr: str, *, bulk: bool = False) -> None:
|
||||
"addr2line-failed", "Could not decode crash address %s (%s)", addr, err
|
||||
):
|
||||
# The detailed warning already named this address; mark only
|
||||
# the addresses whose warning was rate-limited away
|
||||
_LOGGER.warning("Not decoded %s (addr2line failed)", addr)
|
||||
# the rate-limited ones, and bulk stack words at debug
|
||||
log = _LOGGER.debug if bulk else _LOGGER.warning
|
||||
log("Not decoded %s (addr2line failed)", addr)
|
||||
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
|
||||
return
|
||||
|
||||
|
||||
@@ -5,16 +5,14 @@ from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from esphome.build_helpers.tools_cache import SDK_NRF_TOOLS_CACHE, tools_cache_path
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.framework_helpers import (
|
||||
archive_extract_all,
|
||||
create_venv,
|
||||
download_from_mirrors,
|
||||
download_and_extract,
|
||||
get_python_env_executable_path,
|
||||
rmdir,
|
||||
run_command_ok,
|
||||
@@ -338,34 +336,37 @@ def check_and_install() -> None:
|
||||
if not sentinel.exists():
|
||||
rmdir(toolchains_dir, msg=f"Clean up {TOOLCHAIN_VERSION} toolchain environment")
|
||||
sysname, machine, extension = _get_toolchain_platform_info()
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
_LOGGER.info("Downloading Zephyr SDK %s minimal ...", TOOLCHAIN_VERSION)
|
||||
download_from_mirrors(
|
||||
SDK_NG_MINIMAL_MIRRORS,
|
||||
{
|
||||
"VERSION": TOOLCHAIN_VERSION,
|
||||
"sysname": sysname,
|
||||
"machine": machine,
|
||||
"extension": extension,
|
||||
},
|
||||
tmp.file,
|
||||
)
|
||||
archive_extract_all(tmp.file, toolchains_dir, progress_header="Extracting")
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
_LOGGER.info("Downloading %s toolchain ...", TOOLCHAIN_VERSION)
|
||||
download_from_mirrors(
|
||||
substitutions = {
|
||||
"VERSION": TOOLCHAIN_VERSION,
|
||||
"sysname": sysname,
|
||||
"machine": machine,
|
||||
"extension": extension,
|
||||
}
|
||||
# Downloaded next to the destination (not a temp file) so an
|
||||
# interrupted download's .part file resumes on the next run.
|
||||
for mirrors, extract_dir, what, slug in (
|
||||
(SDK_NG_MINIMAL_MIRRORS, toolchains_dir, "Zephyr SDK minimal", "minimal"),
|
||||
(
|
||||
SDK_NG_TOOLCHAIN_MIRRORS,
|
||||
{
|
||||
"VERSION": TOOLCHAIN_VERSION,
|
||||
"sysname": sysname,
|
||||
"machine": machine,
|
||||
"extension": extension,
|
||||
},
|
||||
tmp.file,
|
||||
)
|
||||
archive_extract_all(
|
||||
tmp.file,
|
||||
toolchains_dir / "arm-zephyr-eabi",
|
||||
"toolchain",
|
||||
"toolchain",
|
||||
),
|
||||
):
|
||||
_LOGGER.info("Downloading %s %s ...", TOOLCHAIN_VERSION, what)
|
||||
download_and_extract(
|
||||
mirrors,
|
||||
substitutions,
|
||||
toolchains_dir.with_name(f"{toolchains_dir.name}.{slug}.archive"),
|
||||
extract_dir,
|
||||
progress_header="Extracting",
|
||||
)
|
||||
# Best-effort prune of resume leftovers, including a previous
|
||||
# TOOLCHAIN_VERSION's orphans; the SDK archives are hundreds of MB.
|
||||
# A locked file must not discard the just-completed install.
|
||||
for leftover in toolchains_dir.parent.glob("*.archive.part*"):
|
||||
try:
|
||||
leftover.unlink()
|
||||
except OSError as err:
|
||||
_LOGGER.debug("Could not remove %s: %s", leftover, err)
|
||||
sentinel.touch()
|
||||
|
||||
Reference in New Issue
Block a user