mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
Merge remote-tracking branch 'origin/dev' into rtttl-play-without-copy
This commit is contained in:
@@ -106,6 +106,7 @@ esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bq25186/* @Rapsssito
|
||||
esphome/components/bq27220/* @rggammon
|
||||
esphome/components/bq76972/* @limpkin
|
||||
esphome/components/bridge/* @kbx81
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
esphome/components/button/* @esphome/core
|
||||
@@ -213,6 +214,7 @@ esphome/components/feedback/* @ianchi
|
||||
esphome/components/file/* @esphome/core
|
||||
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
||||
esphome/components/font/* @clydebarrow @esphome/core
|
||||
esphome/components/friedrich/* @rwrozelle
|
||||
esphome/components/fs3000/* @kahrendt
|
||||
esphome/components/ft5x06/* @clydebarrow
|
||||
esphome/components/ft63x6/* @gpambrozio
|
||||
|
||||
+21
-6
@@ -248,6 +248,23 @@ def flash_string(config: ConfigType, value: str) -> str:
|
||||
return str(cg.progmem_string(value))
|
||||
|
||||
|
||||
def flash_compare(config: ConfigType, value: str) -> str:
|
||||
"""Default renderer for ``std::string`` constants in conditions; compares in flash on ESP8266."""
|
||||
if CORE.is_esp8266:
|
||||
return f"::esphome::ProgmemStringRef{{PSTR({cg.safe_exp(value)}), {len(value.encode('utf-8'))}}}"
|
||||
return str(cg.safe_exp(value))
|
||||
|
||||
|
||||
def _default_std_string(
|
||||
members: list[tuple[str, Any, Any]], renderer: Callable[[ConfigType, str], str]
|
||||
) -> list[tuple[str, Any, Any]]:
|
||||
"""Give ``cg.std_string`` members without a renderer the given default."""
|
||||
return [
|
||||
(key, t, fn or (renderer if t is cg.std_string else None))
|
||||
for key, t, fn in members
|
||||
]
|
||||
|
||||
|
||||
def literal_with_length(config: ConfigType, value: str) -> str:
|
||||
"""Renderer for a ``(const char *, size_t)`` target: a plain literal plus its byte length.
|
||||
|
||||
@@ -491,10 +508,7 @@ def apply_action_builder(
|
||||
statements_spec = [
|
||||
(
|
||||
c.target,
|
||||
[
|
||||
(key, t, fn or (flash_string if t is cg.std_string else None))
|
||||
for key, t, fn in c.members
|
||||
],
|
||||
_default_std_string(c.members, flash_string),
|
||||
)
|
||||
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
|
||||
]
|
||||
@@ -563,11 +577,12 @@ def register_apply_condition(
|
||||
``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an
|
||||
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
|
||||
against config values, all of which must be present. Write ``== false`` to negate.
|
||||
String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
|
||||
A ``cg.std_string`` constant stays in flash on ESP8266 and supports only ``==`` or ``!=``
|
||||
against a ``std::string`` or ``StringRef`` member; other string types are plain literals.
|
||||
Generates one static predicate and an ``ApplyCondition`` templated on it.
|
||||
"""
|
||||
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
|
||||
members = call.members
|
||||
members = _default_std_string(call.members, flash_compare)
|
||||
_check_key_in_schema(name, schema, id_key)
|
||||
for conf_key, _, _ in members:
|
||||
_check_key_in_schema(name, schema, conf_key)
|
||||
|
||||
@@ -5,8 +5,8 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, ID, Lambda
|
||||
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, CONF_WATCHDOG
|
||||
from esphome.core import CORE, ID, EsphomeError, Lambda, TimePeriod
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
CODEOWNERS = ["@Rapsssito"]
|
||||
@@ -14,6 +14,7 @@ DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "bq25186"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_BQ25186_ID = "bq25186_id"
|
||||
CONF_CONFIGURE_ON_BOOT = "configure_on_boot"
|
||||
CONF_BATTERY_REGULATION_VOLTAGE = "battery_regulation_voltage"
|
||||
CONF_FAST_CHARGE_CURRENT = "fast_charge_current"
|
||||
@@ -43,6 +44,7 @@ CONF_WAKE2_TIMER = "wake2_timer"
|
||||
CONF_ENABLE_PUSH_BUTTON = "enable_push_button"
|
||||
CONF_SYSTEM_REGULATION = "system_regulation"
|
||||
CONF_SYS_MODE = "sys_mode"
|
||||
CONF_WATCHDOG_15S_ENABLE = "watchdog_15s_enable"
|
||||
CONF_DISABLE_VDPPM = "disable_vdppm"
|
||||
CONF_TS_HOT = "ts_hot"
|
||||
CONF_TS_COLD = "ts_cold"
|
||||
@@ -56,7 +58,26 @@ CONF_MASK_BAT_INTERRUPT = "mask_bat_interrupt"
|
||||
CONF_MASK_PG_INTERRUPT = "mask_pg_interrupt"
|
||||
|
||||
bq25186_ns = cg.esphome_ns.namespace("bq25186")
|
||||
BQ25186Component = bq25186_ns.class_("BQ25186Component", cg.Component, i2c.I2CDevice)
|
||||
BQ25186Component = bq25186_ns.class_(
|
||||
"BQ25186Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
BQ25186_CHILD_SCHEMA = cv.Schema(
|
||||
{cv.GenerateID(CONF_BQ25186_ID): cv.use_id(BQ25186Component)}
|
||||
)
|
||||
_request_binary_sensor_slot = cg.slot_counter("BQ25186_BINARY_SENSOR_COUNT")
|
||||
_request_text_sensor_slot = cg.slot_counter("BQ25186_TEXT_SENSOR_COUNT")
|
||||
|
||||
|
||||
def register_binary_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
|
||||
_request_binary_sensor_slot(str(hub))
|
||||
return hub.register_binary_sensor(var)
|
||||
|
||||
|
||||
def register_text_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
|
||||
_request_text_sensor_slot(str(hub))
|
||||
return hub.register_text_sensor(var)
|
||||
|
||||
|
||||
REG_VBAT_CTRL = 0x03
|
||||
REG_ICHG_CTRL = 0x04
|
||||
@@ -69,7 +90,6 @@ REG_SYS_REG = 0x0A
|
||||
REG_TS_CONTROL = 0x0B
|
||||
REG_MASK_ID = 0x0C
|
||||
REGISTER_COUNT = REG_MASK_ID - REG_VBAT_CTRL + 1
|
||||
WATCHDOG_DISABLED = 0x03
|
||||
|
||||
TERMINATION_PERCENT_OPTIONS = {"disabled": 0, "5%": 1, "10%": 2, "20%": 3}
|
||||
VINDPM_MODE_OPTIONS = {"battery_tracking": 0, "4.5v": 1, "4.7v": 2, "disabled": 3}
|
||||
@@ -86,6 +106,22 @@ BATTERY_UVLO_OPTIONS = {
|
||||
VLOWV_OPTIONS = {"3.0v": 0, "2.8v": 1}
|
||||
RECHARGE_THRESHOLD_OPTIONS = {"100mv": 0, "200mv": 1}
|
||||
SAFETY_TIMER_OPTIONS = {"3h": 0, "6h": 1, "12h": 2, "disabled": 3}
|
||||
# The I2C watchdog restores the power-on registers when the bus stays quiet for its period
|
||||
WATCHDOG_OPTIONS = {
|
||||
"160s_software_reset": 0,
|
||||
"160s_hardware_reset": 1,
|
||||
"40s_hardware_reset": 2,
|
||||
"disabled": 3,
|
||||
}
|
||||
WATCHDOG_PERIOD_S = {
|
||||
"160s_software_reset": 160,
|
||||
"160s_hardware_reset": 160,
|
||||
"40s_hardware_reset": 40,
|
||||
}
|
||||
# SYS_REG watchdog_15s_enable: a hardware reset when the bus stays quiet for 15 s after VIN connects
|
||||
WATCHDOG_15S_PERIOD_S = 15
|
||||
# The chip boots with the 160 s software reset watchdog, which runs until the configuration is applied
|
||||
POWER_ON_WATCHDOG_PERIOD_S = 160
|
||||
LONG_PRESS_DURATION_OPTIONS = {"5s": 0, "10s": 1, "15s": 2, "20s": 3}
|
||||
AUTOWAKE_OPTIONS = {"0.5s": 0, "1s": 1, "2s": 2, "4s": 3}
|
||||
INPUT_CURRENT_LIMIT_OPTIONS = {
|
||||
@@ -146,8 +182,8 @@ def _on_current_step(amps: float) -> float:
|
||||
|
||||
|
||||
# (config key, register, bit shift, validator or enum options, default).
|
||||
# VBAT_CTRL bit 7 (pg_mode) and SYS_REG bit 4 (pg_gpo_level) stay 0; they are
|
||||
# driven at runtime, not from configuration.
|
||||
# VBAT_CTRL bit 7 (pg_mode) and SYS_REG bit 4 (pg_gpo_level) stay 0; the PG/GPO
|
||||
# switch drives them.
|
||||
FIELDS: tuple[tuple[str, int, int, Any, Any], ...] = (
|
||||
(
|
||||
CONF_BATTERY_REGULATION_VOLTAGE,
|
||||
@@ -179,6 +215,7 @@ FIELDS: tuple[tuple[str, int, int, Any, Any], ...] = (
|
||||
(CONF_RECHARGE_THRESHOLD, REG_IC_CTRL, 5, RECHARGE_THRESHOLD_OPTIONS, "100mv"),
|
||||
(CONF_DOUBLE_TIMER_DURING_DPM, REG_IC_CTRL, 4, cv.boolean, False),
|
||||
(CONF_SAFETY_TIMER, REG_IC_CTRL, 2, SAFETY_TIMER_OPTIONS, "6h"),
|
||||
(CONF_WATCHDOG, REG_IC_CTRL, 0, WATCHDOG_OPTIONS, "disabled"),
|
||||
(CONF_LONG_PRESS_DURATION, REG_TMR_ILIM, 6, LONG_PRESS_DURATION_OPTIONS, "10s"),
|
||||
(CONF_HW_RESET_REQUIRES_VIN, REG_TMR_ILIM, 5, cv.boolean, False),
|
||||
(CONF_AUTOWAKE, REG_TMR_ILIM, 3, AUTOWAKE_OPTIONS, "1s"),
|
||||
@@ -195,6 +232,7 @@ FIELDS: tuple[tuple[str, int, int, Any, Any], ...] = (
|
||||
(CONF_ENABLE_PUSH_BUTTON, REG_SHIP_RST, 0, cv.boolean, True),
|
||||
(CONF_SYSTEM_REGULATION, REG_SYS_REG, 5, SYSTEM_REGULATION_OPTIONS, "4.5v"),
|
||||
(CONF_SYS_MODE, REG_SYS_REG, 2, SYS_MODE_OPTIONS, "normal"),
|
||||
(CONF_WATCHDOG_15S_ENABLE, REG_SYS_REG, 1, cv.boolean, False),
|
||||
(CONF_DISABLE_VDPPM, REG_SYS_REG, 0, cv.boolean, False),
|
||||
(CONF_TS_HOT, REG_TS_CONTROL, 6, TS_HOT_OPTIONS, "60c"),
|
||||
(CONF_TS_COLD, REG_TS_CONTROL, 4, TS_COLD_OPTIONS, "0c"),
|
||||
@@ -232,9 +270,6 @@ _ENCODERS: dict[str, Callable[[Any], int]] = {
|
||||
def pack_registers(config: ConfigType) -> list[int]:
|
||||
"""Pack the validated field values into the VBAT_CTRL..MASK_ID register bytes."""
|
||||
regs = [0] * REGISTER_COUNT
|
||||
# Watchdog disabled (IC_CTRL[1:0] = 3): nothing talks to the chip after boot yet, and a running
|
||||
# watchdog restores the power-on defaults after 160 s. Part 2 adds the option with the polling.
|
||||
regs[REG_IC_CTRL - REG_VBAT_CTRL] = WATCHDOG_DISABLED
|
||||
for key, register, shift, validator, _ in FIELDS:
|
||||
value = config[key]
|
||||
if isinstance(validator, dict):
|
||||
@@ -259,7 +294,28 @@ def _field_schema(with_defaults: bool) -> cv.Schema:
|
||||
|
||||
BQ25186_CONFIG_SCHEMA = _field_schema(with_defaults=True)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
def _check_watchdog(config: ConfigType) -> ConfigType:
|
||||
"""Only the status poll talks to the chip, so it has to come round before any watchdog does."""
|
||||
periods = []
|
||||
if (period := WATCHDOG_PERIOD_S.get(config[CONF_WATCHDOG])) is not None:
|
||||
periods.append(period)
|
||||
if config[CONF_WATCHDOG_15S_ENABLE]:
|
||||
periods.append(WATCHDOG_15S_PERIOD_S)
|
||||
if not config.get(CONF_CONFIGURE_ON_BOOT, True):
|
||||
periods.append(POWER_ON_WATCHDOG_PERIOD_S)
|
||||
if not periods:
|
||||
return config
|
||||
if config[CONF_UPDATE_INTERVAL] >= TimePeriod(seconds=(limit := min(periods))):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_UPDATE_INTERVAL} must be shorter than {limit} s, or the "
|
||||
f"watchdog resets the charger between status polls",
|
||||
path=[CONF_UPDATE_INTERVAL],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BQ25186Component),
|
||||
@@ -267,7 +323,9 @@ CONFIG_SCHEMA = (
|
||||
}
|
||||
)
|
||||
.extend(BQ25186_CONFIG_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x6A))
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x6A)),
|
||||
_check_watchdog,
|
||||
)
|
||||
|
||||
CONFIGURE_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
@@ -305,7 +363,12 @@ async def bq25186_configure_to_code(
|
||||
# Options the action leaves out fall back to the component's configuration
|
||||
path = CORE.config.get_path_for_id(config[CONF_ID])[:-1]
|
||||
parent_config = CORE.config.get_config_for_path(path)
|
||||
regs = pack_registers({**parent_config, **config})
|
||||
merged = {**parent_config, **config}
|
||||
try:
|
||||
_check_watchdog(merged)
|
||||
except cv.Invalid as err:
|
||||
raise EsphomeError(f"bq25186.apply_configuration: {err.msg}") from err
|
||||
regs = pack_registers(merged)
|
||||
|
||||
text = str(cg.statement(parent.apply_config(cg.ArrayInitializer(*regs))))
|
||||
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
from functools import partial
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_BATTERY_CHARGING,
|
||||
DEVICE_CLASS_OPENING,
|
||||
DEVICE_CLASS_PLUG,
|
||||
DEVICE_CLASS_RUNNING,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_binary_sensor
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
BQ25186BinarySensor = bq25186_ns.class_(
|
||||
"BQ25186BinarySensor", binary_sensor.BinarySensor
|
||||
)
|
||||
|
||||
REG_STAT0 = 0
|
||||
REG_STAT1 = 1
|
||||
REG_FLAG0 = 2
|
||||
# Bit set for each field value that reads as on: a single bit is on when it is 1
|
||||
_BIT_SET = 0b10
|
||||
|
||||
# (key, status register, bit shift, field mask, on-values bitmap, device class)
|
||||
_FIELDS = (
|
||||
("ts", REG_STAT0, 7, 0x01, _BIT_SET, DEVICE_CLASS_OPENING),
|
||||
("charging", REG_STAT0, 5, 0x03, 0b0110, DEVICE_CLASS_BATTERY_CHARGING),
|
||||
("charge_done", REG_STAT0, 5, 0x03, 0b1000, cv.UNDEFINED),
|
||||
("ilim", REG_STAT0, 4, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("vdppm", REG_STAT0, 3, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("vindpm", REG_STAT0, 2, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("thermreg", REG_STAT0, 1, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("power_good", REG_STAT0, 0, 0x01, _BIT_SET, DEVICE_CLASS_PLUG),
|
||||
("vin_ovp", REG_STAT1, 7, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("battery_uvlo", REG_STAT1, 6, 0x01, _BIT_SET, DEVICE_CLASS_RUNNING),
|
||||
("safety_timer_flag", REG_STAT1, 2, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("wake1_flag", REG_STAT1, 1, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("wake2_flag", REG_STAT1, 0, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("ts_flag", REG_FLAG0, 7, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("ilim_flag", REG_FLAG0, 6, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("vdppm_flag", REG_FLAG0, 5, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("vindpm_flag", REG_FLAG0, 4, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("thermreg_flag", REG_FLAG0, 3, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("vin_ovp_flag", REG_FLAG0, 2, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("battery_uvlo_flag", REG_FLAG0, 1, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
("battery_ocp_flag", REG_FLAG0, 0, 0x01, _BIT_SET, cv.UNDEFINED),
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(key): binary_sensor.binary_sensor_schema(
|
||||
BQ25186BinarySensor, device_class=device_class
|
||||
)
|
||||
for key, *_, device_class in _FIELDS
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(*(key for key, *_ in _FIELDS)),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_BQ25186_ID])
|
||||
sensors = binary_sensor.sub_binary_sensors(config)
|
||||
for key, reg, shift, mask, match, _ in _FIELDS:
|
||||
await sensors(
|
||||
key, partial(register_binary_sensor, hub), reg, shift, mask, match
|
||||
)
|
||||
@@ -1,4 +1,10 @@
|
||||
#include "bq25186.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "bq25186_binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "bq25186_text_sensor.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -7,10 +13,10 @@ namespace esphome::bq25186 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bq25186");
|
||||
|
||||
// Bits the configuration owns in each register; the rest are runtime state (VBAT_CTRL pg_mode,
|
||||
// SYS_REG GPO level), reset triggers (SHIP_RST) or the device id (MASK_ID) and are kept as read
|
||||
// Bits the configuration owns in each register; the rest are the SYS_REG GPO level, reset
|
||||
// triggers (SHIP_RST) or the device id (MASK_ID) and are kept as read
|
||||
static const uint8_t BQ25186_CONFIG_MASKS[BQ25186_CONFIG_REGISTER_COUNT] PROGMEM = {
|
||||
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xEF, 0xFF, 0xF0,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xEF, 0xFF, 0xF0,
|
||||
};
|
||||
|
||||
bool BQ25186Component::write_register_(uint8_t reg, uint8_t value) {
|
||||
@@ -31,16 +37,38 @@ bool BQ25186Component::update_register_(uint8_t reg, uint8_t mask, uint8_t value
|
||||
return this->write_register_(reg, reg_value);
|
||||
}
|
||||
|
||||
uint8_t BQ25186Component::expected_register_(size_t index) const {
|
||||
uint8_t value = this->regs_[index];
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
if (index == 0 && this->pg_gpo_switch_ != nullptr)
|
||||
value |= BQ25186_VBAT_CTRL_PG_MODE;
|
||||
#endif
|
||||
return value;
|
||||
}
|
||||
|
||||
bool BQ25186Component::config_matches_(const BQ25186StatusRegisters ®s) const {
|
||||
for (uint8_t reg = BQ25186_REG_VBAT_CTRL; reg <= BQ25186_REG_MASK_ID; reg++) {
|
||||
const size_t index = reg - BQ25186_REG_VBAT_CTRL;
|
||||
const uint8_t mask = progmem_read_byte(&BQ25186_CONFIG_MASKS[index]);
|
||||
if (((regs[reg] ^ this->expected_register_(index)) & mask) != 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BQ25186Component::apply_config(const BQ25186ConfigRegisters ®s) {
|
||||
// Setup rejects an unknown chip; the action must not program it before that check or after it failed
|
||||
if (!this->is_ready() || this->is_failed()) {
|
||||
ESP_LOGW(TAG, "Not applying configuration, setup has not succeeded");
|
||||
return false;
|
||||
}
|
||||
this->regs_ = regs;
|
||||
this->configured_ = true;
|
||||
for (size_t i = 0; i < regs.size(); i++) {
|
||||
const uint8_t reg = BQ25186_REG_VBAT_CTRL + i;
|
||||
const uint8_t mask = progmem_read_byte(&BQ25186_CONFIG_MASKS[i]);
|
||||
const bool ok = mask == 0xFF ? this->write_register_(reg, regs[i]) : this->update_register_(reg, mask, regs[i]);
|
||||
const uint8_t value = this->expected_register_(i);
|
||||
const bool ok = mask == 0xFF ? this->write_register_(reg, value) : this->update_register_(reg, mask, value);
|
||||
if (!ok) {
|
||||
this->status_set_warning(LOG_STR("Configuration partly applied"));
|
||||
return false;
|
||||
@@ -68,12 +96,80 @@ void BQ25186Component::setup() {
|
||||
// A failed write only sets the warning status: the chip has identified itself, so a later
|
||||
// apply_configuration may retry
|
||||
if (this->configure_on_boot_)
|
||||
this->apply_config(this->setup_regs_);
|
||||
this->apply_config(this->regs_);
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
if (this->pg_gpo_switch_ != nullptr) {
|
||||
// The poll below puts the pin in output mode; without a restore mode the switch takes its level from it
|
||||
auto initial = this->pg_gpo_switch_->get_initial_state_with_restore_mode();
|
||||
if (initial.has_value())
|
||||
this->pg_gpo_switch_->publish_state(*initial); // the first poll writes it to the pin
|
||||
}
|
||||
#endif
|
||||
// The poller first runs after a random share of the interval; the entities get a state now
|
||||
this->update();
|
||||
}
|
||||
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
// The pin mode and level come from the switch, so every poll puts back whatever the chip lost
|
||||
void BQ25186Component::check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending) {
|
||||
if (this->pg_gpo_switch_ == nullptr)
|
||||
return;
|
||||
if ((status[BQ25186_REG_VBAT_CTRL] & BQ25186_VBAT_CTRL_PG_MODE) == 0 &&
|
||||
!this->update_register_(BQ25186_REG_VBAT_CTRL, BQ25186_VBAT_CTRL_PG_MODE, BQ25186_VBAT_CTRL_PG_MODE)) {
|
||||
this->status_set_warning(LOG_STR("PG/GPO pin not in output mode"));
|
||||
pending = true;
|
||||
}
|
||||
const bool level = (status[BQ25186_REG_SYS_REG] & BQ25186_SYS_REG_PG_GPO_LEVEL) != 0;
|
||||
if (!this->pg_gpo_switch_->has_state()) {
|
||||
this->pg_gpo_switch_->publish_state(level);
|
||||
} else if (level != this->pg_gpo_switch_->state && !this->write_pg_gpo_level(this->pg_gpo_switch_->state)) {
|
||||
this->status_set_warning(LOG_STR("PG/GPO level write failed"));
|
||||
pending = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Every poll also serves the I2C watchdog, which wants to see traffic within its period
|
||||
void BQ25186Component::update() {
|
||||
BQ25186StatusRegisters status;
|
||||
if (!this->read_bytes(BQ25186_REG_STAT0, status.data(), status.size())) {
|
||||
this->status_set_warning(LOG_STR("Status read failed"));
|
||||
return;
|
||||
}
|
||||
// Whatever failed is tried again here, so the warning only stays while something is still wrong
|
||||
bool pending = false;
|
||||
// A watchdog or hardware reset puts the power-on values back, and a failed write leaves a register
|
||||
// behind; either way the configuration is written again
|
||||
if (this->configured_ && !this->config_matches_(status)) {
|
||||
ESP_LOGW(TAG, "Configuration registers differ from the values applied, writing them again");
|
||||
if (this->apply_config(this->regs_)) {
|
||||
status[BQ25186_REG_VBAT_CTRL] |= BQ25186_VBAT_CTRL_PG_MODE; // part of the write when a switch is configured
|
||||
} else {
|
||||
pending = true;
|
||||
}
|
||||
}
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
this->check_pg_gpo_(status, pending);
|
||||
#endif
|
||||
if (!pending)
|
||||
this->status_clear_warning();
|
||||
#ifdef BQ25186_BINARY_SENSOR_COUNT
|
||||
for (auto *sensor : this->binary_sensors_)
|
||||
sensor->on_status(status);
|
||||
#endif
|
||||
#ifdef BQ25186_TEXT_SENSOR_COUNT
|
||||
for (auto *sensor : this->text_sensors_)
|
||||
sensor->on_status(status);
|
||||
#endif
|
||||
}
|
||||
|
||||
void BQ25186Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BQ25186:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
LOG_SWITCH(" ", "PG/GPO", this->pg_gpo_switch_);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
static constexpr uint8_t BQ25186_REG_STAT0 = 0x00;
|
||||
static constexpr uint8_t BQ25186_REG_STAT1 = 0x01;
|
||||
static constexpr uint8_t BQ25186_REG_FLAG0 = 0x02;
|
||||
static constexpr uint8_t BQ25186_REG_VBAT_CTRL = 0x03;
|
||||
static constexpr uint8_t BQ25186_REG_ICHG_CTRL = 0x04;
|
||||
static constexpr uint8_t BQ25186_REG_CHARGECTRL0 = 0x05;
|
||||
@@ -18,27 +26,70 @@ static constexpr uint8_t BQ25186_REG_SYS_REG = 0x0A;
|
||||
static constexpr uint8_t BQ25186_REG_TS_CONTROL = 0x0B;
|
||||
static constexpr uint8_t BQ25186_REG_MASK_ID = 0x0C;
|
||||
static constexpr size_t BQ25186_CONFIG_REGISTER_COUNT = BQ25186_REG_MASK_ID - BQ25186_REG_VBAT_CTRL + 1;
|
||||
// STAT0 to MASK_ID, read together by every poll: the status for the entities, then the configuration
|
||||
// registers, which show whether the chip was reset or a write failed
|
||||
static constexpr size_t BQ25186_STATUS_REGISTER_COUNT = BQ25186_REG_MASK_ID - BQ25186_REG_STAT0 + 1;
|
||||
|
||||
// VBAT_CTRL pg_mode: the /PG pin is a general purpose output driven by SYS_REG pg_gpo_level
|
||||
static constexpr uint8_t BQ25186_VBAT_CTRL_PG_MODE = 0x80;
|
||||
static constexpr uint8_t BQ25186_SYS_REG_PG_GPO_LEVEL = 0x10;
|
||||
|
||||
// Register values for VBAT_CTRL..MASK_ID, packed by code generation
|
||||
using BQ25186ConfigRegisters = std::array<uint8_t, BQ25186_CONFIG_REGISTER_COUNT>;
|
||||
using BQ25186StatusRegisters = std::array<uint8_t, BQ25186_STATUS_REGISTER_COUNT>;
|
||||
|
||||
class BQ25186Component : public Component, public i2c::I2CDevice {
|
||||
class BQ25186BinarySensor;
|
||||
class BQ25186TextSensor;
|
||||
|
||||
class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_setup_registers(const BQ25186ConfigRegisters ®s) {
|
||||
this->setup_regs_ = regs;
|
||||
this->regs_ = regs;
|
||||
this->configure_on_boot_ = true;
|
||||
}
|
||||
bool apply_config(const BQ25186ConfigRegisters ®s);
|
||||
#ifdef BQ25186_BINARY_SENSOR_COUNT
|
||||
void register_binary_sensor(BQ25186BinarySensor *sensor) { this->binary_sensors_.push_back(sensor); }
|
||||
#endif
|
||||
#ifdef BQ25186_TEXT_SENSOR_COUNT
|
||||
void register_text_sensor(BQ25186TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
|
||||
#endif
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
/// The /PG/GPO pin is put in general purpose output mode when a switch drives it
|
||||
void set_pg_gpo_switch(switch_::Switch *pg_gpo_switch) { this->pg_gpo_switch_ = pg_gpo_switch; }
|
||||
bool write_pg_gpo_level(bool level) {
|
||||
return this->update_register_(BQ25186_REG_SYS_REG, BQ25186_SYS_REG_PG_GPO_LEVEL,
|
||||
level ? BQ25186_SYS_REG_PG_GPO_LEVEL : 0);
|
||||
}
|
||||
#endif
|
||||
/// Write the masked bits of SHIP_RST
|
||||
bool write_ship_rst(uint8_t mask, uint8_t value) { return this->update_register_(BQ25186_REG_SHIP_RST, mask, value); }
|
||||
|
||||
protected:
|
||||
bool write_register_(uint8_t reg, uint8_t value);
|
||||
uint8_t expected_register_(size_t index) const;
|
||||
bool config_matches_(const BQ25186StatusRegisters ®s) const;
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
void check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending);
|
||||
#endif
|
||||
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
|
||||
|
||||
BQ25186ConfigRegisters setup_regs_{};
|
||||
#ifdef BQ25186_BINARY_SENSOR_COUNT
|
||||
StaticVector<BQ25186BinarySensor *, BQ25186_BINARY_SENSOR_COUNT> binary_sensors_;
|
||||
#endif
|
||||
#ifdef BQ25186_TEXT_SENSOR_COUNT
|
||||
StaticVector<BQ25186TextSensor *, BQ25186_TEXT_SENSOR_COUNT> text_sensors_;
|
||||
#endif
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
switch_::Switch *pg_gpo_switch_{nullptr};
|
||||
#endif
|
||||
BQ25186ConfigRegisters regs_{}; // the configuration last applied, written again after a chip reset
|
||||
bool configure_on_boot_{false};
|
||||
bool configured_{false}; // regs_ went to the chip at least once
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
/// One field of a status register; `match` has a bit set for each field value that reads as on
|
||||
class BQ25186BinarySensor final : public binary_sensor::BinarySensor {
|
||||
public:
|
||||
BQ25186BinarySensor(uint8_t reg, uint8_t shift, uint8_t mask, uint8_t match)
|
||||
: reg_(reg), shift_(shift), mask_(mask), match_(match) {}
|
||||
|
||||
void on_status(const BQ25186StatusRegisters &status) {
|
||||
const uint8_t value = (status[this->reg_] >> this->shift_) & this->mask_;
|
||||
this->publish_state((this->match_ >> value) & 1);
|
||||
}
|
||||
|
||||
protected:
|
||||
const uint8_t reg_;
|
||||
const uint8_t shift_;
|
||||
const uint8_t mask_;
|
||||
const uint8_t match_;
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_BINARY_SENSOR
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BUTTON
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/button/button.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
/// Writes one SHIP_RST field when pressed
|
||||
class BQ25186Button final : public button::Button, public Parented<BQ25186Component> {
|
||||
public:
|
||||
BQ25186Button(uint8_t mask, uint8_t value) : mask_(mask), value_(value) {}
|
||||
|
||||
protected:
|
||||
void press_action() override { this->parent_->write_ship_rst(this->mask_, this->value_); }
|
||||
|
||||
const uint8_t mask_;
|
||||
const uint8_t value_;
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_BUTTON
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BQ25186_PG_GPO
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
class BQ25186PgGpoSwitch final : public switch_::Switch, public Parented<BQ25186Component> {
|
||||
protected:
|
||||
void write_state(bool state) override {
|
||||
if (this->parent_->write_pg_gpo_level(state))
|
||||
this->publish_state(state);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_BQ25186_PG_GPO
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "bq25186_text_sensor.h"
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
// STAT0[6:5] charge status, then STAT1[4:3] TS status
|
||||
PROGMEM_STRING_TABLE(StatusStrings, "Not Charging", "Constant Current", "Constant Voltage", "Charge Done/Disabled",
|
||||
"Normal", "Hot/Cold (Charging Suspended)", "Cool (Current Reduced)", "Warm (Voltage Reduced)");
|
||||
|
||||
void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
|
||||
const uint8_t value = (status[this->reg_] >> this->shift_) & 0x03;
|
||||
if (value == this->last_)
|
||||
return;
|
||||
this->last_ = value;
|
||||
const ProgmemStr name = StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX);
|
||||
char text[32];
|
||||
ESPHOME_strncpy_P(text, reinterpret_cast<ESPHOME_PGM_P>(name), sizeof(text));
|
||||
text[sizeof(text) - 1] = '\0';
|
||||
this->publish_state(text);
|
||||
}
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_TEXT_SENSOR
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
/// A two-bit status field; `first` is the string table index of the field's value 0
|
||||
class BQ25186TextSensor final : public text_sensor::TextSensor {
|
||||
public:
|
||||
BQ25186TextSensor(uint8_t reg, uint8_t shift, uint8_t first) : reg_(reg), shift_(shift), first_(first) {}
|
||||
void on_status(const BQ25186StatusRegisters &status);
|
||||
|
||||
protected:
|
||||
const uint8_t reg_;
|
||||
const uint8_t shift_;
|
||||
const uint8_t first_;
|
||||
uint8_t last_{0xFF}; // the field value last published
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_TEXT_SENSOR
|
||||
@@ -0,0 +1,52 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_RESTART,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_POWER,
|
||||
ICON_RESTART,
|
||||
ICON_RESTART_ALERT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
BQ25186Button = bq25186_ns.class_("BQ25186Button", button.Button)
|
||||
|
||||
# SHIP_RST: bit 7 is the software reset, bits 6:5 the shutdown, ship and hardware reset codes
|
||||
SOFTWARE_RESET_MASK = 0x80
|
||||
MODE_MASK = 0x60
|
||||
|
||||
# (key, SHIP_RST mask, value, device class, icon)
|
||||
_BUTTONS = (
|
||||
("software_reset", SOFTWARE_RESET_MASK, 0x80, DEVICE_CLASS_RESTART, ICON_RESTART),
|
||||
("shutdown", MODE_MASK, 0x20, cv.UNDEFINED, ICON_POWER),
|
||||
("ship_mode", MODE_MASK, 0x40, cv.UNDEFINED, ICON_POWER),
|
||||
("hardware_reset", MODE_MASK, 0x60, DEVICE_CLASS_RESTART, ICON_RESTART_ALERT),
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(key): button.button_schema(
|
||||
BQ25186Button,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=icon,
|
||||
device_class=device_class,
|
||||
)
|
||||
for key, _, _, device_class, icon in _BUTTONS
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(*(key for key, *_ in _BUTTONS)),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
for key, mask, value, _, _ in _BUTTONS:
|
||||
if (conf := config.get(key)) is not None:
|
||||
var = await button.new_button(conf, mask, value)
|
||||
await cg.register_parented(var, config[CONF_BQ25186_ID])
|
||||
@@ -0,0 +1,27 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
CONF_PG_GPO = "pg_gpo"
|
||||
|
||||
BQ25186PgGpoSwitch = bq25186_ns.class_("BQ25186PgGpoSwitch", switch.Switch)
|
||||
|
||||
CONFIG_SCHEMA = BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_PG_GPO): switch.switch_schema(
|
||||
BQ25186PgGpoSwitch, default_restore_mode="DISABLED"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_BQ25186_PG_GPO")
|
||||
hub = await cg.get_variable(config[CONF_BQ25186_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(CONF_PG_GPO, hub.set_pg_gpo_switch)
|
||||
@@ -0,0 +1,38 @@
|
||||
from functools import partial
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_text_sensor
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
BQ25186TextSensor = bq25186_ns.class_("BQ25186TextSensor", text_sensor.TextSensor)
|
||||
|
||||
REG_STAT0 = 0
|
||||
REG_STAT1 = 1
|
||||
|
||||
# (key, status register, bit shift, index of the field's value 0 in the C++ string table)
|
||||
_FIELDS = (
|
||||
("charge_status", REG_STAT0, 5, 0),
|
||||
("ts_status", REG_STAT1, 3, 4),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(key): text_sensor.text_sensor_schema(BQ25186TextSensor)
|
||||
for key, *_ in _FIELDS
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(*(key for key, *_ in _FIELDS)),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_BQ25186_ID])
|
||||
sensors = text_sensor.sub_text_sensors(config)
|
||||
for key, reg, shift, first in _FIELDS:
|
||||
await sensors(key, partial(register_text_sensor, hub), reg, shift, first)
|
||||
@@ -0,0 +1,111 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["sensor"]
|
||||
CODEOWNERS = ["@limpkin"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "bq76972"
|
||||
MULTI_CONF = True
|
||||
|
||||
bq76972_ns = cg.esphome_ns.namespace("bq76972")
|
||||
BQ76972Component = bq76972_ns.class_(
|
||||
"BQ76972Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONF_REG_DISABLED = "reg_disabled"
|
||||
CONF_CRC_ENABLED = "crc_enabled"
|
||||
CONF_STACK_VOLTAGE = "stack_voltage"
|
||||
|
||||
# Ordered as the chip reports them, starting at command 0x68
|
||||
CONF_TEMPERATURES = [
|
||||
"internal_temperature",
|
||||
"cfetoff_temperature",
|
||||
"dfetoff_temperature",
|
||||
"alert_temperature",
|
||||
"ts1_temperature",
|
||||
"ts2_temperature",
|
||||
"ts3_temperature",
|
||||
"hdq_temperature",
|
||||
"dchg_temperature",
|
||||
"ddsg_temperature",
|
||||
]
|
||||
|
||||
CONF_CELL_VOLTAGES = [f"cell_{i}_voltage" for i in range(1, 17)]
|
||||
|
||||
voltage_sensor_schema = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
temp_sensor_schema = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BQ76972Component),
|
||||
cv.Optional(CONF_CRC_ENABLED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REG_DISABLED, default=False): cv.boolean,
|
||||
cv.Optional(CONF_STACK_VOLTAGE): voltage_sensor_schema,
|
||||
**{cv.Optional(key): voltage_sensor_schema for key in CONF_CELL_VOLTAGES},
|
||||
**{cv.Optional(key): temp_sensor_schema for key in CONF_TEMPERATURES},
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x08))
|
||||
.extend(cv.polling_component_schema("10s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_crc_mode(config[CONF_CRC_ENABLED]))
|
||||
cg.add(var.set_reg_disable(config[CONF_REG_DISABLED]))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_STACK_VOLTAGE, var.set_stack_voltage_sensor)
|
||||
|
||||
for idx, key in enumerate(CONF_TEMPERATURES):
|
||||
if (conf := config.get(key)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_temperature_sensor(sens, idx))
|
||||
|
||||
for idx, key in enumerate(CONF_CELL_VOLTAGES):
|
||||
if (conf := config.get(key)) is not None:
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(var.set_cell_sensor(sens, idx))
|
||||
|
||||
|
||||
BQ76972_PROGRAM_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BQ76972Component),
|
||||
cv.Required(CONF_ADDRESS): cv.templatable(cv.i2c_address),
|
||||
},
|
||||
key=CONF_ADDRESS,
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"bq76972.program_i2c_address",
|
||||
BQ76972_PROGRAM_I2C_ADDRESS_SCHEMA,
|
||||
automation.ApplyField(CONF_ADDRESS, "program_i2c_address", cg.uint8),
|
||||
)
|
||||
@@ -0,0 +1,298 @@
|
||||
#include "bq76972.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bq76972 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bq76972");
|
||||
|
||||
static constexpr uint8_t BQ76972_REG_SUBCOMMAND = 0x3E; // 0x3E low byte, 0x3F high byte
|
||||
static constexpr uint8_t BQ76972_REG_TRANSFER_BUFFER = 0x40;
|
||||
static constexpr uint8_t BQ76972_REG_CHECKSUM = 0x60; // 0x60 checksum, 0x61 length
|
||||
static constexpr uint8_t BQ76972_CMD_BAT_STATUS = 0x12;
|
||||
static constexpr uint8_t BQ76972_CMD_CELL_START = 0x14;
|
||||
static constexpr uint8_t BQ76972_CMD_STACK_VOLTAGE = 0x34;
|
||||
static constexpr uint8_t BQ76972_CMD_TEMP_START = 0x68;
|
||||
static constexpr uint16_t BQ76972_SUBCMD_ENTER_CFG_UPDATE = 0x0090;
|
||||
static constexpr uint16_t BQ76972_SUBCMD_EXIT_CFG_UPDATE = 0x0092;
|
||||
static constexpr uint16_t BQ76972_MEM_REG12_CONFIG = 0x9236;
|
||||
static constexpr uint16_t BQ76972_MEM_REG0_CONFIG = 0x9237;
|
||||
static constexpr uint16_t BQ76972_MEM_I2C_ADDRESS = 0x923A;
|
||||
static constexpr uint16_t BQ76972_MEM_SWAP_COMM_MODE = 0x29BC;
|
||||
// Pin config of temperature reading N (1 = CFETOFF ... 9 = DDSG) is at this base + N
|
||||
static constexpr uint16_t BQ76972_MEM_TEMP_PIN_CONFIG_BASE = 0x92F9;
|
||||
static constexpr size_t BQ76972_TS1_INDEX = 4; // a thermistor input by default
|
||||
static constexpr uint8_t BQ76972_PIN_CONFIG_THERMISTOR = 0x07;
|
||||
|
||||
// 16 words with a CRC byte after every data byte
|
||||
static constexpr size_t BQ76972_MAX_TRANSFER = 64;
|
||||
static constexpr uint8_t BQ76972_MAX_POLLS = 50;
|
||||
|
||||
static float raw_to_celsius(uint16_t raw) { return static_cast<int16_t>(raw) * 0.1f - 273.15f; }
|
||||
|
||||
bool BQ76972Component::read_block_(uint8_t start_register, uint8_t *data, size_t len) {
|
||||
if (!this->crc_mode_)
|
||||
return this->read_bytes(start_register, data, len);
|
||||
|
||||
// With CRC enabled, every data byte is followed by a CRC byte
|
||||
std::array<uint8_t, BQ76972_MAX_TRANSFER> buffer;
|
||||
if (len == 0 || len * 2 > buffer.size())
|
||||
return false;
|
||||
if (!this->read_bytes(start_register, buffer.data(), len * 2))
|
||||
return false;
|
||||
|
||||
// The first CRC also covers the write address, register and read address
|
||||
const uint8_t crc0_input[4] = {static_cast<uint8_t>(this->address_ << 1), start_register,
|
||||
static_cast<uint8_t>((this->address_ << 1) | 1), buffer[0]};
|
||||
if (crc8(crc0_input, 4, 0x00, 0x07, true) != buffer[1]) {
|
||||
ESP_LOGD(TAG, "CRC mismatch at register 0x%02X byte 0", start_register);
|
||||
return false;
|
||||
}
|
||||
data[0] = buffer[0];
|
||||
for (size_t i = 1; i < len; ++i) {
|
||||
if (crc8(&buffer[i * 2], 1, 0x00, 0x07, true) != buffer[i * 2 + 1]) {
|
||||
ESP_LOGD(TAG, "CRC mismatch at register 0x%02X byte %u", start_register, static_cast<unsigned>(i));
|
||||
return false;
|
||||
}
|
||||
data[i] = buffer[i * 2];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BQ76972Component::write_block_(uint8_t start_register, const uint8_t *data, size_t len) {
|
||||
if (!this->crc_mode_)
|
||||
return this->write_bytes(start_register, data, len);
|
||||
|
||||
std::array<uint8_t, BQ76972_MAX_TRANSFER> buffer;
|
||||
if (len == 0 || len * 2 > buffer.size())
|
||||
return false;
|
||||
|
||||
// The first CRC also covers the write address and register
|
||||
const uint8_t crc0_input[3] = {static_cast<uint8_t>(this->address_ << 1), start_register, data[0]};
|
||||
buffer[0] = data[0];
|
||||
buffer[1] = crc8(crc0_input, 3, 0x00, 0x07, true);
|
||||
for (size_t i = 1; i < len; ++i) {
|
||||
buffer[i * 2] = data[i];
|
||||
buffer[i * 2 + 1] = crc8(&data[i], 1, 0x00, 0x07, true);
|
||||
}
|
||||
return this->write_bytes(start_register, buffer.data(), len * 2);
|
||||
}
|
||||
|
||||
bool BQ76972Component::read_words_(uint8_t start_register, uint16_t *data, size_t num_words) {
|
||||
std::array<uint8_t, BQ76972_MAX_TRANSFER / 2> bytes;
|
||||
if (num_words * 2 > bytes.size() || !this->read_block_(start_register, bytes.data(), num_words * 2))
|
||||
return false;
|
||||
for (size_t i = 0; i < num_words; i++)
|
||||
data[i] = encode_uint16(bytes[2 * i + 1], bytes[2 * i]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BQ76972Component::send_subcommand_(uint16_t subcommand) {
|
||||
const uint8_t bytes[2] = {static_cast<uint8_t>(subcommand & 0xFF), static_cast<uint8_t>(subcommand >> 8)};
|
||||
return this->write_block_(BQ76972_REG_SUBCOMMAND, bytes, 2);
|
||||
}
|
||||
|
||||
bool BQ76972Component::wait_for_subcommand_() {
|
||||
uint8_t bytes[2];
|
||||
for (uint8_t i = 0; i < BQ76972_MAX_POLLS; i++) {
|
||||
if (!this->read_block_(BQ76972_REG_SUBCOMMAND, bytes, 2))
|
||||
return false;
|
||||
if (bytes[0] != 0xFF && bytes[1] != 0xFF)
|
||||
return true;
|
||||
delay(1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BQ76972Component::wait_for_cfgupdate_() {
|
||||
uint16_t status;
|
||||
for (uint8_t i = 0; i < BQ76972_MAX_POLLS; i++) {
|
||||
if (!this->read_words_(BQ76972_CMD_BAT_STATUS, &status, 1))
|
||||
return false;
|
||||
if (status & 0x0001)
|
||||
return true;
|
||||
delay(1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BQ76972Component::read_subcommand_(uint16_t subcommand, uint8_t *data, size_t len) {
|
||||
return this->send_subcommand_(subcommand) && this->wait_for_subcommand_() &&
|
||||
this->read_block_(BQ76972_REG_TRANSFER_BUFFER, data, len);
|
||||
}
|
||||
|
||||
bool BQ76972Component::write_subcommand_(uint16_t subcommand, const uint8_t *data, size_t len) {
|
||||
if (!this->send_subcommand_(subcommand))
|
||||
return false;
|
||||
if (len == 0)
|
||||
return true;
|
||||
if (!this->write_block_(BQ76972_REG_TRANSFER_BUFFER, data, len))
|
||||
return false;
|
||||
|
||||
// Inverted 8-bit sum of 0x3E, 0x3F and the data; length counts 0x3E, 0x3F, 0x60, 0x61 and the data
|
||||
uint8_t sum = static_cast<uint8_t>(subcommand & 0xFF) + static_cast<uint8_t>(subcommand >> 8);
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
sum += data[i];
|
||||
const uint8_t tail[2] = {static_cast<uint8_t>(~sum), static_cast<uint8_t>(4 + len)};
|
||||
return this->write_block_(BQ76972_REG_CHECKSUM, tail, 2);
|
||||
}
|
||||
|
||||
bool BQ76972Component::enter_cfg_update_() {
|
||||
if (!this->write_subcommand_(BQ76972_SUBCMD_ENTER_CFG_UPDATE, nullptr, 0))
|
||||
return false;
|
||||
delay(10);
|
||||
return this->wait_for_cfgupdate_();
|
||||
}
|
||||
|
||||
bool BQ76972Component::exit_cfg_update_() {
|
||||
return this->write_subcommand_(BQ76972_SUBCMD_EXIT_CFG_UPDATE, nullptr, 0);
|
||||
}
|
||||
|
||||
bool BQ76972Component::regs_disabled_(bool &out) {
|
||||
uint8_t reg12;
|
||||
uint8_t reg0;
|
||||
if (!this->read_subcommand_(BQ76972_MEM_REG12_CONFIG, ®12, 1) ||
|
||||
!this->read_subcommand_(BQ76972_MEM_REG0_CONFIG, ®0, 1))
|
||||
return false;
|
||||
out = reg12 == 0 && (reg0 & 0x01) == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
const LogString *BQ76972Component::configure_() {
|
||||
uint16_t internal_temp;
|
||||
if (!this->read_words_(BQ76972_CMD_TEMP_START, &internal_temp, 1))
|
||||
return LOG_STR(ESP_LOG_MSG_COMM_FAIL);
|
||||
|
||||
// Only write the regulator config when it differs, see
|
||||
// https://e2e.ti.com/support/power-management-group/power-management/f/power-management-forum/1583718/bq76972-undocumented-4-minute-config_update-auto-exit-timeout
|
||||
bool regs_disabled = true;
|
||||
if (this->reg_disable_ && !this->regs_disabled_(regs_disabled))
|
||||
return LOG_STR("Reading regulator config failed");
|
||||
const bool write_regs = !regs_disabled;
|
||||
|
||||
bool write_thermistors = false;
|
||||
for (size_t i = 1; i < BQ76972_NUM_TEMPERATURES; i++) {
|
||||
if (i != BQ76972_TS1_INDEX && this->temperature_sensors_[i] != nullptr)
|
||||
write_thermistors = true;
|
||||
}
|
||||
|
||||
if (write_regs || write_thermistors) {
|
||||
if (!this->enter_cfg_update_())
|
||||
return LOG_STR("Entering config update mode failed");
|
||||
const LogString *error = this->write_config_(write_regs);
|
||||
// The chip stops measuring in config update mode, so leave it even after a failed write
|
||||
if (!this->exit_cfg_update_() && error == nullptr)
|
||||
error = LOG_STR("Exiting config update mode failed");
|
||||
if (error != nullptr)
|
||||
return error;
|
||||
if (write_regs && (!this->regs_disabled_(regs_disabled) || !regs_disabled))
|
||||
return LOG_STR("Disabling regulators failed");
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Internal temperature: %.1f°C", raw_to_celsius(internal_temp));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const LogString *BQ76972Component::write_config_(bool write_regs) {
|
||||
if (write_regs) {
|
||||
const uint8_t zero = 0;
|
||||
if (!this->write_subcommand_(BQ76972_MEM_REG12_CONFIG, &zero, 1) ||
|
||||
!this->write_subcommand_(BQ76972_MEM_REG0_CONFIG, &zero, 1))
|
||||
return LOG_STR("Writing regulator config failed");
|
||||
}
|
||||
for (size_t i = 1; i < BQ76972_NUM_TEMPERATURES; i++) {
|
||||
if (i == BQ76972_TS1_INDEX || this->temperature_sensors_[i] == nullptr)
|
||||
continue;
|
||||
if (!this->write_subcommand_(BQ76972_MEM_TEMP_PIN_CONFIG_BASE + i, &BQ76972_PIN_CONFIG_THERMISTOR, 1))
|
||||
return LOG_STR("Writing thermistor pin config failed");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BQ76972Component::setup() {
|
||||
if (const LogString *error = this->configure_(); error != nullptr)
|
||||
this->mark_failed(error);
|
||||
}
|
||||
|
||||
void BQ76972Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BQ76972:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" CRC Mode: %s\n"
|
||||
" Regulators disabled: %s",
|
||||
this->crc_mode_ ? LOG_STR_LITERAL("Enabled") : LOG_STR_LITERAL("Disabled"), YESNO(this->reg_disable_));
|
||||
}
|
||||
|
||||
void BQ76972Component::program_i2c_address(uint8_t address) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
const uint8_t shifted = address << 1;
|
||||
if (!this->enter_cfg_update_()) {
|
||||
this->status_set_warning(LOG_STR("Programming I2C address failed"));
|
||||
return;
|
||||
}
|
||||
// The new address only takes effect with the swap, so after a failed write the chip still answers at the
|
||||
// old one and must still be taken out of config update mode
|
||||
const bool written = this->write_subcommand_(BQ76972_MEM_I2C_ADDRESS, &shifted, 1) &&
|
||||
this->write_subcommand_(BQ76972_MEM_SWAP_COMM_MODE, nullptr, 0);
|
||||
if (written) {
|
||||
delay(10);
|
||||
this->set_i2c_address(address);
|
||||
}
|
||||
if (!this->exit_cfg_update_() || !written) {
|
||||
this->status_set_warning(LOG_STR("Programming I2C address failed"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "I2C address set to 0x%02X", address);
|
||||
}
|
||||
|
||||
template<size_t N> static bool any_set(const std::array<sensor::Sensor *, N> &sensors) {
|
||||
for (auto *sensor : sensors) {
|
||||
if (sensor != nullptr)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BQ76972Component::update() {
|
||||
// Each group publishes as soon as its read succeeds, so one failed read does not hold back the others
|
||||
bool ok = true;
|
||||
uint16_t words[BQ76972_NUM_CELLS];
|
||||
if (this->stack_voltage_sensor_ != nullptr) {
|
||||
if (this->read_words_(BQ76972_CMD_STACK_VOLTAGE, words, 1)) {
|
||||
if (words[0] != 0)
|
||||
this->stack_voltage_sensor_->publish_state(words[0] * 0.01f); // 10 mV units
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (any_set(this->cell_sensors_)) {
|
||||
if (this->read_words_(BQ76972_CMD_CELL_START, words, BQ76972_NUM_CELLS)) {
|
||||
for (size_t i = 0; i < BQ76972_NUM_CELLS; i++) {
|
||||
if (this->cell_sensors_[i] != nullptr && words[i] != 0)
|
||||
this->cell_sensors_[i]->publish_state(words[i] * 0.001f); // mV
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (any_set(this->temperature_sensors_)) {
|
||||
if (this->read_words_(BQ76972_CMD_TEMP_START, words, BQ76972_NUM_TEMPERATURES)) {
|
||||
for (size_t i = 0; i < BQ76972_NUM_TEMPERATURES; i++) {
|
||||
if (this->temperature_sensors_[i] != nullptr)
|
||||
this->temperature_sensors_[i]->publish_state(raw_to_celsius(words[i]));
|
||||
}
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::bq76972
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::bq76972 {
|
||||
|
||||
static constexpr size_t BQ76972_NUM_CELLS = 16;
|
||||
static constexpr size_t BQ76972_NUM_TEMPERATURES = 10;
|
||||
|
||||
class BQ76972Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_crc_mode(bool crc_mode) { this->crc_mode_ = crc_mode; }
|
||||
void set_reg_disable(bool reg_disable) { this->reg_disable_ = reg_disable; }
|
||||
void set_stack_voltage_sensor(sensor::Sensor *s) { this->stack_voltage_sensor_ = s; }
|
||||
void set_cell_sensor(sensor::Sensor *s, size_t index) { this->cell_sensors_[index] = s; }
|
||||
void set_temperature_sensor(sensor::Sensor *s, size_t index) { this->temperature_sensors_[index] = s; }
|
||||
|
||||
void program_i2c_address(uint8_t address);
|
||||
|
||||
protected:
|
||||
bool read_block_(uint8_t start_register, uint8_t *data, size_t len);
|
||||
bool write_block_(uint8_t start_register, const uint8_t *data, size_t len);
|
||||
bool read_words_(uint8_t start_register, uint16_t *data, size_t num_words);
|
||||
bool send_subcommand_(uint16_t subcommand);
|
||||
bool read_subcommand_(uint16_t subcommand, uint8_t *data, size_t len);
|
||||
bool write_subcommand_(uint16_t subcommand, const uint8_t *data, size_t len);
|
||||
bool wait_for_subcommand_();
|
||||
bool wait_for_cfgupdate_();
|
||||
bool enter_cfg_update_();
|
||||
bool exit_cfg_update_();
|
||||
bool regs_disabled_(bool &out);
|
||||
const LogString *configure_();
|
||||
const LogString *write_config_(bool write_regs);
|
||||
|
||||
bool reg_disable_{false};
|
||||
bool crc_mode_{false};
|
||||
|
||||
sensor::Sensor *stack_voltage_sensor_{nullptr};
|
||||
std::array<sensor::Sensor *, BQ76972_NUM_CELLS> cell_sensors_{};
|
||||
std::array<sensor::Sensor *, BQ76972_NUM_TEMPERATURES> temperature_sensors_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::bq76972
|
||||
@@ -59,6 +59,7 @@ CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_ROLE = "role"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_SCAN_PARAMETERS = "scan_parameters"
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
CONF_SHA256 = "sha256"
|
||||
CONF_SLOT = "slot"
|
||||
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DOMAIN = "friedrich"
|
||||
@@ -0,0 +1,32 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir, remote_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MODEL, CONF_SENSOR
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
CODEOWNERS = ["@rwrozelle"]
|
||||
|
||||
friedrich_ns = cg.esphome_ns.namespace("friedrich")
|
||||
FriedrichClimate = friedrich_ns.class_("FriedrichClimate", climate_ir.ClimateIR)
|
||||
|
||||
Model = friedrich_ns.enum("Model", is_class=True)
|
||||
MODELS = {
|
||||
"MW12Y3H": Model.MODEL_MW12Y3H,
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(FriedrichClimate).extend(
|
||||
{
|
||||
cv.Optional(CONF_MODEL, default="MW12Y3H"): cv.enum(MODELS, upper=True),
|
||||
# The unit works in Fahrenheit, so a Celsius temperature sensor cannot feed it
|
||||
cv.Optional(CONF_SENSOR): cv.invalid(
|
||||
"'sensor' is not supported by the Friedrich climate component"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
remote_base.request_protocol("aeha") # used from C++
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
@@ -0,0 +1,234 @@
|
||||
#include "friedrich.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "esphome/components/remote_base/aeha_protocol.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::friedrich {
|
||||
// clang-format off
|
||||
/*
|
||||
* MW12Y3H - Currently the only supported model. Model field is retained to allow future per-model code differentiation.
|
||||
* AEHA
|
||||
* Byte0 Fixed = 0x00
|
||||
* Byte1 Fixed = 0x08
|
||||
* Byte2 Fixed = 0x08
|
||||
* Byte3 Command: Move Louver = 0x36 Off = 0x40, else Fixed = 0x7F
|
||||
* Byte4 Checksum when command, else Fixed = 0x90
|
||||
* Byte5 N/A when command, else Fixed = 0x0C
|
||||
* Byte6 N/A when command, else Temp(F), add 0x80 for On
|
||||
* Byte7 N/A when command, else Mode (Min Heat = 0xD0 and Coil Dry = 0x10 are not exposed)
|
||||
* Byte8 N/A when command, else Fan, add 0x08 for Vertical Swing
|
||||
* Byte9 N/A when command, else Fixed = 0x00 // not interested
|
||||
* Byte10 N/A when command, else Fixed = 0x00 // not interested
|
||||
* Byte11 N/A when command, else Fixed = 0x00 // not interested
|
||||
* Byte12 N/A when command, else Economy (On = 0x00, Off = 0x04)
|
||||
* Byte13 N/A when command, else CheckSum
|
||||
*/
|
||||
// clang-format on
|
||||
ESPHOME_LOG_TAG(TAG, "friedrich");
|
||||
|
||||
static constexpr uint8_t TEMP_MIN_NOT_HEAT = 64;
|
||||
static constexpr uint8_t TEMP_DEFAULT = 72;
|
||||
static constexpr uint8_t STATE_MESSAGE_LENGTH = FriedrichClimate::STATE_MESSAGE_LENGTH;
|
||||
static constexpr uint8_t STATE_MESSAGE_LENGTH_UTIL = 5;
|
||||
static constexpr uint16_t CARRIER_ADDRESS = 0x28C6;
|
||||
static constexpr uint32_t CARRIER_FREQUENCY = 38000;
|
||||
|
||||
static constexpr uint8_t BYTE3_POWER_OFF = 0x40;
|
||||
static constexpr uint8_t BYTE6_POWER_ON = 0x80;
|
||||
static constexpr uint8_t BYTE7_MODE_AUTO = 0x00;
|
||||
static constexpr uint8_t BYTE8_FAN_AUTO = 0x00;
|
||||
static constexpr uint8_t BYTE8_FAN_SWING = 0x08;
|
||||
|
||||
// Every state frame: power on, economy off; bytes 6, 7, 8 and 13 are patched per state
|
||||
static constexpr uint8_t STATE_TEMPLATE[STATE_MESSAGE_LENGTH] PROGMEM = {
|
||||
0x00, 0x08, 0x08, 0x7F, 0x90, 0x0C, BYTE6_POWER_ON, BYTE7_MODE_AUTO, BYTE8_FAN_AUTO, 0x00, 0x00, 0x00, 0x04, 0x00};
|
||||
// The off command, including its checksum (255 - byte 3)
|
||||
static constexpr uint8_t OFF_FRAME[STATE_MESSAGE_LENGTH_UTIL] PROGMEM = {0x00, 0x08, 0x08, BYTE3_POWER_OFF,
|
||||
255 - BYTE3_POWER_OFF};
|
||||
|
||||
struct Code {
|
||||
uint8_t value; // a climate enum, all of which are uint8_t
|
||||
uint8_t code;
|
||||
};
|
||||
static constexpr Code MODES[] PROGMEM = {
|
||||
{climate::CLIMATE_MODE_HEAT_COOL, BYTE7_MODE_AUTO},
|
||||
{climate::CLIMATE_MODE_COOL, 0x80},
|
||||
{climate::CLIMATE_MODE_DRY, 0x40},
|
||||
{climate::CLIMATE_MODE_FAN_ONLY, 0xC0},
|
||||
{climate::CLIMATE_MODE_HEAT, 0x20},
|
||||
};
|
||||
static constexpr Code FANS[] PROGMEM = {
|
||||
{climate::CLIMATE_FAN_AUTO, BYTE8_FAN_AUTO}, {climate::CLIMATE_FAN_HIGH, 0x80},
|
||||
{climate::CLIMATE_FAN_MEDIUM, 0x40}, {climate::CLIMATE_FAN_LOW, 0xC0},
|
||||
{climate::CLIMATE_FAN_QUIET, 0x20},
|
||||
};
|
||||
|
||||
// The entry whose value (or code) matches key. Entries are copied out of PROGMEM.
|
||||
template<size_t N> static optional<Code> find_entry(const Code (&table)[N], uint8_t key, bool match_code) {
|
||||
for (const Code &slot : table) {
|
||||
Code entry;
|
||||
progmem_memcpy(&entry, &slot, sizeof(entry));
|
||||
if ((match_code ? entry.code : entry.value) == key)
|
||||
return entry;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// The temperature code is the 4-bit reversal of (F - TEMP_MIN) / TEMP_STEP; the mapping is its own inverse.
|
||||
static uint8_t reverse_nibble(uint8_t value) { return reverse_bits(value) >> 4; }
|
||||
|
||||
static bool matches_template(const uint8_t *data, uint8_t from, uint8_t to) {
|
||||
for (uint8_t i = from; i <= to; i++) {
|
||||
if (data[i] != progmem_read_byte(&STATE_TEMPLATE[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* https://gist.github.com/GeorgeDewar/11171561
|
||||
* 1. Reverse (flip) bytes 6 - 13)
|
||||
* 2. Sum those bytes
|
||||
* 3. (208 - sum) % 256
|
||||
* 4. Reverse (flip) bytes of result
|
||||
*/
|
||||
static uint8_t checksum_state(const uint8_t *data) {
|
||||
uint8_t sum = 0;
|
||||
for (uint8_t i = 6; i < STATE_MESSAGE_LENGTH - 1; ++i) {
|
||||
sum += reverse_bits(data[i]);
|
||||
}
|
||||
return reverse_bits(static_cast<uint8_t>(208 - sum));
|
||||
}
|
||||
|
||||
static const LogString *model_to_string(Model model) {
|
||||
switch (model) {
|
||||
case Model::MODEL_MW12Y3H:
|
||||
default:
|
||||
return LOG_STR("MW12Y3H");
|
||||
}
|
||||
}
|
||||
|
||||
climate::ClimateTraits FriedrichClimate::traits() {
|
||||
auto traits = ClimateIR::traits();
|
||||
traits.set_temperature_unit(TemperatureUnit::FAHRENHEIT);
|
||||
return traits;
|
||||
}
|
||||
|
||||
void FriedrichClimate::setup() {
|
||||
ClimateIR::setup();
|
||||
// The base class defaults assume Celsius. This also discards a value saved in Celsius by an earlier version.
|
||||
if (std::isnan(this->target_temperature) || this->target_temperature < TEMP_MIN) {
|
||||
this->target_temperature = TEMP_DEFAULT;
|
||||
}
|
||||
this->target_temperature = std::min<float>(this->target_temperature, TEMP_MAX);
|
||||
}
|
||||
|
||||
void FriedrichClimate::dump_config() {
|
||||
// ClimateIR::dump_config() is not called: it prints the temperature range in Celsius
|
||||
LOG_CLIMATE("", "Friedrich Climate", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Temperature range: %u to %u °F\n"
|
||||
" Model: %s",
|
||||
TEMP_MIN, TEMP_MAX, LOG_STR_ARG(model_to_string(this->model_)));
|
||||
}
|
||||
|
||||
void FriedrichClimate::transmit_state() {
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
this->transmit_off_();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Transmit state");
|
||||
|
||||
// Snap to what the unit accepts (even values, 64°F minimum unless heating) and store it,
|
||||
// so the state published after this call matches the frame sent.
|
||||
const uint8_t effective_min = this->mode == climate::CLIMATE_MODE_HEAT ? TEMP_MIN : TEMP_MIN_NOT_HEAT;
|
||||
uint8_t temperature = (uint8_t) roundf(clamp<float>(this->target_temperature, effective_min, TEMP_MAX));
|
||||
temperature += temperature % 2;
|
||||
this->target_temperature = temperature;
|
||||
|
||||
const auto mode = find_entry(MODES, this->mode, false);
|
||||
const auto fan = find_entry(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), false);
|
||||
|
||||
uint8_t frame[STATE_MESSAGE_LENGTH];
|
||||
progmem_memcpy(frame, STATE_TEMPLATE, sizeof(frame));
|
||||
frame[6] |= reverse_nibble((temperature - TEMP_MIN) / TEMP_STEP);
|
||||
frame[7] = mode.has_value() ? mode->code : BYTE7_MODE_AUTO;
|
||||
frame[8] = fan.has_value() ? fan->code : BYTE8_FAN_AUTO;
|
||||
if (this->swing_mode != climate::CLIMATE_SWING_OFF)
|
||||
frame[8] |= BYTE8_FAN_SWING;
|
||||
frame[STATE_MESSAGE_LENGTH - 1] = checksum_state(frame);
|
||||
|
||||
this->transmit_(frame, sizeof(frame));
|
||||
}
|
||||
|
||||
void FriedrichClimate::transmit_off_() {
|
||||
ESP_LOGV(TAG, "Transmit off");
|
||||
|
||||
uint8_t frame[STATE_MESSAGE_LENGTH_UTIL];
|
||||
progmem_memcpy(frame, OFF_FRAME, sizeof(frame));
|
||||
this->transmit_(frame, sizeof(frame));
|
||||
}
|
||||
|
||||
void FriedrichClimate::transmit_(const uint8_t *data, uint8_t len) {
|
||||
ESP_LOGV(TAG, "Transmit message");
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *dst = transmit.get_data();
|
||||
dst->set_carrier_frequency(CARRIER_FREQUENCY);
|
||||
remote_base::AEHAProtocol().encode(dst, CARRIER_ADDRESS, data, len);
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
bool FriedrichClimate::on_receive(remote_base::RemoteReceiveData src) {
|
||||
ESP_LOGV(TAG, "Received message");
|
||||
const auto odata = remote_base::AEHAProtocol().decode(src);
|
||||
if (!odata.has_value())
|
||||
return false;
|
||||
const auto &d = odata->data;
|
||||
// Other vendors also use AEHA; only accept frames with our address and fixed header bytes.
|
||||
if (odata->address != CARRIER_ADDRESS || d.size() < STATE_MESSAGE_LENGTH_UTIL || !matches_template(d.data(), 0, 2))
|
||||
return false;
|
||||
|
||||
if (d.size() == STATE_MESSAGE_LENGTH_UTIL) {
|
||||
// Not looking for other types of messages
|
||||
if (d[3] != BYTE3_POWER_OFF || d[4] != 255 - d[3])
|
||||
return false;
|
||||
ESP_LOGV(TAG, "Received off message");
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (d.size() != STATE_MESSAGE_LENGTH || d[STATE_MESSAGE_LENGTH - 1] != checksum_state(d.data()) ||
|
||||
!matches_template(d.data(), 3, 5))
|
||||
return false;
|
||||
|
||||
// Ignore frames with a mode this component does not expose (e.g. MIN_HEAT, COIL_DRY)
|
||||
const auto mode = find_entry(MODES, d[7], true);
|
||||
if (!mode.has_value()) {
|
||||
ESP_LOGV(TAG, "Ignoring unknown mode 0x%02X", d[7]);
|
||||
return false;
|
||||
}
|
||||
this->mode = static_cast<climate::ClimateMode>(mode->value);
|
||||
|
||||
// State frames always carry the power-on flag in bit 7 (power off is the separate 5-byte frame), so it is
|
||||
// stripped rather than acted on
|
||||
const uint8_t temp_code = d[6] & ~BYTE6_POWER_ON;
|
||||
const uint8_t temp_index = reverse_nibble(temp_code);
|
||||
if (temp_code <= 0x0F && temp_index <= (TEMP_MAX - TEMP_MIN) / TEMP_STEP)
|
||||
this->target_temperature = TEMP_MIN + temp_index * TEMP_STEP;
|
||||
|
||||
this->swing_mode = (d[8] & BYTE8_FAN_SWING) ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
|
||||
const auto fan = find_entry(FANS, d[8] & ~BYTE8_FAN_SWING, true);
|
||||
this->fan_mode = fan.has_value() ? static_cast<climate::ClimateFanMode>(fan->value) : climate::CLIMATE_FAN_AUTO;
|
||||
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::friedrich
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome::friedrich {
|
||||
|
||||
// Extension point for other units: add new models here and branch on model_ where they differ.
|
||||
// The default must stay MW12Y3H so existing configurations do not change.
|
||||
enum class Model : uint8_t {
|
||||
MODEL_MW12Y3H, // MW12Y3H built from a remote that only provided Fahrenheit
|
||||
};
|
||||
|
||||
class FriedrichClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
// The unit reports temperatures in Fahrenheit (see traits()); the unit only accepts even values.
|
||||
static constexpr uint8_t TEMP_MIN = 60; // actually 64 for anything but heating
|
||||
static constexpr uint8_t TEMP_MAX = 88;
|
||||
static constexpr uint8_t TEMP_STEP = 2;
|
||||
static constexpr uint8_t STATE_MESSAGE_LENGTH = 14;
|
||||
|
||||
FriedrichClimate()
|
||||
: ClimateIR(TEMP_MIN, TEMP_MAX, TEMP_STEP, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_QUIET},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}
|
||||
|
||||
void set_model(Model model) { this->model_ = model; }
|
||||
|
||||
protected:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
climate::ClimateTraits traits() override;
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
/// Transmit via IR power off command.
|
||||
void transmit_off_();
|
||||
|
||||
/// Parse incoming message
|
||||
bool on_receive(remote_base::RemoteReceiveData src) override;
|
||||
|
||||
/// Transmit message as IR pulses
|
||||
void transmit_(const uint8_t *data, uint8_t len);
|
||||
|
||||
Model model_{Model::MODEL_MW12Y3H};
|
||||
};
|
||||
|
||||
} // namespace esphome::friedrich
|
||||
@@ -24,9 +24,13 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
|
||||
return; \
|
||||
}
|
||||
|
||||
// Reset sequence (Goodix GT911 datasheet): INT held low while RST is released selects address 0x5D, then
|
||||
// T3 + T4 pass before INT is handed back to the chip as its interrupt output
|
||||
static constexpr uint32_t RESET_LOW_MS = 10;
|
||||
static constexpr uint32_t RESET_SETTLE_MS = 5 + 50 + 1;
|
||||
|
||||
void GT911Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
// temporarily set the interrupt pin to output to control address selection
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(false);
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
@@ -34,20 +38,19 @@ void GT911Touchscreen::setup() {
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
this->interrupt_pin_->digital_write(false);
|
||||
}
|
||||
delay(2);
|
||||
this->reset_pin_->digital_write(true);
|
||||
// wait at least T3+T4 ms as per the datasheet
|
||||
this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); });
|
||||
this->set_timeout(RESET_LOW_MS, [this] {
|
||||
this->reset_pin_->digital_write(true);
|
||||
this->set_timeout(RESET_SETTLE_MS, [this] { this->setup_internal_(); });
|
||||
});
|
||||
return;
|
||||
}
|
||||
this->setup_internal_();
|
||||
}
|
||||
|
||||
void GT911Touchscreen::setup_internal_() {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
if (this->interrupt_pin_->is_internal())
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
// Expander pins must leave output mode too, or the chip cannot drive its interrupt line
|
||||
if (this->interrupt_pin_ != nullptr)
|
||||
this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
|
||||
uint8_t data[4];
|
||||
i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||
@@ -72,7 +75,7 @@ void GT911Touchscreen::setup_internal_() {
|
||||
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s",
|
||||
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
|
||||
} else {
|
||||
// IO expander pin: leave as output for configuration only.
|
||||
// IO expander pin: no hardware interrupt, the touch state is polled.
|
||||
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s",
|
||||
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
|
||||
}
|
||||
|
||||
@@ -14,17 +14,8 @@ class GT911ButtonListener {
|
||||
|
||||
class GT911Touchscreen final : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
/// @brief Initialize the GT911 touchscreen.
|
||||
///
|
||||
/// If @ref reset_pin_ is set, the touchscreen will be hardware reset,
|
||||
/// and the rest of the setup will be scheduled to run 50ms later using @ref set_timeout()
|
||||
/// to allow the device to stabilize after reset.
|
||||
///
|
||||
/// If @ref interrupt_pin_ is set, it will be temporarily configured during reset
|
||||
/// to control I2C address selection.
|
||||
///
|
||||
/// After the timeout, or immediately if no reset is performed, @ref setup_internal_()
|
||||
/// is called to complete the initialization.
|
||||
/// With a reset pin the chip is reset with the interrupt pin held low, which selects address 0x5D, and the
|
||||
/// rest of the setup runs from timeouts once the chip is ready. Without one setup_internal_() runs at once.
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
bool can_proceed() override { return this->setup_done_; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.components.const import CONF_SERIAL_NUMBER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP
|
||||
from esphome.types import ConfigType
|
||||
@@ -9,7 +10,6 @@ from .. import CONF_HOERMANN_HCP_ID, HoermannHcp, hoermann_hcp_ns
|
||||
DEPENDENCIES = ["hoermann_hcp"]
|
||||
|
||||
CONF_DOOR_STATE = "door_state"
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
|
||||
HoermannHcpDoorStateTextSensor = hoermann_hcp_ns.class_(
|
||||
"HoermannHcpDoorStateTextSensor", text_sensor.TextSensor, cg.Component
|
||||
|
||||
@@ -13,6 +13,24 @@ static const uint8_t INA3221_REGISTER_CHANNEL2_SHUNT_VOLTAGE = 0x03;
|
||||
static const uint8_t INA3221_REGISTER_CHANNEL2_BUS_VOLTAGE = 0x04;
|
||||
static const uint8_t INA3221_REGISTER_CHANNEL3_SHUNT_VOLTAGE = 0x05;
|
||||
static const uint8_t INA3221_REGISTER_CHANNEL3_BUS_VOLTAGE = 0x06;
|
||||
#ifdef USE_INA3221_ALERT_LIMITS
|
||||
static constexpr uint8_t INA3221_REGISTER_CHANNEL1_CRITICAL_ALERT = 0x07;
|
||||
static constexpr uint8_t INA3221_REGISTER_CHANNEL1_WARNING_ALERT = 0x0A;
|
||||
#endif
|
||||
#ifdef USE_INA3221_SUMMATION
|
||||
static constexpr uint8_t INA3221_REGISTER_SHUNT_VOLTAGE_SUM = 0x0D;
|
||||
static constexpr uint8_t INA3221_REGISTER_MASK_ENABLE = 0x0F;
|
||||
#endif
|
||||
|
||||
// Configuration register: bit 15 resets the chip, bits 14..12 enable channels 1..3 (the same bit
|
||||
// positions select channels for summation in the mask/enable register) and bits 2..0 set the mode.
|
||||
// Code generation fills in the averaging and conversion time fields.
|
||||
static constexpr uint16_t INA3221_CONFIG_RESET = 0x8000;
|
||||
static constexpr uint16_t INA3221_CONFIG_CHANNEL1_ENABLE = 0x4000;
|
||||
static constexpr uint16_t INA3221_CONFIG_CHANNEL_MASK = 0x7000;
|
||||
static constexpr uint16_t INA3221_CONFIG_MODE_MASK = 0x0007;
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
// Addresses:
|
||||
// A0 = GND -> 0x40
|
||||
@@ -21,37 +39,38 @@ static const uint8_t INA3221_REGISTER_CHANNEL3_BUS_VOLTAGE = 0x06;
|
||||
// A0 = SCL -> 0x43
|
||||
|
||||
void INA3221Component::setup() {
|
||||
// Config Register
|
||||
// 0bx000000000000000 << 15 RESET Bit (1 -> trigger reset)
|
||||
if (!this->write_byte_16(INA3221_REGISTER_CONFIG, 0x8000)) {
|
||||
if (!this->write_byte_16(INA3221_REGISTER_CONFIG, INA3221_CONFIG_RESET)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(1);
|
||||
|
||||
uint16_t config = 0;
|
||||
// 0b0xxx000000000000 << 12 Channel Enables (1 -> ON)
|
||||
if (this->channels_[0].exists()) {
|
||||
config |= 0b0100000000000000;
|
||||
}
|
||||
if (this->channels_[1].exists()) {
|
||||
config |= 0b0010000000000000;
|
||||
}
|
||||
if (this->channels_[2].exists()) {
|
||||
config |= 0b0001000000000000;
|
||||
}
|
||||
// 0b0000xxx000000000 << 9 Averaging Mode (0 -> 1 sample, 111 -> 1024 samples)
|
||||
config |= 0b0000000000000000;
|
||||
// 0b0000000xxx000000 << 6 Bus Voltage Conversion time (100 -> 1.1ms, 111 -> 8.244 ms)
|
||||
config |= 0b0000000111000000;
|
||||
// 0b0000000000xxx000 << 3 Shunt Voltage Conversion time (same as above)
|
||||
config |= 0b0000000000111000;
|
||||
// 0b0000000000000xxx << 0 Operating mode (111 -> Shunt and bus, continuous)
|
||||
config |= 0b0000000000000111;
|
||||
if (!this->write_byte_16(INA3221_REGISTER_CONFIG, config)) {
|
||||
if (!this->write_byte_16(INA3221_REGISTER_CONFIG, this->config_)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_INA3221_ALERT_LIMITS
|
||||
for (uint8_t i = 0; i < 3; i++) {
|
||||
const INA3221Channel &channel = this->channels_[i];
|
||||
if ((channel.critical_limit_ != 0 &&
|
||||
!this->write_byte_16(INA3221_REGISTER_CHANNEL1_CRITICAL_ALERT + i, channel.critical_limit_)) ||
|
||||
(channel.warning_limit_ != 0 &&
|
||||
!this->write_byte_16(INA3221_REGISTER_CHANNEL1_WARNING_ALERT + i, channel.warning_limit_))) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_INA3221_SUMMATION
|
||||
// Sum the shunt voltages of every enabled channel
|
||||
if (this->sum_shunt_voltage_sensor_ != nullptr &&
|
||||
!this->write_byte_16(INA3221_REGISTER_MASK_ENABLE, this->config_ & INA3221_CONFIG_CHANNEL_MASK)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void INA3221Component::dump_config() {
|
||||
@@ -61,6 +80,10 @@ void INA3221Component::dump_config() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG, " Config register: 0x%04X", this->config_);
|
||||
if (this->single_shot_wait_ms_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Single-shot conversion: %u ms", this->single_shot_wait_ms_);
|
||||
}
|
||||
|
||||
LOG_SENSOR(" ", "Bus Voltage #1", this->channels_[0].bus_voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Shunt Voltage #1", this->channels_[0].shunt_voltage_sensor_);
|
||||
@@ -74,6 +97,11 @@ void INA3221Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Shunt Voltage #3", this->channels_[2].shunt_voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current #3", this->channels_[2].current_sensor_);
|
||||
LOG_SENSOR(" ", "Power #3", this->channels_[2].power_sensor_);
|
||||
#ifdef USE_INA3221_SUMMATION
|
||||
LOG_SENSOR(" ", "Sum Shunt Voltage", this->sum_shunt_voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Sum Current", this->sum_current_sensor_);
|
||||
LOG_SENSOR(" ", "Sum Power", this->sum_power_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
inline uint8_t ina3221_bus_voltage_register(int channel) { return 0x02 + channel * 2; }
|
||||
@@ -81,11 +109,36 @@ inline uint8_t ina3221_bus_voltage_register(int channel) { return 0x02 + channel
|
||||
inline uint8_t ina3221_shunt_voltage_register(int channel) { return 0x01 + channel * 2; }
|
||||
|
||||
void INA3221Component::update() {
|
||||
if (this->single_shot_wait_ms_ == 0) {
|
||||
this->read_data_();
|
||||
return;
|
||||
}
|
||||
// In single-shot mode, writing the configuration register starts one conversion of every channel
|
||||
if (!this->write_byte_16(INA3221_REGISTER_CONFIG, this->config_)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
this->set_timeout(READ_TIMEOUT_ID, this->single_shot_wait_ms_, [this]() { this->read_data_(); });
|
||||
}
|
||||
|
||||
void INA3221Component::read_data_() {
|
||||
#ifdef USE_INA3221_SUMMATION
|
||||
// The software sums need every enabled channel's readings, not only the ones with their own sensors
|
||||
const bool sum_all = this->sum_current_sensor_ != nullptr || this->sum_power_sensor_ != nullptr;
|
||||
float total_current_a = 0.0f;
|
||||
float total_power_w = 0.0f;
|
||||
#else
|
||||
constexpr bool sum_all = false;
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if ((this->config_ & (INA3221_CONFIG_CHANNEL1_ENABLE >> i)) == 0) {
|
||||
continue;
|
||||
}
|
||||
INA3221Channel &channel = this->channels_[i];
|
||||
float bus_voltage_v = NAN, current_a = NAN;
|
||||
uint16_t raw;
|
||||
if (channel.should_measure_bus_voltage()) {
|
||||
if (sum_all || channel.should_measure_bus_voltage()) {
|
||||
if (!this->read_byte_16(ina3221_bus_voltage_register(i), &raw)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
@@ -94,7 +147,7 @@ void INA3221Component::update() {
|
||||
if (channel.bus_voltage_sensor_ != nullptr)
|
||||
channel.bus_voltage_sensor_->publish_state(bus_voltage_v);
|
||||
}
|
||||
if (channel.should_measure_shunt_voltage()) {
|
||||
if (sum_all || channel.should_measure_shunt_voltage()) {
|
||||
if (!this->read_byte_16(ina3221_shunt_voltage_register(i), &raw)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
@@ -109,17 +162,42 @@ void INA3221Component::update() {
|
||||
if (channel.power_sensor_ != nullptr) {
|
||||
channel.power_sensor_->publish_state(bus_voltage_v * current_a);
|
||||
}
|
||||
#ifdef USE_INA3221_SUMMATION
|
||||
if (sum_all) {
|
||||
total_current_a += current_a;
|
||||
total_power_w += bus_voltage_v * current_a;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_INA3221_SUMMATION
|
||||
if (this->sum_shunt_voltage_sensor_ != nullptr) {
|
||||
uint16_t raw;
|
||||
if (!this->read_byte_16(INA3221_REGISTER_SHUNT_VOLTAGE_SUM, &raw)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// 40 uV per step, held in bits 15..1
|
||||
this->sum_shunt_voltage_sensor_->publish_state(int16_t(raw) * 40.0f / 2.0f / 1000000.0f);
|
||||
}
|
||||
if (this->sum_current_sensor_ != nullptr) {
|
||||
this->sum_current_sensor_->publish_state(total_current_a);
|
||||
}
|
||||
if (this->sum_power_sensor_ != nullptr) {
|
||||
this->sum_power_sensor_->publish_state(total_power_w);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void INA3221Component::on_powerdown() {
|
||||
// Mode 0 powers the chip down; setup() resets it on the next boot
|
||||
this->write_byte_16(INA3221_REGISTER_CONFIG, static_cast<uint16_t>(this->config_ & ~INA3221_CONFIG_MODE_MASK));
|
||||
}
|
||||
|
||||
void INA3221Component::set_shunt_resistance(int channel, float resistance_ohm) {
|
||||
this->channels_[channel].shunt_resistance_ = resistance_ohm;
|
||||
}
|
||||
|
||||
bool INA3221Component::INA3221Channel::exists() {
|
||||
return this->bus_voltage_sensor_ != nullptr || this->shunt_voltage_sensor_ != nullptr ||
|
||||
this->current_sensor_ != nullptr || this->power_sensor_ != nullptr;
|
||||
}
|
||||
bool INA3221Component::INA3221Channel::should_measure_shunt_voltage() {
|
||||
return this->shunt_voltage_sensor_ != nullptr || this->current_sensor_ != nullptr || this->power_sensor_ != nullptr;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,17 @@
|
||||
namespace esphome::ina3221 {
|
||||
|
||||
class INA3221Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
#ifdef USE_INA3221_SUMMATION
|
||||
SUB_SENSOR(sum_shunt_voltage)
|
||||
SUB_SENSOR(sum_current)
|
||||
SUB_SENSOR(sum_power)
|
||||
#endif
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void on_powerdown() override;
|
||||
|
||||
void set_bus_voltage_sensor(int channel, sensor::Sensor *obj) { this->channels_[channel].bus_voltage_sensor_ = obj; }
|
||||
void set_shunt_voltage_sensor(int channel, sensor::Sensor *obj) {
|
||||
@@ -19,19 +26,35 @@ class INA3221Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_current_sensor(int channel, sensor::Sensor *obj) { this->channels_[channel].current_sensor_ = obj; }
|
||||
void set_power_sensor(int channel, sensor::Sensor *obj) { this->channels_[channel].power_sensor_ = obj; }
|
||||
void set_shunt_resistance(int channel, float resistance_ohm);
|
||||
/// Configuration register value built by code generation: channel enables, averaging, conversion times, mode
|
||||
void set_config_register(uint16_t config) { this->config_ = config; }
|
||||
/// Time one single-shot conversion of every enabled channel takes; zero means continuous mode
|
||||
void set_single_shot_wait_ms(uint16_t wait_ms) { this->single_shot_wait_ms_ = wait_ms; }
|
||||
#ifdef USE_INA3221_ALERT_LIMITS
|
||||
void set_warning_limit(int channel, uint16_t reg) { this->channels_[channel].warning_limit_ = reg; }
|
||||
void set_critical_limit(int channel, uint16_t reg) { this->channels_[channel].critical_limit_ = reg; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void read_data_();
|
||||
|
||||
struct INA3221Channel {
|
||||
float shunt_resistance_{0.1f};
|
||||
sensor::Sensor *bus_voltage_sensor_{nullptr};
|
||||
sensor::Sensor *shunt_voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
sensor::Sensor *power_sensor_{nullptr};
|
||||
#ifdef USE_INA3221_ALERT_LIMITS
|
||||
// Alert limit register values; zero keeps the chip default, which never trips
|
||||
uint16_t warning_limit_{0};
|
||||
uint16_t critical_limit_{0};
|
||||
#endif
|
||||
|
||||
bool exists();
|
||||
bool should_measure_shunt_voltage();
|
||||
bool should_measure_bus_voltage();
|
||||
} channels_[3];
|
||||
uint16_t config_{0};
|
||||
uint16_t single_shot_wait_ms_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::ina3221
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import CONF_ADC_AVERAGING, CONF_ADC_TIME
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUS_VOLTAGE,
|
||||
@@ -8,6 +11,7 @@ from esphome.const import (
|
||||
CONF_POWER,
|
||||
CONF_SHUNT_RESISTANCE,
|
||||
CONF_SHUNT_VOLTAGE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
@@ -23,69 +27,195 @@ DEPENDENCIES = ["i2c"]
|
||||
CONF_CHANNEL_1 = "channel_1"
|
||||
CONF_CHANNEL_2 = "channel_2"
|
||||
CONF_CHANNEL_3 = "channel_3"
|
||||
CONF_CONTINUOUS_MODE = "continuous_mode"
|
||||
CONF_WARNING_CURRENT_LIMIT = "warning_current_limit"
|
||||
CONF_CRITICAL_CURRENT_LIMIT = "critical_current_limit"
|
||||
CONF_SUM_SHUNT_VOLTAGE = "sum_shunt_voltage"
|
||||
CONF_SUM_CURRENT = "sum_current"
|
||||
CONF_SUM_POWER = "sum_power"
|
||||
|
||||
CHANNELS = (CONF_CHANNEL_1, CONF_CHANNEL_2, CONF_CHANNEL_3)
|
||||
|
||||
# Configuration register layout: channel enables in bits 14..12, averaging in 11..9,
|
||||
# bus conversion time in 8..6, shunt conversion time in 5..3, operating mode in 2..0
|
||||
CHANNEL_ENABLE_BITS = (1 << 14, 1 << 13, 1 << 12)
|
||||
ADC_AVERAGING_SHIFT = 9
|
||||
ADC_TIME_BUS_SHIFT = 6
|
||||
ADC_TIME_SHUNT_SHIFT = 3
|
||||
MODE_SINGLE_SHOT = 0b011
|
||||
MODE_CONTINUOUS = 0b111
|
||||
|
||||
# Register field value by sample count and by conversion time in microseconds
|
||||
ADC_AVERAGING = {1: 0, 4: 1, 16: 2, 64: 3, 128: 4, 256: 5, 512: 6, 1024: 7}
|
||||
ADC_TIMES_US = {140: 0, 204: 1, 332: 2, 588: 3, 1100: 4, 2116: 5, 4156: 6, 8244: 7}
|
||||
|
||||
# Alert limits are shunt voltages in 40 uV steps, held in bits 15..3
|
||||
ALERT_LIMIT_LSB_V = 40e-6
|
||||
ALERT_LIMIT_MAX_V = 0x0FFF * ALERT_LIMIT_LSB_V
|
||||
# Headroom on top of the computed conversion time before the results are read
|
||||
SINGLE_SHOT_MARGIN_MS = 5
|
||||
|
||||
ina3221_ns = cg.esphome_ns.namespace("ina3221")
|
||||
INA3221Component = ina3221_ns.class_(
|
||||
"INA3221Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
INA3221_CHANNEL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(
|
||||
cv.resistance, cv.Range(min=0.0, max=32.0)
|
||||
),
|
||||
}
|
||||
VOLTAGE_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
CURRENT_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
POWER_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
CURRENT_LIMIT_SCHEMA = cv.All(cv.current, cv.Range(min=0.0, min_included=False))
|
||||
|
||||
|
||||
def validate_adc_time(value: Any) -> int:
|
||||
value = cv.positive_time_period_microseconds(value).total_microseconds
|
||||
return cv.one_of(*ADC_TIMES_US, int=True)(value)
|
||||
|
||||
|
||||
def validate_alert_limits(config: ConfigType) -> ConfigType:
|
||||
shunt = config[CONF_SHUNT_RESISTANCE]
|
||||
for key in (CONF_WARNING_CURRENT_LIMIT, CONF_CRITICAL_CURRENT_LIMIT):
|
||||
if (limit := config.get(key)) is None:
|
||||
continue
|
||||
if limit * shunt > ALERT_LIMIT_MAX_V:
|
||||
raise cv.Invalid(
|
||||
f"{key} times shunt_resistance exceeds the "
|
||||
f"{ALERT_LIMIT_MAX_V * 1000:.1f} mV alert range",
|
||||
path=[key],
|
||||
)
|
||||
if alert_limit_register(limit, shunt) == 0:
|
||||
raise cv.Invalid(
|
||||
f"{key} times shunt_resistance is below the "
|
||||
f"{ALERT_LIMIT_LSB_V * 1e6:.0f} µV alert resolution",
|
||||
path=[key],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
INA3221_CHANNEL_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_BUS_VOLTAGE): VOLTAGE_SCHEMA,
|
||||
cv.Optional(CONF_SHUNT_VOLTAGE): VOLTAGE_SCHEMA,
|
||||
cv.Optional(CONF_CURRENT): CURRENT_SCHEMA,
|
||||
cv.Optional(CONF_POWER): POWER_SCHEMA,
|
||||
cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(
|
||||
cv.resistance, cv.Range(min=0.0, max=32.0)
|
||||
),
|
||||
cv.Optional(CONF_WARNING_CURRENT_LIMIT): CURRENT_LIMIT_SCHEMA,
|
||||
cv.Optional(CONF_CRITICAL_CURRENT_LIMIT): CURRENT_LIMIT_SCHEMA,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(
|
||||
CONF_BUS_VOLTAGE,
|
||||
CONF_SHUNT_VOLTAGE,
|
||||
CONF_CURRENT,
|
||||
CONF_POWER,
|
||||
CONF_WARNING_CURRENT_LIMIT,
|
||||
CONF_CRITICAL_CURRENT_LIMIT,
|
||||
),
|
||||
validate_alert_limits,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
def adc_times_us(config: ConfigType) -> tuple[int, int]:
|
||||
"""Return the bus and shunt conversion times in microseconds."""
|
||||
adc_time = config[CONF_ADC_TIME]
|
||||
if isinstance(adc_time, dict):
|
||||
return adc_time[CONF_BUS_VOLTAGE], adc_time[CONF_SHUNT_VOLTAGE]
|
||||
return adc_time, adc_time
|
||||
|
||||
|
||||
def single_shot_wait_ms(config: ConfigType) -> int:
|
||||
"""Time one conversion of every enabled channel takes, plus headroom."""
|
||||
bus_us, shunt_us = adc_times_us(config)
|
||||
channels = sum(1 for channel in CHANNELS if channel in config)
|
||||
total_us = (bus_us + shunt_us) * config[CONF_ADC_AVERAGING] * channels
|
||||
return -(-total_us // 1000) + SINGLE_SHOT_MARGIN_MS
|
||||
|
||||
|
||||
def validate_single_shot_interval(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_CONTINUOUS_MODE]:
|
||||
return config
|
||||
wait_ms = single_shot_wait_ms(config)
|
||||
if config[CONF_UPDATE_INTERVAL].total_milliseconds <= wait_ms:
|
||||
raise cv.Invalid(
|
||||
f"A single-shot conversion of all channels takes {wait_ms} ms, which does "
|
||||
f"not fit in the update_interval; lower {CONF_ADC_AVERAGING} or "
|
||||
f"{CONF_ADC_TIME}, or raise the interval",
|
||||
path=[CONF_UPDATE_INTERVAL],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(INA3221Component),
|
||||
cv.Optional(CONF_CHANNEL_1): INA3221_CHANNEL_SCHEMA,
|
||||
cv.Optional(CONF_CHANNEL_2): INA3221_CHANNEL_SCHEMA,
|
||||
cv.Optional(CONF_CHANNEL_3): INA3221_CHANNEL_SCHEMA,
|
||||
cv.Optional(CONF_CONTINUOUS_MODE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADC_AVERAGING, default=1): cv.one_of(
|
||||
*ADC_AVERAGING, int=True
|
||||
),
|
||||
cv.Optional(CONF_ADC_TIME, default="8244us"): cv.Any(
|
||||
validate_adc_time,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_BUS_VOLTAGE): validate_adc_time,
|
||||
cv.Required(CONF_SHUNT_VOLTAGE): validate_adc_time,
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_SUM_SHUNT_VOLTAGE): VOLTAGE_SCHEMA,
|
||||
cv.Optional(CONF_SUM_CURRENT): CURRENT_SCHEMA,
|
||||
cv.Optional(CONF_SUM_POWER): POWER_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x40))
|
||||
.extend(i2c.i2c_device_schema(0x40)),
|
||||
cv.has_at_least_one_key(*CHANNELS),
|
||||
validate_single_shot_interval,
|
||||
)
|
||||
|
||||
|
||||
def alert_limit_register(current_a: float, shunt_ohm: float) -> int:
|
||||
return round(current_a * shunt_ohm / ALERT_LIMIT_LSB_V) << 3
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
for i, channel in enumerate([CONF_CHANNEL_1, CONF_CHANNEL_2, CONF_CHANNEL_3]):
|
||||
if channel not in config:
|
||||
bus_us, shunt_us = adc_times_us(config)
|
||||
register = (
|
||||
ADC_AVERAGING[config[CONF_ADC_AVERAGING]] << ADC_AVERAGING_SHIFT
|
||||
| ADC_TIMES_US[bus_us] << ADC_TIME_BUS_SHIFT
|
||||
| ADC_TIMES_US[shunt_us] << ADC_TIME_SHUNT_SHIFT
|
||||
| (MODE_CONTINUOUS if config[CONF_CONTINUOUS_MODE] else MODE_SINGLE_SHOT)
|
||||
)
|
||||
for i, channel in enumerate(CHANNELS):
|
||||
if (conf := config.get(channel)) is None:
|
||||
continue
|
||||
conf = config[channel]
|
||||
if CONF_SHUNT_RESISTANCE in conf:
|
||||
cg.add(var.set_shunt_resistance(i, conf[CONF_SHUNT_RESISTANCE]))
|
||||
register |= CHANNEL_ENABLE_BITS[i]
|
||||
shunt = conf[CONF_SHUNT_RESISTANCE]
|
||||
cg.add(var.set_shunt_resistance(i, shunt))
|
||||
if CONF_BUS_VOLTAGE in conf:
|
||||
sens = await sensor.new_sensor(conf[CONF_BUS_VOLTAGE])
|
||||
cg.add(var.set_bus_voltage_sensor(i, sens))
|
||||
@@ -98,3 +228,23 @@ async def to_code(config: ConfigType) -> None:
|
||||
if CONF_POWER in conf:
|
||||
sens = await sensor.new_sensor(conf[CONF_POWER])
|
||||
cg.add(var.set_power_sensor(i, sens))
|
||||
for key, setter in (
|
||||
(CONF_WARNING_CURRENT_LIMIT, var.set_warning_limit),
|
||||
(CONF_CRITICAL_CURRENT_LIMIT, var.set_critical_limit),
|
||||
):
|
||||
if (limit := conf.get(key)) is not None:
|
||||
cg.add_define("USE_INA3221_ALERT_LIMITS")
|
||||
cg.add(setter(i, alert_limit_register(limit, shunt)))
|
||||
cg.add(var.set_config_register(register))
|
||||
if not config[CONF_CONTINUOUS_MODE]:
|
||||
cg.add(var.set_single_shot_wait_ms(single_shot_wait_ms(config)))
|
||||
|
||||
if any(
|
||||
key in config
|
||||
for key in (CONF_SUM_SHUNT_VOLTAGE, CONF_SUM_CURRENT, CONF_SUM_POWER)
|
||||
):
|
||||
cg.add_define("USE_INA3221_SUMMATION")
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_SUM_SHUNT_VOLTAGE, var.set_sum_shunt_voltage_sensor)
|
||||
await sensors(CONF_SUM_CURRENT, var.set_sum_current_sensor)
|
||||
await sensors(CONF_SUM_POWER, var.set_sum_power_sensor)
|
||||
|
||||
@@ -4,6 +4,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["ld24xx"]
|
||||
CODEOWNERS = ["@NovakIrs"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "ld2410s"
|
||||
@@ -13,6 +14,7 @@ CONF_LD2410S_ID = "ld2410s_id"
|
||||
|
||||
ld2410s_ns = cg.esphome_ns.namespace(DOMAIN)
|
||||
LD2410SComponent = ld2410s_ns.class_("LD2410SComponent", cg.Component, uart.UARTDevice)
|
||||
Parameter = ld2410s_ns.enum("Parameter", is_class=True)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ld2410s.h"
|
||||
#include "ld2410s_number.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "esphome/core/application.h"
|
||||
@@ -25,18 +26,46 @@ static constexpr uint16_t CMD_CONFIRMATION = 0x0100; // set in the command word
|
||||
static constexpr uint16_t CONFIG_MODE_START_CMD = 0x00FF;
|
||||
static constexpr uint16_t CONFIG_MODE_START_VALUE = 0x0001;
|
||||
static constexpr uint16_t CONFIG_MODE_END_CMD = 0x00FE;
|
||||
static constexpr uint16_t VERSION_READ_CMD = 0x0000;
|
||||
static constexpr uint16_t PARAMETERS_WRITE_CMD = 0x0070;
|
||||
static constexpr uint16_t PARAMETERS_READ_CMD = 0x0071;
|
||||
static constexpr uint16_t OUTPUT_MODE_SWITCH_CMD = 0x007A;
|
||||
// Minimal output mode (short frames carrying presence and distance only) is selected by six zero bytes
|
||||
static constexpr uint8_t OUTPUT_MODE_MINIMAL_SIZE = 6;
|
||||
static constexpr uint8_t CMD_FRAME_MAX_SIZE =
|
||||
LONG_PAYLOAD_POS + 2 + OUTPUT_MODE_MINIMAL_SIZE + sizeof(CMD_FRAME_FOOTER);
|
||||
// The parameter write carries a 16-bit key and a 32-bit value per parameter, the read only the keys
|
||||
static constexpr uint8_t PARAMETER_KEY_SIZE = 2;
|
||||
static constexpr uint8_t PARAMETER_VALUE_SIZE = 4;
|
||||
static constexpr uint8_t PARAMETERS_WRITE_SIZE = PARAMETER_COUNT * (PARAMETER_KEY_SIZE + PARAMETER_VALUE_SIZE);
|
||||
static constexpr uint8_t CMD_FRAME_MAX_SIZE = LONG_PAYLOAD_POS + 2 + PARAMETERS_WRITE_SIZE + sizeof(CMD_FRAME_FOOTER);
|
||||
// Keys of the parameters, in Parameter order
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
static constexpr uint16_t PARAMETER_KEYS[] PROGMEM = {0x0005, 0x000A, 0x0006, 0x0002, 0x000C, 0x000B};
|
||||
static_assert(sizeof(PARAMETER_KEYS) / sizeof(PARAMETER_KEYS[0]) == PARAMETER_COUNT, "one key per parameter");
|
||||
#endif
|
||||
// The version answer: 4 bytes device type, then major, minor and patch as 16-bit words
|
||||
static constexpr uint8_t VERSION_TYPE_SIZE = 4;
|
||||
static constexpr uint8_t VERSION_SIZE = VERSION_TYPE_SIZE + 3 * 2;
|
||||
|
||||
// Everything the module needs at start: enter configuration mode, pick the output mode, leave it
|
||||
static constexpr uint16_t INIT_SEQUENCE[] PROGMEM = {CONFIG_MODE_START_CMD, OUTPUT_MODE_SWITCH_CMD,
|
||||
CONFIG_MODE_END_CMD};
|
||||
// Everything the module needs at start: configuration mode on, read the version and parameters when
|
||||
// an entity shows them, pick the output mode, configuration mode off
|
||||
static constexpr uint16_t INIT_SEQUENCE[] PROGMEM = {
|
||||
CONFIG_MODE_START_CMD,
|
||||
#ifdef USE_LD2410S_VERSION
|
||||
VERSION_READ_CMD,
|
||||
#endif
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
PARAMETERS_READ_CMD,
|
||||
#endif
|
||||
OUTPUT_MODE_SWITCH_CMD, CONFIG_MODE_END_CMD,
|
||||
};
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
static constexpr uint16_t WRITE_SEQUENCE[] PROGMEM = {CONFIG_MODE_START_CMD, PARAMETERS_WRITE_CMD, CONFIG_MODE_END_CMD};
|
||||
#endif
|
||||
static constexpr uint8_t INIT_SEQUENCE_LENGTH = sizeof(INIT_SEQUENCE) / sizeof(INIT_SEQUENCE[0]);
|
||||
static uint16_t init_command(uint8_t step) { return progmem_read_uint16(&INIT_SEQUENCE[step]); }
|
||||
static constexpr uint8_t INIT_MAX_TIMEOUTS = 2;
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
static constexpr uint8_t WRITE_SEQUENCE_LENGTH = sizeof(WRITE_SEQUENCE) / sizeof(WRITE_SEQUENCE[0]);
|
||||
#endif
|
||||
static constexpr uint8_t STEP_MAX_TIMEOUTS = 2;
|
||||
static constexpr uint32_t ACK_TIMEOUT_MS = 300;
|
||||
static constexpr uint32_t COMMAND_GAP_MS = 300; // pause after an acknowledgement before the next command
|
||||
static constexpr uint32_t REINIT_PAUSE_MS = 15000; // pause before starting over after the module stopped answering
|
||||
@@ -49,6 +78,20 @@ void LD2410SComponent::dump_config() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Has Target", this->target_binary_sensor_);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR_WITH_DEDUP_SAFE(" ", "Distance", this->distance_sensor_);
|
||||
#endif
|
||||
#ifdef USE_LD2410S_VERSION
|
||||
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
|
||||
#endif
|
||||
#if defined(USE_NUMBER) && defined(USE_LD2410S_PARAMETERS)
|
||||
for (auto *number : this->numbers_) {
|
||||
LOG_NUMBER(" ", "Setting", number);
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_SELECT) && defined(USE_LD2410S_PARAMETERS)
|
||||
LOG_SELECT(" ", "Response Speed", this->response_speed_select_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void LD2410SComponent::loop() {
|
||||
@@ -67,35 +110,80 @@ void LD2410SComponent::loop() {
|
||||
avail -= len;
|
||||
}
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->run_init_sequence_(now);
|
||||
if (this->init_step_ >= INIT_SEQUENCE_LENGTH && now - this->last_frame_at_ > LINK_TIMEOUT_MS) {
|
||||
if (this->sequence_ == nullptr) {
|
||||
if (!this->init_done_) {
|
||||
this->start_sequence_(INIT_SEQUENCE, INIT_SEQUENCE_LENGTH, now);
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
} else if (this->parameters_dirty_) {
|
||||
this->parameters_dirty_ = false;
|
||||
this->start_sequence_(WRITE_SEQUENCE, WRITE_SEQUENCE_LENGTH, this->next_send_at_);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
this->run_sequence_(now);
|
||||
if (this->init_done_ && now - this->last_frame_at_ > LINK_TIMEOUT_MS) {
|
||||
// The count tells real silence apart from frames that never parse (wrong baud rate or firmware)
|
||||
ESP_LOGW(TAG, "No data for %" PRIu32 " s (%u frames dropped), starting over", LINK_TIMEOUT_MS / 1000,
|
||||
this->dropped_frames_);
|
||||
this->dropped_frames_ = 0;
|
||||
this->status_set_warning();
|
||||
this->init_step_ = 0;
|
||||
this->init_timeouts_ = 0;
|
||||
this->awaiting_ack_ = false;
|
||||
this->next_send_at_ = now;
|
||||
this->restart_init_(now);
|
||||
}
|
||||
}
|
||||
|
||||
void LD2410SComponent::run_init_sequence_(uint32_t now) {
|
||||
if (this->init_step_ >= INIT_SEQUENCE_LENGTH || static_cast<int32_t>(now - this->next_send_at_) < 0) {
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
bool LD2410SComponent::set_parameter(Parameter parameter, uint8_t value) {
|
||||
// All six are written together, so the other five must be the module's own values
|
||||
if (!this->init_done_) {
|
||||
ESP_LOGW(TAG, "Parameters not read yet, change ignored");
|
||||
return false;
|
||||
}
|
||||
if (!this->parameters_known_) {
|
||||
ESP_LOGW(TAG, "The module reported a parameter out of range, changes are disabled");
|
||||
return false;
|
||||
}
|
||||
this->parameters_[static_cast<uint8_t>(parameter)] = value;
|
||||
this->parameters_dirty_ = true; // written by loop() once no sequence is running
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void LD2410SComponent::restart_init_(uint32_t at) {
|
||||
this->init_done_ = false;
|
||||
this->status_set_warning(); // cleared again when the new init sequence is acknowledged
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
// The module's own values count again once it is set up anew
|
||||
if (this->parameters_dirty_) {
|
||||
ESP_LOGW(TAG, "Dropping the pending parameter change, the module's values are read back");
|
||||
}
|
||||
this->parameters_dirty_ = false;
|
||||
this->parameters_known_ = false; // a refused value may sit in parameters_ until the read replaces it
|
||||
#endif
|
||||
this->start_sequence_(INIT_SEQUENCE, INIT_SEQUENCE_LENGTH, at);
|
||||
}
|
||||
|
||||
void LD2410SComponent::start_sequence_(const uint16_t *sequence, uint8_t length, uint32_t now) {
|
||||
this->sequence_ = sequence;
|
||||
this->sequence_len_ = length;
|
||||
this->step_ = 0;
|
||||
this->step_timeouts_ = 0;
|
||||
this->awaiting_ack_ = false;
|
||||
this->next_send_at_ = now;
|
||||
}
|
||||
|
||||
uint16_t LD2410SComponent::current_command_() const { return progmem_read_uint16(&this->sequence_[this->step_]); }
|
||||
|
||||
void LD2410SComponent::run_sequence_(uint32_t now) {
|
||||
if (this->sequence_ == nullptr || static_cast<int32_t>(now - this->next_send_at_) < 0) {
|
||||
return;
|
||||
}
|
||||
const uint16_t command = init_command(this->init_step_);
|
||||
const uint16_t command = this->current_command_();
|
||||
if (this->awaiting_ack_) {
|
||||
if (++this->init_timeouts_ > INIT_MAX_TIMEOUTS) {
|
||||
if (++this->step_timeouts_ > STEP_MAX_TIMEOUTS) {
|
||||
ESP_LOGW(TAG, "No acknowledgement for command %04X, starting over in %" PRIu32 " s", command,
|
||||
REINIT_PAUSE_MS / 1000);
|
||||
// Leave configuration mode whatever state the module is in, then begin again
|
||||
this->send_command_(CONFIG_MODE_END_CMD);
|
||||
this->init_step_ = 0;
|
||||
this->init_timeouts_ = 0;
|
||||
this->awaiting_ack_ = false;
|
||||
this->next_send_at_ = now + REINIT_PAUSE_MS;
|
||||
this->restart_init_(now + REINIT_PAUSE_MS);
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "No acknowledgement for command %04X, resending", command);
|
||||
@@ -120,6 +208,22 @@ void LD2410SComponent::send_command_(uint16_t command) {
|
||||
frame[pos++] = CONFIG_MODE_START_VALUE & 0xFF;
|
||||
frame[pos++] = CONFIG_MODE_START_VALUE >> 8;
|
||||
break;
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
case PARAMETERS_READ_CMD:
|
||||
case PARAMETERS_WRITE_CMD:
|
||||
for (uint8_t i = 0; i < PARAMETER_COUNT; i++) {
|
||||
const uint16_t key = progmem_read_uint16(&PARAMETER_KEYS[i]);
|
||||
frame[pos++] = key & 0xFF;
|
||||
frame[pos++] = key >> 8;
|
||||
if (command == PARAMETERS_WRITE_CMD) {
|
||||
const uint32_t value = this->parameters_[i]; // a 32-bit field on the wire
|
||||
for (uint8_t shift = 0; shift < 32; shift += 8) {
|
||||
frame[pos++] = (value >> shift) & 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -252,27 +356,27 @@ bool LD2410SComponent::handle_frame_() {
|
||||
}
|
||||
|
||||
bool LD2410SComponent::handle_data_frame_(const uint8_t *payload, uint16_t len) {
|
||||
uint8_t state;
|
||||
if (this->frame_type_ == FrameType::SHORT_DATA) {
|
||||
// [state][distance low][distance high]
|
||||
state = payload[0];
|
||||
} else if (len >= 4 && payload[0] == DATA_TYPE_STANDARD) {
|
||||
// [type][state][distance low][distance high]...; the module only sends these before the init
|
||||
// sequence switches it to minimal output
|
||||
state = payload[1];
|
||||
} else {
|
||||
if (len >= 1) {
|
||||
ESP_LOGV(TAG, "Ignoring data frame type %02X", payload[0]);
|
||||
// Short frames carry [state][distance low][distance high]; standard frames put a type byte first, and the
|
||||
// module only sends them before the init sequence switches it to minimal output
|
||||
const uint8_t *data = payload;
|
||||
if (this->frame_type_ == FrameType::STD_DATA) {
|
||||
if (len < 4 || payload[0] != DATA_TYPE_STANDARD) {
|
||||
if (len >= 1) {
|
||||
ESP_LOGV(TAG, "Ignoring data frame type %02X", payload[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
data = payload + 1;
|
||||
}
|
||||
const uint8_t state = data[0];
|
||||
const uint16_t distance = encode_uint16(data[2], data[1]);
|
||||
// States 0 and 1 mean no target, 2 and 3 a target; anything else is line noise that happened to
|
||||
// carry the header and footer bytes
|
||||
if (state > 3) {
|
||||
ESP_LOGV(TAG, "Dropping frame with impossible state %u", state);
|
||||
return false;
|
||||
}
|
||||
this->publish_target_(state > 1);
|
||||
this->publish_target_(state > 1, distance);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -283,32 +387,147 @@ void LD2410SComponent::handle_command_ack_(const uint8_t *payload, uint16_t len)
|
||||
}
|
||||
const uint16_t command_word = encode_uint16(payload[1], payload[0]);
|
||||
const uint16_t ack = encode_uint16(payload[3], payload[2]);
|
||||
if (!this->awaiting_ack_ || command_word != (init_command(this->init_step_) | CMD_CONFIRMATION)) {
|
||||
if (!this->awaiting_ack_) {
|
||||
ESP_LOGV(TAG, "Unexpected acknowledgement %04X", command_word);
|
||||
return;
|
||||
}
|
||||
if (ack != 0) {
|
||||
// Leave the command pending so the timeout resends it
|
||||
ESP_LOGW(TAG, "Module rejected command %04X, ack %04X", init_command(this->init_step_), ack);
|
||||
const uint16_t command = this->current_command_();
|
||||
if (command_word != (command | CMD_CONFIRMATION)) {
|
||||
ESP_LOGV(TAG, "Unexpected acknowledgement %04X", command_word);
|
||||
return;
|
||||
}
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
if (ack != 0 && command == PARAMETERS_WRITE_CMD) {
|
||||
// Resending the same values would be rejected again; leave configuration mode and read the
|
||||
// module's own values back, which republishes them
|
||||
ESP_LOGW(TAG, "Module rejected the parameters, ack %04X", ack);
|
||||
this->send_command_(CONFIG_MODE_END_CMD);
|
||||
this->restart_init_(App.get_loop_component_start_time() + COMMAND_GAP_MS);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (ack != 0) {
|
||||
// Leave the command pending so the timeout resends it
|
||||
ESP_LOGW(TAG, "Module rejected command %04X, ack %04X", command, ack);
|
||||
return;
|
||||
}
|
||||
// A short answer leaves the read pending the same way, so init cannot finish without the values
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
if (command == PARAMETERS_READ_CMD && !this->handle_parameters_(&payload[4], len - 4))
|
||||
return;
|
||||
#endif
|
||||
#ifdef USE_LD2410S_VERSION
|
||||
if (command == VERSION_READ_CMD && !this->handle_version_(&payload[4], len - 4))
|
||||
return;
|
||||
#endif
|
||||
this->awaiting_ack_ = false;
|
||||
this->init_timeouts_ = 0;
|
||||
this->init_step_++;
|
||||
this->next_send_at_ = App.get_loop_component_start_time() + COMMAND_GAP_MS;
|
||||
if (this->init_step_ >= INIT_SEQUENCE_LENGTH) {
|
||||
this->step_timeouts_ = 0;
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->next_send_at_ = now + COMMAND_GAP_MS;
|
||||
if (++this->step_ < this->sequence_len_) {
|
||||
return;
|
||||
}
|
||||
const bool was_init = this->sequence_ == INIT_SEQUENCE;
|
||||
this->sequence_ = nullptr;
|
||||
if (was_init) {
|
||||
ESP_LOGD(TAG, "Setup done");
|
||||
this->last_frame_at_ = App.get_loop_component_start_time();
|
||||
this->init_done_ = true;
|
||||
this->last_frame_at_ = now;
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
|
||||
void LD2410SComponent::publish_target_(bool target) {
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
// The answer lists the parameter values in key order, 32 bits each; returns false when it is cut short
|
||||
bool LD2410SComponent::handle_parameters_(const uint8_t *values, uint16_t len) {
|
||||
if (len < PARAMETER_COUNT * PARAMETER_VALUE_SIZE) {
|
||||
ESP_LOGW(TAG, "Parameter answer too short (%u bytes)", len);
|
||||
return false;
|
||||
}
|
||||
// Every parameter must fit the low byte of its little-endian value; otherwise nothing is stored or
|
||||
// published and writes stay refused, so a cut-down value is never shown or sent back
|
||||
for (uint8_t i = 0; i < PARAMETER_COUNT; i++) {
|
||||
const uint8_t *v = &values[i * PARAMETER_VALUE_SIZE];
|
||||
if ((v[1] | v[2] | v[3]) != 0) {
|
||||
ESP_LOGW(TAG, "Parameter %u out of range: %" PRIu32 ", changes stay disabled", i,
|
||||
encode_uint32(v[3], v[2], v[1], v[0]));
|
||||
this->parameters_known_ = false;
|
||||
#ifdef USE_NUMBER
|
||||
// A value a number showed before the module answered is not confirmed
|
||||
for (auto *number : this->numbers_) {
|
||||
if (number != nullptr) {
|
||||
number->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (uint8_t i = 0; i < PARAMETER_COUNT; i++) {
|
||||
this->parameters_[i] = values[i * PARAMETER_VALUE_SIZE];
|
||||
}
|
||||
this->parameters_known_ = true;
|
||||
this->publish_parameters_();
|
||||
ESP_LOGD(TAG, "Parameters: gates %u to %u, no delay %u s, frequencies %u/%u dHz, response speed %u",
|
||||
this->parameters_[1], this->parameters_[0], this->parameters_[2], this->parameters_[3], this->parameters_[4],
|
||||
this->parameters_[5]);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LD2410S_VERSION
|
||||
// Returns false when the answer is cut short
|
||||
bool LD2410SComponent::handle_version_(const uint8_t *values, uint16_t len) {
|
||||
if (len < VERSION_SIZE) {
|
||||
ESP_LOGW(TAG, "Version answer too short (%u bytes)", len);
|
||||
return false;
|
||||
}
|
||||
const uint8_t *v = &values[VERSION_TYPE_SIZE];
|
||||
char version[18];
|
||||
snprintf(version, sizeof(version), "%u.%u.%u", encode_uint16(v[1], v[0]), encode_uint16(v[3], v[2]),
|
||||
encode_uint16(v[5], v[4]));
|
||||
ESP_LOGD(TAG, "Firmware %s", version);
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void LD2410SComponent::publish_target_(bool target, uint16_t distance) {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->target_binary_sensor_ != nullptr) {
|
||||
this->target_binary_sensor_->publish_state(target);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
// The module keeps reporting the last distance after the target left
|
||||
SAFE_PUBLISH_SENSOR(this->distance_sensor_, target ? distance : 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
void LD2410SComponent::publish_parameters_() {
|
||||
#ifdef USE_NUMBER
|
||||
for (uint8_t i = 0; i < PARAMETER_COUNT; i++) {
|
||||
if (this->numbers_[i] != nullptr) {
|
||||
this->numbers_[i]->publish_raw(this->parameters_[i]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (this->response_speed_select_ != nullptr) {
|
||||
const uint8_t speed = this->parameters_[static_cast<uint8_t>(Parameter::PARAMETER_RESPONSE_SPEED)];
|
||||
for (uint8_t i = 0; i < RESPONSE_SPEED_COUNT; i++) {
|
||||
if (progmem_read_byte(&RESPONSE_SPEEDS[i]) == speed) {
|
||||
this->response_speed_select_->publish_state(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Unknown response speed %u", speed);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::ld2410s
|
||||
|
||||
@@ -2,10 +2,21 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/ld24xx/ld24xx.h"
|
||||
#endif
|
||||
#ifdef USE_LD2410S_VERSION
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#if defined(USE_SELECT) && defined(USE_LD2410S_PARAMETERS)
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::ld2410s {
|
||||
|
||||
@@ -13,16 +24,55 @@ namespace esphome::ld2410s {
|
||||
static constexpr size_t RX_BUFFER_SIZE = 96;
|
||||
static_assert(RX_BUFFER_SIZE <= 255, "frame lengths are kept in a byte");
|
||||
|
||||
/// The module's common parameters, in the order the parameter read answers them
|
||||
enum class Parameter : uint8_t {
|
||||
PARAMETER_MAX_DISTANCE,
|
||||
PARAMETER_MIN_DISTANCE,
|
||||
PARAMETER_NO_DELAY,
|
||||
PARAMETER_STATUS_FREQUENCY,
|
||||
PARAMETER_DISTANCE_FREQUENCY,
|
||||
PARAMETER_RESPONSE_SPEED,
|
||||
PARAMETER_COUNT,
|
||||
};
|
||||
static constexpr uint8_t PARAMETER_COUNT = static_cast<uint8_t>(Parameter::PARAMETER_COUNT);
|
||||
// Response speed values, in the order of the select's options (Normal, Fast)
|
||||
static constexpr uint8_t RESPONSE_SPEEDS[] PROGMEM = {5, 10};
|
||||
static constexpr uint8_t RESPONSE_SPEED_COUNT = sizeof(RESPONSE_SPEEDS);
|
||||
|
||||
#if defined(USE_NUMBER) && defined(USE_LD2410S_PARAMETERS)
|
||||
class LD2410SNumber;
|
||||
#endif
|
||||
|
||||
class LD2410SComponent final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(target)
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
SUB_SENSOR_WITH_DEDUP(distance, uint16_t)
|
||||
#endif
|
||||
#ifdef USE_LD2410S_VERSION
|
||||
SUB_TEXT_SENSOR(version)
|
||||
#endif
|
||||
#if defined(USE_SELECT) && defined(USE_LD2410S_PARAMETERS)
|
||||
SUB_SELECT(response_speed)
|
||||
#endif
|
||||
|
||||
public:
|
||||
void setup() override { this->status_set_warning(); } // cleared once the module acknowledged the init sequence
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Store a parameter in the module's units; loop() then writes all of them to the module.
|
||||
/// Returns false, and changes nothing, until the module's own values have been read.
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
bool set_parameter(Parameter parameter, uint8_t value);
|
||||
#ifdef USE_NUMBER
|
||||
void set_number(Parameter parameter, LD2410SNumber *number) {
|
||||
this->numbers_[static_cast<uint8_t>(parameter)] = number;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
protected:
|
||||
enum class FrameType : uint8_t { NONE, SHORT_DATA, STD_DATA, COMMAND };
|
||||
|
||||
@@ -32,6 +82,12 @@ class LD2410SComponent final : public Component, public uart::UARTDevice {
|
||||
/// Returns false for a data frame whose state byte is impossible, so it counts as dropped.
|
||||
bool handle_data_frame_(const uint8_t *payload, uint16_t len);
|
||||
void handle_command_ack_(const uint8_t *payload, uint16_t len);
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
bool handle_parameters_(const uint8_t *values, uint16_t len);
|
||||
#endif
|
||||
#ifdef USE_LD2410S_VERSION
|
||||
bool handle_version_(const uint8_t *values, uint16_t len);
|
||||
#endif
|
||||
void count_dropped_frame_() {
|
||||
if (this->dropped_frames_ < UINT8_MAX) {
|
||||
this->dropped_frames_++;
|
||||
@@ -42,21 +98,41 @@ class LD2410SComponent final : public Component, public uart::UARTDevice {
|
||||
this->expected_len_ = 0;
|
||||
this->frame_type_ = FrameType::NONE;
|
||||
}
|
||||
void run_init_sequence_(uint32_t now);
|
||||
void start_sequence_(const uint16_t *sequence, uint8_t length, uint32_t now);
|
||||
void restart_init_(uint32_t at);
|
||||
void run_sequence_(uint32_t now);
|
||||
uint16_t current_command_() const;
|
||||
void send_command_(uint16_t command);
|
||||
void publish_target_(bool target);
|
||||
void publish_target_(bool target, uint16_t distance);
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
void publish_parameters_();
|
||||
#endif
|
||||
|
||||
#if defined(USE_NUMBER) && defined(USE_LD2410S_PARAMETERS)
|
||||
LD2410SNumber *numbers_[PARAMETER_COUNT]{}; // null for parameters without a number
|
||||
#endif
|
||||
|
||||
const uint16_t *sequence_{nullptr}; // PROGMEM; null while nothing is being sent
|
||||
uint32_t next_send_at_{0};
|
||||
uint32_t last_frame_at_{0};
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
uint8_t parameters_[PARAMETER_COUNT]{}; // all fit a byte: gates, seconds, tenths of a hertz, speed
|
||||
#endif
|
||||
uint8_t rx_buffer_[RX_BUFFER_SIZE];
|
||||
uint8_t rx_len_{0};
|
||||
uint8_t expected_len_{0};
|
||||
uint8_t init_step_{0};
|
||||
uint8_t init_timeouts_{0};
|
||||
uint8_t sequence_len_{0};
|
||||
uint8_t step_{0};
|
||||
uint8_t step_timeouts_{0};
|
||||
uint8_t dropped_frames_{0}; // since the last accepted data frame, saturates at 255
|
||||
FrameType frame_type_{FrameType::NONE};
|
||||
bool awaiting_ack_{false};
|
||||
bool rescanning_{false};
|
||||
bool init_done_{false};
|
||||
#ifdef USE_LD2410S_PARAMETERS
|
||||
bool parameters_known_{false}; // the module answered the parameter read at least once
|
||||
bool parameters_dirty_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410s
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_NUMBER) && defined(USE_LD2410S_PARAMETERS)
|
||||
|
||||
#include "ld2410s.h"
|
||||
#include "esphome/components/number/number.h"
|
||||
|
||||
namespace esphome::ld2410s {
|
||||
|
||||
/// Edits one common parameter; scale converts the module's units to the number's.
|
||||
class LD2410SNumber final : public number::Number, public Parented<LD2410SComponent> {
|
||||
public:
|
||||
LD2410SNumber(Parameter parameter, float scale) : parameter_(parameter), scale_(scale) {}
|
||||
|
||||
void publish_raw(uint8_t raw) { this->publish_state(raw * this->scale_); }
|
||||
|
||||
protected:
|
||||
void control(float value) override {
|
||||
// Show the step the module got, not a value between two steps
|
||||
const uint8_t raw = lroundf(value / this->scale_); // the schema keeps every parameter within a byte
|
||||
if (this->parent_->set_parameter(this->parameter_, raw))
|
||||
this->publish_raw(raw);
|
||||
}
|
||||
|
||||
const Parameter parameter_;
|
||||
const float scale_;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410s
|
||||
|
||||
#endif // USE_NUMBER && USE_LD2410S_PARAMETERS
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_SELECT) && defined(USE_LD2410S_PARAMETERS)
|
||||
|
||||
#include "ld2410s.h"
|
||||
#include "esphome/components/select/select.h"
|
||||
|
||||
namespace esphome::ld2410s {
|
||||
|
||||
class LD2410SSelect final : public select::Select, public Parented<LD2410SComponent> {
|
||||
protected:
|
||||
void control(size_t index) override {
|
||||
if (this->parent_->set_parameter(Parameter::PARAMETER_RESPONSE_SPEED, progmem_read_byte(&RESPONSE_SPEEDS[index])))
|
||||
this->publish_state(index);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410s
|
||||
|
||||
#endif // USE_SELECT && USE_LD2410S_PARAMETERS
|
||||
@@ -0,0 +1,108 @@
|
||||
from functools import partial
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ICON_PULSE,
|
||||
ICON_TIMELAPSE,
|
||||
UNIT_HERTZ,
|
||||
UNIT_METER,
|
||||
UNIT_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_LD2410S_ID, LD2410SComponent, Parameter, ld2410s_ns
|
||||
|
||||
DEPENDENCIES = ["ld2410s"]
|
||||
|
||||
CONF_DISTANCE_REPORTING_FREQUENCY = "distance_reporting_frequency"
|
||||
CONF_MAX_DISTANCE = "max_distance"
|
||||
CONF_MIN_DISTANCE = "min_distance"
|
||||
CONF_NO_DELAY = "no_delay"
|
||||
CONF_STATUS_REPORTING_FREQUENCY = "status_reporting_frequency"
|
||||
|
||||
LD2410SNumber = ld2410s_ns.class_("LD2410SNumber", number.Number)
|
||||
|
||||
# The module counts distances in 0.7 m gates and frequencies in tenths of a hertz
|
||||
GATE_WIDTH_M = 0.7
|
||||
FREQUENCY_SCALE = 0.1
|
||||
_DISTANCE_RANGE = {"min_value": 0, "max_value": 16 * GATE_WIDTH_M, "step": GATE_WIDTH_M}
|
||||
_FREQUENCY_RANGE = {"min_value": 0.5, "max_value": 8, "step": 0.5}
|
||||
|
||||
|
||||
def _number_schema(device_class: str, unit: str, icon: str) -> cv.Schema:
|
||||
return number.number_schema(
|
||||
LD2410SNumber,
|
||||
device_class=device_class,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
unit_of_measurement=unit,
|
||||
icon=icon,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_LD2410S_ID): cv.use_id(LD2410SComponent),
|
||||
cv.Optional(CONF_MAX_DISTANCE): _number_schema(
|
||||
DEVICE_CLASS_DISTANCE, UNIT_METER, "mdi:arrow-collapse-right"
|
||||
),
|
||||
cv.Optional(CONF_MIN_DISTANCE): _number_schema(
|
||||
DEVICE_CLASS_DISTANCE, UNIT_METER, "mdi:arrow-collapse-left"
|
||||
),
|
||||
cv.Optional(CONF_NO_DELAY): _number_schema(
|
||||
DEVICE_CLASS_DURATION, UNIT_SECOND, ICON_TIMELAPSE
|
||||
),
|
||||
cv.Optional(CONF_STATUS_REPORTING_FREQUENCY): _number_schema(
|
||||
DEVICE_CLASS_FREQUENCY, UNIT_HERTZ, ICON_PULSE
|
||||
),
|
||||
cv.Optional(CONF_DISTANCE_REPORTING_FREQUENCY): _number_schema(
|
||||
DEVICE_CLASS_FREQUENCY, UNIT_HERTZ, ICON_PULSE
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_LD2410S_PARAMETERS")
|
||||
hub = await cg.get_variable(config[CONF_LD2410S_ID])
|
||||
numbers = number.sub_numbers(config, parent=hub)
|
||||
await numbers(
|
||||
CONF_MAX_DISTANCE,
|
||||
partial(hub.set_number, Parameter.PARAMETER_MAX_DISTANCE),
|
||||
Parameter.PARAMETER_MAX_DISTANCE,
|
||||
GATE_WIDTH_M,
|
||||
**_DISTANCE_RANGE,
|
||||
)
|
||||
await numbers(
|
||||
CONF_MIN_DISTANCE,
|
||||
partial(hub.set_number, Parameter.PARAMETER_MIN_DISTANCE),
|
||||
Parameter.PARAMETER_MIN_DISTANCE,
|
||||
GATE_WIDTH_M,
|
||||
**_DISTANCE_RANGE,
|
||||
)
|
||||
await numbers(
|
||||
CONF_NO_DELAY,
|
||||
partial(hub.set_number, Parameter.PARAMETER_NO_DELAY),
|
||||
Parameter.PARAMETER_NO_DELAY,
|
||||
1,
|
||||
min_value=1,
|
||||
max_value=120,
|
||||
step=1,
|
||||
)
|
||||
await numbers(
|
||||
CONF_STATUS_REPORTING_FREQUENCY,
|
||||
partial(hub.set_number, Parameter.PARAMETER_STATUS_FREQUENCY),
|
||||
Parameter.PARAMETER_STATUS_FREQUENCY,
|
||||
FREQUENCY_SCALE,
|
||||
**_FREQUENCY_RANGE,
|
||||
)
|
||||
await numbers(
|
||||
CONF_DISTANCE_REPORTING_FREQUENCY,
|
||||
partial(hub.set_number, Parameter.PARAMETER_DISTANCE_FREQUENCY),
|
||||
Parameter.PARAMETER_DISTANCE_FREQUENCY,
|
||||
FREQUENCY_SCALE,
|
||||
**_FREQUENCY_RANGE,
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_LD2410S_ID, LD2410SComponent, ld2410s_ns
|
||||
|
||||
DEPENDENCIES = ["ld2410s"]
|
||||
|
||||
CONF_RESPONSE_SPEED = "response_speed"
|
||||
|
||||
LD2410SSelect = ld2410s_ns.class_("LD2410SSelect", select.Select)
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_LD2410S_ID): cv.use_id(LD2410SComponent),
|
||||
cv.Required(CONF_RESPONSE_SPEED): select.select_schema(
|
||||
LD2410SSelect,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon="mdi:speedometer",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_LD2410S_PARAMETERS")
|
||||
hub = await cg.get_variable(config[CONF_LD2410S_ID])
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
await selects(
|
||||
CONF_RESPONSE_SPEED, hub.set_response_speed_select, options=["Normal", "Fast"]
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DISTANCE,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
ICON_SIGNAL_DISTANCE_VARIANT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CENTIMETER,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_LD2410S_ID, LD2410SComponent
|
||||
|
||||
DEPENDENCIES = ["ld2410s"]
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_LD2410S_ID): cv.use_id(LD2410SComponent),
|
||||
cv.Required(CONF_DISTANCE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CENTIMETER,
|
||||
icon=ICON_SIGNAL_DISTANCE_VARIANT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_LD2410S_ID])
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_DISTANCE, hub.set_distance_sensor)
|
||||
@@ -0,0 +1,23 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_LD2410S_ID, LD2410SComponent
|
||||
|
||||
DEPENDENCIES = ["ld2410s"]
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_LD2410S_ID): cv.use_id(LD2410SComponent),
|
||||
cv.Required(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_LD2410S_VERSION")
|
||||
hub = await cg.get_variable(config[CONF_LD2410S_ID])
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include "esphome/core/finite_set_mask.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::light {
|
||||
|
||||
@@ -108,8 +109,9 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
|
||||
using color_mode_bitmask_t = uint16_t;
|
||||
|
||||
// Lookup table for ColorMode bit mapping
|
||||
// This array defines the canonical order of color modes (bit 0-9)
|
||||
constexpr ColorMode COLOR_MODE_LOOKUP[] = {
|
||||
// This array defines the canonical order of color modes (bit 0-9). One shared copy in flash; read it at run time
|
||||
// through color_mode_at().
|
||||
inline constexpr ColorMode COLOR_MODE_LOOKUP[] PROGMEM = {
|
||||
ColorMode::UNKNOWN, // bit 0
|
||||
ColorMode::ON_OFF, // bit 1
|
||||
ColorMode::BRIGHTNESS, // bit 2
|
||||
@@ -122,6 +124,12 @@ constexpr ColorMode COLOR_MODE_LOOKUP[] = {
|
||||
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
|
||||
};
|
||||
|
||||
constexpr ColorMode color_mode_at(unsigned bit) {
|
||||
if (__builtin_is_constant_evaluated())
|
||||
return COLOR_MODE_LOOKUP[bit];
|
||||
return static_cast<ColorMode>(progmem_read_byte(reinterpret_cast<const uint8_t *>(&COLOR_MODE_LOOKUP[bit])));
|
||||
}
|
||||
|
||||
/// Bit mapping policy for ColorMode
|
||||
/// Uses lookup table for non-contiguous enum values
|
||||
struct ColorModeBitPolicy {
|
||||
@@ -129,17 +137,15 @@ struct ColorModeBitPolicy {
|
||||
static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]);
|
||||
|
||||
static constexpr unsigned to_bit(ColorMode mode) {
|
||||
// Linear search through lookup table
|
||||
// Compiler optimizes this to efficient code since array is constexpr
|
||||
for (int i = 0; i < MAX_BITS; ++i) {
|
||||
if (COLOR_MODE_LOOKUP[i] == mode)
|
||||
if (color_mode_at(i) == mode)
|
||||
return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static constexpr ColorMode from_bit(unsigned bit) {
|
||||
return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN;
|
||||
return (bit < MAX_BITS) ? color_mode_at(bit) : ColorMode::UNKNOWN;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,7 +163,7 @@ constexpr uint16_t compute_capability_bitmask(ColorCapability capability) {
|
||||
// Check each ColorMode to see if it has this capability
|
||||
constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]);
|
||||
for (int bit = 0; bit < color_mode_count; ++bit) {
|
||||
uint8_t mode_val = static_cast<uint8_t>(COLOR_MODE_LOOKUP[bit]);
|
||||
uint8_t mode_val = static_cast<uint8_t>(color_mode_at(bit));
|
||||
if ((mode_val & cap_bit) != 0) {
|
||||
mask |= (1 << bit);
|
||||
}
|
||||
|
||||
@@ -29,4 +29,7 @@ void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *ImageDecoder::image_buffer_() const { return this->image_->buffer_; }
|
||||
bool ImageDecoder::image_is_big_endian_() const { return this->image_->is_big_endian_; }
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -127,6 +127,10 @@ class ImageDecoder {
|
||||
}
|
||||
|
||||
protected:
|
||||
/// The image's pixel buffer, for decoders that write their output straight into it
|
||||
uint8_t *image_buffer_() const;
|
||||
bool image_is_big_endian_() const;
|
||||
|
||||
double x_scale_ = 1.0;
|
||||
double y_scale_ = 1.0;
|
||||
RuntimeImage *image_;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/image/image.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -15,6 +16,67 @@ ESPHOME_LOG_TAG(TAG, "image_decoder.jpeg");
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
|
||||
if (this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) {
|
||||
return false;
|
||||
}
|
||||
if (this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->source_width_ = width;
|
||||
this->source_height_ = height;
|
||||
this->x_boundaries_.clear();
|
||||
this->y_boundaries_.clear();
|
||||
const uint32_t target_width = this->image_->get_buffer_width();
|
||||
const uint32_t target_height = this->image_->get_buffer_height();
|
||||
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
|
||||
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6;
|
||||
// the generic draw() path rounds down and keeps the odd ones instead
|
||||
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
|
||||
}
|
||||
for (uint32_t y = 0; y <= static_cast<uint32_t>(height); y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>((y * target_height + height - 1) / height));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels) {
|
||||
if (pixels == nullptr) {
|
||||
return;
|
||||
}
|
||||
uint8_t *const buffer = this->image_buffer_();
|
||||
const uint32_t buffer_width = this->image_->get_buffer_width();
|
||||
// JPEGDEC gives little-endian words; a big-endian image stores the high byte first
|
||||
const uint8_t first = this->image_is_big_endian_() ? 1 : 0;
|
||||
for (int source_y = 0; source_y < h; source_y++) {
|
||||
const int absolute_y = y + source_y;
|
||||
if (absolute_y < 0 || absolute_y >= this->source_height_) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t y1 = this->y_boundaries_[absolute_y];
|
||||
const uint16_t y2 = this->y_boundaries_[absolute_y + 1];
|
||||
for (int source_x = 0; source_x < w; source_x++) {
|
||||
const int absolute_x = x + source_x;
|
||||
if (absolute_x < 0 || absolute_x >= this->source_width_) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t x1 = this->x_boundaries_[absolute_x];
|
||||
const uint16_t x2 = this->x_boundaries_[absolute_x + 1];
|
||||
const uint8_t *pixel = pixels + (source_y * w + source_x) * 2;
|
||||
for (uint16_t destination_y = y1; destination_y < y2; destination_y++) {
|
||||
uint8_t *destination = buffer + (destination_y * buffer_width + x1) * 2;
|
||||
for (uint16_t destination_x = x1; destination_x < x2; destination_x++) {
|
||||
destination[0] = pixel[first];
|
||||
destination[1] = pixel[1 - first];
|
||||
destination += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback method that will be called by the JPEGDEC engine when a chunk
|
||||
* of the image is decoded.
|
||||
@@ -22,7 +84,12 @@ namespace esphome::runtime_image {
|
||||
* @param jpeg The JPEGDRAW object, including the context data.
|
||||
*/
|
||||
static int draw_callback(JPEGDRAW *jpeg) {
|
||||
ImageDecoder *decoder = (ImageDecoder *) jpeg->pUser;
|
||||
JpegDecoder *decoder = (JpegDecoder *) jpeg->pUser;
|
||||
|
||||
if (decoder == nullptr) {
|
||||
ESP_LOGE(TAG, "Decoder pointer is null!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Some very big images take too long to decode, so feed the watchdog on each callback
|
||||
// to avoid crashing if the executing task has a watchdog enabled.
|
||||
@@ -33,6 +100,11 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
||||
#ifdef USE_ESP_IDF
|
||||
}
|
||||
#endif
|
||||
if (decoder->uses_direct_rgb565()) {
|
||||
decoder->draw_rgb565(jpeg->x, jpeg->y, jpeg->iWidth, jpeg->iHeight,
|
||||
reinterpret_cast<const uint8_t *>(jpeg->pPixels));
|
||||
return 1;
|
||||
}
|
||||
size_t position = 0;
|
||||
size_t height = static_cast<size_t>(jpeg->iHeight);
|
||||
size_t width = static_cast<size_t>(jpeg->iWidth);
|
||||
@@ -42,10 +114,6 @@ static int draw_callback(JPEGDRAW *jpeg) {
|
||||
auto ba = decode_value(jpeg->pPixels[position++]);
|
||||
Color color(rg[1], rg[0], ba[1], ba[0]);
|
||||
|
||||
if (!decoder) {
|
||||
ESP_LOGE(TAG, "Decoder pointer is null!");
|
||||
return 0;
|
||||
}
|
||||
decoder->draw(jpeg->x + x, jpeg->y + y, 1, 1, color);
|
||||
}
|
||||
}
|
||||
@@ -78,10 +146,20 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
|
||||
|
||||
this->jpeg_.setUserPointer(this);
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
|
||||
if (this->direct_rgb565_) {
|
||||
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
|
||||
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
|
||||
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
}
|
||||
if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (!this->jpeg_.decode(0, 0, 0)) {
|
||||
auto error = this->jpeg_.getLastError();
|
||||
ESP_LOGE(TAG, "Error while decoding: %d", error);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "image_decoder.h"
|
||||
#include "runtime_image.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
#include <JPEGDEC.h>
|
||||
|
||||
@@ -23,8 +23,18 @@ class JpegDecoder : public ImageDecoder {
|
||||
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
|
||||
|
||||
protected:
|
||||
bool prepare_rgb565_scaling_(int width, int height);
|
||||
|
||||
JPEGDEC jpeg_{};
|
||||
FixedVector<uint16_t> x_boundaries_{};
|
||||
FixedVector<uint16_t> y_boundaries_{};
|
||||
int source_width_{0};
|
||||
int source_height_{0};
|
||||
bool direct_rgb565_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -234,6 +234,8 @@ class RuntimeImage : public image::Image {
|
||||
bool is_big_endian_{false};
|
||||
/** Whether buffer_ belongs to the caller, so it must not be freed or resized here. */
|
||||
bool external_buffer_{false};
|
||||
|
||||
friend class ImageDecoder;
|
||||
};
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -1,54 +1,358 @@
|
||||
#include "selec_meter.h"
|
||||
#include "selec_meter_registers.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace esphome::selec_meter {
|
||||
|
||||
namespace helpers = modbus::helpers;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "selec_meter");
|
||||
|
||||
static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers
|
||||
static constexpr uint8_t EM2M_REGISTER_COUNT = 34; // 34 x 16-bit registers
|
||||
|
||||
// Durable refusals an optional read tolerates before it is given up on
|
||||
static constexpr uint8_t MAX_OPTIONAL_READ_FAILURES = 5;
|
||||
|
||||
static const LogString *read_state_name(ReadState state) {
|
||||
switch (state) {
|
||||
case ReadState::READ_STATE_MAIN_BLOCK:
|
||||
return LOG_STR("main block");
|
||||
case ReadState::READ_STATE_SERIAL_NUMBER:
|
||||
return LOG_STR("serial number");
|
||||
case ReadState::READ_STATE_DG_SENSING:
|
||||
return LOG_STR("DG sensing");
|
||||
case ReadState::READ_STATE_IDLE:
|
||||
default:
|
||||
return LOG_STR("idle");
|
||||
}
|
||||
}
|
||||
|
||||
// FP32 at `reg` in the configured word order (MSRF high word first, LSRF low word first); NaN when
|
||||
// `reg` is not wholly inside `registers`
|
||||
static float read_float(std::span<const uint16_t> registers, uint16_t start_address, uint16_t reg, bool word_swapped) {
|
||||
auto value = word_swapped ? helpers::value_at<helpers::SensorValueType::FP32_R>(registers, start_address, reg)
|
||||
: helpers::value_at<helpers::SensorValueType::FP32>(registers, start_address, reg);
|
||||
return value.value_or(NAN);
|
||||
}
|
||||
|
||||
// Publishes the field at `reg` (x1000 when `kilo`) unless it decodes to NaN/Inf, e.g. an unloaded
|
||||
// phase's power factor; a non-finite field is skipped on its own so the others still publish.
|
||||
static void publish_float(sensor::Sensor *sens, std::span<const uint16_t> registers, uint16_t reg, bool kilo,
|
||||
bool word_swapped) {
|
||||
float value = read_float(registers, 0, reg, word_swapped);
|
||||
if (!std::isfinite(value)) {
|
||||
ESP_LOGD(TAG, "Skipping non-finite value for '%s'", sens->get_name().c_str());
|
||||
return;
|
||||
}
|
||||
sens->publish_state(kilo ? value * 1000.0f : value);
|
||||
}
|
||||
|
||||
#if defined(USE_SELEC_METER_EM4M) && (defined(USE_TEXT_SENSOR) || defined(USE_BINARY_SENSOR))
|
||||
// Counts one failure of an optional read; callers only run while `failures` is below the limit
|
||||
static void note_optional_read_failure(uint8_t &failures, const LogString *name) {
|
||||
if (++failures == MAX_OPTIONAL_READ_FAILURES) {
|
||||
ESP_LOGW(TAG, "Reading %s failed %u times in a row, giving up", LOG_STR_ARG(name), failures);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
SelecMeter::ReadRange SelecMeter::read_range_(ReadState state) const {
|
||||
switch (state) {
|
||||
case ReadState::READ_STATE_MAIN_BLOCK:
|
||||
#ifdef USE_SELEC_METER_EM4M
|
||||
if (this->model_ == Model::MODEL_EM4M)
|
||||
return {0, EM4M_REGISTER_COUNT};
|
||||
#endif
|
||||
return {0, EM2M_REGISTER_COUNT};
|
||||
case ReadState::READ_STATE_SERIAL_NUMBER:
|
||||
return {EM4M_SERIAL_NUMBER, 2};
|
||||
case ReadState::READ_STATE_DG_SENSING:
|
||||
return {EM4M_DG_SENSING, 2};
|
||||
case ReadState::READ_STATE_IDLE:
|
||||
default:
|
||||
return {0, 0};
|
||||
}
|
||||
}
|
||||
|
||||
bool SelecMeter::wanted_(ReadState state) const {
|
||||
switch (state) {
|
||||
case ReadState::READ_STATE_MAIN_BLOCK:
|
||||
return true;
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_TEXT_SENSOR)
|
||||
case ReadState::READ_STATE_SERIAL_NUMBER:
|
||||
return this->serial_number_sensor_ != nullptr && this->serial_number_failures_ < MAX_OPTIONAL_READ_FAILURES;
|
||||
#endif
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_BINARY_SENSOR)
|
||||
case ReadState::READ_STATE_DG_SENSING:
|
||||
return this->dg_sensing_sensor_ != nullptr && this->dg_sensing_failures_ < MAX_OPTIONAL_READ_FAILURES;
|
||||
#endif
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ReadState SelecMeter::next_read_state_after_(ReadState current) const {
|
||||
for (auto next = static_cast<uint8_t>(current) + 1; next < static_cast<uint8_t>(ReadState::READ_STATE_IDLE); next++) {
|
||||
if (this->wanted_(static_cast<ReadState>(next)))
|
||||
return static_cast<ReadState>(next);
|
||||
}
|
||||
return ReadState::READ_STATE_IDLE;
|
||||
}
|
||||
|
||||
void SelecMeter::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return; // the hub already logs exception responses
|
||||
|
||||
// Publish a sensor if both of its registers are in this response; skipping absent registers keeps
|
||||
// this correct for any read range, so the poll may be split into multiple requests.
|
||||
// Values are 32-bit floats, low word first.
|
||||
auto publish = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::FP32_R>(registers, start_address, reg))
|
||||
sensor->publish_state(*value * unit);
|
||||
ReadState current = this->read_state_;
|
||||
if (current == ReadState::READ_STATE_IDLE) {
|
||||
ESP_LOGW(TAG, "Unexpected response while idle, dropping");
|
||||
return;
|
||||
}
|
||||
// start_address comes from the exact request this answers, so a stale reply cannot match another read
|
||||
if (start_address != this->read_range_(current).start) {
|
||||
this->fail_current_read_(LOG_STR("Response does not match pending read"), /*durable=*/false);
|
||||
return;
|
||||
}
|
||||
if (status.has_value()) {
|
||||
const auto code = *status;
|
||||
ESP_LOGW(TAG, "Modbus error 0x%02X while reading %s", static_cast<uint8_t>(code),
|
||||
LOG_STR_ARG(read_state_name(current)));
|
||||
// Illegal function/address are durable; busy, acknowledge and the rest are transient
|
||||
const bool durable =
|
||||
code == modbus::ExceptionCode::ILLEGAL_FUNCTION || code == modbus::ExceptionCode::ILLEGAL_DATA_ADDRESS;
|
||||
this->fail_current_read_(nullptr, durable);
|
||||
return;
|
||||
}
|
||||
switch (current) {
|
||||
case ReadState::READ_STATE_MAIN_BLOCK: {
|
||||
#ifdef USE_SELEC_METER_EM4M
|
||||
bool ok = this->model_ == Model::MODEL_EM4M ? this->decode_em4m_(registers) : this->decode_em2m_(registers);
|
||||
#else
|
||||
bool ok = this->decode_em2m_(registers);
|
||||
#endif
|
||||
if (ok) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->status_set_warning();
|
||||
}
|
||||
break;
|
||||
}
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_TEXT_SENSOR)
|
||||
case ReadState::READ_STATE_SERIAL_NUMBER:
|
||||
this->decode_serial_number_(registers);
|
||||
break;
|
||||
#endif
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_BINARY_SENSOR)
|
||||
case ReadState::READ_STATE_DG_SENSING:
|
||||
this->decode_dg_sensing_(registers);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Safe per modbus.h's callback contract: sending from inside a callback is picked up normally.
|
||||
this->start_read_(this->next_read_state_after_(current));
|
||||
}
|
||||
|
||||
void SelecMeter::fail_current_read_(const LogString *reason, bool durable) {
|
||||
if (this->read_state_ == ReadState::READ_STATE_IDLE) {
|
||||
ESP_LOGW(TAG, "Unexpected failure while idle, dropping");
|
||||
return;
|
||||
}
|
||||
ReadState failed_state = this->read_state_;
|
||||
if (reason != nullptr) {
|
||||
ESP_LOGW(TAG, "%s while reading %s", LOG_STR_ARG(reason), LOG_STR_ARG(read_state_name(failed_state)));
|
||||
}
|
||||
// Only the main block drives component health; an unsupported optional read is not a fault
|
||||
if (failed_state == ReadState::READ_STATE_MAIN_BLOCK)
|
||||
this->status_set_warning();
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_TEXT_SENSOR)
|
||||
if (failed_state == ReadState::READ_STATE_SERIAL_NUMBER && durable)
|
||||
note_optional_read_failure(this->serial_number_failures_, read_state_name(failed_state));
|
||||
#endif
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_BINARY_SENSOR)
|
||||
if (failed_state == ReadState::READ_STATE_DG_SENSING && durable)
|
||||
note_optional_read_failure(this->dg_sensing_failures_, read_state_name(failed_state));
|
||||
#endif
|
||||
this->start_read_(this->next_read_state_after_(failed_state));
|
||||
}
|
||||
|
||||
bool SelecMeter::on_no_response(std::span<const uint8_t> /*request_pdu*/) {
|
||||
this->fail_current_read_(LOG_STR("No Modbus response"));
|
||||
return false;
|
||||
}
|
||||
|
||||
void SelecMeter::on_not_sent(std::span<const uint8_t> /*request_pdu*/) {
|
||||
this->fail_current_read_(LOG_STR("Modbus request not sent"));
|
||||
}
|
||||
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_TEXT_SENSOR)
|
||||
void SelecMeter::decode_serial_number_(std::span<const uint16_t> registers) {
|
||||
auto serial =
|
||||
this->word_swap_
|
||||
? helpers::value_at<helpers::SensorValueType::U_DWORD_R>(registers, EM4M_SERIAL_NUMBER, EM4M_SERIAL_NUMBER)
|
||||
: helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, EM4M_SERIAL_NUMBER, EM4M_SERIAL_NUMBER);
|
||||
if (!serial) {
|
||||
ESP_LOGW(TAG, "Unexpected response size for serial number");
|
||||
note_optional_read_failure(this->serial_number_failures_, read_state_name(ReadState::READ_STATE_SERIAL_NUMBER));
|
||||
return;
|
||||
}
|
||||
char buf[9];
|
||||
snprintf(buf, sizeof(buf), "%08" PRIX32, *serial);
|
||||
this->serial_number_sensor_->publish_state(buf);
|
||||
// The serial number never changes, so stop reading it
|
||||
this->serial_number_failures_ = MAX_OPTIONAL_READ_FAILURES;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_BINARY_SENSOR)
|
||||
void SelecMeter::decode_dg_sensing_(std::span<const uint16_t> registers) {
|
||||
float value = read_float(registers, EM4M_DG_SENSING, EM4M_DG_SENSING, this->word_swap_);
|
||||
if (!std::isfinite(value)) {
|
||||
ESP_LOGW(TAG, "Short or non-finite DG sensing response, ignoring");
|
||||
note_optional_read_failure(this->dg_sensing_failures_, read_state_name(ReadState::READ_STATE_DG_SENSING));
|
||||
return;
|
||||
}
|
||||
this->dg_sensing_sensor_->publish_state(value != 0);
|
||||
this->dg_sensing_failures_ = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool SelecMeter::publish_fields_(std::span<const uint16_t> registers, std::span<const FloatField> fields,
|
||||
uint8_t min_count) {
|
||||
if (registers.size() < min_count) {
|
||||
ESP_LOGW(TAG, "Short response: %u registers", static_cast<unsigned>(registers.size()));
|
||||
return false;
|
||||
}
|
||||
for (const auto &entry : fields) {
|
||||
FloatField field;
|
||||
progmem_memcpy(&field, &entry, sizeof(field));
|
||||
sensor::Sensor *sens = this->*field.sensor;
|
||||
if (sens != nullptr)
|
||||
publish_float(sens, registers, field.reg, field.kilo, this->word_swap_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SelecMeter::decode_em2m_(std::span<const uint16_t> registers) {
|
||||
static constexpr FloatField EM2M_FIELDS[] PROGMEM = {
|
||||
{&SelecMeter::total_active_energy_sensor_, SELEC_TOTAL_ACTIVE_ENERGY, false},
|
||||
{&SelecMeter::import_active_energy_sensor_, SELEC_IMPORT_ACTIVE_ENERGY, false},
|
||||
{&SelecMeter::export_active_energy_sensor_, SELEC_EXPORT_ACTIVE_ENERGY, false},
|
||||
{&SelecMeter::total_reactive_energy_sensor_, SELEC_TOTAL_REACTIVE_ENERGY, false},
|
||||
{&SelecMeter::import_reactive_energy_sensor_, SELEC_IMPORT_REACTIVE_ENERGY, false},
|
||||
{&SelecMeter::export_reactive_energy_sensor_, SELEC_EXPORT_REACTIVE_ENERGY, false},
|
||||
{&SelecMeter::apparent_energy_sensor_, SELEC_APPARENT_ENERGY, false},
|
||||
{&SelecMeter::active_power_sensor_, SELEC_ACTIVE_POWER, true},
|
||||
{&SelecMeter::reactive_power_sensor_, SELEC_REACTIVE_POWER, true},
|
||||
{&SelecMeter::apparent_power_sensor_, SELEC_APPARENT_POWER, true},
|
||||
{&SelecMeter::voltage_sensor_, SELEC_VOLTAGE, false},
|
||||
{&SelecMeter::current_sensor_, SELEC_CURRENT, false},
|
||||
{&SelecMeter::power_factor_sensor_, SELEC_POWER_FACTOR, false},
|
||||
{&SelecMeter::frequency_sensor_, SELEC_FREQUENCY, false},
|
||||
{&SelecMeter::maximum_demand_active_power_sensor_, SELEC_MAXIMUM_DEMAND_ACTIVE_POWER, true},
|
||||
{&SelecMeter::maximum_demand_reactive_power_sensor_, SELEC_MAXIMUM_DEMAND_REACTIVE_POWER, true},
|
||||
{&SelecMeter::maximum_demand_apparent_power_sensor_, SELEC_MAXIMUM_DEMAND_APPARENT_POWER, true},
|
||||
};
|
||||
return this->publish_fields_(registers, EM2M_FIELDS, EM2M_REGISTER_COUNT);
|
||||
}
|
||||
|
||||
#ifdef USE_SELEC_METER_EM4M
|
||||
bool SelecMeter::decode_em4m_(std::span<const uint16_t> registers) {
|
||||
static constexpr FloatField EM4M_FIELDS[] PROGMEM = {
|
||||
// Common quantities, shared sensor keys with EM2M
|
||||
{&SelecMeter::voltage_sensor_, EM4M_VOLTAGE, false},
|
||||
{&SelecMeter::current_sensor_, EM4M_CURRENT, false},
|
||||
{&SelecMeter::active_power_sensor_, EM4M_ACTIVE_POWER, true},
|
||||
{&SelecMeter::reactive_power_sensor_, EM4M_REACTIVE_POWER, true},
|
||||
{&SelecMeter::apparent_power_sensor_, EM4M_APPARENT_POWER, true},
|
||||
{&SelecMeter::power_factor_sensor_, EM4M_POWER_FACTOR, false},
|
||||
{&SelecMeter::frequency_sensor_, EM4M_FREQUENCY, false},
|
||||
{&SelecMeter::maximum_demand_active_power_sensor_, EM4M_MAXIMUM_DEMAND_ACTIVE_POWER, true},
|
||||
{&SelecMeter::maximum_demand_reactive_power_sensor_, EM4M_MAXIMUM_DEMAND_REACTIVE_POWER, true},
|
||||
{&SelecMeter::maximum_demand_apparent_power_sensor_, EM4M_MAXIMUM_DEMAND_APPARENT_POWER, true},
|
||||
{&SelecMeter::import_active_energy_sensor_, EM4M_IMPORT_ACTIVE_ENERGY, false},
|
||||
{&SelecMeter::export_active_energy_sensor_, EM4M_EXPORT_ACTIVE_ENERGY, false},
|
||||
{&SelecMeter::import_reactive_energy_sensor_, EM4M_IMPORT_REACTIVE_ENERGY, false},
|
||||
{&SelecMeter::export_reactive_energy_sensor_, EM4M_EXPORT_REACTIVE_ENERGY, false},
|
||||
// Line-to-line and net (mains/DG) quantities have no per-phase equivalent
|
||||
{&SelecMeter::voltage_l12_sensor_, EM4M_VOLTAGE_L12, false},
|
||||
{&SelecMeter::voltage_l23_sensor_, EM4M_VOLTAGE_L23, false},
|
||||
{&SelecMeter::voltage_l31_sensor_, EM4M_VOLTAGE_L31, false},
|
||||
{&SelecMeter::average_voltage_ll_sensor_, EM4M_AVERAGE_VOLTAGE_LL, false},
|
||||
{&SelecMeter::net_active_energy_mains_sensor_, EM4M_NET_ACTIVE_ENERGY_MAINS, false},
|
||||
{&SelecMeter::net_reactive_energy_mains_sensor_, EM4M_NET_REACTIVE_ENERGY_MAINS, false},
|
||||
{&SelecMeter::net_apparent_energy_mains_sensor_, EM4M_NET_APPARENT_ENERGY_MAINS, false},
|
||||
{&SelecMeter::net_active_energy_dg_sensor_, EM4M_NET_ACTIVE_ENERGY_DG, false},
|
||||
{&SelecMeter::net_reactive_energy_dg_sensor_, EM4M_NET_REACTIVE_ENERGY_DG, false},
|
||||
{&SelecMeter::net_apparent_energy_dg_sensor_, EM4M_NET_APPARENT_ENERGY_DG, false},
|
||||
};
|
||||
|
||||
// reg_l1 is the L1 register; L2/L3 follow EM4M_PHASE_STRIDE apart
|
||||
struct PhaseField {
|
||||
sensor::Sensor *Phase::*sensor;
|
||||
uint16_t reg_l1;
|
||||
bool kilo;
|
||||
};
|
||||
static constexpr PhaseField PHASE_FIELDS[] PROGMEM = {
|
||||
{&Phase::voltage_sensor_, EM4M_VOLTAGE_L1, false},
|
||||
{&Phase::current_sensor_, EM4M_CURRENT_L1, false},
|
||||
{&Phase::active_power_sensor_, EM4M_ACTIVE_POWER_L1, true},
|
||||
{&Phase::reactive_power_sensor_, EM4M_REACTIVE_POWER_L1, true},
|
||||
{&Phase::apparent_power_sensor_, EM4M_APPARENT_POWER_L1, true},
|
||||
{&Phase::power_factor_sensor_, EM4M_POWER_FACTOR_L1, false},
|
||||
{&Phase::import_active_energy_sensor_, EM4M_IMPORT_ACTIVE_ENERGY_L1, false},
|
||||
{&Phase::export_active_energy_sensor_, EM4M_EXPORT_ACTIVE_ENERGY_L1, false},
|
||||
{&Phase::import_reactive_energy_sensor_, EM4M_IMPORT_REACTIVE_ENERGY_L1, false},
|
||||
{&Phase::export_reactive_energy_sensor_, EM4M_EXPORT_REACTIVE_ENERGY_L1, false},
|
||||
{&Phase::apparent_energy_sensor_, EM4M_APPARENT_ENERGY_L1, false},
|
||||
};
|
||||
|
||||
if (!this->publish_fields_(registers, EM4M_FIELDS, EM4M_REGISTER_COUNT))
|
||||
return false;
|
||||
for (const auto &entry : PHASE_FIELDS) {
|
||||
PhaseField field;
|
||||
progmem_memcpy(&field, &entry, sizeof(field));
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
sensor::Sensor *sens = this->phases_[phase].*field.sensor;
|
||||
if (sens != nullptr)
|
||||
publish_float(sens, registers, field.reg_l1 + phase * EM4M_PHASE_STRIDE, field.kilo, this->word_swap_);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void SelecMeter::update() {
|
||||
if (this->read_state_ != ReadState::READ_STATE_IDLE) {
|
||||
ESP_LOGD(TAG, "Skipping update: previous read cycle (%s) still in progress",
|
||||
LOG_STR_ARG(read_state_name(this->read_state_)));
|
||||
return;
|
||||
}
|
||||
this->start_read_(ReadState::READ_STATE_MAIN_BLOCK);
|
||||
}
|
||||
|
||||
publish(this->total_active_energy_sensor_, SELEC_TOTAL_ACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->import_active_energy_sensor_, SELEC_IMPORT_ACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->export_active_energy_sensor_, SELEC_EXPORT_ACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->total_reactive_energy_sensor_, SELEC_TOTAL_REACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->import_reactive_energy_sensor_, SELEC_IMPORT_REACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->export_reactive_energy_sensor_, SELEC_EXPORT_REACTIVE_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->apparent_energy_sensor_, SELEC_APPARENT_ENERGY, NO_DEC_UNIT);
|
||||
publish(this->active_power_sensor_, SELEC_ACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->reactive_power_sensor_, SELEC_REACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->apparent_power_sensor_, SELEC_APPARENT_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->voltage_sensor_, SELEC_VOLTAGE, NO_DEC_UNIT);
|
||||
publish(this->current_sensor_, SELEC_CURRENT, NO_DEC_UNIT);
|
||||
publish(this->power_factor_sensor_, SELEC_POWER_FACTOR, NO_DEC_UNIT);
|
||||
publish(this->frequency_sensor_, SELEC_FREQUENCY, NO_DEC_UNIT);
|
||||
publish(this->maximum_demand_active_power_sensor_, SELEC_MAXIMUM_DEMAND_ACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->maximum_demand_reactive_power_sensor_, SELEC_MAXIMUM_DEMAND_REACTIVE_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
publish(this->maximum_demand_apparent_power_sensor_, SELEC_MAXIMUM_DEMAND_APPARENT_POWER, MULTIPLY_THOUSAND_UNIT);
|
||||
void SelecMeter::start_read_(ReadState state) {
|
||||
this->read_state_ = state;
|
||||
if (state == ReadState::READ_STATE_IDLE)
|
||||
return;
|
||||
const ReadRange range = this->read_range_(state);
|
||||
// A refused send gets no callback, so unwind here like the async failure paths
|
||||
if (!this->read_input_registers(range.start, range.count))
|
||||
this->fail_current_read_(LOG_STR("Modbus request refused"));
|
||||
}
|
||||
|
||||
void SelecMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); }
|
||||
void SelecMeter::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"SELEC Meter:\n"
|
||||
" Model: %s\n"
|
||||
" Byte Order: %s\n"
|
||||
" Address: 0x%02X",
|
||||
this->model_ == Model::MODEL_EM4M ? LOG_STR_LITERAL("EM4M") : LOG_STR_LITERAL("EM2M"),
|
||||
this->word_swap_ ? LOG_STR_LITERAL("little_endian (LSRF)") : LOG_STR_LITERAL("big_endian (MSRF)"),
|
||||
this->address_);
|
||||
LOG_SENSOR(" ", "Total Active Energy", this->total_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Import Active Energy", this->import_active_energy_sensor_);
|
||||
@@ -67,6 +371,41 @@ void SelecMeter::dump_config() {
|
||||
LOG_SENSOR(" ", "Maximum Demand Active Power", this->maximum_demand_active_power_sensor_);
|
||||
LOG_SENSOR(" ", "Maximum Demand Reactive Power", this->maximum_demand_reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "Maximum Demand Apparent Power", this->maximum_demand_apparent_power_sensor_);
|
||||
#ifdef USE_SELEC_METER_EM4M
|
||||
if (this->model_ == Model::MODEL_EM4M) {
|
||||
LOG_SENSOR(" ", "Voltage L1-L2", this->voltage_l12_sensor_);
|
||||
LOG_SENSOR(" ", "Voltage L2-L3", this->voltage_l23_sensor_);
|
||||
LOG_SENSOR(" ", "Voltage L3-L1", this->voltage_l31_sensor_);
|
||||
LOG_SENSOR(" ", "Average Voltage LL", this->average_voltage_ll_sensor_);
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
const auto &p = this->phases_[phase];
|
||||
ESP_LOGCONFIG(TAG, " Phase %c", 'A' + phase);
|
||||
LOG_SENSOR(" ", "Voltage", p.voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", p.current_sensor_);
|
||||
LOG_SENSOR(" ", "Active Power", p.active_power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power", p.reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "Apparent Power", p.apparent_power_sensor_);
|
||||
LOG_SENSOR(" ", "Power Factor", p.power_factor_sensor_);
|
||||
LOG_SENSOR(" ", "Import Active Energy", p.import_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Export Active Energy", p.export_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Import Reactive Energy", p.import_reactive_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Export Reactive Energy", p.export_reactive_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Apparent Energy", p.apparent_energy_sensor_);
|
||||
}
|
||||
LOG_SENSOR(" ", "Net Active Energy (Mains)", this->net_active_energy_mains_sensor_);
|
||||
LOG_SENSOR(" ", "Net Reactive Energy (Mains)", this->net_reactive_energy_mains_sensor_);
|
||||
LOG_SENSOR(" ", "Net Apparent Energy (Mains)", this->net_apparent_energy_mains_sensor_);
|
||||
LOG_SENSOR(" ", "Net Active Energy (DG)", this->net_active_energy_dg_sensor_);
|
||||
LOG_SENSOR(" ", "Net Reactive Energy (DG)", this->net_reactive_energy_dg_sensor_);
|
||||
LOG_SENSOR(" ", "Net Apparent Energy (DG)", this->net_apparent_energy_dg_sensor_);
|
||||
}
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Serial Number", this->serial_number_sensor_);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "DG Sensing", this->dg_sensing_sensor_);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::selec_meter
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
#include <span>
|
||||
|
||||
@@ -15,6 +22,24 @@ namespace esphome::selec_meter {
|
||||
public: \
|
||||
void set_##name##_sensor(sensor::Sensor *(name)) { this->name##_sensor_ = name; }
|
||||
|
||||
// Per-phase sensor in the Phase struct, setter indexed by phase (0 = L1/A, 1 = L2/B, 2 = L3/C)
|
||||
#define SELEC_METER_PHASE_SENSOR(name) \
|
||||
void set_##name##_sensor(uint8_t phase, sensor::Sensor *(name)) { this->phases_[phase].name##_sensor_ = name; }
|
||||
|
||||
enum class Model : uint8_t {
|
||||
MODEL_EM2M,
|
||||
MODEL_EM4M,
|
||||
};
|
||||
|
||||
// Read cycle steps in the order they run. The extra reads (serial number, DG sensing) sit far outside
|
||||
// the main block and get their own transaction.
|
||||
enum class ReadState : uint8_t {
|
||||
READ_STATE_MAIN_BLOCK,
|
||||
READ_STATE_SERIAL_NUMBER,
|
||||
READ_STATE_DG_SENSING,
|
||||
READ_STATE_IDLE,
|
||||
};
|
||||
|
||||
class SelecMeter final : public PollingComponent, public modbus::ModbusClientDevice {
|
||||
public:
|
||||
SELEC_METER_SENSOR(total_active_energy)
|
||||
@@ -35,12 +60,122 @@ class SelecMeter final : public PollingComponent, public modbus::ModbusClientDev
|
||||
SELEC_METER_SENSOR(maximum_demand_reactive_power)
|
||||
SELEC_METER_SENSOR(maximum_demand_apparent_power)
|
||||
|
||||
#ifdef USE_SELEC_METER_EM4M
|
||||
// EM4M per-phase sensors, indexed by phase (see Phase below)
|
||||
SELEC_METER_PHASE_SENSOR(voltage)
|
||||
SELEC_METER_PHASE_SENSOR(current)
|
||||
SELEC_METER_PHASE_SENSOR(active_power)
|
||||
SELEC_METER_PHASE_SENSOR(reactive_power)
|
||||
SELEC_METER_PHASE_SENSOR(apparent_power)
|
||||
SELEC_METER_PHASE_SENSOR(power_factor)
|
||||
SELEC_METER_PHASE_SENSOR(import_active_energy)
|
||||
SELEC_METER_PHASE_SENSOR(export_active_energy)
|
||||
SELEC_METER_PHASE_SENSOR(import_reactive_energy)
|
||||
SELEC_METER_PHASE_SENSOR(export_reactive_energy)
|
||||
SELEC_METER_PHASE_SENSOR(apparent_energy)
|
||||
|
||||
// Line-to-line and aggregate sensors, not tied to one phase
|
||||
SELEC_METER_SENSOR(voltage_l12)
|
||||
SELEC_METER_SENSOR(voltage_l23)
|
||||
SELEC_METER_SENSOR(voltage_l31)
|
||||
SELEC_METER_SENSOR(average_voltage_ll)
|
||||
SELEC_METER_SENSOR(net_active_energy_mains)
|
||||
SELEC_METER_SENSOR(net_reactive_energy_mains)
|
||||
SELEC_METER_SENSOR(net_apparent_energy_mains)
|
||||
SELEC_METER_SENSOR(net_active_energy_dg)
|
||||
SELEC_METER_SENSOR(net_reactive_energy_dg)
|
||||
SELEC_METER_SENSOR(net_apparent_energy_dg)
|
||||
#endif
|
||||
|
||||
void set_model(Model model) { this->model_ = model; }
|
||||
// true: registers are word-swapped (LSRF / Mid Little Endian, CDAB byte order)
|
||||
// false: registers are in the meter's straightforward order (MSRF / Big Endian, ABCD byte order)
|
||||
void set_word_swap(bool word_swap) { this->word_swap_ = word_swap; }
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_TEXT_SENSOR)
|
||||
void set_serial_number_sensor(text_sensor::TextSensor *serial_number) { this->serial_number_sensor_ = serial_number; }
|
||||
#endif
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_BINARY_SENSOR)
|
||||
void set_dg_sensing_sensor(binary_sensor::BinarySensor *dg_sensing) { this->dg_sensing_sensor_ = dg_sensing; }
|
||||
#endif
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
bool on_no_response(std::span<const uint8_t> request_pdu) override;
|
||||
void on_not_sent(std::span<const uint8_t> request_pdu) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
// One float field of the main block: target sensor, register and whether it is published x1000
|
||||
struct FloatField {
|
||||
sensor::Sensor *SelecMeter::*sensor;
|
||||
uint16_t reg;
|
||||
bool kilo;
|
||||
};
|
||||
|
||||
// Publishes each configured field in `fields` (a PROGMEM table); false on a payload shorter than
|
||||
// `min_count`, and the caller sets or clears the warning from the result
|
||||
bool publish_fields_(std::span<const uint16_t> registers, std::span<const FloatField> fields, uint8_t min_count);
|
||||
bool decode_em2m_(std::span<const uint16_t> registers);
|
||||
#ifdef USE_SELEC_METER_EM4M
|
||||
bool decode_em4m_(std::span<const uint16_t> registers);
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void decode_serial_number_(std::span<const uint16_t> registers);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void decode_dg_sensing_(std::span<const uint16_t> registers);
|
||||
#endif
|
||||
#endif
|
||||
// First register and register count of the read for `state`
|
||||
struct ReadRange {
|
||||
uint16_t start;
|
||||
uint8_t count;
|
||||
};
|
||||
ReadRange read_range_(ReadState state) const;
|
||||
// Whether the read cycle should run `state` on this pass
|
||||
bool wanted_(ReadState state) const;
|
||||
// Next wanted step after `current`, so one failed range does not cancel the other reads
|
||||
ReadState next_read_state_after_(ReadState current) const;
|
||||
// Sends the read for `state`; safe from a callback per the modbus callback contract
|
||||
void start_read_(ReadState state);
|
||||
// Clears the pending read and advances the chain. Only a MAIN_BLOCK failure sets the warning and
|
||||
// only a `durable` refusal counts toward MAX_OPTIONAL_READ_FAILURES; `reason` nullptr means already logged
|
||||
void fail_current_read_(const LogString *reason, bool durable = false);
|
||||
|
||||
#ifdef USE_SELEC_METER_EM4M
|
||||
// phases_[0..2] are L1/L2/L3 (A/B/C), EM4M_PHASE_STRIDE registers apart
|
||||
struct Phase {
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
sensor::Sensor *active_power_sensor_{nullptr};
|
||||
sensor::Sensor *reactive_power_sensor_{nullptr};
|
||||
sensor::Sensor *apparent_power_sensor_{nullptr};
|
||||
sensor::Sensor *power_factor_sensor_{nullptr};
|
||||
sensor::Sensor *import_active_energy_sensor_{nullptr};
|
||||
sensor::Sensor *export_active_energy_sensor_{nullptr};
|
||||
sensor::Sensor *import_reactive_energy_sensor_{nullptr};
|
||||
sensor::Sensor *export_reactive_energy_sensor_{nullptr};
|
||||
sensor::Sensor *apparent_energy_sensor_{nullptr};
|
||||
};
|
||||
Phase phases_[3];
|
||||
#endif
|
||||
|
||||
Model model_{Model::MODEL_EM2M};
|
||||
bool word_swap_{false};
|
||||
ReadState read_state_{ReadState::READ_STATE_IDLE};
|
||||
|
||||
// An optional read stops once its counter reaches MAX_OPTIONAL_READ_FAILURES
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_TEXT_SENSOR)
|
||||
text_sensor::TextSensor *serial_number_sensor_{nullptr};
|
||||
// Also set to the limit once the serial number is published, since it never changes
|
||||
uint8_t serial_number_failures_{0};
|
||||
#endif
|
||||
#if defined(USE_SELEC_METER_EM4M) && defined(USE_BINARY_SENSOR)
|
||||
binary_sensor::BinarySensor *dg_sensing_sensor_{nullptr};
|
||||
uint8_t dg_sensing_failures_{0};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::selec_meter
|
||||
|
||||
@@ -1,30 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::selec_meter {
|
||||
|
||||
static const float TWO_DEC_UNIT = 0.01;
|
||||
static const float ONE_DEC_UNIT = 0.1;
|
||||
static const float NO_DEC_UNIT = 1;
|
||||
static const float MULTIPLY_TEN_UNIT = 10;
|
||||
static const float MULTIPLY_THOUSAND_UNIT = 1000;
|
||||
|
||||
/* PHASE STATUS REGISTERS */
|
||||
static const uint16_t SELEC_TOTAL_ACTIVE_ENERGY = 0x0000;
|
||||
static const uint16_t SELEC_IMPORT_ACTIVE_ENERGY = 0x0002;
|
||||
static const uint16_t SELEC_EXPORT_ACTIVE_ENERGY = 0x0004;
|
||||
static const uint16_t SELEC_TOTAL_REACTIVE_ENERGY = 0x0006;
|
||||
static const uint16_t SELEC_IMPORT_REACTIVE_ENERGY = 0x0008;
|
||||
static const uint16_t SELEC_EXPORT_REACTIVE_ENERGY = 0x000A;
|
||||
static const uint16_t SELEC_APPARENT_ENERGY = 0x000C;
|
||||
static const uint16_t SELEC_ACTIVE_POWER = 0x000E;
|
||||
static const uint16_t SELEC_REACTIVE_POWER = 0x0010;
|
||||
static const uint16_t SELEC_APPARENT_POWER = 0x0012;
|
||||
static const uint16_t SELEC_VOLTAGE = 0x0014;
|
||||
static const uint16_t SELEC_CURRENT = 0x0016;
|
||||
static const uint16_t SELEC_POWER_FACTOR = 0x0018;
|
||||
static const uint16_t SELEC_FREQUENCY = 0x001A;
|
||||
static const uint16_t SELEC_MAXIMUM_DEMAND_ACTIVE_POWER = 0x001C;
|
||||
static const uint16_t SELEC_MAXIMUM_DEMAND_REACTIVE_POWER = 0x001E;
|
||||
static const uint16_t SELEC_MAXIMUM_DEMAND_APPARENT_POWER = 0x0020;
|
||||
static constexpr uint16_t SELEC_TOTAL_ACTIVE_ENERGY = 0x0000;
|
||||
static constexpr uint16_t SELEC_IMPORT_ACTIVE_ENERGY = 0x0002;
|
||||
static constexpr uint16_t SELEC_EXPORT_ACTIVE_ENERGY = 0x0004;
|
||||
static constexpr uint16_t SELEC_TOTAL_REACTIVE_ENERGY = 0x0006;
|
||||
static constexpr uint16_t SELEC_IMPORT_REACTIVE_ENERGY = 0x0008;
|
||||
static constexpr uint16_t SELEC_EXPORT_REACTIVE_ENERGY = 0x000A;
|
||||
static constexpr uint16_t SELEC_APPARENT_ENERGY = 0x000C;
|
||||
static constexpr uint16_t SELEC_ACTIVE_POWER = 0x000E;
|
||||
static constexpr uint16_t SELEC_REACTIVE_POWER = 0x0010;
|
||||
static constexpr uint16_t SELEC_APPARENT_POWER = 0x0012;
|
||||
static constexpr uint16_t SELEC_VOLTAGE = 0x0014;
|
||||
static constexpr uint16_t SELEC_CURRENT = 0x0016;
|
||||
static constexpr uint16_t SELEC_POWER_FACTOR = 0x0018;
|
||||
static constexpr uint16_t SELEC_FREQUENCY = 0x001A;
|
||||
static constexpr uint16_t SELEC_MAXIMUM_DEMAND_ACTIVE_POWER = 0x001C;
|
||||
static constexpr uint16_t SELEC_MAXIMUM_DEMAND_REACTIVE_POWER = 0x001E;
|
||||
static constexpr uint16_t SELEC_MAXIMUM_DEMAND_APPARENT_POWER = 0x0020;
|
||||
|
||||
/* EM4M REGISTERS -- offsets are register indices from input register 30000 */
|
||||
static constexpr uint8_t EM4M_REGISTER_COUNT = 114; // 114 x 16-bit registers
|
||||
|
||||
/* EM4M -- common quantities shared with the EM2M sensor keys */
|
||||
static constexpr uint16_t EM4M_VOLTAGE = 0x0006; // Average Voltage LN
|
||||
static constexpr uint16_t EM4M_CURRENT = 0x0016; // Average Current
|
||||
static constexpr uint16_t EM4M_ACTIVE_POWER = 0x002A; // Total kW
|
||||
static constexpr uint16_t EM4M_REACTIVE_POWER = 0x002C; // Total kVAr
|
||||
static constexpr uint16_t EM4M_APPARENT_POWER = 0x002E; // Total kVA
|
||||
static constexpr uint16_t EM4M_POWER_FACTOR = 0x0036; // Average PF
|
||||
static constexpr uint16_t EM4M_FREQUENCY = 0x0038;
|
||||
static constexpr uint16_t EM4M_MAXIMUM_DEMAND_ACTIVE_POWER = 0x0046;
|
||||
static constexpr uint16_t EM4M_MAXIMUM_DEMAND_REACTIVE_POWER = 0x0048;
|
||||
static constexpr uint16_t EM4M_MAXIMUM_DEMAND_APPARENT_POWER = 0x004A;
|
||||
static constexpr uint16_t EM4M_IMPORT_ACTIVE_ENERGY = 0x0058; // Total kWh - Import
|
||||
static constexpr uint16_t EM4M_EXPORT_ACTIVE_ENERGY = 0x005A; // Total kWh - Export
|
||||
static constexpr uint16_t EM4M_IMPORT_REACTIVE_ENERGY = 0x0068; // Total kVArh - Import
|
||||
static constexpr uint16_t EM4M_EXPORT_REACTIVE_ENERGY = 0x006A; // Total kVArh - Export
|
||||
static constexpr uint16_t EM4M_AVERAGE_VOLTAGE_LL = 0x000E; // Average Voltage LL
|
||||
|
||||
// Net (import - export) energy totals, split by source. Only meaningful when the
|
||||
// meter's "Dual Source" setting is Yes; DG registers read 0 on single-source installs.
|
||||
static constexpr uint16_t EM4M_NET_ACTIVE_ENERGY_MAINS = 0x003A; // Total Net kWh (MAINS)
|
||||
static constexpr uint16_t EM4M_NET_REACTIVE_ENERGY_MAINS = 0x003C; // Total Net kVArh (MAINS)
|
||||
static constexpr uint16_t EM4M_NET_APPARENT_ENERGY_MAINS = 0x003E; // Total Net kVAh (MAINS)
|
||||
static constexpr uint16_t EM4M_NET_ACTIVE_ENERGY_DG = 0x0040; // Total Net kWh (DG)
|
||||
static constexpr uint16_t EM4M_NET_REACTIVE_ENERGY_DG = 0x0042; // Total Net kVArh (DG)
|
||||
static constexpr uint16_t EM4M_NET_APPARENT_ENERGY_DG = 0x0044; // Total Net kVAh (DG)
|
||||
|
||||
/* EM4M -- per-phase quantities, no EM2M equivalent */
|
||||
static constexpr uint16_t EM4M_VOLTAGE_L1 = 0x0000; // Voltage V1N
|
||||
static constexpr uint16_t EM4M_VOLTAGE_L2 = 0x0002; // Voltage V2N
|
||||
static constexpr uint16_t EM4M_VOLTAGE_L3 = 0x0004; // Voltage V3N
|
||||
static constexpr uint16_t EM4M_VOLTAGE_L12 = 0x0008; // Voltage V12
|
||||
static constexpr uint16_t EM4M_VOLTAGE_L23 = 0x000A; // Voltage V23
|
||||
static constexpr uint16_t EM4M_VOLTAGE_L31 = 0x000C; // Voltage V31
|
||||
static constexpr uint16_t EM4M_CURRENT_L1 = 0x0010;
|
||||
static constexpr uint16_t EM4M_CURRENT_L2 = 0x0012;
|
||||
static constexpr uint16_t EM4M_CURRENT_L3 = 0x0014;
|
||||
static constexpr uint16_t EM4M_ACTIVE_POWER_L1 = 0x0018; // kW1
|
||||
static constexpr uint16_t EM4M_ACTIVE_POWER_L2 = 0x001A; // kW2
|
||||
static constexpr uint16_t EM4M_ACTIVE_POWER_L3 = 0x001C; // kW3
|
||||
static constexpr uint16_t EM4M_REACTIVE_POWER_L1 = 0x001E; // kVAr1
|
||||
static constexpr uint16_t EM4M_REACTIVE_POWER_L2 = 0x0020; // kVAr2
|
||||
static constexpr uint16_t EM4M_REACTIVE_POWER_L3 = 0x0022; // kVAr3
|
||||
static constexpr uint16_t EM4M_APPARENT_POWER_L1 = 0x0024; // kVA1
|
||||
static constexpr uint16_t EM4M_APPARENT_POWER_L2 = 0x0026; // kVA2
|
||||
static constexpr uint16_t EM4M_APPARENT_POWER_L3 = 0x0028; // kVA3
|
||||
static constexpr uint16_t EM4M_POWER_FACTOR_L1 = 0x0030;
|
||||
static constexpr uint16_t EM4M_POWER_FACTOR_L2 = 0x0032;
|
||||
static constexpr uint16_t EM4M_POWER_FACTOR_L3 = 0x0034;
|
||||
static constexpr uint16_t EM4M_IMPORT_ACTIVE_ENERGY_L1 = 0x004C; // kWh1 - Import
|
||||
static constexpr uint16_t EM4M_IMPORT_ACTIVE_ENERGY_L2 = 0x004E; // kWh2 - Import
|
||||
static constexpr uint16_t EM4M_IMPORT_ACTIVE_ENERGY_L3 = 0x0050; // kWh3 - Import
|
||||
static constexpr uint16_t EM4M_EXPORT_ACTIVE_ENERGY_L1 = 0x0052; // kWh1 - Export
|
||||
static constexpr uint16_t EM4M_EXPORT_ACTIVE_ENERGY_L2 = 0x0054; // kWh2 - Export
|
||||
static constexpr uint16_t EM4M_EXPORT_ACTIVE_ENERGY_L3 = 0x0056; // kWh3 - Export
|
||||
static constexpr uint16_t EM4M_IMPORT_REACTIVE_ENERGY_L1 = 0x005C; // kVArh1 - Import
|
||||
static constexpr uint16_t EM4M_IMPORT_REACTIVE_ENERGY_L2 = 0x005E; // kVArh2 - Import
|
||||
static constexpr uint16_t EM4M_IMPORT_REACTIVE_ENERGY_L3 = 0x0060; // kVArh3 - Import
|
||||
static constexpr uint16_t EM4M_EXPORT_REACTIVE_ENERGY_L1 = 0x0062; // kVArh1 - Export
|
||||
static constexpr uint16_t EM4M_EXPORT_REACTIVE_ENERGY_L2 = 0x0064; // kVArh2 - Export
|
||||
static constexpr uint16_t EM4M_EXPORT_REACTIVE_ENERGY_L3 = 0x0066; // kVArh3 - Export
|
||||
static constexpr uint16_t EM4M_APPARENT_ENERGY_L1 = 0x006C; // kVAh-1
|
||||
static constexpr uint16_t EM4M_APPARENT_ENERGY_L2 = 0x006E; // kVAh-2
|
||||
static constexpr uint16_t EM4M_APPARENT_ENERGY_L3 = 0x0070; // kVAh-3
|
||||
|
||||
// The decoder reads L2/L3 as the L1 register plus one or two strides; the L2/L3 names above
|
||||
// document the datasheet and are checked against that stride here.
|
||||
static constexpr uint16_t EM4M_PHASE_STRIDE = 2;
|
||||
#define SELEC_EM4M_CHECK_STRIDE(name) \
|
||||
static_assert(EM4M_##name##_L2 == EM4M_##name##_L1 + EM4M_PHASE_STRIDE && \
|
||||
EM4M_##name##_L3 == EM4M_##name##_L1 + 2 * EM4M_PHASE_STRIDE, \
|
||||
#name " per-phase registers must be EM4M_PHASE_STRIDE apart");
|
||||
SELEC_EM4M_CHECK_STRIDE(VOLTAGE)
|
||||
SELEC_EM4M_CHECK_STRIDE(CURRENT)
|
||||
SELEC_EM4M_CHECK_STRIDE(ACTIVE_POWER)
|
||||
SELEC_EM4M_CHECK_STRIDE(REACTIVE_POWER)
|
||||
SELEC_EM4M_CHECK_STRIDE(APPARENT_POWER)
|
||||
SELEC_EM4M_CHECK_STRIDE(POWER_FACTOR)
|
||||
SELEC_EM4M_CHECK_STRIDE(IMPORT_ACTIVE_ENERGY)
|
||||
SELEC_EM4M_CHECK_STRIDE(EXPORT_ACTIVE_ENERGY)
|
||||
SELEC_EM4M_CHECK_STRIDE(IMPORT_REACTIVE_ENERGY)
|
||||
SELEC_EM4M_CHECK_STRIDE(EXPORT_REACTIVE_ENERGY)
|
||||
SELEC_EM4M_CHECK_STRIDE(APPARENT_ENERGY)
|
||||
#undef SELEC_EM4M_CHECK_STRIDE
|
||||
|
||||
// Far outside the main 0-113 register block; read via a separate Modbus transaction.
|
||||
static constexpr uint16_t EM4M_SERIAL_NUMBER = 0x02AC; // Serial No. (Hex), 30684
|
||||
static constexpr uint16_t EM4M_DG_SENSING = 0x02C6; // DG Sensing (1: Pass, 0: Fail), 30710
|
||||
|
||||
} // namespace esphome::selec_meter
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import modbus, sensor
|
||||
from esphome.components import binary_sensor, modbus, sensor, text_sensor
|
||||
from esphome.components.const import (
|
||||
BYTE_ORDER_BIG,
|
||||
BYTE_ORDER_LITTLE,
|
||||
CONF_BYTE_ORDER,
|
||||
CONF_SERIAL_NUMBER,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTIVE_POWER,
|
||||
@@ -11,6 +17,10 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_IMPORT_ACTIVE_ENERGY,
|
||||
CONF_IMPORT_REACTIVE_ENERGY,
|
||||
CONF_MODEL,
|
||||
CONF_PHASE_A,
|
||||
CONF_PHASE_B,
|
||||
CONF_PHASE_C,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_REACTIVE_POWER,
|
||||
CONF_VOLTAGE,
|
||||
@@ -24,6 +34,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ICON_CURRENT_AC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_HERTZ,
|
||||
@@ -37,9 +48,22 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["modbus"]
|
||||
CODEOWNERS = ["@sourabhjaiswal"]
|
||||
|
||||
CONF_DG_SENSING = "dg_sensing"
|
||||
|
||||
|
||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
return ["modbus"] + [
|
||||
component
|
||||
for key, component in (
|
||||
(CONF_SERIAL_NUMBER, "text_sensor"),
|
||||
(CONF_DG_SENSING, "binary_sensor"),
|
||||
)
|
||||
if not config or key in config
|
||||
]
|
||||
|
||||
|
||||
CONF_TOTAL_ACTIVE_ENERGY = "total_active_energy"
|
||||
CONF_TOTAL_REACTIVE_ENERGY = "total_reactive_energy"
|
||||
CONF_APPARENT_ENERGY = "apparent_energy"
|
||||
@@ -47,19 +71,68 @@ CONF_MAXIMUM_DEMAND_ACTIVE_POWER = "maximum_demand_active_power"
|
||||
CONF_MAXIMUM_DEMAND_REACTIVE_POWER = "maximum_demand_reactive_power"
|
||||
CONF_MAXIMUM_DEMAND_APPARENT_POWER = "maximum_demand_apparent_power"
|
||||
|
||||
CONF_VOLTAGE_L12 = "voltage_l12"
|
||||
CONF_VOLTAGE_L23 = "voltage_l23"
|
||||
CONF_VOLTAGE_L31 = "voltage_l31"
|
||||
|
||||
CONF_AVERAGE_VOLTAGE_LL = "average_voltage_ll"
|
||||
CONF_NET_ACTIVE_ENERGY_MAINS = "net_active_energy_mains"
|
||||
CONF_NET_REACTIVE_ENERGY_MAINS = "net_reactive_energy_mains"
|
||||
CONF_NET_APPARENT_ENERGY_MAINS = "net_apparent_energy_mains"
|
||||
CONF_NET_ACTIVE_ENERGY_DG = "net_active_energy_dg"
|
||||
CONF_NET_REACTIVE_ENERGY_DG = "net_reactive_energy_dg"
|
||||
CONF_NET_APPARENT_ENERGY_DG = "net_apparent_energy_dg"
|
||||
|
||||
selec_meter_ns = cg.esphome_ns.namespace("selec_meter")
|
||||
SelecMeter = selec_meter_ns.class_(
|
||||
"SelecMeter", cg.PollingComponent, modbus.ModbusClientDevice
|
||||
)
|
||||
SelecMeterModel = selec_meter_ns.enum("Model", is_class=True)
|
||||
|
||||
SENSORS = {
|
||||
MODEL_EM2M = "em2m"
|
||||
MODEL_EM4M = "em4m"
|
||||
MODELS = {
|
||||
MODEL_EM2M: SelecMeterModel.MODEL_EM2M,
|
||||
MODEL_EM4M: SelecMeterModel.MODEL_EM4M,
|
||||
}
|
||||
|
||||
# Register word order of the FP32 values: big_endian sends the high word first (ABCD),
|
||||
# little_endian the low word first (CDAB). msrf/lsrf are aliases matching the meter's own
|
||||
# register 40070 (Endianness Selection) menu names.
|
||||
BYTE_ORDER_MSRF = "msrf"
|
||||
BYTE_ORDER_LSRF = "lsrf"
|
||||
# Maps the YAML option directly to the C++ word_swap bool -- no enum needed on the C++ side.
|
||||
BYTE_ORDER_WORD_SWAP = {
|
||||
BYTE_ORDER_BIG: False,
|
||||
BYTE_ORDER_LITTLE: True,
|
||||
BYTE_ORDER_MSRF: False,
|
||||
BYTE_ORDER_LSRF: True,
|
||||
}
|
||||
validate_byte_order = cv.enum(BYTE_ORDER_WORD_SWAP, lower=True)
|
||||
|
||||
# EM2M-only: total combined energy figures with no non-DG equivalent on the EM4M.
|
||||
EM2M_SENSORS = {
|
||||
CONF_TOTAL_ACTIVE_ENERGY: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT_HOURS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
CONF_APPARENT_ENERGY: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
|
||||
# Sensors available on both EM2M and EM4M. On EM4M these read the aggregate
|
||||
# (Total/Average) registers rather than a single-phase measurement.
|
||||
COMMON_SENSORS = {
|
||||
CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOWATT_HOURS,
|
||||
accuracy_decimals=2,
|
||||
@@ -72,11 +145,6 @@ SENSORS = {
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
accuracy_decimals=2,
|
||||
@@ -87,11 +155,6 @@ SENSORS = {
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
CONF_APPARENT_ENERGY: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_TOTAL_INCREASING,
|
||||
),
|
||||
CONF_ACTIVE_POWER: sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=3,
|
||||
@@ -154,13 +217,115 @@ SENSORS = {
|
||||
),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema({cv.GenerateID(): cv.declare_id(SelecMeter)})
|
||||
.extend(
|
||||
{cv.Optional(sensor_name): schema for sensor_name, schema in SENSORS.items()}
|
||||
|
||||
def _sensor_group(*keys: str, **schema_kwargs) -> dict[str, cv.Schema]:
|
||||
"""One identical sensor_schema() per key."""
|
||||
return {key: sensor.sensor_schema(**schema_kwargs) for key in keys}
|
||||
|
||||
|
||||
# EM4M-only, genuinely per-phase quantities, nested under phase_a/phase_b/phase_c like
|
||||
# sdm_meter/atm90e32 do for 3-phase meters.
|
||||
PHASE_SENSORS = {
|
||||
key: {**COMMON_SENSORS, **EM2M_SENSORS}[key]
|
||||
for key in (
|
||||
CONF_VOLTAGE,
|
||||
CONF_CURRENT,
|
||||
CONF_ACTIVE_POWER,
|
||||
CONF_REACTIVE_POWER,
|
||||
CONF_APPARENT_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_IMPORT_ACTIVE_ENERGY,
|
||||
CONF_EXPORT_ACTIVE_ENERGY,
|
||||
CONF_IMPORT_REACTIVE_ENERGY,
|
||||
CONF_EXPORT_REACTIVE_ENERGY,
|
||||
CONF_APPARENT_ENERGY,
|
||||
)
|
||||
.extend(cv.polling_component_schema("10s"))
|
||||
.extend(modbus.modbus_device_schema(0x01))
|
||||
}
|
||||
|
||||
PHASE_SCHEMA = cv.Schema(
|
||||
{cv.Optional(key): schema for key, schema in PHASE_SENSORS.items()}
|
||||
)
|
||||
|
||||
# EM4M-only, line-to-line and mains/DG net totals -- these don't map to a single phase, so they
|
||||
# stay flat rather than living under phase_a/phase_b/phase_c.
|
||||
EM4M_SENSORS = {
|
||||
**_sensor_group(
|
||||
CONF_VOLTAGE_L12,
|
||||
CONF_VOLTAGE_L23,
|
||||
CONF_VOLTAGE_L31,
|
||||
CONF_AVERAGE_VOLTAGE_LL,
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
# Net (import - export) energy totals, split by source. Only meaningful when the meter's
|
||||
# "Dual Source" setting is Yes; DG registers read 0 on single-source installs.
|
||||
**_sensor_group(
|
||||
CONF_NET_ACTIVE_ENERGY_MAINS,
|
||||
CONF_NET_ACTIVE_ENERGY_DG,
|
||||
unit_of_measurement=UNIT_KILOWATT_HOURS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_ENERGY,
|
||||
state_class=STATE_CLASS_TOTAL,
|
||||
),
|
||||
**_sensor_group(
|
||||
CONF_NET_REACTIVE_ENERGY_MAINS,
|
||||
CONF_NET_REACTIVE_ENERGY_DG,
|
||||
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_TOTAL,
|
||||
),
|
||||
**_sensor_group(
|
||||
CONF_NET_APPARENT_ENERGY_MAINS,
|
||||
CONF_NET_APPARENT_ENERGY_DG,
|
||||
unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_TOTAL,
|
||||
),
|
||||
}
|
||||
|
||||
PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C)
|
||||
|
||||
|
||||
def _optional(sensors: dict[str, cv.Schema]) -> dict[cv.Optional, cv.Schema]:
|
||||
return {cv.Optional(key): schema for key, schema in sensors.items()}
|
||||
|
||||
|
||||
_BASE_SCHEMA = cv.Schema({cv.GenerateID(): cv.declare_id(SelecMeter)}).extend(
|
||||
cv.polling_component_schema("10s"), modbus.modbus_device_schema(0x01)
|
||||
)
|
||||
|
||||
# EM2M keeps this component's original word-swapped decode; EM4M uses the meter's factory
|
||||
# default (MSRF / Big Endian), confirmed against real hardware.
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
MODEL_EM2M: _BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_BYTE_ORDER, default=BYTE_ORDER_LITTLE
|
||||
): validate_byte_order,
|
||||
**_optional(COMMON_SENSORS),
|
||||
**_optional(EM2M_SENSORS),
|
||||
}
|
||||
),
|
||||
MODEL_EM4M: _BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_BYTE_ORDER, default=BYTE_ORDER_BIG
|
||||
): validate_byte_order,
|
||||
**_optional(COMMON_SENSORS),
|
||||
**_optional(EM4M_SENSORS),
|
||||
**{cv.Optional(phase): PHASE_SCHEMA for phase in PHASES},
|
||||
cv.Optional(CONF_SERIAL_NUMBER): text_sensor.text_sensor_schema(),
|
||||
cv.Optional(CONF_DG_SENSING): binary_sensor.binary_sensor_schema(),
|
||||
}
|
||||
),
|
||||
},
|
||||
key=CONF_MODEL,
|
||||
default_type=MODEL_EM2M,
|
||||
enum=MODELS,
|
||||
lower=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -175,7 +340,58 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await modbus.register_modbus_client_device(var, config)
|
||||
for name in SENSORS:
|
||||
if name in config:
|
||||
sens = await sensor.new_sensor(config[name])
|
||||
cg.add(getattr(var, f"set_{name}_sensor")(sens))
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
cg.add(var.set_word_swap(config[CONF_BYTE_ORDER]))
|
||||
if config[CONF_MODEL] == MODEL_EM4M:
|
||||
cg.add_define("USE_SELEC_METER_EM4M")
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TOTAL_ACTIVE_ENERGY, var.set_total_active_energy_sensor)
|
||||
await sensors(CONF_IMPORT_ACTIVE_ENERGY, var.set_import_active_energy_sensor)
|
||||
await sensors(CONF_EXPORT_ACTIVE_ENERGY, var.set_export_active_energy_sensor)
|
||||
await sensors(CONF_TOTAL_REACTIVE_ENERGY, var.set_total_reactive_energy_sensor)
|
||||
await sensors(CONF_IMPORT_REACTIVE_ENERGY, var.set_import_reactive_energy_sensor)
|
||||
await sensors(CONF_EXPORT_REACTIVE_ENERGY, var.set_export_reactive_energy_sensor)
|
||||
await sensors(CONF_APPARENT_ENERGY, var.set_apparent_energy_sensor)
|
||||
await sensors(CONF_ACTIVE_POWER, var.set_active_power_sensor)
|
||||
await sensors(CONF_REACTIVE_POWER, var.set_reactive_power_sensor)
|
||||
await sensors(CONF_APPARENT_POWER, var.set_apparent_power_sensor)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT, var.set_current_sensor)
|
||||
await sensors(CONF_POWER_FACTOR, var.set_power_factor_sensor)
|
||||
await sensors(CONF_FREQUENCY, var.set_frequency_sensor)
|
||||
await sensors(
|
||||
CONF_MAXIMUM_DEMAND_ACTIVE_POWER, var.set_maximum_demand_active_power_sensor
|
||||
)
|
||||
await sensors(
|
||||
CONF_MAXIMUM_DEMAND_REACTIVE_POWER, var.set_maximum_demand_reactive_power_sensor
|
||||
)
|
||||
await sensors(
|
||||
CONF_MAXIMUM_DEMAND_APPARENT_POWER, var.set_maximum_demand_apparent_power_sensor
|
||||
)
|
||||
await sensors(CONF_VOLTAGE_L12, var.set_voltage_l12_sensor)
|
||||
await sensors(CONF_VOLTAGE_L23, var.set_voltage_l23_sensor)
|
||||
await sensors(CONF_VOLTAGE_L31, var.set_voltage_l31_sensor)
|
||||
await sensors(CONF_AVERAGE_VOLTAGE_LL, var.set_average_voltage_ll_sensor)
|
||||
await sensors(CONF_NET_ACTIVE_ENERGY_MAINS, var.set_net_active_energy_mains_sensor)
|
||||
await sensors(
|
||||
CONF_NET_REACTIVE_ENERGY_MAINS, var.set_net_reactive_energy_mains_sensor
|
||||
)
|
||||
await sensors(
|
||||
CONF_NET_APPARENT_ENERGY_MAINS, var.set_net_apparent_energy_mains_sensor
|
||||
)
|
||||
await sensors(CONF_NET_ACTIVE_ENERGY_DG, var.set_net_active_energy_dg_sensor)
|
||||
await sensors(CONF_NET_REACTIVE_ENERGY_DG, var.set_net_reactive_energy_dg_sensor)
|
||||
await sensors(CONF_NET_APPARENT_ENERGY_DG, var.set_net_apparent_energy_dg_sensor)
|
||||
for phase_index, phase in enumerate(PHASES):
|
||||
if (phase_config := config.get(phase)) is None:
|
||||
continue
|
||||
for name in PHASE_SENSORS:
|
||||
if name in phase_config:
|
||||
sens = await sensor.new_sensor(phase_config[name])
|
||||
cg.add(getattr(var, f"set_{name}_sensor")(phase_index, sens))
|
||||
await text_sensor.sub_text_sensors(config)(
|
||||
CONF_SERIAL_NUMBER, var.set_serial_number_sensor
|
||||
)
|
||||
await binary_sensor.sub_binary_sensors(config)(
|
||||
CONF_DG_SENSING, var.set_dg_sensing_sensor
|
||||
)
|
||||
|
||||
@@ -113,7 +113,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WIND_SPEED,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
|
||||
from esphome.core.entity_helpers import (
|
||||
SubEntities,
|
||||
@@ -421,16 +421,30 @@ async def multiply_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id, template_)
|
||||
|
||||
|
||||
TemplatableFloat = cg.esphome_ns.class_("TemplatableFn").template(cg.float_)
|
||||
|
||||
|
||||
async def _value_list_table(values: list[Any]) -> MockObj:
|
||||
"""PROGMEM value table, ended by an empty entry so filters store no count."""
|
||||
fns = [await cg.templatable(x, [], cg.float_) for x in values]
|
||||
# Identical lambdas (e.g. a YAML anchor) may keep static state, so only constants share.
|
||||
return cg.shared_progmem_array(
|
||||
"sensor_value_list",
|
||||
TemplatableFloat,
|
||||
[*fns, TemplatableFloat()],
|
||||
share=not any(isinstance(v, Lambda) for v in values),
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"filter_out",
|
||||
FilterOutValueFilter,
|
||||
cv.Any(cv.templatable(cv.float_), [cv.templatable(cv.float_)]),
|
||||
)
|
||||
async def filter_out_filter_to_code(config, filter_id):
|
||||
async def filter_out_filter_to_code(config: Any, filter_id: ID) -> MockObj:
|
||||
if not isinstance(config, list):
|
||||
config = [config]
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in config]
|
||||
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(template_)), template_)
|
||||
return cg.new_Pvariable(filter_id, await _value_list_table(config))
|
||||
|
||||
|
||||
QUANTILE_SCHEMA = cv.All(
|
||||
@@ -704,7 +718,9 @@ def _is_nan_only(values: Any) -> bool:
|
||||
ThrottleWithPriorityFilter,
|
||||
THROTTLE_WITH_PRIORITY_SCHEMA,
|
||||
)
|
||||
async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
async def throttle_with_priority_filter_to_code(
|
||||
config: ConfigType, filter_id: ID
|
||||
) -> MockObj:
|
||||
values = config[CONF_VALUE]
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
@@ -716,10 +732,8 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
filter_id = filter_id.copy()
|
||||
filter_id.type = ThrottleWithPriorityNanFilter
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in values]
|
||||
return cg.new_Pvariable(
|
||||
filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_
|
||||
)
|
||||
table = await _value_list_table(values)
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], table)
|
||||
|
||||
|
||||
HEARTBEAT_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -226,14 +226,14 @@ MultiplyFilter::MultiplyFilter(TemplatableFn<float> multiplier) : multiplier_(mu
|
||||
|
||||
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
|
||||
|
||||
// ValueListFilter helper (non-template, shared by all ValueListFilter<N> instantiations)
|
||||
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn<float> *values, size_t count) {
|
||||
int8_t accuracy = parent->get_accuracy_decimals();
|
||||
// ValueListFilter
|
||||
bool ValueListFilter::value_matches_any_(float sensor_value) {
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
float accuracy_mult = pow10_int(accuracy);
|
||||
float rounded_sensor = roundf(accuracy_mult * sensor_value);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
float fv = values[i].value();
|
||||
for (const TemplatableFn<float> *v = this->values_; v->has_value(); v++) {
|
||||
float fv = v->value();
|
||||
|
||||
// Handle NaN comparison
|
||||
if (std::isnan(fv)) {
|
||||
@@ -261,13 +261,12 @@ optional<float> ThrottleFilter::new_value(float value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp)
|
||||
optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn<float> *values,
|
||||
size_t count, uint32_t &last_input, uint32_t min_time_between_inputs) {
|
||||
// ThrottleWithPriorityFilter
|
||||
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (last_input == 0 || now - last_input >= min_time_between_inputs ||
|
||||
value_list_matches_any(parent, value, values, count)) {
|
||||
last_input = now;
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ ||
|
||||
this->value_matches_any_(value)) {
|
||||
this->last_input_ = now;
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -330,36 +330,27 @@ class MultiplyFilter : public Filter {
|
||||
TemplatableFn<float> multiplier_;
|
||||
};
|
||||
|
||||
/// Non-template helper for value matching (implementation in filter.cpp)
|
||||
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn<float> *values, size_t count);
|
||||
// Value tables are read in place from flash, which ESP8266 only allows for whole words.
|
||||
static_assert(sizeof(TemplatableFn<float>) == sizeof(void *), "TemplatableFn<float> must stay a single pointer");
|
||||
|
||||
/** Base class for filters that compare sensor values against a fixed list of configured values.
|
||||
*
|
||||
* Templated on N (the number of values) so the list is stored inline in a std::array,
|
||||
* avoiding heap allocation and the overhead of FixedVector.
|
||||
*
|
||||
* @tparam N Number of values in the filter list, set by code generation to match
|
||||
* the exact number of values configured in YAML.
|
||||
*/
|
||||
template<size_t N> class ValueListFilter : public Filter {
|
||||
/// Base class for filters that compare sensor values against a fixed list of configured values.
|
||||
class ValueListFilter : public Filter {
|
||||
protected:
|
||||
explicit ValueListFilter(std::initializer_list<TemplatableFn<float>> values) {
|
||||
init_array_from(this->values_, values);
|
||||
}
|
||||
explicit ValueListFilter(const TemplatableFn<float> *values) : values_(values) {}
|
||||
|
||||
/// Check if sensor value matches any configured value (with accuracy rounding)
|
||||
bool value_matches_any_(float sensor_value) {
|
||||
return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N);
|
||||
}
|
||||
bool value_matches_any_(float sensor_value);
|
||||
|
||||
std::array<TemplatableFn<float>, N> values_{};
|
||||
// Shared PROGMEM table ended by an empty entry, so no count is stored; entries are function
|
||||
// pointers, so aligned 32-bit loads from flash are safe on ESP8266.
|
||||
const TemplatableFn<float> *values_;
|
||||
};
|
||||
|
||||
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
|
||||
template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> {
|
||||
class FilterOutValueFilter : public ValueListFilter {
|
||||
public:
|
||||
explicit FilterOutValueFilter(std::initializer_list<TemplatableFn<float>> values_to_filter_out)
|
||||
: ValueListFilter<N>(values_to_filter_out) {}
|
||||
explicit FilterOutValueFilter(const TemplatableFn<float> *values_to_filter_out)
|
||||
: ValueListFilter(values_to_filter_out) {}
|
||||
|
||||
optional<float> new_value(float value) override {
|
||||
if (this->value_matches_any_(value))
|
||||
@@ -379,21 +370,13 @@ class ThrottleFilter : public Filter {
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
/// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp)
|
||||
optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn<float> *values,
|
||||
size_t count, uint32_t &last_input, uint32_t min_time_between_inputs);
|
||||
|
||||
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
|
||||
template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> {
|
||||
class ThrottleWithPriorityFilter : public ValueListFilter {
|
||||
public:
|
||||
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
||||
std::initializer_list<TemplatableFn<float>> prioritized_values)
|
||||
: ValueListFilter<N>(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||
ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, const TemplatableFn<float> *prioritized_values)
|
||||
: ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||
|
||||
optional<float> new_value(float value) override {
|
||||
return throttle_with_priority_new_value(this->parent_, value, this->values_.data(), N, this->last_input_,
|
||||
this->min_time_between_inputs_);
|
||||
}
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
|
||||
@@ -12,14 +12,15 @@ enum class MeasurementMode : uint8_t {
|
||||
};
|
||||
|
||||
class STCC4Component final : public PollingComponent, public sensirion_common::SensirionI2CDevice {
|
||||
SUB_SENSOR(co2)
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(humidity)
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
void set_co2_sensor(sensor::Sensor *co2) { this->co2_sensor_ = co2; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; }
|
||||
void set_temperature_source(sensor::Sensor *temperature) { this->temperature_source_ = temperature; }
|
||||
void set_humidity_source(sensor::Sensor *humidity) { this->humidity_source_ = humidity; }
|
||||
void set_ambient_pressure_compensation(float pressure_in_hpa);
|
||||
@@ -36,9 +37,6 @@ class STCC4Component final : public PollingComponent, public sensirion_common::S
|
||||
bool write_rht_compensation_(uint16_t temperature_in_ticks, uint16_t humidity_in_ticks);
|
||||
bool write_ambient_pressure_compensation_(uint16_t pressure_in_pa_2);
|
||||
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_source_{nullptr};
|
||||
sensor::Sensor *humidity_source_{nullptr};
|
||||
sensor::Sensor *ambient_pressure_source_{nullptr};
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.components.const import CONF_SERIAL_NUMBER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_STATE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_SUN_GTIL2_ID, SunGTIL2Component
|
||||
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"""Shared TMP102 types and configuration validation."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import math
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import CONF_CONVERSION_RATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_PLATFORM
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -16,6 +20,7 @@ tmp102_ns = cg.esphome_ns.namespace("tmp102")
|
||||
TMP102Component = tmp102_ns.class_(
|
||||
"TMP102Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor
|
||||
)
|
||||
TMP102LimitType = tmp102_ns.enum("TMP102LimitType")
|
||||
|
||||
CONF_EXTENDED_MODE = "extended_mode"
|
||||
CONF_ONE_SHOT_MODE = "one_shot_mode"
|
||||
@@ -34,10 +39,40 @@ TMP102_CONVERSION_FACTOR = 0.0625
|
||||
TMP102_DEFAULT_THIGH = 80.0
|
||||
TMP102_DEFAULT_TLOW = 75.0
|
||||
|
||||
TMP102_MIN_TEMPERATURE = -55.0
|
||||
TMP102_MAX_TEMPERATURE = 127.9375
|
||||
TMP102_MAX_TEMPERATURE_EXTENDED = 150.0
|
||||
|
||||
|
||||
def max_temperature(extended: bool) -> float:
|
||||
return TMP102_MAX_TEMPERATURE_EXTENDED if extended else TMP102_MAX_TEMPERATURE
|
||||
|
||||
|
||||
@dataclass
|
||||
class TMP102Data:
|
||||
# Extended mode of each sensor that has threshold numbers, keyed by sensor ID.
|
||||
number_parents: dict[str, bool] = field(default_factory=dict)
|
||||
|
||||
|
||||
def _get_data() -> TMP102Data:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = TMP102Data()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def add_number_parent(parent_id: object, extended: bool) -> None:
|
||||
_get_data().number_parents[str(parent_id)] = extended
|
||||
|
||||
|
||||
def number_parent_extended(parent_id: object) -> bool | None:
|
||||
"""Return the extended mode of a sensor with threshold numbers, or None without numbers."""
|
||||
return _get_data().number_parents.get(str(parent_id))
|
||||
|
||||
|
||||
def encode_temperature(temperature: float, extended: bool) -> int:
|
||||
"""Encode a temperature as a TMP102 limit-register word."""
|
||||
steps = round(temperature / TMP102_CONVERSION_FACTOR)
|
||||
scaled = temperature / TMP102_CONVERSION_FACTOR
|
||||
steps = int(scaled + math.copysign(0.5, scaled))
|
||||
return (steps << (3 if extended else 4)) & 0xFFFF
|
||||
|
||||
|
||||
@@ -56,8 +91,8 @@ def build_configuration(config: ConfigType) -> int:
|
||||
|
||||
def validate_tmp102_thresholds(config: ConfigType) -> ConfigType:
|
||||
extended = config.get(CONF_EXTENDED_MODE, False)
|
||||
t_min = -55.0
|
||||
t_max = 150.0 if extended else 127.9375
|
||||
t_min = TMP102_MIN_TEMPERATURE
|
||||
t_max = max_temperature(extended)
|
||||
high = config.get(CONF_TEMPERATURE_HIGH)
|
||||
low = config.get(CONF_TEMPERATURE_LOW)
|
||||
for key, value in ((CONF_TEMPERATURE_HIGH, high), (CONF_TEMPERATURE_LOW, low)):
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Adjustable TMP102 thermostat thresholds."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_RESTORE_VALUE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
UNIT_CELSIUS,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
CONF_EXTENDED_MODE,
|
||||
CONF_TEMPERATURE_HIGH,
|
||||
CONF_TEMPERATURE_LOW,
|
||||
CONF_TMP102_ID,
|
||||
TMP102_CONVERSION_FACTOR,
|
||||
TMP102_MIN_TEMPERATURE,
|
||||
TMP102Component,
|
||||
TMP102LimitType,
|
||||
add_number_parent,
|
||||
max_temperature,
|
||||
number_parent_extended,
|
||||
tmp102_ns,
|
||||
validate_unique_child,
|
||||
)
|
||||
|
||||
TMP102LimitNumber = tmp102_ns.class_(
|
||||
"TMP102LimitNumber", number.Number, cg.Parented.template(TMP102Component)
|
||||
)
|
||||
|
||||
_LIMIT_SCHEMA = number.number_schema(
|
||||
TMP102LimitNumber,
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
).extend({cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean})
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_TMP102_ID): cv.use_id(TMP102Component),
|
||||
cv.Optional(CONF_TEMPERATURE_HIGH): _LIMIT_SCHEMA,
|
||||
cv.Optional(CONF_TEMPERATURE_LOW): _LIMIT_SCHEMA,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_TEMPERATURE_HIGH, CONF_TEMPERATURE_LOW),
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
validate_unique_child(config, "number")
|
||||
full = fv.full_config.get()
|
||||
parent = full.get_config_for_path(full.get_path_for_id(config[CONF_TMP102_ID])[:-1])
|
||||
add_number_parent(config[CONF_TMP102_ID], parent.get(CONF_EXTENDED_MODE, False))
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_TMP102_NUMBER")
|
||||
parent = await cg.get_variable(config[CONF_TMP102_ID])
|
||||
limits = {
|
||||
"min_value": TMP102_MIN_TEMPERATURE,
|
||||
"max_value": max_temperature(number_parent_extended(config[CONF_TMP102_ID])),
|
||||
"step": TMP102_CONVERSION_FACTOR,
|
||||
}
|
||||
numbers = number.sub_numbers(config, parent=parent)
|
||||
if var := await numbers(
|
||||
CONF_TEMPERATURE_HIGH,
|
||||
parent.set_limit_control,
|
||||
TMP102LimitType.TMP102_LIMIT_HIGH,
|
||||
**limits,
|
||||
):
|
||||
cg.add(var.set_restore_value(config[CONF_TEMPERATURE_HIGH][CONF_RESTORE_VALUE]))
|
||||
if var := await numbers(
|
||||
CONF_TEMPERATURE_LOW,
|
||||
parent.set_limit_control,
|
||||
TMP102LimitType.TMP102_LIMIT_LOW,
|
||||
**limits,
|
||||
):
|
||||
cg.add(var.set_restore_value(config[CONF_TEMPERATURE_LOW][CONF_RESTORE_VALUE]))
|
||||
@@ -5,6 +5,7 @@ from esphome.components import i2c, sensor
|
||||
from esphome.components.const import CONF_CONVERSION_RATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
@@ -28,6 +29,7 @@ from . import (
|
||||
TMP102Component,
|
||||
build_configuration,
|
||||
encode_temperature,
|
||||
number_parent_extended,
|
||||
validate_tmp102_thresholds,
|
||||
)
|
||||
|
||||
@@ -66,17 +68,20 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if not any(key in config for key in _ADVANCED_SCHEMA):
|
||||
has_numbers = number_parent_extended(config[CONF_ID]) is not None
|
||||
if not has_numbers and not any(key in config for key in _ADVANCED_SCHEMA):
|
||||
return
|
||||
|
||||
extended = config.get(CONF_EXTENDED_MODE, False)
|
||||
high = config.get(CONF_TEMPERATURE_HIGH, TMP102_DEFAULT_THIGH)
|
||||
low = config.get(CONF_TEMPERATURE_LOW, TMP102_DEFAULT_TLOW)
|
||||
cg.add_define("USE_TMP102_CONFIGURE")
|
||||
cg.add(
|
||||
var.set_configuration(
|
||||
build_configuration(config),
|
||||
encode_temperature(high, extended),
|
||||
encode_temperature(low, extended),
|
||||
encode_temperature(
|
||||
config.get(CONF_TEMPERATURE_HIGH, TMP102_DEFAULT_THIGH), extended
|
||||
),
|
||||
encode_temperature(
|
||||
config.get(CONF_TEMPERATURE_LOW, TMP102_DEFAULT_TLOW), extended
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::tmp102 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "tmp102");
|
||||
@@ -18,6 +20,9 @@ static constexpr uint16_t TMP102_ALERT_POLARITY_BIT = 0x0400;
|
||||
#ifdef USE_TMP102_CONFIGURE
|
||||
static constexpr uint8_t TMP102_REGISTER_LOW_LIMIT = 0x02;
|
||||
static constexpr uint8_t TMP102_REGISTER_HIGH_LIMIT = 0x03;
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
static_assert(TMP102_REGISTER_LOW_LIMIT + TMP102_LIMIT_HIGH == TMP102_REGISTER_HIGH_LIMIT);
|
||||
#endif
|
||||
|
||||
static constexpr uint16_t TMP102_CONFIGURATION_MASK = 0x1FD0;
|
||||
static constexpr uint16_t TMP102_RESET_CONFIGURATION = 0x60A0;
|
||||
@@ -31,11 +36,39 @@ static float decode_temperature(uint16_t raw, bool extended) {
|
||||
return (static_cast<int16_t>(raw) >> (extended ? 3 : 4)) * TMP102_CONVERSION_FACTOR;
|
||||
}
|
||||
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
static uint16_t encode_temperature(float temperature, bool extended) {
|
||||
const int16_t steps = static_cast<int16_t>(std::lround(temperature / TMP102_CONVERSION_FACTOR));
|
||||
return static_cast<uint16_t>(static_cast<uint32_t>(static_cast<uint16_t>(steps)) << (extended ? 3 : 4));
|
||||
}
|
||||
#endif
|
||||
|
||||
void TMP102Component::setup() {
|
||||
if (!this->configure_)
|
||||
return;
|
||||
if (!this->write_configuration_())
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
float restored[2]{};
|
||||
this->prepare_number_limits_(restored);
|
||||
#endif
|
||||
if (!this->write_configuration_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
bool saved = true;
|
||||
for (uint8_t limit = 0; limit < 2; ++limit) {
|
||||
TMP102LimitNumber *control = this->limit_controls_[limit];
|
||||
if (control == nullptr)
|
||||
continue;
|
||||
const float value = this->get_limit_temperature_(static_cast<TMP102LimitType>(limit));
|
||||
if (value != restored[limit])
|
||||
saved &= control->save_value_(value);
|
||||
control->publish_state(value);
|
||||
}
|
||||
if (!saved) {
|
||||
ESP_LOGW(TAG, "Failed to save threshold preferences");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -75,6 +108,10 @@ void TMP102Component::dump_config() {
|
||||
this->config_ & TMP102_ALERT_POLARITY_BIT ? LOG_STR_LITERAL("Active High") : LOG_STR_LITERAL("Active Low"),
|
||||
fault_bits == 3 ? 6 : 1U << fault_bits, decode_temperature(this->high_limit_, extended),
|
||||
decode_temperature(this->low_limit_, extended));
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
LOG_NUMBER(" ", "High Limit", this->limit_controls_[TMP102_LIMIT_HIGH]);
|
||||
LOG_NUMBER(" ", "Low Limit", this->limit_controls_[TMP102_LIMIT_LOW]);
|
||||
#endif
|
||||
if (this->config_ & TMP102_THERMOSTAT_MODE_BIT) {
|
||||
ESP_LOGW(TAG, "Register reads clear the interrupt-mode ALERT pin");
|
||||
}
|
||||
@@ -185,6 +222,55 @@ void TMP102Component::read_temperature_() {
|
||||
#endif
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
void TMP102Component::set_limit_control(TMP102LimitNumber *number) {
|
||||
this->limit_controls_[number->limit_type_] = number;
|
||||
}
|
||||
|
||||
void TMP102Component::prepare_number_limits_(float *restored) {
|
||||
const bool extended = this->config_ & TMP102_EXTENDED_MODE_BIT;
|
||||
uint16_t raw[2] = {this->low_limit_, this->high_limit_};
|
||||
for (uint8_t limit = 0; limit < 2; ++limit) {
|
||||
TMP102LimitNumber *control = this->limit_controls_[limit];
|
||||
if (control == nullptr)
|
||||
continue;
|
||||
restored[limit] = control->load_value_(decode_temperature(raw[limit], extended));
|
||||
raw[limit] = encode_temperature(restored[limit], extended);
|
||||
}
|
||||
if (static_cast<int16_t>(raw[TMP102_LIMIT_LOW]) > static_cast<int16_t>(raw[TMP102_LIMIT_HIGH])) {
|
||||
ESP_LOGW(TAG, "Restored thresholds are inverted; using configured values");
|
||||
return;
|
||||
}
|
||||
this->low_limit_ = raw[TMP102_LIMIT_LOW];
|
||||
this->high_limit_ = raw[TMP102_LIMIT_HIGH];
|
||||
}
|
||||
|
||||
float TMP102Component::get_limit_temperature_(TMP102LimitType limit) const {
|
||||
const uint16_t raw = limit == TMP102_LIMIT_HIGH ? this->high_limit_ : this->low_limit_;
|
||||
return decode_temperature(raw, this->config_ & TMP102_EXTENDED_MODE_BIT);
|
||||
}
|
||||
|
||||
bool TMP102Component::set_limit_temperature_(TMP102LimitType limit, float temperature) {
|
||||
const bool extended = this->config_ & TMP102_EXTENDED_MODE_BIT;
|
||||
const uint16_t raw = encode_temperature(temperature, extended);
|
||||
const bool high = limit == TMP102_LIMIT_HIGH;
|
||||
const uint16_t other = high ? this->low_limit_ : this->high_limit_;
|
||||
if (high ? static_cast<int16_t>(raw) < static_cast<int16_t>(other)
|
||||
: static_cast<int16_t>(raw) > static_cast<int16_t>(other)) {
|
||||
ESP_LOGW(TAG, "Rejected %s threshold %.4f°C: crosses %s threshold %.4f°C",
|
||||
high ? LOG_STR_LITERAL("high") : LOG_STR_LITERAL("low"), decode_temperature(raw, extended),
|
||||
high ? LOG_STR_LITERAL("low") : LOG_STR_LITERAL("high"), decode_temperature(other, extended));
|
||||
return false;
|
||||
}
|
||||
if (!this->write_register_(static_cast<uint8_t>(TMP102_REGISTER_LOW_LIMIT + limit), raw)) {
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
(high ? this->high_limit_ : this->low_limit_) = raw;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_TMP102_BINARY_SENSOR
|
||||
@@ -207,4 +293,35 @@ bool TMP102Component::read_alert_state_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
float TMP102LimitNumber::load_value_(float fallback) {
|
||||
float value = fallback;
|
||||
if (this->restore_value_) {
|
||||
this->pref_ = this->make_entity_preference<float>();
|
||||
this->pref_.load(&value);
|
||||
}
|
||||
if (!std::isfinite(value) || value < this->traits.get_min_value() || value > this->traits.get_max_value()) {
|
||||
ESP_LOGW(TAG, "Invalid restored threshold; using configured value");
|
||||
value = fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool TMP102LimitNumber::save_value_(float value) { return !this->restore_value_ || this->pref_.save(&value); }
|
||||
|
||||
void TMP102LimitNumber::control(float value) {
|
||||
// A chip that failed setup holds nothing the entity could show
|
||||
if (this->parent_->is_failed()) {
|
||||
ESP_LOGW(TAG, "Rejected threshold change: sensor setup failed");
|
||||
return;
|
||||
}
|
||||
const bool accepted = this->parent_->set_limit_temperature_(this->limit_type_, value);
|
||||
value = this->parent_->get_limit_temperature_(this->limit_type_);
|
||||
this->publish_state(value);
|
||||
if (accepted && !this->save_value_(value)) {
|
||||
ESP_LOGW(TAG, "Failed to save threshold preference");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::tmp102
|
||||
|
||||
@@ -6,12 +6,26 @@
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#endif
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::tmp102 {
|
||||
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
enum TMP102LimitType : uint8_t {
|
||||
TMP102_LIMIT_LOW = 0,
|
||||
TMP102_LIMIT_HIGH = 1,
|
||||
};
|
||||
|
||||
class TMP102LimitNumber;
|
||||
#endif
|
||||
|
||||
class TMP102Component final : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor {
|
||||
public:
|
||||
#ifdef USE_TMP102_BINARY_SENSOR
|
||||
@@ -25,6 +39,9 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
|
||||
this->low_limit_ = low_limit;
|
||||
this->configure_ = true;
|
||||
}
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
void set_limit_control(TMP102LimitNumber *number);
|
||||
#endif
|
||||
#endif
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
@@ -46,6 +63,14 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
|
||||
bool check_configuration_();
|
||||
void read_temperature_();
|
||||
void update_configured_();
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
friend class TMP102LimitNumber;
|
||||
bool set_limit_temperature_(TMP102LimitType limit, float temperature);
|
||||
float get_limit_temperature_(TMP102LimitType limit) const;
|
||||
void prepare_number_limits_(float *restored);
|
||||
|
||||
TMP102LimitNumber *limit_controls_[2]{};
|
||||
#endif
|
||||
|
||||
uint16_t config_{0};
|
||||
uint16_t high_limit_{0};
|
||||
@@ -54,4 +79,22 @@ class TMP102Component final : public PollingComponent, public i2c::I2CDevice, pu
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef USE_TMP102_NUMBER
|
||||
class TMP102LimitNumber final : public number::Number, public Parented<TMP102Component> {
|
||||
public:
|
||||
explicit TMP102LimitNumber(TMP102LimitType limit_type) : limit_type_(limit_type) {}
|
||||
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
|
||||
|
||||
protected:
|
||||
friend class TMP102Component;
|
||||
float load_value_(float fallback);
|
||||
bool save_value_(float value);
|
||||
void control(float value) override;
|
||||
|
||||
ESPPreferenceObject pref_;
|
||||
TMP102LimitType limit_type_;
|
||||
bool restore_value_{true};
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome::tmp102
|
||||
|
||||
@@ -24,6 +24,8 @@ static constexpr uint32_t RX_TICKS_PER_SECOND = 16000000;
|
||||
__attribute__((always_inline)) static inline uint32_t rx_now() { return micros() << 4; }
|
||||
bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool ESP8266UartComponent::serial0_swapped = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
uint32_t ESP8266UartComponent::get_config() {
|
||||
uint32_t config = 0;
|
||||
|
||||
@@ -111,6 +113,7 @@ void ESP8266UartComponent::setup() {
|
||||
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
|
||||
this->hw_serial_->swap();
|
||||
ESP8266UartComponent::serial0_in_use = true;
|
||||
ESP8266UartComponent::serial0_swapped = true;
|
||||
} else
|
||||
#endif // USE_ESP8266_UART_SERIAL
|
||||
#ifdef USE_ESP8266_UART_SERIAL1
|
||||
@@ -133,6 +136,11 @@ void ESP8266UartComponent::load_settings(bool dump_config) {
|
||||
SerialConfig config = static_cast<SerialConfig>(get_config());
|
||||
this->hw_serial_->begin(this->baud_rate_, config);
|
||||
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
|
||||
#ifdef USE_ESP8266_UART_SERIAL
|
||||
// begin() puts UART0 back on GPIO1/GPIO3, so swap it again like setup() did for GPIO15/GPIO13
|
||||
if (this->hw_serial_ == &Serial && ESP8266UartComponent::serial0_swapped)
|
||||
this->hw_serial_->swap();
|
||||
#endif // USE_ESP8266_UART_SERIAL
|
||||
} else {
|
||||
this->sw_serial_->setup(this->tx_pin_, this->rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_,
|
||||
this->parity_, this->rx_buffer_size_);
|
||||
|
||||
@@ -92,7 +92,8 @@ class ESP8266UartComponent final : public UARTComponent, public Component {
|
||||
ESP8266SoftwareSerial *sw_serial_{nullptr};
|
||||
|
||||
private:
|
||||
static bool serial0_in_use; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool serial0_in_use; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool serial0_swapped; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
};
|
||||
|
||||
} // namespace esphome::uart
|
||||
|
||||
@@ -19,11 +19,12 @@ namespace esphome {
|
||||
/// Codegen wraps constants in stateless lambdas so only a function pointer is needed.
|
||||
template<typename T, typename... X> class TemplatableFn {
|
||||
public:
|
||||
TemplatableFn() = default;
|
||||
constexpr TemplatableFn() = default;
|
||||
TemplatableFn(std::nullptr_t) = delete;
|
||||
|
||||
// Exact return type match — direct function pointer storage
|
||||
template<typename F> TemplatableFn(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {}
|
||||
// constexpr so codegen can place arrays of these in flash.
|
||||
template<typename F> constexpr TemplatableFn(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {}
|
||||
|
||||
// Convertible return type (e.g., int -> uint8_t) — casting trampoline.
|
||||
// Stateless lambdas are default-constructible in C++20, so F{} recreates the lambda inside
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
#define USE_BINARY_SENSOR_MULTI_CLICK_TRIGGER
|
||||
#define USE_BLE_DEVICE_IRK
|
||||
#define USE_BMI270_BMM150
|
||||
#define USE_BQ25186_PG_GPO
|
||||
#define BQ25186_BINARY_SENSOR_COUNT 1
|
||||
#define BQ25186_TEXT_SENSOR_COUNT 1
|
||||
#define USE_BUTTON
|
||||
#define USE_CAMERA
|
||||
#define USE_CLIMATE
|
||||
@@ -88,12 +91,16 @@
|
||||
#define USE_I2S_AUDIO_SPDIF_MODE
|
||||
#define USE_IMAGE
|
||||
#define USE_IMPROV_BLE_STATE_CALLBACK
|
||||
#define USE_INA3221_ALERT_LIMITS
|
||||
#define USE_INA3221_SUMMATION
|
||||
#define USE_INFRARED
|
||||
#define USE_IR_RF
|
||||
#define USE_IR_RF_TRANSMIT_COMPLETE
|
||||
#define USE_JSON
|
||||
#define USE_JSON_ARENA
|
||||
#define USE_RADIO_FREQUENCY
|
||||
#define USE_LD2410S_PARAMETERS
|
||||
#define USE_LD2410S_VERSION
|
||||
#define USE_LIGHT
|
||||
#define USE_LIGHT_FLASH_TRANSITION_LENGTH
|
||||
#define USE_LIGHT_GAMMA_LUT
|
||||
@@ -244,6 +251,7 @@
|
||||
#define USE_SAFE_MODE_CALLBACK
|
||||
#define ESPHOME_SAFE_MODE_CALLBACK_COUNT 1
|
||||
#define USE_SELECT
|
||||
#define USE_SELEC_METER_EM4M
|
||||
#define USE_SENSOR
|
||||
#define USE_SENSOR_FILTER
|
||||
#define USE_SERIAL_PROXY
|
||||
@@ -259,6 +267,7 @@
|
||||
#define USE_TIME_TRIGGERS
|
||||
#define USE_TMP102_BINARY_SENSOR
|
||||
#define USE_TMP102_CONFIGURE
|
||||
#define USE_TMP102_NUMBER
|
||||
#define USE_TOUCHSCREEN
|
||||
#define USE_UART_DEBUGGER
|
||||
#define USE_UART_WAKE_LOOP_ON_RX
|
||||
|
||||
@@ -188,6 +188,25 @@ inline bool operator==(const __FlashStringHelper *lhs, const StringRef &rhs) { r
|
||||
inline bool operator!=(const StringRef &lhs, const __FlashStringHelper *rhs) { return !(lhs == rhs); }
|
||||
|
||||
inline bool operator!=(const __FlashStringHelper *lhs, const StringRef &rhs) { return !(rhs == lhs); }
|
||||
|
||||
/// Codegen use only: a flash string literal with its byte length, compared without a runtime strlen.
|
||||
struct ProgmemStringRef {
|
||||
PGM_P str;
|
||||
size_t len;
|
||||
};
|
||||
|
||||
inline bool operator==(const std::string &lhs, ProgmemStringRef rhs) {
|
||||
return lhs.size() == rhs.len && (rhs.len == 0 || memcmp_P(lhs.data(), rhs.str, rhs.len) == 0);
|
||||
}
|
||||
|
||||
inline bool operator!=(const std::string &lhs, ProgmemStringRef rhs) { return !(lhs == rhs); }
|
||||
|
||||
inline bool operator==(const StringRef &lhs, ProgmemStringRef rhs) {
|
||||
// A StringRef may be {nullptr, 0}, and memcmp_P with a null pointer is undefined even for 0 bytes
|
||||
return lhs.size() == rhs.len && (rhs.len == 0 || memcmp_P(lhs.c_str(), rhs.str, rhs.len) == 0);
|
||||
}
|
||||
|
||||
inline bool operator!=(const StringRef &lhs, ProgmemStringRef rhs) { return !(lhs == rhs); }
|
||||
#endif // USE_ESP8266
|
||||
|
||||
inline bool operator<(const StringRef &lhs, const StringRef &rhs) {
|
||||
|
||||
+14
-1
@@ -191,10 +191,23 @@ def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any:
|
||||
If the value is already tagged, the new context vars are merged with existing ones,
|
||||
with new vars taking precedence. Returns the value tagged with ConfigContext. Returns
|
||||
the original value if value is not a list/string/dict.
|
||||
|
||||
Raises EsphomeError if a `defaults:` key is present but is not a mapping.
|
||||
"""
|
||||
if isinstance(value, dict) and CONF_DEFAULTS in value:
|
||||
defaults = value.pop(CONF_DEFAULTS)
|
||||
if defaults is None:
|
||||
raise EsphomeError(
|
||||
f"'{CONF_DEFAULTS}' is empty; "
|
||||
f"remove the key or give it at least one value"
|
||||
)
|
||||
if not isinstance(defaults, dict):
|
||||
raise EsphomeError(
|
||||
f"'{CONF_DEFAULTS}' must be a key to value mapping, "
|
||||
f"got {type(defaults)}"
|
||||
)
|
||||
context_vars = {
|
||||
**value.pop(CONF_DEFAULTS),
|
||||
**defaults,
|
||||
**(context_vars or {}),
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ ISOLATED_COMPONENTS = {
|
||||
"animation": "Has display lambda in common.yaml that requires existing display platform - breaks when merged without display",
|
||||
"cdc_acm_uart": "Depends on tinyusb which conflicts with usb_host",
|
||||
"esphome": "Defines devices/areas in esphome: section that are referenced in other sections - breaks when merged",
|
||||
"ecocomfort2": "Needs esp32_ble with io_capability: none and sc_bond, which a merged esp32_ble: block overrides",
|
||||
"ethernet": "Defines ethernet: which conflicts with wifi: used by most components",
|
||||
"ethernet_info": "Related to ethernet component which conflicts with wifi",
|
||||
"gps": "TinyGPSPlus library declares millis() function that creates ambiguity with ESPHome millis() macro when merged with components using millis() in lambdas",
|
||||
|
||||
@@ -10,12 +10,16 @@ from esphome.components.bq25186 import (
|
||||
CONF_FAST_CHARGE_CURRENT,
|
||||
CONF_MASK_TS_INTERRUPT,
|
||||
CONF_PUSH_BUTTON_LONG_PRESS_ACTION,
|
||||
CONF_WATCHDOG_15S_ENABLE,
|
||||
CONFIG_SCHEMA,
|
||||
REG_ICHG_CTRL,
|
||||
REG_MASK_ID,
|
||||
REG_SHIP_RST,
|
||||
REG_VBAT_CTRL,
|
||||
_check_watchdog,
|
||||
pack_registers,
|
||||
)
|
||||
from esphome.const import CONF_UPDATE_INTERVAL, CONF_WATCHDOG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# Datasheet power-on values for VBAT_CTRL..MASK_ID, except that IC_CTRL has the
|
||||
@@ -85,3 +89,49 @@ def test_masked_registers_only_carry_configurable_bits() -> None:
|
||||
def test_values_between_steps_are_rejected(key: str, value: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
BQ25186_CONFIG_SCHEMA({key: value})
|
||||
|
||||
|
||||
def _watchdog_config(interval: str, options: ConfigType) -> ConfigType:
|
||||
config = BQ25186_CONFIG_SCHEMA(options)
|
||||
config[CONF_UPDATE_INTERVAL] = cv.update_interval(interval)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"interval, options",
|
||||
[
|
||||
("60s", {CONF_WATCHDOG: "160s_software_reset"}),
|
||||
("60s", {CONF_WATCHDOG: "160s_hardware_reset"}),
|
||||
("30s", {CONF_WATCHDOG: "40s_hardware_reset"}),
|
||||
("60s", {CONF_WATCHDOG: "disabled"}),
|
||||
("10s", {CONF_WATCHDOG_15S_ENABLE: True}),
|
||||
],
|
||||
)
|
||||
def test_update_interval_within_watchdog_passes(
|
||||
interval: str, options: ConfigType
|
||||
) -> None:
|
||||
_check_watchdog(_watchdog_config(interval, options))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"interval, options",
|
||||
[
|
||||
("60s", {CONF_WATCHDOG: "40s_hardware_reset"}),
|
||||
("40s", {CONF_WATCHDOG: "40s_hardware_reset"}),
|
||||
("60s", {CONF_WATCHDOG_15S_ENABLE: True}),
|
||||
("20s", {CONF_WATCHDOG: "160s_software_reset", CONF_WATCHDOG_15S_ENABLE: True}),
|
||||
("never", {CONF_WATCHDOG: "160s_software_reset"}),
|
||||
],
|
||||
)
|
||||
def test_update_interval_past_watchdog_is_rejected(
|
||||
interval: str, options: ConfigType
|
||||
) -> None:
|
||||
with pytest.raises(cv.Invalid, match=CONF_UPDATE_INTERVAL):
|
||||
_check_watchdog(_watchdog_config(interval, options))
|
||||
|
||||
|
||||
def test_unconfigured_chip_keeps_its_power_on_watchdog() -> None:
|
||||
config = {"configure_on_boot": False, "update_interval": "160s"}
|
||||
with pytest.raises(cv.Invalid, match="shorter than 160 s"):
|
||||
CONFIG_SCHEMA(config)
|
||||
CONFIG_SCHEMA({**config, "update_interval": "60s"})
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Tests for the sensor value list filter codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import re
|
||||
|
||||
|
||||
def test_value_list_filters_share_progmem_tables(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""Identical value lists share one PROGMEM table; the NaN throttle stays array free."""
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_value_list.yaml")
|
||||
|
||||
tables = re.findall(
|
||||
r"static constexpr TemplatableFn<float> (\w+)\[\] PROGMEM", main_cpp
|
||||
)
|
||||
# Every table ends with an empty entry so the filters store no count.
|
||||
assert main_cpp.count("TemplatableFn<float>()}") == len(tables)
|
||||
assert (
|
||||
len(tables) == 5
|
||||
) # single, shared list, lambda, two anchored stateful lambdas
|
||||
single, shared, lam = tables[:3]
|
||||
assert f"sensor::FilterOutValueFilter({single});" in main_cpp
|
||||
assert main_cpp.count(f"sensor::FilterOutValueFilter({shared});") == 2
|
||||
assert f"sensor::FilterOutValueFilter({lam});" in main_cpp
|
||||
assert f"sensor::ThrottleWithPriorityFilter(1000, {shared});" in main_cpp
|
||||
assert "sensor::ThrottleWithPriorityNanFilter(1000);" in main_cpp
|
||||
assert "return 42.0;" in main_cpp
|
||||
|
||||
|
||||
def test_value_list_lambda_tables_are_not_shared(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""An anchored lambda may keep static state, so each filter keeps its own table."""
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_value_list.yaml")
|
||||
|
||||
tables = re.findall(
|
||||
r"static constexpr TemplatableFn<float> (\w+)\[\] PROGMEM = (.*?)TemplatableFn<float>\(\)\}",
|
||||
main_cpp,
|
||||
re.DOTALL,
|
||||
)
|
||||
stateful = [name for name, body in tables if "static float n" in body]
|
||||
assert len(stateful) == 2
|
||||
assert stateful[0] != stateful[1]
|
||||
@@ -0,0 +1,51 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: fo_single
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: 85.0
|
||||
- platform: template
|
||||
id: fo_list_a
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: [0.0, 85.0]
|
||||
- platform: template
|
||||
id: fo_list_b
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: [0.0, 85.0]
|
||||
- platform: template
|
||||
id: fo_lambda
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: !lambda return 42.0;
|
||||
- platform: template
|
||||
id: twp
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 1s
|
||||
value: [0.0, 85.0]
|
||||
- platform: template
|
||||
id: twp_nan
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- throttle_with_priority: 1s
|
||||
- platform: template
|
||||
id: fo_stateful_a
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: &stateful !lambda |-
|
||||
static float n = 0;
|
||||
return ++n;
|
||||
- platform: template
|
||||
id: fo_stateful_b
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: *stateful
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
esphome:
|
||||
name: tmp102-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: chip
|
||||
name: Temperature
|
||||
|
||||
number:
|
||||
- platform: tmp102
|
||||
tmp102_id: chip
|
||||
temperature_high:
|
||||
name: High One
|
||||
- platform: tmp102
|
||||
tmp102_id: chip
|
||||
temperature_low:
|
||||
name: Low Two
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
esphome:
|
||||
name: tmp102-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: chip
|
||||
name: Temperature
|
||||
|
||||
number:
|
||||
- platform: tmp102
|
||||
tmp102_id: chip
|
||||
temperature_high:
|
||||
name: High
|
||||
temperature_low:
|
||||
name: Low
|
||||
restore_value: false
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
esphome:
|
||||
name: tmp102-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: chip
|
||||
name: Temperature
|
||||
extended_mode: true
|
||||
temperature_high: 140
|
||||
temperature_low: 120
|
||||
|
||||
number:
|
||||
- platform: tmp102
|
||||
temperature_high:
|
||||
name: High
|
||||
temperature_low:
|
||||
name: Low
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
esphome:
|
||||
name: tmp102-test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
|
||||
sensor:
|
||||
- platform: tmp102
|
||||
id: chip
|
||||
name: Temperature
|
||||
|
||||
number:
|
||||
- platform: tmp102
|
||||
tmp102_id: chip
|
||||
temperature_high:
|
||||
name: High
|
||||
initial_value: 30
|
||||
@@ -58,6 +58,54 @@ def test_duplicate_alert_entities_are_rejected(
|
||||
)
|
||||
|
||||
|
||||
def test_threshold_numbers_enable_configuration_once(
|
||||
generate_main: Callable[[Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
source = generate_main(component_config_path("numbers.yaml"))
|
||||
assert source.count("set_configuration(") == 1
|
||||
assert "set_configuration(128, 20480, 19200)" in source
|
||||
assert source.count("set_limit_control(") == 2
|
||||
assert "tmp102::TMP102_LIMIT_HIGH" in source
|
||||
assert "tmp102::TMP102_LIMIT_LOW" in source
|
||||
assert "set_restore_value(false)" in source
|
||||
assert "set_max_value(127.9375f)" in source
|
||||
defines = " ".join(define.name for define in CORE.defines)
|
||||
assert "USE_TMP102_NUMBER" in defines
|
||||
assert "USE_TMP102_CONFIGURE" in defines
|
||||
|
||||
|
||||
def test_threshold_numbers_use_sensor_thresholds(
|
||||
generate_main: Callable[[Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
source = generate_main(component_config_path("numbers_inherit.yaml"))
|
||||
assert source.count("set_configuration(") == 1
|
||||
assert "set_configuration(144, 17920, 15360)" in source
|
||||
assert "set_max_value(150.0f)" in source
|
||||
|
||||
|
||||
def test_threshold_number_initial_value_is_rejected(
|
||||
component_config_path: Callable[[str], Path],
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
CORE.config_path = component_config_path("numbers_initial_value.yaml")
|
||||
assert read_config({}) is None
|
||||
assert "[initial_value] is an invalid option" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_duplicate_numbers_are_rejected(
|
||||
component_config_path: Callable[[str], Path],
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
CORE.config_path = component_config_path("duplicate_numbers.yaml")
|
||||
assert read_config({}) is None
|
||||
assert (
|
||||
"Only one TMP102 number may reference each temperature sensor"
|
||||
in capsys.readouterr().out
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extended", "high", "low"),
|
||||
[
|
||||
@@ -117,5 +165,12 @@ def test_precomputed_register_values() -> None:
|
||||
assert component.encode_temperature(config["temperature_low"], True) == 0xF9E0
|
||||
|
||||
|
||||
def test_threshold_encoding_rounds_half_steps_away_from_zero() -> None:
|
||||
assert component.encode_temperature(30.03125, False) == 481 << 4
|
||||
assert component.encode_temperature(-30.03125, False) == (-481 << 4) & 0xFFFF
|
||||
assert component.encode_temperature(30.03125, True) == 481 << 3
|
||||
assert component.encode_temperature(-30.03125, True) == (-481 << 3) & 0xFFFF
|
||||
|
||||
|
||||
def test_defaults_match_chip_reset() -> None:
|
||||
assert component.build_configuration({}) == 0x0080
|
||||
|
||||
@@ -23,3 +23,77 @@ bq25186:
|
||||
address: 0x6B
|
||||
configure_on_boot: true
|
||||
fast_charge_current: 20mA
|
||||
watchdog: 160s_software_reset
|
||||
update_interval: 30s
|
||||
|
||||
binary_sensor:
|
||||
- platform: bq25186
|
||||
bq25186_id: bq25186_charger
|
||||
ts:
|
||||
name: TS Open
|
||||
charging:
|
||||
name: Charging
|
||||
charge_done:
|
||||
name: Charge Done
|
||||
ilim:
|
||||
name: ILIM Active
|
||||
vdppm:
|
||||
name: VDPPM Active
|
||||
vindpm:
|
||||
name: VINDPM Active
|
||||
thermreg:
|
||||
name: Thermal Regulation Active
|
||||
power_good:
|
||||
name: VIN Power Good
|
||||
vin_ovp:
|
||||
name: VIN OVP Active
|
||||
battery_uvlo:
|
||||
name: Battery UVLO Active
|
||||
safety_timer_flag:
|
||||
name: Safety Timer Flag
|
||||
wake1_flag:
|
||||
name: Wake 1 Flag
|
||||
wake2_flag:
|
||||
name: Wake 2 Flag
|
||||
ts_flag:
|
||||
name: TS Flag
|
||||
ilim_flag:
|
||||
name: ILIM Flag
|
||||
vdppm_flag:
|
||||
name: VDPPM Flag
|
||||
vindpm_flag:
|
||||
name: VINDPM Flag
|
||||
thermreg_flag:
|
||||
name: Thermal Regulation Flag
|
||||
vin_ovp_flag:
|
||||
name: VIN OVP Flag
|
||||
battery_uvlo_flag:
|
||||
name: Battery UVLO Flag
|
||||
battery_ocp_flag:
|
||||
name: Battery OCP Flag
|
||||
|
||||
text_sensor:
|
||||
- platform: bq25186
|
||||
bq25186_id: bq25186_charger
|
||||
charge_status:
|
||||
name: Charge Status
|
||||
ts_status:
|
||||
name: TS Status
|
||||
|
||||
button:
|
||||
- platform: bq25186
|
||||
bq25186_id: bq25186_charger
|
||||
software_reset:
|
||||
name: Software Reset
|
||||
shutdown:
|
||||
name: Shutdown
|
||||
ship_mode:
|
||||
name: Ship Mode
|
||||
hardware_reset:
|
||||
name: Hardware Reset
|
||||
|
||||
switch:
|
||||
- platform: bq25186
|
||||
bq25186_id: bq25186_boot
|
||||
pg_gpo:
|
||||
name: PG GPO
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
bq76972:
|
||||
id: bq76972_id
|
||||
address: 0x08
|
||||
crc_enabled: true
|
||||
reg_disabled: true
|
||||
update_interval: 10s
|
||||
stack_voltage:
|
||||
name: "Pack Total Voltage"
|
||||
internal_temperature:
|
||||
name: "Temperature - Internal"
|
||||
ts1_temperature:
|
||||
name: "Temperature - Pin TS1"
|
||||
ts2_temperature:
|
||||
name: "Temperature - Pin TS2"
|
||||
ts3_temperature:
|
||||
name: "Temperature - Pin TS3"
|
||||
hdq_temperature:
|
||||
name: "Temperature - Pin HDQ"
|
||||
cell_1_voltage:
|
||||
name: "Cell 1 Voltage"
|
||||
cell_2_voltage:
|
||||
name: "Cell 2 Voltage"
|
||||
cell_3_voltage:
|
||||
name: "Cell 3 Voltage"
|
||||
cell_4_voltage:
|
||||
name: "Cell 4 Voltage"
|
||||
cell_5_voltage:
|
||||
name: "Cell 5 Voltage"
|
||||
cell_6_voltage:
|
||||
name: "Cell 6 Voltage"
|
||||
cell_7_voltage:
|
||||
name: "Cell 7 Voltage"
|
||||
cell_8_voltage:
|
||||
name: "Cell 8 Voltage"
|
||||
cell_9_voltage:
|
||||
name: "Cell 9 Voltage"
|
||||
cell_10_voltage:
|
||||
name: "Cell 10 Voltage"
|
||||
cell_11_voltage:
|
||||
name: "Cell 11 Voltage"
|
||||
cell_12_voltage:
|
||||
name: "Cell 12 Voltage"
|
||||
cell_13_voltage:
|
||||
name: "Cell 13 Voltage"
|
||||
cell_14_voltage:
|
||||
name: "Cell 14 Voltage"
|
||||
cell_15_voltage:
|
||||
name: "Cell 15 Voltage"
|
||||
cell_16_voltage:
|
||||
name: "Cell 16 Voltage"
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Program BQ I2C Address"
|
||||
on_press:
|
||||
- bq76972.program_i2c_address:
|
||||
id: bq76972_id
|
||||
address: 0x09
|
||||
- bq76972.program_i2c_address: !lambda "return 0x08;"
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
bq76972: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
bq76972: !include common.yaml
|
||||
@@ -5,7 +5,7 @@ power_management:
|
||||
min_frequency: ${esp32_power_management_min_frequency}
|
||||
enable_light_sleep: true
|
||||
idle_time_before_sleep: 3
|
||||
power_down_flash: true
|
||||
power_down_flash: ${esp32_power_management_power_down_flash}
|
||||
power_down_peripherals: ${esp32_power_management_power_down_peripherals}
|
||||
profiling: true
|
||||
trace: true
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
esp32_power_management_max_frequency: 160MHZ
|
||||
esp32_power_management_min_frequency: 40MHZ
|
||||
esp32_power_management_power_down_peripherals: "false"
|
||||
esp32_power_management_power_down_flash: "true"
|
||||
|
||||
packages:
|
||||
esp32_power_management: !include common.yaml
|
||||
@@ -2,6 +2,7 @@ substitutions:
|
||||
esp32_power_management_max_frequency: 160MHZ
|
||||
esp32_power_management_min_frequency: 40MHZ
|
||||
esp32_power_management_power_down_peripherals: "false"
|
||||
esp32_power_management_power_down_flash: "false"
|
||||
|
||||
packages:
|
||||
esp32_power_management: !include common.yaml
|
||||
|
||||
@@ -2,6 +2,7 @@ substitutions:
|
||||
esp32_power_management_max_frequency: 96MHZ
|
||||
esp32_power_management_min_frequency: 32MHZ
|
||||
esp32_power_management_power_down_peripherals: "true"
|
||||
esp32_power_management_power_down_flash: "false"
|
||||
|
||||
packages:
|
||||
esp32_power_management: !include common.yaml
|
||||
|
||||
@@ -2,6 +2,7 @@ substitutions:
|
||||
esp32_power_management_max_frequency: 160MHZ
|
||||
esp32_power_management_min_frequency: 40MHZ
|
||||
esp32_power_management_power_down_peripherals: "false"
|
||||
esp32_power_management_power_down_flash: "false"
|
||||
|
||||
packages:
|
||||
esp32_power_management: !include common.yaml
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
from esphome.components.remote_base import request_protocol
|
||||
|
||||
# The harness walks the component manifest, not the climate platform, so declare its auto-load here
|
||||
manifest.auto_load = ["climate_ir", "climate"]
|
||||
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
# The AEHA source is compiled only behind its define; climate.py requests it in the real build.
|
||||
request_protocol("aeha")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,5 @@
|
||||
climate:
|
||||
- platform: friedrich
|
||||
name: Friedrich Climate
|
||||
transmitter_id: xmitr
|
||||
receiver_id: rcvr
|
||||
@@ -0,0 +1,215 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <functional>
|
||||
|
||||
#include "esphome/components/friedrich/friedrich.h"
|
||||
#include "esphome/components/remote_base/aeha_protocol.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::friedrich::testing {
|
||||
|
||||
using remote_base::AEHAData;
|
||||
using remote_base::AEHAProtocol;
|
||||
|
||||
// Records the last transmission instead of sending it
|
||||
class CapturingTransmitter : public remote_base::RemoteTransmitterBase {
|
||||
public:
|
||||
CapturingTransmitter() : RemoteTransmitterBase(nullptr) {}
|
||||
int sends{0};
|
||||
remote_base::RawTimings last;
|
||||
|
||||
protected:
|
||||
void send_internal(uint32_t, uint32_t) override {
|
||||
this->sends++;
|
||||
this->last = this->temp_.get_data();
|
||||
}
|
||||
};
|
||||
|
||||
class TestFriedrich : public FriedrichClimate {
|
||||
public:
|
||||
using FriedrichClimate::on_receive;
|
||||
using FriedrichClimate::setup;
|
||||
using FriedrichClimate::traits;
|
||||
using FriedrichClimate::transmit_state;
|
||||
};
|
||||
|
||||
constexpr uint16_t ADDRESS = 0x28C6;
|
||||
constexpr uint8_t STATE_MESSAGE_LENGTH = FriedrichClimate::STATE_MESSAGE_LENGTH;
|
||||
// Byte 6 holds the temperature code (bit 7 is the power flag), byte 7 the mode, byte 8 the fan
|
||||
constexpr uint8_t TEMP_72_CODE = 0x06;
|
||||
|
||||
struct Fixture {
|
||||
TestFriedrich sut;
|
||||
CapturingTransmitter transmitter;
|
||||
|
||||
Fixture() { this->sut.set_transmitter(&this->transmitter); }
|
||||
|
||||
// The last transmission decoded, or an empty frame if it could not be decoded
|
||||
AEHAData sent() { return AEHAProtocol().decode(rx_of(this->transmitter.last)).value_or(AEHAData{}); }
|
||||
|
||||
bool receive(const AEHAData &frame) {
|
||||
remote_base::RemoteTransmitData tx;
|
||||
AEHAProtocol().encode(&tx, frame);
|
||||
return this->sut.on_receive(rx_of(tx.get_data()));
|
||||
}
|
||||
|
||||
static remote_base::RemoteReceiveData rx_of(const remote_base::RawTimings &timings) {
|
||||
return remote_base::RemoteReceiveData(timings, 25, remote_base::TOLERANCE_MODE_PERCENTAGE);
|
||||
}
|
||||
};
|
||||
|
||||
// Independent implementation of the 14 byte frame checksum
|
||||
static AEHAData state_frame(uint8_t temp_code, uint8_t mode, uint8_t fan = 0x00) {
|
||||
AEHAData f{ADDRESS, {0x00, 0x08, 0x08, 0x7F, 0x90, 0x0C, (uint8_t) (temp_code | 0x80), mode, fan, 0, 0, 0, 0x04, 0}};
|
||||
uint8_t sum = 0;
|
||||
for (int i = 6; i < 13; i++)
|
||||
sum += reverse_bits(f.data[i]);
|
||||
f.data[13] = reverse_bits(static_cast<uint8_t>(208 - sum));
|
||||
return f;
|
||||
}
|
||||
|
||||
static AEHAData off_frame() { return AEHAData{ADDRESS, {0x00, 0x08, 0x08, 0x40, 0xBF}}; }
|
||||
|
||||
TEST(FriedrichTests, TraitsReportFahrenheit) {
|
||||
Fixture f;
|
||||
auto traits = f.sut.traits();
|
||||
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::FAHRENHEIT);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 60.0f);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 88.0f);
|
||||
EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 2.0f);
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, OffIsAlwaysSentAsFiveByteFrame) {
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_OFF;
|
||||
f.sut.transmit_state();
|
||||
ASSERT_EQ(f.transmitter.sends, 1);
|
||||
EXPECT_EQ(f.sent(), off_frame());
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, StateFrameEncodesFahrenheitTarget) {
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_COOL;
|
||||
f.sut.target_temperature = 72;
|
||||
f.sut.fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
f.sut.swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
f.sut.transmit_state();
|
||||
EXPECT_EQ(f.sent(), state_frame(TEMP_72_CODE, 0x80));
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, OddTargetStepsUpToEven) {
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_COOL;
|
||||
f.sut.target_temperature = 71;
|
||||
f.sut.transmit_state();
|
||||
auto data = f.sent();
|
||||
ASSERT_EQ(data.data.size(), STATE_MESSAGE_LENGTH);
|
||||
EXPECT_EQ(data.data[6], TEMP_72_CODE | 0x80);
|
||||
EXPECT_FLOAT_EQ(f.sut.target_temperature, 72.0f); // stored value matches what was sent
|
||||
}
|
||||
|
||||
// Codes captured from the remote
|
||||
TEST(FriedrichTests, EveryTemperatureEncodesToRemoteCode) {
|
||||
static constexpr uint8_t CODES[] = {0x00, 0x08, 0x04, 0x0C, 0x02, 0x0A, 0x06, 0x0E,
|
||||
0x01, 0x09, 0x05, 0x0D, 0x03, 0x0B, 0x07};
|
||||
for (size_t i = 0; i < sizeof(CODES); i++) {
|
||||
const auto fahrenheit = static_cast<uint8_t>(60 + i * 2);
|
||||
SCOPED_TRACE(fahrenheit);
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_HEAT;
|
||||
f.sut.target_temperature = fahrenheit;
|
||||
f.sut.transmit_state();
|
||||
EXPECT_EQ(f.sent(), state_frame(CODES[i], 0x20));
|
||||
|
||||
Fixture g;
|
||||
EXPECT_TRUE(g.receive(state_frame(CODES[i], 0x20)));
|
||||
EXPECT_FLOAT_EQ(g.sut.target_temperature, fahrenheit);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, NonHeatModesAreRaisedToSixtyFour) {
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_COOL;
|
||||
f.sut.target_temperature = 60;
|
||||
f.sut.transmit_state();
|
||||
EXPECT_FLOAT_EQ(f.sut.target_temperature, 64.0f);
|
||||
auto data = f.sent();
|
||||
ASSERT_EQ(data.data.size(), STATE_MESSAGE_LENGTH);
|
||||
EXPECT_EQ(data.data[6], 0x04 | 0x80); // 64 F
|
||||
|
||||
f.sut.mode = climate::CLIMATE_MODE_HEAT;
|
||||
f.sut.target_temperature = 60;
|
||||
f.sut.transmit_state();
|
||||
EXPECT_FLOAT_EQ(f.sut.target_temperature, 60.0f);
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, ReceivesStateFrame) {
|
||||
Fixture f;
|
||||
EXPECT_TRUE(f.receive(state_frame(TEMP_72_CODE, 0x20, 0x80)));
|
||||
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_HEAT);
|
||||
EXPECT_FLOAT_EQ(f.sut.target_temperature, 72.0f);
|
||||
EXPECT_EQ(f.sut.fan_mode, climate::CLIMATE_FAN_HIGH);
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, ReceivesOffFrame) {
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_COOL;
|
||||
EXPECT_TRUE(f.receive(off_frame()));
|
||||
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_OFF);
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, IgnoresUnknownModeWithoutChangingState) {
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_COOL;
|
||||
f.sut.target_temperature = 80;
|
||||
EXPECT_FALSE(f.receive(state_frame(TEMP_72_CODE, 0xD0))); // MIN_HEAT is not exposed
|
||||
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_COOL);
|
||||
EXPECT_FLOAT_EQ(f.sut.target_temperature, 80.0f);
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, IgnoresCorruptedFrames) {
|
||||
struct Case {
|
||||
const char *name;
|
||||
AEHAData frame;
|
||||
std::function<void(AEHAData &)> corrupt;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{"other address", off_frame(), [](AEHAData &f) { f.address = 0x1234; }},
|
||||
{"bad header byte", off_frame(), [](AEHAData &f) { f.data[1] = 0x09; }},
|
||||
{"bad off checksum", off_frame(), [](AEHAData &f) { f.data[4] ^= 0xFF; }},
|
||||
{"bad state checksum", state_frame(TEMP_72_CODE, 0x20), [](AEHAData &f) { f.data[13] ^= 0xFF; }},
|
||||
};
|
||||
for (const auto &c : cases) {
|
||||
SCOPED_TRACE(c.name);
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_COOL;
|
||||
AEHAData frame = c.frame;
|
||||
c.corrupt(frame);
|
||||
EXPECT_FALSE(f.receive(frame));
|
||||
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_COOL);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, OwnTransmissionIsAccepted) {
|
||||
Fixture f;
|
||||
f.sut.mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
f.sut.target_temperature = 76;
|
||||
f.sut.fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
f.sut.swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
f.sut.transmit_state();
|
||||
|
||||
Fixture g;
|
||||
EXPECT_TRUE(g.sut.on_receive(Fixture::rx_of(f.transmitter.last)));
|
||||
EXPECT_EQ(g.sut.mode, climate::CLIMATE_MODE_FAN_ONLY);
|
||||
EXPECT_FLOAT_EQ(g.sut.target_temperature, 76.0f);
|
||||
EXPECT_EQ(g.sut.fan_mode, climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_EQ(g.sut.swing_mode, climate::CLIMATE_SWING_VERTICAL);
|
||||
}
|
||||
|
||||
TEST(FriedrichTests, SetupDefaultsTargetToSeventyTwoFahrenheit) {
|
||||
Fixture f;
|
||||
f.sut.setup();
|
||||
EXPECT_FLOAT_EQ(f.sut.target_temperature, 72.0f);
|
||||
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_OFF);
|
||||
}
|
||||
|
||||
} // namespace esphome::friedrich::testing
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
friedrich: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
friedrich: !include common.yaml
|
||||
@@ -13,3 +13,32 @@ sensor:
|
||||
shunt_voltage:
|
||||
name: INA3221 Channel 1 Shunt Voltage
|
||||
update_interval: 15s
|
||||
- platform: ina3221
|
||||
i2c_id: i2c_bus
|
||||
address: 0x41
|
||||
continuous_mode: false
|
||||
adc_averaging: 4
|
||||
adc_time:
|
||||
bus_voltage: 2116us
|
||||
shunt_voltage: 1.1ms
|
||||
channel_1:
|
||||
shunt_resistance: 0.05 ohm
|
||||
current:
|
||||
name: INA3221 B Channel 1 Current
|
||||
warning_current_limit: 1A
|
||||
critical_current_limit: 2A
|
||||
channel_2:
|
||||
shunt_resistance: 0.05 ohm
|
||||
bus_voltage:
|
||||
name: INA3221 B Channel 2 Bus Voltage
|
||||
channel_3:
|
||||
shunt_resistance: 0.05 ohm
|
||||
power:
|
||||
name: INA3221 B Channel 3 Power
|
||||
sum_shunt_voltage:
|
||||
name: INA3221 B Sum Shunt Voltage
|
||||
sum_current:
|
||||
name: INA3221 B Sum Current
|
||||
sum_power:
|
||||
name: INA3221 B Sum Power
|
||||
update_interval: 15s
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
ina3221: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
ina3221: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
ina3221: !include common.yaml
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
ld2410s:
|
||||
id: ld2410s_bare
|
||||
uart_id: uart_bus
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2410s
|
||||
ld2410s_id: ld2410s_bare
|
||||
has_target:
|
||||
name: Has Target
|
||||
|
||||
# Entities of other platforms must not pull the parameter code into a bare hub
|
||||
number:
|
||||
- platform: template
|
||||
name: Unrelated Number
|
||||
optimistic: true
|
||||
min_value: 0
|
||||
max_value: 10
|
||||
step: 1
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
name: Unrelated Select
|
||||
optimistic: true
|
||||
options: ["a", "b"]
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: Unrelated Text
|
||||
@@ -5,3 +5,31 @@ binary_sensor:
|
||||
- platform: ld2410s
|
||||
has_target:
|
||||
name: Presence
|
||||
|
||||
sensor:
|
||||
- platform: ld2410s
|
||||
distance:
|
||||
name: Distance
|
||||
|
||||
text_sensor:
|
||||
- platform: ld2410s
|
||||
version:
|
||||
name: Firmware
|
||||
|
||||
number:
|
||||
- platform: ld2410s
|
||||
max_distance:
|
||||
name: Max Distance
|
||||
min_distance:
|
||||
name: Min Distance
|
||||
no_delay:
|
||||
name: No Delay
|
||||
status_reporting_frequency:
|
||||
name: Status Reporting Frequency
|
||||
distance_reporting_frequency:
|
||||
name: Distance Reporting Frequency
|
||||
|
||||
select:
|
||||
- platform: ld2410s
|
||||
response_speed:
|
||||
name: Response Speed
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
|
||||
ld2410s: !include common-bare.yaml
|
||||
@@ -101,6 +101,9 @@ class TestableRuntimeImage : public RuntimeImage {
|
||||
explicit TestableRuntimeImage(ImageFormat format, image::Transparency transparency = image::TRANSPARENCY_OPAQUE)
|
||||
: RuntimeImage(format, image::IMAGE_TYPE_RGB, transparency, nullptr, false, 0, 0) {}
|
||||
|
||||
TestableRuntimeImage(ImageFormat format, image::ImageType type, bool is_big_endian, int fixed_width, int fixed_height)
|
||||
: RuntimeImage(format, type, image::TRANSPARENCY_OPAQUE, nullptr, is_big_endian, fixed_width, fixed_height) {}
|
||||
|
||||
ImageDecoder *decoder() { return this->decoder_.get(); }
|
||||
};
|
||||
|
||||
@@ -373,6 +376,87 @@ TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
|
||||
EXPECT_EQ(img.decoder(), first);
|
||||
EXPECT_EQ(pixel_bytes(img), first_pixels) << "reused decoder must reproduce identical pixels";
|
||||
}
|
||||
|
||||
// JPEGDEC's colour conversion differs by one step between its SIMD and scalar builds, so each
|
||||
// channel is checked within one step; the byte order is still exact
|
||||
static void expect_rgb565_near(uint16_t actual, uint16_t expected, const char *order, size_t pixel) {
|
||||
EXPECT_NEAR(actual >> 11, expected >> 11, 1) << order << " pixel " << pixel << " red";
|
||||
EXPECT_NEAR((actual >> 5) & 0x3F, (expected >> 5) & 0x3F, 1) << order << " pixel " << pixel << " green";
|
||||
EXPECT_NEAR(actual & 0x1F, expected & 0x1F, 1) << order << " pixel " << pixel << " blue";
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
|
||||
// Halving the 8x8 gradient keeps its even rows and columns. The words are JPEGDEC's output for those
|
||||
// pixels, red in the top five bits: a decode of the fixture with PIL gives (51, 7, 30) for source
|
||||
// pixel (2, 0), which is word 0x3803 here.
|
||||
static const uint16_t EXPECTED[4 * 4] = {
|
||||
0x0000, 0x3803, 0x8008, 0xB80B, 0x0203, 0x3A07, 0x81EC, 0xBA0F,
|
||||
0x03E7, 0x3C0B, 0x83F0, 0xBC13, 0x05EC, 0x3E0F, 0x85F4, 0xBE17,
|
||||
};
|
||||
TestableRuntimeImage rgb565_little_endian(JPEG, image::IMAGE_TYPE_RGB565, false, 4, 4);
|
||||
TestableRuntimeImage rgb565_big_endian(JPEG, image::IMAGE_TYPE_RGB565, true, 4, 4);
|
||||
|
||||
ASSERT_TRUE(decode_all(rgb565_little_endian, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_TRUE(decode_all(rgb565_big_endian, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(rgb565_little_endian.get_width(), 4);
|
||||
ASSERT_EQ(rgb565_little_endian.get_height(), 4);
|
||||
ASSERT_EQ(rgb565_big_endian.get_width(), 4);
|
||||
ASSERT_EQ(rgb565_big_endian.get_height(), 4);
|
||||
|
||||
const uint8_t *little = rgb565_little_endian.get_data_start();
|
||||
const uint8_t *big = rgb565_big_endian.get_data_start();
|
||||
for (size_t i = 0; i < 4 * 4; i++) {
|
||||
expect_rgb565_near(little[i * 2] | (little[i * 2 + 1] << 8), EXPECTED[i], "little-endian", i);
|
||||
expect_rgb565_near((big[i * 2] << 8) | big[i * 2 + 1], EXPECTED[i], "big-endian", i);
|
||||
}
|
||||
}
|
||||
|
||||
// Target pixel index each source pixel paints when `source` pixels map onto `target`, the way
|
||||
// prepare_rgb565_scaling_ rounds: source n covers targets ceil(n * target / source) up to the next start
|
||||
static uint16_t scaled_boundary(int n, int source, int target) { return (n * target + source - 1) / source; }
|
||||
|
||||
// The RGB565 word at (x, y) of a decoded image, in its own byte order
|
||||
static uint16_t rgb565_word(const TestableRuntimeImage &img, int x, int y) {
|
||||
const uint8_t *p = img.get_data_start() + (y * img.get_width() + x) * 2;
|
||||
return p[0] | (p[1] << 8);
|
||||
}
|
||||
|
||||
// Every scaled direct decode must equal the unscaled one sampled through the boundary table,
|
||||
// for shrinking, growing and a ratio that is not a whole number
|
||||
TEST(RuntimeImageDecoder, JpegDirectRgb565ScalesBySourcePixelSpans) {
|
||||
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB565, false, 8, 8);
|
||||
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
|
||||
for (int size : {4, 6, 16}) {
|
||||
TestableRuntimeImage scaled(JPEG, image::IMAGE_TYPE_RGB565, false, size, size);
|
||||
ASSERT_TRUE(decode_all(scaled, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(scaled.get_width(), size);
|
||||
ASSERT_EQ(scaled.get_height(), size);
|
||||
for (int n = 0; n < 8; n++) {
|
||||
for (int m = 0; m < 8; m++) {
|
||||
for (int y = scaled_boundary(m, 8, size); y < scaled_boundary(m + 1, 8, size); y++) {
|
||||
for (int x = scaled_boundary(n, 8, size); x < scaled_boundary(n + 1, 8, size); x++) {
|
||||
EXPECT_EQ(rgb565_word(scaled, x, y), rgb565_word(full, n, m))
|
||||
<< size << "x" << size << " pixel " << x << "," << y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A second decode into the same image reuses the boundary tables and gives the same pixels
|
||||
TEST(RuntimeImageDecoder, JpegDirectRgb565RepeatsWithReusedTables) {
|
||||
TestableRuntimeImage img(JPEG, image::IMAGE_TYPE_RGB565, false, 6, 6);
|
||||
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
std::vector<uint8_t> first_pixels = pixel_bytes(img);
|
||||
ImageDecoder *first = img.decoder();
|
||||
ASSERT_NE(first, nullptr);
|
||||
|
||||
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
EXPECT_EQ(img.decoder(), first);
|
||||
EXPECT_EQ(pixel_bytes(img), first_pixels);
|
||||
}
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
|
||||
TEST(RuntimeImageDecoder, QoiDecoderStaysWarmAcrossDecodes) {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
sensor:
|
||||
- platform: selec_meter
|
||||
modbus_id: modbus_bus
|
||||
model: em4m
|
||||
byte_order: little_endian
|
||||
phase_a:
|
||||
voltage:
|
||||
name: SelecEM4M Voltage L1
|
||||
current:
|
||||
name: SelecEM4M Current L1
|
||||
active_power:
|
||||
name: SelecEM4M Active Power L1
|
||||
reactive_power:
|
||||
name: SelecEM4M Reactive Power L1
|
||||
apparent_power:
|
||||
name: SelecEM4M Apparent Power L1
|
||||
power_factor:
|
||||
name: SelecEM4M Power Factor L1
|
||||
import_active_energy:
|
||||
name: SelecEM4M Import Active Energy L1
|
||||
export_active_energy:
|
||||
name: SelecEM4M Export Active Energy L1
|
||||
import_reactive_energy:
|
||||
name: SelecEM4M Import Reactive Energy L1
|
||||
export_reactive_energy:
|
||||
name: SelecEM4M Export Reactive Energy L1
|
||||
apparent_energy:
|
||||
name: SelecEM4M Apparent Energy L1
|
||||
phase_b:
|
||||
voltage:
|
||||
name: SelecEM4M Voltage L2
|
||||
current:
|
||||
name: SelecEM4M Current L2
|
||||
active_power:
|
||||
name: SelecEM4M Active Power L2
|
||||
reactive_power:
|
||||
name: SelecEM4M Reactive Power L2
|
||||
apparent_power:
|
||||
name: SelecEM4M Apparent Power L2
|
||||
power_factor:
|
||||
name: SelecEM4M Power Factor L2
|
||||
import_active_energy:
|
||||
name: SelecEM4M Import Active Energy L2
|
||||
export_active_energy:
|
||||
name: SelecEM4M Export Active Energy L2
|
||||
import_reactive_energy:
|
||||
name: SelecEM4M Import Reactive Energy L2
|
||||
export_reactive_energy:
|
||||
name: SelecEM4M Export Reactive Energy L2
|
||||
apparent_energy:
|
||||
name: SelecEM4M Apparent Energy L2
|
||||
phase_c:
|
||||
voltage:
|
||||
name: SelecEM4M Voltage L3
|
||||
current:
|
||||
name: SelecEM4M Current L3
|
||||
active_power:
|
||||
name: SelecEM4M Active Power L3
|
||||
reactive_power:
|
||||
name: SelecEM4M Reactive Power L3
|
||||
apparent_power:
|
||||
name: SelecEM4M Apparent Power L3
|
||||
power_factor:
|
||||
name: SelecEM4M Power Factor L3
|
||||
import_active_energy:
|
||||
name: SelecEM4M Import Active Energy L3
|
||||
export_active_energy:
|
||||
name: SelecEM4M Export Active Energy L3
|
||||
import_reactive_energy:
|
||||
name: SelecEM4M Import Reactive Energy L3
|
||||
export_reactive_energy:
|
||||
name: SelecEM4M Export Reactive Energy L3
|
||||
apparent_energy:
|
||||
name: SelecEM4M Apparent Energy L3
|
||||
voltage_l12:
|
||||
name: SelecEM4M Voltage L12
|
||||
voltage_l23:
|
||||
name: SelecEM4M Voltage L23
|
||||
voltage_l31:
|
||||
name: SelecEM4M Voltage L31
|
||||
average_voltage_ll:
|
||||
name: SelecEM4M Average Voltage LL
|
||||
active_power:
|
||||
name: SelecEM4M Active Power
|
||||
reactive_power:
|
||||
name: SelecEM4M Reactive Power
|
||||
apparent_power:
|
||||
name: SelecEM4M Apparent Power
|
||||
voltage:
|
||||
name: SelecEM4M Average Voltage
|
||||
current:
|
||||
name: SelecEM4M Average Current
|
||||
power_factor:
|
||||
name: SelecEM4M Average Power Factor
|
||||
frequency:
|
||||
name: SelecEM4M Frequency
|
||||
import_active_energy:
|
||||
name: SelecEM4M Total Import Active Energy
|
||||
export_active_energy:
|
||||
name: SelecEM4M Total Export Active Energy
|
||||
import_reactive_energy:
|
||||
name: SelecEM4M Total Import Reactive Energy
|
||||
export_reactive_energy:
|
||||
name: SelecEM4M Total Export Reactive Energy
|
||||
net_active_energy_mains:
|
||||
name: SelecEM4M Net Active Energy Mains
|
||||
net_reactive_energy_mains:
|
||||
name: SelecEM4M Net Reactive Energy Mains
|
||||
net_apparent_energy_mains:
|
||||
name: SelecEM4M Net Apparent Energy Mains
|
||||
net_active_energy_dg:
|
||||
name: SelecEM4M Net Active Energy DG
|
||||
net_reactive_energy_dg:
|
||||
name: SelecEM4M Net Reactive Energy DG
|
||||
net_apparent_energy_dg:
|
||||
name: SelecEM4M Net Apparent Energy DG
|
||||
maximum_demand_active_power:
|
||||
name: SelecEM4M Maximum Demand Active Power
|
||||
disabled_by_default: true
|
||||
maximum_demand_reactive_power:
|
||||
name: SelecEM4M Maximum Demand Reactive Power
|
||||
disabled_by_default: true
|
||||
maximum_demand_apparent_power:
|
||||
name: SelecEM4M Maximum Demand Apparent Power
|
||||
disabled_by_default: true
|
||||
serial_number:
|
||||
name: SelecEM4M Serial Number
|
||||
dg_sensing:
|
||||
name: SelecEM4M DG Sensing
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
flow_control_pin: GPIO26
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||
selec_meter: !include common-em4m.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO2
|
||||
flow_control_pin: GPIO15
|
||||
|
||||
packages:
|
||||
modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml
|
||||
selec_meter: !include common-em4m.yaml
|
||||
@@ -4,6 +4,5 @@ substitutions:
|
||||
flow_control_pin: GPIO26
|
||||
|
||||
packages:
|
||||
selec_meter: !include common.yaml
|
||||
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -4,6 +4,5 @@ substitutions:
|
||||
flow_control_pin: GPIO15
|
||||
|
||||
packages:
|
||||
selec_meter: !include common.yaml
|
||||
modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -4,6 +4,5 @@ substitutions:
|
||||
flow_control_pin: GPIO6
|
||||
|
||||
packages:
|
||||
selec_meter: !include common.yaml
|
||||
modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user