mirror of
https://github.com/esphome/esphome.git
synced 2026-10-26 16:02:13 +00:00
Compare commits
76
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9cd7228bb8 | ||
|
|
5df6c65cc2 | ||
|
|
af5efabdc6 | ||
|
|
a2754f655c | ||
|
|
34e6d3ec9c | ||
|
|
21362f8c92 | ||
|
|
626d84bc64 | ||
|
|
68fa7b9847 | ||
|
|
32dc3b8535 | ||
|
|
74bcec34ad | ||
|
|
c1687aba4c | ||
|
|
ec642f928f | ||
|
|
ec9a207f42 | ||
|
|
3b7847d951 | ||
|
|
1197f22f00 | ||
|
|
e034662dc6 | ||
|
|
42e849967a | ||
|
|
e6f1ed8d51 | ||
|
|
bb9067acd7 | ||
|
|
928cf7d696 | ||
|
|
89afb111ad | ||
|
|
d1ec52e95a | ||
|
|
818d43f742 | ||
|
|
1a28a9220b | ||
|
|
022733c46b | ||
|
|
0f6fc503bd | ||
|
|
ae62960193 | ||
|
|
325c747dcd | ||
|
|
5759c6b8e6 | ||
|
|
c9ba6c355c | ||
|
|
d02da3c8de | ||
|
|
a576092f25 | ||
|
|
792d650fb2 | ||
|
|
f72857151a | ||
|
|
5f8f075e76 | ||
|
|
c2c8cb5eb3 | ||
|
|
5c2b05d0eb | ||
|
|
e110613ef1 | ||
|
|
4324b7d084 | ||
|
|
37d6140193 | ||
|
|
4f192130f8 | ||
|
|
4ef17cebdb | ||
|
|
35276a38bc | ||
|
|
635df9c6ad | ||
|
|
d16d8c111e | ||
|
|
1ead61f11f | ||
|
|
bd06d37d1a | ||
|
|
7c6fac91ff | ||
|
|
052a6096e6 | ||
|
|
25b684f762 | ||
|
|
62f8cc3436 | ||
|
|
66f15df7be | ||
|
|
d1a237500c | ||
|
|
87350f7d6d | ||
|
|
5357d7f9f8 | ||
|
|
5397546ecd | ||
|
|
34f9152d61 | ||
|
|
73af6b0500 | ||
|
|
a384aae5b1 | ||
|
|
905267a9c3 | ||
|
|
d08c845c65 | ||
|
|
91dfb92c60 | ||
|
|
cef9418a0f | ||
|
|
c0a693dfce | ||
|
|
1b4721d445 | ||
|
|
f8d31c3e54 | ||
|
|
eafcca8274 | ||
|
|
06b585d373 | ||
|
|
885f2fe5d8 | ||
|
|
4e27b2fc12 | ||
|
|
375900bfcd | ||
|
|
6d664d3fef | ||
|
|
4dc2b5911b | ||
|
|
687466b6cd | ||
|
|
dcdc042a4b | ||
|
|
d2a1f7ee31 |
No files matched your search
+9
-1
@@ -157,6 +157,9 @@ esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dew_point/* @CFlix
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
esphome/components/dfrobot_sen0465_base/* @tzabbi
|
||||
esphome/components/dfrobot_sen0465_i2c/* @tzabbi
|
||||
esphome/components/dfrobot_sen0465_uart/* @tzabbi
|
||||
esphome/components/dfrobot_sen0609/* @remcom
|
||||
esphome/components/dht/* @OttoWinter
|
||||
esphome/components/display_menu_base/* @numo68
|
||||
@@ -425,6 +428,9 @@ esphome/components/mpl3115a2/* @kbickar
|
||||
esphome/components/mpu6886/* @fabaff
|
||||
esphome/components/ms8607/* @e28eta
|
||||
esphome/components/msa3xx/* @latonita
|
||||
esphome/components/mt6701/* @slimcdk
|
||||
esphome/components/mt6701_i2c/* @slimcdk
|
||||
esphome/components/mt6701_spi/* @slimcdk
|
||||
esphome/components/nau7802/* @cujomalainey
|
||||
esphome/components/network/* @esphome/core
|
||||
esphome/components/nextion/* @edwardtfn @senexcrenshaw
|
||||
@@ -509,6 +515,7 @@ esphome/components/rp2040_pwm/* @jesserockz
|
||||
esphome/components/rp2_ble_tracker/* @bdraco
|
||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtsp_server/* @max246
|
||||
esphome/components/rtttl/* @glmnet @ximex
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
@@ -684,7 +691,7 @@ esphome/components/version/* @esphome/core
|
||||
esphome/components/voice_assistant/* @jesserockz @kahrendt
|
||||
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
esphome/components/water_heater/* @dhoeben
|
||||
esphome/components/water_heater/* @esphome/core
|
||||
esphome/components/waveshare_epaper/* @clydebarrow
|
||||
esphome/components/waveshare_io_ch32v003/* @latonita
|
||||
esphome/components/web_server/ota/* @esphome/core
|
||||
@@ -723,6 +730,7 @@ esphome/components/xl9535/* @mreditor97
|
||||
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
||||
esphome/components/xxtea/* @clydebarrow
|
||||
esphome/components/zephyr/* @tomaszduda23
|
||||
esphome/components/zephyr_coredump/* @tomaszduda23
|
||||
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
|
||||
esphome/components/zephyr_pwm/* @wiomoc
|
||||
esphome/components/zhlt01/* @cfeenstra1024
|
||||
|
||||
+126
-3
@@ -14,6 +14,7 @@ from esphome.const import (
|
||||
CONF_COUNT,
|
||||
CONF_ELSE,
|
||||
CONF_ID,
|
||||
CONF_IF,
|
||||
CONF_THEN,
|
||||
CONF_TIME,
|
||||
CONF_TIMEOUT,
|
||||
@@ -34,6 +35,9 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
from esphome.util import Registry
|
||||
|
||||
CONF_CHOOSE = "choose"
|
||||
CONF_CHOICE_ID = "choice_id"
|
||||
|
||||
|
||||
def progmem_bytes(name: str, data: bytes | list[int]) -> MockObj:
|
||||
"""Shared PROGMEM table for constant bytes; equal payloads share one, empty is nullptr."""
|
||||
@@ -840,10 +844,39 @@ async def delay_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def _wrap_bare_if_condition(value: Any) -> Any:
|
||||
"""Allow a single condition directly under `if:` in place of `condition: {...}`."""
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
# `any` and `all` are also registered conditions, but here they are the if action's own keys
|
||||
bare = [
|
||||
key
|
||||
for key in value
|
||||
if key in CONDITION_REGISTRY and key not in (CONF_ANY, CONF_ALL)
|
||||
]
|
||||
if not bare:
|
||||
return value
|
||||
if len(bare) > 1:
|
||||
raise cv.Invalid(
|
||||
f"Only one condition may be given directly under 'if', found {', '.join(bare)}. "
|
||||
f"Use '{CONF_CONDITION}', '{CONF_ALL}' or '{CONF_ANY}' to combine conditions"
|
||||
)
|
||||
if other := [k for k in (CONF_CONDITION, CONF_ANY, CONF_ALL) if k in value]:
|
||||
raise cv.Invalid(
|
||||
f"'{bare[0]}' given directly under 'if' cannot be used with '{other[0]}'. "
|
||||
f"Move it into '{other[0]}'"
|
||||
)
|
||||
key = bare[0]
|
||||
result = {k: v for k, v in value.items() if k != key}
|
||||
result[CONF_CONDITION] = {key: value[key]}
|
||||
return result
|
||||
|
||||
|
||||
@register_action(
|
||||
"if",
|
||||
IfAction,
|
||||
cv.All(
|
||||
_wrap_bare_if_condition,
|
||||
{
|
||||
cv.Exclusive(
|
||||
CONF_CONDITION, CONF_CONDITION
|
||||
@@ -870,14 +903,104 @@ async def if_action_to_code(
|
||||
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
|
||||
condition = await build_condition(config[cond_conf], template_arg, args)
|
||||
var = cg.new_Pvariable(action_id, if_template_arg, condition)
|
||||
then_actions = else_actions = None
|
||||
if CONF_THEN in config:
|
||||
for action in await build_action_list(config[CONF_THEN], template_arg, args):
|
||||
then_actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
||||
if has_else:
|
||||
else_actions = await build_action_list(config[CONF_ELSE], template_arg, args)
|
||||
_add_if_branches(var, then_actions, else_actions)
|
||||
return var
|
||||
|
||||
|
||||
def _add_if_branches(
|
||||
var: MockObj,
|
||||
then_actions: list[MockObj] | None,
|
||||
else_actions: list[MockObj] | None,
|
||||
) -> None:
|
||||
"""Add the branches of an IfAction one action at a time; None leaves a branch out."""
|
||||
if then_actions is not None:
|
||||
for action in then_actions:
|
||||
cg.add(var.add_then_action(action))
|
||||
cg.add(var.finish_then())
|
||||
if has_else:
|
||||
for action in await build_action_list(config[CONF_ELSE], template_arg, args):
|
||||
if else_actions is not None:
|
||||
for action in else_actions:
|
||||
cg.add(var.add_else_action(action))
|
||||
cg.add(var.finish_else())
|
||||
|
||||
|
||||
def _validate_choice(choice: ConfigType) -> ConfigType:
|
||||
"""A choice is either `if` with `then`, or `else` on its own."""
|
||||
if CONF_ELSE in choice:
|
||||
for key in (CONF_IF, CONF_THEN):
|
||||
if key in choice:
|
||||
raise cv.Invalid(f"'{key}' cannot be used with 'else'", path=[key])
|
||||
return choice
|
||||
for key in (CONF_IF, CONF_THEN):
|
||||
if key not in choice:
|
||||
raise cv.Invalid(f"'{key}' is required", path=[key])
|
||||
return choice
|
||||
|
||||
|
||||
def _validate_choose_else(choices: list[ConfigType]) -> list[ConfigType]:
|
||||
"""`else` may only be the last choice, and not the only one."""
|
||||
for index, choice in enumerate(choices[:-1]):
|
||||
if CONF_ELSE in choice:
|
||||
raise cv.Invalid(
|
||||
"Only the last choice may be 'else'", path=[index, CONF_ELSE]
|
||||
)
|
||||
if len(choices) == 1 and CONF_ELSE in choices[0]:
|
||||
raise cv.Invalid(
|
||||
"At least one choice must have 'if' and 'then'", path=[0, CONF_ELSE]
|
||||
)
|
||||
return choices
|
||||
|
||||
|
||||
@register_action(
|
||||
CONF_CHOOSE,
|
||||
IfAction,
|
||||
cv.All(
|
||||
cv.ensure_list(
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_CHOICE_ID): cv.declare_id(IfAction),
|
||||
cv.Optional(CONF_IF): validate_potentially_and_condition,
|
||||
cv.Optional(CONF_THEN): validate_action_list,
|
||||
cv.Optional(CONF_ELSE): validate_action_list,
|
||||
}
|
||||
),
|
||||
_validate_choice,
|
||||
)
|
||||
),
|
||||
cv.Length(min=1),
|
||||
_validate_choose_else,
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def choose_action_to_code(
|
||||
config: list[ConfigType],
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
# Built as a chain of if/else actions: each choice's else branch holds the next choice,
|
||||
# and the last choice's else branch holds the `else` actions, if any.
|
||||
else_actions: list[MockObj] | None = None
|
||||
if CONF_ELSE in config[-1]:
|
||||
else_actions = await build_action_list(
|
||||
config[-1][CONF_ELSE], template_arg, args
|
||||
)
|
||||
config = config[:-1]
|
||||
var: MockObj | None = None
|
||||
for index in reversed(range(len(config))):
|
||||
choice = config[index]
|
||||
if_template_arg = cg.TemplateArguments(else_actions is not None, *template_arg)
|
||||
condition = await build_condition(choice[CONF_IF], template_arg, args)
|
||||
choice_id = action_id if index == 0 else choice[CONF_CHOICE_ID]
|
||||
var = cg.new_Pvariable(choice_id, if_template_arg, condition)
|
||||
then_actions = await build_action_list(choice[CONF_THEN], template_arg, args)
|
||||
_add_if_branches(var, then_actions, else_actions)
|
||||
else_actions = [var]
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ from esphome.cpp_helpers import ( # noqa: F401
|
||||
extract_registry_entry_config,
|
||||
get_slot_count,
|
||||
gpio_pin_expression,
|
||||
new_bus_component,
|
||||
past_safe_mode,
|
||||
register_component,
|
||||
register_parented,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
@@ -20,6 +19,4 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.new_spi_component(config)
|
||||
@@ -282,9 +282,7 @@ async def power_channel(config: ConfigType) -> MockObj:
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
if irq0_pin := config.get(CONF_IRQ0_PIN):
|
||||
pin = await cg.gpio_pin_expression(irq0_pin)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -27,8 +26,6 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
cg.add(var.set_continuous_mode(config[CONF_CONTINUOUS_MODE]))
|
||||
@@ -1,7 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@solomondg1"]
|
||||
@@ -26,6 +25,4 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
await spi.new_spi_component(config)
|
||||
@@ -4,7 +4,6 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_TVOC,
|
||||
CONF_VALUE,
|
||||
@@ -58,9 +57,7 @@ FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency=
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TVOC, var.set_tvoc)
|
||||
|
||||
@@ -3,7 +3,6 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_VARIANT,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
@@ -49,9 +48,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
cg.add(var.set_variant(config[CONF_VARIANT]))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
|
||||
@@ -14,80 +14,80 @@ ESPHOME_LOG_TAG(TAG, "aic3204");
|
||||
return; \
|
||||
}
|
||||
|
||||
static constexpr i2c::I2CRegValue AIC3204_INIT[] PROGMEM = {
|
||||
// Set register page to 0
|
||||
{AIC3204_PAGE_CTRL, 0x00},
|
||||
// Initiate SW reset (PLL is powered off as part of reset)
|
||||
{AIC3204_SW_RST, 0x01},
|
||||
// *** Program clock settings ***
|
||||
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
|
||||
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
|
||||
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
|
||||
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
|
||||
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
|
||||
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
|
||||
// for determining these settings.
|
||||
// Power up NDAC and set to 2
|
||||
{AIC3204_NDAC, 0x82},
|
||||
// Power up MDAC and set to 2
|
||||
{AIC3204_MDAC, 0x82},
|
||||
// Program DOSR = 128
|
||||
{AIC3204_DOSR, 0x80},
|
||||
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
|
||||
{AIC3204_CODEC_IF, 0x30},
|
||||
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
|
||||
{AIC3204_SCLK_MFP3, 0x02},
|
||||
{AIC3204_AUDIO_IF_4, 0x01},
|
||||
{AIC3204_AUDIO_IF_5, 0x01},
|
||||
// Program the DAC processing block to be used - PRB_P1
|
||||
{AIC3204_DAC_SIG_PROC, 0x01},
|
||||
// *** Select Page 1 ***
|
||||
{AIC3204_PAGE_CTRL, 0x01},
|
||||
// Enable the internal AVDD_LDO:
|
||||
{AIC3204_LDO_CTRL, 0x09},
|
||||
// *** Program Analog Blocks ***
|
||||
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
|
||||
{AIC3204_PWR_CFG, 0x08},
|
||||
// Enable Master Analog Power Control
|
||||
{AIC3204_LDO_CTRL, 0x01},
|
||||
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
|
||||
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
|
||||
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
|
||||
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
|
||||
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
|
||||
{AIC3204_CM_CTRL, 0x40},
|
||||
// *** Set PowerTune Modes ***
|
||||
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
|
||||
{AIC3204_PLAY_CFG1, 0x00},
|
||||
{AIC3204_PLAY_CFG2, 0x00},
|
||||
// Set the REF charging time to 40ms
|
||||
{AIC3204_REF_STARTUP, 0x01},
|
||||
// HP soft stepping settings for optimal pop performance at power up
|
||||
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
|
||||
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
|
||||
{AIC3204_HP_START, 0x25},
|
||||
// Route Left DAC to HPL
|
||||
{AIC3204_HPL_ROUTE, 0x08},
|
||||
// Route Right DAC to HPR
|
||||
{AIC3204_HPR_ROUTE, 0x08},
|
||||
// Route Left DAC to LOL
|
||||
{AIC3204_LOL_ROUTE, 0x08},
|
||||
// Route Right DAC to LOR
|
||||
{AIC3204_LOR_ROUTE, 0x08},
|
||||
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
|
||||
{AIC3204_HPL_GAIN, 0x3e},
|
||||
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
|
||||
{AIC3204_HPR_GAIN, 0x3e},
|
||||
// Unmute LOL and set gain to 0dB
|
||||
{AIC3204_LOL_DRV_GAIN, 0x00},
|
||||
// Unmute LOR and set gain to 0dB
|
||||
{AIC3204_LOR_DRV_GAIN, 0x00},
|
||||
// Power up HPL and HPR, LOL and LOR drivers
|
||||
{AIC3204_OP_PWR_CTRL, 0x3C},
|
||||
};
|
||||
|
||||
void AIC3204::setup() {
|
||||
// Set register page to 0
|
||||
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
|
||||
// Initiate SW reset (PLL is powered off as part of reset)
|
||||
ERROR_CHECK(this->write_byte(AIC3204_SW_RST, 0x01), "Software reset failed");
|
||||
// *** Program clock settings ***
|
||||
// Default is CODEC_CLKIN is from MCLK pin. Don't need to change this.
|
||||
// MDAC*NDAC*FOSR*48Khz = mClk (24.576 MHz when the XMOS is expecting 48kHz audio)
|
||||
// (See page 51 of https://www.ti.com/lit/ml/slaa557/slaa557.pdf)
|
||||
// We do need MDAC*DOSR/32 >= the resource compute level for the processing block
|
||||
// So here 2*128/32 = 8, which is equal to processing block 1 's resource compute
|
||||
// See page 5 of https://www.ti.com/lit/an/slaa404c/slaa404c.pdf for the workflow
|
||||
// for determining these settings.
|
||||
|
||||
// Power up NDAC and set to 2
|
||||
ERROR_CHECK(this->write_byte(AIC3204_NDAC, 0x82), "Set NDAC failed");
|
||||
// Power up MDAC and set to 2
|
||||
ERROR_CHECK(this->write_byte(AIC3204_MDAC, 0x82), "Set MDAC failed");
|
||||
// Program DOSR = 128
|
||||
ERROR_CHECK(this->write_byte(AIC3204_DOSR, 0x80), "Set DOSR failed");
|
||||
// Set Audio Interface Config: I2S, 32 bits, DOUT always driving
|
||||
ERROR_CHECK(this->write_byte(AIC3204_CODEC_IF, 0x30), "Set CODEC_IF failed");
|
||||
// For I2S Firmware only, set SCLK/MFP3 pin as Audio Data In
|
||||
ERROR_CHECK(this->write_byte(AIC3204_SCLK_MFP3, 0x02), "Set SCLK/MFP3 failed");
|
||||
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_4, 0x01), "Set AUDIO_IF_4 failed");
|
||||
ERROR_CHECK(this->write_byte(AIC3204_AUDIO_IF_5, 0x01), "Set AUDIO_IF_5 failed");
|
||||
// Program the DAC processing block to be used - PRB_P1
|
||||
ERROR_CHECK(this->write_byte(AIC3204_DAC_SIG_PROC, 0x01), "Set DAC_SIG_PROC failed");
|
||||
|
||||
// *** Select Page 1 ***
|
||||
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x01), "Set page 1 failed");
|
||||
// Enable the internal AVDD_LDO:
|
||||
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x09), "Set LDO_CTRL failed");
|
||||
// *** Program Analog Blocks ***
|
||||
// Disable Internal Crude AVdd in presence of external AVdd supply or before powering up internal AVdd LDO
|
||||
ERROR_CHECK(this->write_byte(AIC3204_PWR_CFG, 0x08), "Set PWR_CFG failed");
|
||||
// Enable Master Analog Power Control
|
||||
ERROR_CHECK(this->write_byte(AIC3204_LDO_CTRL, 0x01), "Set LDO_CTRL failed");
|
||||
// Page 125: Common mode control register, set d6 to 1 to make the full chip common mode = 0.75 v
|
||||
// We are using the internal AVdd regulator with a nominal output of 1.72 V (see LDO_CTRL_REGISTER on page 123)
|
||||
// Page 86 says to only set the common mode voltage to 0.9 v if AVdd >= 1.8... but it isn't on our hardware
|
||||
// We also adjust the HPL and HPR gains to -2dB gian later in this config flow compensate (see page 47)
|
||||
// (All pages refer to the TLV320AIC3204 Application Reference Guide)
|
||||
ERROR_CHECK(this->write_byte(AIC3204_CM_CTRL, 0x40), "Set CM_CTRL failed");
|
||||
// *** Set PowerTune Modes ***
|
||||
// Set the Left & Right DAC PowerTune mode to PTM_P3/4. Use Class-AB driver.
|
||||
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG1, 0x00), "Set PLAY_CFG1 failed");
|
||||
ERROR_CHECK(this->write_byte(AIC3204_PLAY_CFG2, 0x00), "Set PLAY_CFG2 failed");
|
||||
// Set the REF charging time to 40ms
|
||||
ERROR_CHECK(this->write_byte(AIC3204_REF_STARTUP, 0x01), "Set REF_STARTUP failed");
|
||||
// HP soft stepping settings for optimal pop performance at power up
|
||||
// Rpop used is 6k with N = 6 and soft step = 20usec. This should work with 47uF coupling
|
||||
// capacitor. Can try N=5,6 or 7 time constants as well. Trade-off delay vs “pop” sound.
|
||||
ERROR_CHECK(this->write_byte(AIC3204_HP_START, 0x25), "Set HP_START failed");
|
||||
// Route Left DAC to HPL
|
||||
ERROR_CHECK(this->write_byte(AIC3204_HPL_ROUTE, 0x08), "Set HPL_ROUTE failed");
|
||||
// Route Right DAC to HPR
|
||||
ERROR_CHECK(this->write_byte(AIC3204_HPR_ROUTE, 0x08), "Set HPR_ROUTE failed");
|
||||
// Route Left DAC to LOL
|
||||
ERROR_CHECK(this->write_byte(AIC3204_LOL_ROUTE, 0x08), "Set LOL_ROUTE failed");
|
||||
// Route Right DAC to LOR
|
||||
ERROR_CHECK(this->write_byte(AIC3204_LOR_ROUTE, 0x08), "Set LOR_ROUTE failed");
|
||||
|
||||
// Unmute HPL and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
|
||||
ERROR_CHECK(this->write_byte(AIC3204_HPL_GAIN, 0x3e), "Set HPL_GAIN failed");
|
||||
// Unmute HPR and set gain to -2dB (see comment before configuring the AIC3204_CM_CTRL register)
|
||||
ERROR_CHECK(this->write_byte(AIC3204_HPR_GAIN, 0x3e), "Set HPR_GAIN failed");
|
||||
// Unmute LOL and set gain to 0dB
|
||||
ERROR_CHECK(this->write_byte(AIC3204_LOL_DRV_GAIN, 0x00), "Set LOL_DRV_GAIN failed");
|
||||
// Unmute LOR and set gain to 0dB
|
||||
ERROR_CHECK(this->write_byte(AIC3204_LOR_DRV_GAIN, 0x00), "Set LOR_DRV_GAIN failed");
|
||||
|
||||
// Power up HPL and HPR, LOL and LOR drivers
|
||||
ERROR_CHECK(this->write_byte(AIC3204_OP_PWR_CTRL, 0x3C), "Set OP_PWR_CTRL failed");
|
||||
ERROR_CHECK(this->write_registers(AIC3204_INIT), "Failed to initialize");
|
||||
|
||||
// Wait for 2.5 sec for soft stepping to take effect before attempting power-up
|
||||
this->set_timeout(2500, [this]() {
|
||||
|
||||
@@ -3,7 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.audio_dac import AudioDac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE
|
||||
from esphome.const import CONF_MODE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -41,6 +41,4 @@ automation.register_apply_action(
|
||||
|
||||
|
||||
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)
|
||||
await i2c.new_i2c_component(config)
|
||||
@@ -3,7 +3,6 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -42,9 +41,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -3,7 +3,6 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -44,9 +43,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -28,15 +28,6 @@ static constexpr uint32_t APC1_NO_DATA_TIMEOUT_MS = 15000;
|
||||
// Drop a partial frame when the bytes stop mid-way
|
||||
static constexpr uint32_t APC1_RX_IDLE_TIMEOUT_MS = 200;
|
||||
|
||||
// Frames and commands end with the 16-bit sum of every byte before it
|
||||
static uint16_t apc1_checksum(const uint8_t *data, size_t len) {
|
||||
uint16_t sum = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void APC1Component::setup() {
|
||||
if (this->set_pin_ != nullptr) {
|
||||
this->set_pin_->setup();
|
||||
@@ -157,7 +148,7 @@ void APC1Component::loop() {
|
||||
|
||||
void APC1Component::parse_frame_(uint16_t total_len) {
|
||||
uint16_t expected_checksum = encode_uint16(this->rx_buffer_[total_len - 2], this->rx_buffer_[total_len - 1]);
|
||||
uint16_t calculated_checksum = apc1_checksum(this->rx_buffer_.data(), total_len - 2);
|
||||
uint16_t calculated_checksum = sum16_bytes(this->rx_buffer_.data(), total_len - 2);
|
||||
if (expected_checksum != calculated_checksum) {
|
||||
ESP_LOGW(TAG, "APC1 checksum mismatch: calculated 0x%04X != expected 0x%04X", calculated_checksum,
|
||||
expected_checksum);
|
||||
@@ -343,7 +334,7 @@ void APC1Component::send_command_(APC1Command cmd, uint16_t data) {
|
||||
0,
|
||||
0,
|
||||
};
|
||||
uint16_t checksum = apc1_checksum(buf.data(), 5);
|
||||
uint16_t checksum = sum16_bytes(buf.data(), 5);
|
||||
buf[5] = static_cast<uint8_t>((checksum >> 8) & 0xFF);
|
||||
buf[6] = static_cast<uint8_t>(checksum & 0xFF);
|
||||
this->write_array(buf);
|
||||
|
||||
@@ -7,7 +7,6 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ECO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_PM_0_3UM,
|
||||
CONF_PM_0_5UM,
|
||||
CONF_PM_1_0,
|
||||
@@ -165,9 +164,7 @@ FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_PM_1_0, var.set_pm_1_0_sensor)
|
||||
|
||||
@@ -32,40 +32,35 @@ void APCProteousCover::loop() {
|
||||
if (!this->read_byte(&data)) {
|
||||
continue;
|
||||
}
|
||||
if (data == '\r') {
|
||||
if (this->rx_len_ != 0) {
|
||||
this->rx_buffer_[this->rx_len_] = '\0';
|
||||
ESP_LOGVV(TAG, "rx: '%s'", this->rx_buffer_);
|
||||
this->parse_response_();
|
||||
this->rx_len_ = 0;
|
||||
}
|
||||
} else if (data != '\n') {
|
||||
if (this->rx_len_ >= MAX_RESPONSE_LEN) {
|
||||
ESP_LOGVV(TAG, "rx overflow, discarding");
|
||||
this->rx_len_ = 0;
|
||||
}
|
||||
this->rx_buffer_[this->rx_len_++] = static_cast<char>(data);
|
||||
const char *line = this->line_reader_.feed(data);
|
||||
if (this->line_reader_.overflowed()) {
|
||||
// Start over from this byte, so a frame right after unterminated noise is still read
|
||||
ESP_LOGVV(TAG, "rx overflow, discarding");
|
||||
this->line_reader_.restart_with(data);
|
||||
} else if (line != nullptr && line[0] != '\0') {
|
||||
ESP_LOGVV(TAG, "rx: '%s'", line);
|
||||
this->parse_response_(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APCProteousCover::parse_response_() {
|
||||
void APCProteousCover::parse_response_(const char *line) {
|
||||
// Expected format: "?s=XX" or "?x=XX" where XX is a hex value
|
||||
if (this->rx_len_ < 4 || this->rx_buffer_[0] != '?' || this->rx_buffer_[2] != '=') {
|
||||
ESP_LOGV(TAG, "Invalid response: %s", this->rx_buffer_);
|
||||
if (line[0] != '?' || line[1] == '\0' || line[2] != '=' || line[3] == '\0') {
|
||||
ESP_LOGV(TAG, "Invalid response: %s", line);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *hex_str = this->rx_buffer_ + 3;
|
||||
const char *hex_str = line + 3;
|
||||
char *end_ptr;
|
||||
uint8_t value = static_cast<uint8_t>(strtol(hex_str, &end_ptr, 16));
|
||||
if (end_ptr == hex_str) {
|
||||
ESP_LOGW(TAG, "Failed to parse hex value: %s", this->rx_buffer_);
|
||||
ESP_LOGW(TAG, "Failed to parse hex value: %s", line);
|
||||
return;
|
||||
}
|
||||
|
||||
bool state_changed = false;
|
||||
char type = this->rx_buffer_[1];
|
||||
char type = line[1];
|
||||
if (type == 's') {
|
||||
// bit 0 = operating, bit 1 = direction (0 = opening, 1 = closing)
|
||||
CoverOperation new_operation = COVER_OPERATION_IDLE;
|
||||
@@ -154,11 +149,10 @@ void APCProteousCover::update() {
|
||||
return;
|
||||
}
|
||||
// The line is idle, so anything left in the buffer is an unterminated echo
|
||||
if (this->rx_len_ != 0) {
|
||||
this->rx_buffer_[this->rx_len_] = '\0';
|
||||
ESP_LOGVV(TAG, "discarding stale rx '%s' before query", this->rx_buffer_);
|
||||
this->rx_len_ = 0;
|
||||
if (this->line_reader_.length() != 0) {
|
||||
ESP_LOGVV(TAG, "discarding stale rx '%s' before query", this->line_reader_.partial());
|
||||
}
|
||||
this->line_reader_.reset();
|
||||
|
||||
const char *query = this->query_s_next_ ? QUERY_S : QUERY_X;
|
||||
ESP_LOGVV(TAG, "tx: %s", query);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/line_reader.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
@@ -17,7 +18,7 @@ class APCProteousCover : public cover::Cover, public PollingComponent, public ua
|
||||
|
||||
protected:
|
||||
void control(const cover::CoverCall &call) override;
|
||||
void parse_response_();
|
||||
void parse_response_(const char *line);
|
||||
void send_command_(const char *cmd);
|
||||
void write_command_(const char *cmd, uint32_t now);
|
||||
void stop_cmd_(uint32_t now);
|
||||
@@ -43,8 +44,7 @@ class APCProteousCover : public cover::Cover, public PollingComponent, public ua
|
||||
const char *pending_command_{nullptr};
|
||||
uint32_t pending_command_time_{0};
|
||||
uint32_t last_command_tx_{0};
|
||||
char rx_buffer_[MAX_RESPONSE_LEN + 1];
|
||||
uint8_t rx_len_{0};
|
||||
LineReader<MAX_RESPONSE_LEN + 1, '\r', '\n'> line_reader_;
|
||||
uint8_t command_retries_{0};
|
||||
bool query_s_next_{true};
|
||||
};
|
||||
|
||||
@@ -52,16 +52,9 @@ enum { // APDS9306 registers
|
||||
}
|
||||
|
||||
void APDS9306::setup() {
|
||||
uint8_t id;
|
||||
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
// 0xB1 for APDS9306, 0xB3 for APDS9306-065; they differ only in bit 1
|
||||
if (!this->check_chip_id(this, APDS9306_PART_ID, 0xB1, 0xFD))
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != 0xB1 && id != 0xB3) { // 0xB1 for APDS9306 0xB3 for APDS9306-065
|
||||
this->mark_failed(LOG_STR("APDS9306 has invalid id"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ALS resolution and measurement, see datasheet or init.py for options
|
||||
uint8_t als_meas_rate = ((this->bit_width_ & 0x07) << 4) | (this->measurement_rate_ & 0x07);
|
||||
|
||||
@@ -64,16 +64,8 @@ static constexpr uint16_t ALS_SATURATION = 1024 * (256 - APDS9930_ATIME_VALUE);
|
||||
bool APDS9930Component::write_reg_(uint8_t reg, uint8_t value) { return this->write_byte(reg | APDS9930_CMD, value); }
|
||||
|
||||
void APDS9930Component::setup() {
|
||||
uint8_t id;
|
||||
if (!this->read_byte(APDS9930_ID | APDS9930_CMD, &id)) {
|
||||
this->mark_failed(LOG_STR("Communication failed"));
|
||||
if (!this->check_chip_id(this, APDS9930_ID | APDS9930_CMD, APDS9930_CHIP_ID))
|
||||
return;
|
||||
}
|
||||
if (id != APDS9930_CHIP_ID) {
|
||||
ESP_LOGE(TAG, "Wrong ID 0x%02X (expected 0x%02X)", id, APDS9930_CHIP_ID);
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t control =
|
||||
(this->led_drive_ & 0b11) << CONTROL_PDRIVE_SHIFT | CONTROL_PDIODE_CH1 << CONTROL_PDIODE_SHIFT |
|
||||
|
||||
@@ -3,7 +3,6 @@ from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_ILLUMINANCE,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
@@ -59,9 +58,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
cg.add(var.set_led_drive(config[CONF_LED_DRIVE]))
|
||||
cg.add(var.set_proximity_gain(config[CONF_PROXIMITY_GAIN]))
|
||||
cg.add(var.set_ambient_gain(config[CONF_AMBIENT_LIGHT_GAIN]))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -60,9 +59,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
cg.add(var.set_led_drive(config[CONF_LED_DRIVE]))
|
||||
cg.add(var.set_proximity_gain(config[CONF_PROXIMITY_GAIN]))
|
||||
cg.add(var.set_ambient_gain(config[CONF_AMBIENT_LIGHT_GAIN]))
|
||||
|
||||
@@ -65,6 +65,9 @@ void APIServer::socket_failed_(const LogString *msg) {
|
||||
}
|
||||
|
||||
void APIServer::setup() {
|
||||
#if defined(USE_API_HOMEASSISTANT_STATES) && defined(HOMEASSISTANT_STATE_SUBSCRIPTION_COUNT)
|
||||
this->state_subs_.reserve(HOMEASSISTANT_STATE_SUBSCRIPTION_COUNT);
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
// Always reserve the slot: flash preferences are positional on esp8266, so
|
||||
// a yaml key build must keep the layout of a runtime key build
|
||||
|
||||
@@ -9,7 +9,6 @@ from esphome.const import (
|
||||
CONF_DIR_PIN,
|
||||
CONF_DIRECTION,
|
||||
CONF_HYSTERESIS,
|
||||
CONF_ID,
|
||||
CONF_POWER_MODE,
|
||||
CONF_RANGE,
|
||||
CONF_WATCHDOG,
|
||||
@@ -209,9 +208,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
cg.add(var.set_direction(config[CONF_DIRECTION]))
|
||||
cg.add(var.set_watchdog(config[CONF_WATCHDOG]))
|
||||
|
||||
@@ -9,14 +9,8 @@ ESPHOME_LOG_TAG(TAG, "as7341");
|
||||
void AS7341Component::setup() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
// Verify device ID
|
||||
uint8_t id;
|
||||
this->read_byte(AS7341_ID, &id);
|
||||
ESP_LOGCONFIG(TAG, " Read ID: 0x%X", id);
|
||||
if ((id & 0xFC) != (AS7341_CHIP_ID << 2)) {
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
if (!this->check_chip_id(this, AS7341_ID, AS7341_CHIP_ID << 2, 0xFC))
|
||||
return;
|
||||
}
|
||||
|
||||
// Power on (enter IDLE state)
|
||||
if (!this->enable_power(true)) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CLEAR,
|
||||
CONF_GAIN,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
ICON_BRIGHTNESS_5,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
@@ -97,9 +96,7 @@ SENSORS = {
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
cg.add(var.set_gain(config[CONF_GAIN]))
|
||||
cg.add(var.set_atime(config[CONF_ATIME]))
|
||||
|
||||
@@ -5,7 +5,6 @@ from esphome.const import (
|
||||
CONF_CALIBRATION,
|
||||
CONF_GAIN,
|
||||
CONF_GLASS_ATTENUATION_FACTOR,
|
||||
CONF_ID,
|
||||
CONF_TYPE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
)
|
||||
@@ -166,9 +165,7 @@ def _per_band(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
chip = config[CONF_TYPE]
|
||||
cg.add_define("USE_AS7341" if chip == "AS7341" else "USE_AS7343")
|
||||
var = cg.new_Pvariable(config[CONF_ID], cg.RawExpression(f"&as734x::{chip}_CHIP"))
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
var = await i2c.new_i2c_component(config, cg.RawExpression(f"&as734x::{chip}_CHIP"))
|
||||
cg.add(var.set_gain(config[CONF_GAIN]))
|
||||
cg.add(var.set_atime(config[CONF_ATIME]))
|
||||
cg.add(var.set_astep(config[CONF_ASTEP]))
|
||||
|
||||
@@ -54,7 +54,8 @@ static const char AS7341_NAME[] PROGMEM = "AS7341";
|
||||
static constexpr uint8_t AS7341_ADC_SLOTS = 6;
|
||||
static constexpr uint8_t AS7341_SMUX_LEN = 20;
|
||||
static constexpr uint8_t AS7341_SMUX_STEPS = 2;
|
||||
static const RegValue AS7341_INIT[] PROGMEM = {{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
|
||||
static constexpr i2c::I2CRegValue AS7341_INIT[] PROGMEM = {
|
||||
{0x70, 0x00}}; // CONFIG: SPM mode, INT_SEL and LED_SEL cleared
|
||||
static const uint8_t AS7341_SMUX[] PROGMEM = {
|
||||
0x30, 0x01, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x50, 0x00, // step 0: F1 to F4, NIR, Clear
|
||||
0x00, 0x00, 0x20, 0x04, 0x00, 0x30, 0x01, 0x50, 0x00, 0x06, // step 0, continued
|
||||
@@ -150,8 +151,8 @@ static constexpr uint8_t AS7343_ADC_SLOTS = 18;
|
||||
static constexpr uint8_t AS7343_SMUX_LEN = 10;
|
||||
static const char TCS3448_NAME[] PROGMEM = "TCS3448";
|
||||
// CFG6, FD_CFG0, CFG10, CFG8, CFG20 (auto_smux, 18 channels in 3 cycles), AGC_GAIN_MAX, FD_TIME_1, FD_TIME_2
|
||||
static const RegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
|
||||
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
|
||||
static constexpr i2c::I2CRegValue AS7343_INIT[] PROGMEM = {{0xF5, 0x00}, {0xDF, 0xA1}, {0x65, 0xF2}, {0xC9, 0xC8},
|
||||
{0xD6, 0x62}, {0xD7, 0x99}, {0xE0, 0x64}, {0xE2, 0x21}};
|
||||
static const uint8_t AS7343_SMUX[] PROGMEM = {0x00, 0x04, 0x65, 0x02, 0x00, 0x05, 0x00, 0x01, 0x00, 0x30,
|
||||
0x00, 0x00, 0x60, 0x20, 0x04, 0x50, 0x03, 0x00, 0x01, 0x05,
|
||||
0x05, 0x00, 0x60, 0x00, 0x30, 0x00, 0x40, 0x10, 0x20, 0x00};
|
||||
@@ -230,7 +231,7 @@ static constexpr ChipInfo as7343_chip(const char *name) {
|
||||
.gain_correction = AS7343_GAIN_CORRECTION,
|
||||
.contributions = AS7343_CONTRIBUTIONS,
|
||||
.tristimulus = AS7343_TRISTIMULUS,
|
||||
.init_count = sizeof(AS7343_INIT) / sizeof(RegValue),
|
||||
.init_count = sizeof(AS7343_INIT) / sizeof(i2c::I2CRegValue),
|
||||
.astep_reg = 0xD4,
|
||||
.cfg0_reg = 0xBF,
|
||||
.cfg1_reg = 0xC6,
|
||||
@@ -303,9 +304,9 @@ void AS734XComponent::setup() {
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
return;
|
||||
}
|
||||
const RegValue *init = pgm(chip->init);
|
||||
const i2c::I2CRegValue *init = pgm(chip->init);
|
||||
for (uint8_t i = 0, n = pgm(chip->init_count); ok && i < n; i++) {
|
||||
RegValue rv;
|
||||
i2c::I2CRegValue rv;
|
||||
progmem_memcpy(&rv, &init[i], sizeof(rv));
|
||||
ok = this->select_bank_(rv.reg) && this->write_byte(rv.reg, rv.value);
|
||||
}
|
||||
|
||||
@@ -68,15 +68,10 @@ struct ChannelTristimulus {
|
||||
float z;
|
||||
};
|
||||
|
||||
struct RegValue {
|
||||
uint8_t reg;
|
||||
uint8_t value;
|
||||
};
|
||||
|
||||
/// Per-chip constants, stored in flash. The pointed-to tables are in flash too.
|
||||
struct ChipInfo {
|
||||
const char *name;
|
||||
const RegValue *init; // written in order; the bank is selected per register address
|
||||
const i2c::I2CRegValue *init; // written in order; the bank is selected per register address
|
||||
const uint8_t *smux; // AS7341: SMUX RAM image per step; AS7343: the three chain images
|
||||
const uint8_t *readout; // per step, the ADC slot of each band (0xFF: not read in this step)
|
||||
const uint16_t *wavelengths; // per band, WIDEBAND_NM for clear and ABSENT_NM for a missing band
|
||||
|
||||
@@ -73,9 +73,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
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)
|
||||
await i2c.new_i2c_component(config)
|
||||
|
||||
|
||||
# Actions
|
||||
|
||||
@@ -5,7 +5,6 @@ from esphome.const import (
|
||||
CONF_CURRENT,
|
||||
CONF_FORWARD_ACTIVE_ENERGY,
|
||||
CONF_FREQUENCY,
|
||||
CONF_ID,
|
||||
CONF_LINE_FREQUENCY,
|
||||
CONF_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
@@ -125,9 +124,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
var = await spi.new_spi_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
|
||||
@@ -6,7 +6,6 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_GAIN,
|
||||
CONF_GLASS_ATTENUATION_FACTOR,
|
||||
CONF_ID,
|
||||
CONF_INTEGRATION_TIME,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
@@ -62,9 +61,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
cg.add(var.set_adc_gain(config[CONF_GAIN]))
|
||||
cg.add(var.set_measurement_time(config[CONF_INTEGRATION_TIME]))
|
||||
|
||||
@@ -63,15 +63,8 @@ static float calculate_cct(uint16_t red, uint16_t green, uint16_t blue, bool low
|
||||
}
|
||||
|
||||
void BH1745Component::setup() {
|
||||
uint8_t manufacturer_id;
|
||||
if (!this->read_byte(REG_MANUFACTURER_ID, &manufacturer_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
if (!this->check_chip_id(this, REG_MANUFACTURER_ID, MANUFACTURER_ID))
|
||||
return;
|
||||
}
|
||||
if (manufacturer_id != MANUFACTURER_ID) {
|
||||
this->mark_failed(LOG_STR("Wrong manufacturer ID"));
|
||||
return;
|
||||
}
|
||||
uint8_t system_control;
|
||||
if (!this->read_byte(REG_SYSTEM_CONTROL, &system_control) ||
|
||||
(system_control & SYSTEM_CONTROL_PART_ID_MASK) != PART_ID) {
|
||||
|
||||
@@ -147,9 +147,7 @@ automation.register_apply_action(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_FREQUENCY, var.set_frequency_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -2,7 +2,6 @@ import esphome.codegen as cg
|
||||
from esphome.components import sensor, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INTERNAL_TEMPERATURE,
|
||||
CONF_MODE,
|
||||
CONF_VOLTAGE,
|
||||
@@ -112,9 +111,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
|
||||
if (mode := config[CONF_MODE]) != DEFAULT_MODE:
|
||||
cg.add(var.set_mode(mode))
|
||||
|
||||
@@ -5,7 +5,6 @@ from esphome.const import (
|
||||
CONF_CURRENT,
|
||||
CONF_ENERGY,
|
||||
CONF_EXTERNAL_TEMPERATURE,
|
||||
CONF_ID,
|
||||
CONF_INTERNAL_TEMPERATURE,
|
||||
CONF_POWER,
|
||||
CONF_REFERENCE_VOLTAGE,
|
||||
@@ -227,9 +226,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
|
||||
@@ -6,7 +6,6 @@ from esphome.const import (
|
||||
CONF_CURRENT,
|
||||
CONF_ENERGY,
|
||||
CONF_FREQUENCY,
|
||||
CONF_ID,
|
||||
CONF_LINE_FREQUENCY,
|
||||
CONF_POWER,
|
||||
CONF_RESET,
|
||||
@@ -97,9 +96,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
|
||||
@@ -2,7 +2,7 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DURATION, CONF_ID
|
||||
from esphome.const import CONF_DURATION
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -50,7 +50,5 @@ automation.register_apply_action(
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
await time.register_time(var, config)
|
||||
@@ -7,7 +7,6 @@ from esphome.const import (
|
||||
CONF_GAS_RESISTANCE,
|
||||
CONF_HEATER,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_IIR_FILTER,
|
||||
CONF_OVERSAMPLING,
|
||||
CONF_PRESSURE,
|
||||
@@ -115,9 +114,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
if await sensors(CONF_TEMPERATURE, var.set_temperature_sensor):
|
||||
|
||||
@@ -73,9 +73,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
cg.add(var.set_device_id(str(config[CONF_ID])))
|
||||
cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET]))
|
||||
|
||||
@@ -8,7 +8,6 @@ from esphome.const import (
|
||||
CONF_GYROSCOPE_X,
|
||||
CONF_GYROSCOPE_Y,
|
||||
CONF_GYROSCOPE_Z,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_ACCELERATION_X,
|
||||
@@ -84,9 +83,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_ACCELERATION_X, var.set_accel_x_sensor)
|
||||
|
||||
@@ -78,17 +78,8 @@ bool BMI270Component::load_config_file_() {
|
||||
void BMI270Component::setup() {
|
||||
MotionComponent::setup();
|
||||
// 1. Verify chip ID
|
||||
uint8_t chip_id = 0;
|
||||
if (!this->read_byte(BMI270_REG_CHIP_ID, &chip_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
if (!this->check_chip_id(this, BMI270_REG_CHIP_ID, BMI270_CHIP_ID_VALUE))
|
||||
return;
|
||||
}
|
||||
if (chip_id != BMI270_CHIP_ID_VALUE) {
|
||||
ESP_LOGE(TAG, "Wrong chip ID: 0x%02X (expected 0x%02X)", chip_id, BMI270_CHIP_ID_VALUE);
|
||||
this->mark_failed(LOG_STR("Wrong chip ID"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Chip ID: 0x%02X", chip_id);
|
||||
|
||||
// 2. Soft-reset via CMD register (0x7E = 0xB6)
|
||||
if (!this->write_byte(BMI270_REG_CMD, BMI270_CMD_SOFT_RESET)) {
|
||||
|
||||
@@ -2,7 +2,6 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
@@ -44,9 +43,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature)
|
||||
|
||||
@@ -338,9 +338,7 @@ CONFIGURE_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
if config[CONF_CONFIGURE_ON_BOOT]:
|
||||
cg.add(var.set_setup_registers(cg.ArrayInitializer(*pack_registers(config))))
|
||||
|
||||
@@ -79,19 +79,9 @@ bool BQ25186Component::apply_config(const BQ25186ConfigRegisters ®s) {
|
||||
}
|
||||
|
||||
void BQ25186Component::setup() {
|
||||
uint8_t mask_id;
|
||||
if (!this->read_byte(BQ25186_REG_MASK_ID, &mask_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Another charger at this address could lay its registers out differently, so do not write into it
|
||||
const uint8_t device_id = mask_id & 0x0F;
|
||||
if (device_id != 0x01) {
|
||||
ESP_LOGE(TAG, "Unexpected device id 0x%X (expected 0x1)", device_id);
|
||||
this->mark_failed(LOG_STR("Not a BQ25186"));
|
||||
if (!this->check_chip_id(this, BQ25186_REG_MASK_ID, 0x01, 0x0F))
|
||||
return;
|
||||
}
|
||||
|
||||
// A failed write only sets the warning status: the chip has identified itself, so a later
|
||||
// apply_configuration may retry
|
||||
|
||||
@@ -5,7 +5,6 @@ import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_CURRENT,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
@@ -108,9 +107,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import i2c
|
||||
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.const import CONF_ADDRESS
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["sensor"]
|
||||
CODEOWNERS = ["@limpkin"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "bq76972"
|
||||
@@ -26,37 +17,7 @@ BQ76972Component = bq76972_ns.class_(
|
||||
|
||||
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,
|
||||
)
|
||||
CONF_BQ76972_ID = "bq76972_id"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -64,9 +25,6 @@ CONFIG_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))
|
||||
@@ -75,26 +33,11 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(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(
|
||||
{
|
||||
|
||||
@@ -172,10 +172,12 @@ const LogString *BQ76972Component::configure_() {
|
||||
const bool write_regs = !regs_disabled;
|
||||
|
||||
bool write_thermistors = false;
|
||||
#ifdef USE_SENSOR
|
||||
for (size_t i = 1; i < BQ76972_NUM_TEMPERATURES; i++) {
|
||||
if (i != BQ76972_TS1_INDEX && this->temperature_sensors_[i] != nullptr)
|
||||
write_thermistors = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (write_regs || write_thermistors) {
|
||||
if (!this->enter_cfg_update_())
|
||||
@@ -201,12 +203,14 @@ const LogString *BQ76972Component::write_config_(bool write_regs) {
|
||||
!this->write_subcommand_(BQ76972_MEM_REG0_CONFIG, &zero, 1))
|
||||
return LOG_STR("Writing regulator config failed");
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
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");
|
||||
}
|
||||
#endif
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -248,6 +252,7 @@ void BQ76972Component::program_i2c_address(uint8_t address) {
|
||||
ESP_LOGI(TAG, "I2C address set to 0x%02X", address);
|
||||
}
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
template<size_t N> static bool any_set(const std::array<sensor::Sensor *, N> &sensors) {
|
||||
for (auto *sensor : sensors) {
|
||||
if (sensor != nullptr)
|
||||
@@ -294,5 +299,8 @@ void BQ76972Component::update() {
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
}
|
||||
#else
|
||||
void BQ76972Component::update() {}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::bq76972
|
||||
@@ -1,8 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
|
||||
@@ -19,9 +22,11 @@ class BQ76972Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
|
||||
void set_crc_mode(bool crc_mode) { this->crc_mode_ = crc_mode; }
|
||||
void set_reg_disable(bool reg_disable) { this->reg_disable_ = reg_disable; }
|
||||
#ifdef USE_SENSOR
|
||||
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; }
|
||||
#endif
|
||||
|
||||
void program_i2c_address(uint8_t address);
|
||||
|
||||
@@ -43,9 +48,11 @@ class BQ76972Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
bool reg_disable_{false};
|
||||
bool crc_mode_{false};
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
sensor::Sensor *stack_voltage_sensor_{nullptr};
|
||||
std::array<sensor::Sensor *, BQ76972_NUM_CELLS> cell_sensors_{};
|
||||
std::array<sensor::Sensor *, BQ76972_NUM_TEMPERATURES> temperature_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::bq76972
|
||||
@@ -0,0 +1,73 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_BQ76972_ID, BQ76972Component
|
||||
|
||||
DEPENDENCIES = ["bq76972"]
|
||||
|
||||
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(CONF_BQ76972_ID): cv.use_id(BQ76972Component),
|
||||
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},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_BQ76972_ID])
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_STACK_VOLTAGE, hub.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(hub.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(hub.set_cell_sensor(sens, idx))
|
||||
@@ -15,6 +15,11 @@ from esphome.const import (
|
||||
DEVICE_CLASS_IDENTIFY,
|
||||
DEVICE_CLASS_RESTART,
|
||||
DEVICE_CLASS_UPDATE,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_DATABASE,
|
||||
ICON_RESTART,
|
||||
ICON_RESTART_ALERT,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
@@ -83,6 +88,40 @@ def button_schema(
|
||||
return _BUTTON_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
def factory_reset_button_schema(class_: MockObjClass) -> cv.Schema:
|
||||
"""Schema for a button that resets the connected module to factory settings.
|
||||
|
||||
For a sensor or radio module's own reset, not the ESPHome node.
|
||||
"""
|
||||
return button_schema(
|
||||
class_,
|
||||
device_class=DEVICE_CLASS_RESTART,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_RESTART_ALERT,
|
||||
)
|
||||
|
||||
|
||||
def restart_button_schema(class_: MockObjClass) -> cv.Schema:
|
||||
"""Schema for a button that restarts the connected module.
|
||||
|
||||
For a sensor or radio module's own restart, not the ESPHome node; the core
|
||||
restart button uses the config category instead of diagnostic.
|
||||
"""
|
||||
return button_schema(
|
||||
class_,
|
||||
device_class=DEVICE_CLASS_RESTART,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
icon=ICON_RESTART,
|
||||
)
|
||||
|
||||
|
||||
def query_button_schema(class_: MockObjClass) -> cv.Schema:
|
||||
"""Schema for a button that reads the settings back from the connected module."""
|
||||
return button_schema(
|
||||
class_, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_DATABASE
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_PRESS, "add_on_press_callback"),
|
||||
)
|
||||
|
||||
@@ -11,8 +11,9 @@ namespace esphome::camera {
|
||||
* IDLE: Camera requests to send an image to the API.
|
||||
* API_REQUESTER: API requests a new image.
|
||||
* WEB_REQUESTER: ESP32 web server request an image. Ignored by API.
|
||||
* RTSP_REQUESTER: RTSP server requests a stream of images. Ignored by API and web server.
|
||||
*/
|
||||
enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER };
|
||||
enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER, RTSP_REQUESTER };
|
||||
|
||||
/// Enumeration of different pixel formats.
|
||||
enum PixelFormat : uint8_t {
|
||||
|
||||
@@ -274,9 +274,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
var = await spi.new_spi_component(config)
|
||||
|
||||
for opt in CONFIG_MAP:
|
||||
key = opt.schema
|
||||
|
||||
@@ -5,7 +5,6 @@ from esphome.const import (
|
||||
CONF_BASELINE,
|
||||
CONF_ECO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TVOC,
|
||||
CONF_VERSION,
|
||||
@@ -61,9 +60,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_ECO2, var.set_co2)
|
||||
|
||||
@@ -135,13 +135,14 @@ void LgIrClimate::transmit_state() {
|
||||
default:
|
||||
return;
|
||||
}
|
||||
this->transmit_(remote_state);
|
||||
this->publish_state();
|
||||
return;
|
||||
} else { // just toggle swing when advanced_commands_support is not set
|
||||
remote_state |= HEADER_BASIC;
|
||||
remote_state |= BASIC_SWING_TOGGLE;
|
||||
}
|
||||
// The remote's swing button sends a fixed code with no fan speed or temperature bits, so the
|
||||
// frame goes out as is instead of falling through to them
|
||||
this->transmit_(remote_state);
|
||||
return;
|
||||
} else { // Mode commands
|
||||
const bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF);
|
||||
switch (this->mode) {
|
||||
@@ -208,7 +209,6 @@ void LgIrClimate::transmit_state() {
|
||||
}
|
||||
|
||||
this->transmit_(remote_state);
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
|
||||
@@ -10,7 +10,6 @@ from esphome.const import (
|
||||
CONF_BASELINE,
|
||||
CONF_CO2,
|
||||
CONF_CYCLE,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
ICON_MOLECULE_CO2,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
@@ -68,9 +67,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
"""Code generation entry point."""
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_CO2, var.set_co2_sensor)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DATA_BITS = "data_bits"
|
||||
CONF_DESCRIPTION = "description"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_DRIVE_STRENGTH = "drive_strength"
|
||||
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_GREEN_CHANNEL = "green_channel"
|
||||
|
||||
@@ -103,9 +103,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
var = await spi.new_spi_component(config)
|
||||
|
||||
cg.add(var.set_samples(config[CONF_SAMPLES]))
|
||||
cg.add(var.set_phase_offset(config[CONF_PHASE_OFFSET]))
|
||||
|
||||
@@ -3,7 +3,6 @@ from esphome.components import sensor, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FREQUENCY,
|
||||
CONF_ID,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
@@ -121,9 +120,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_CURRENT_INPUT])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config, config[CONF_CURRENT_INPUT])
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
|
||||
@@ -5,7 +5,6 @@ from esphome.const import (
|
||||
CONF_APPARENT_POWER,
|
||||
CONF_CURRENT,
|
||||
CONF_ENERGY,
|
||||
CONF_ID,
|
||||
CONF_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_REACTIVE_POWER,
|
||||
@@ -94,9 +93,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ADDRESS, CONF_ID
|
||||
from esphome.const import CONF_ADDRESS
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@s1lvi0"]
|
||||
@@ -37,7 +37,5 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
cg.add(var.set_address(config[CONF_ADDRESS]))
|
||||
@@ -34,6 +34,15 @@ void DebugComponent::dump_config() {
|
||||
this->free_heap_ = get_free_heap_();
|
||||
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
|
||||
|
||||
uint32_t uptime = static_cast<uint32_t>(millis_64() / 1000);
|
||||
uint32_t days = uptime / 86400;
|
||||
uptime -= days * 86400;
|
||||
uint32_t hours = uptime / 3600;
|
||||
uptime -= hours * 3600;
|
||||
uint32_t minutes = uptime / 60;
|
||||
uptime -= minutes * 60;
|
||||
ESP_LOGD(TAG, "Uptime: %" PRIu32 "d %02" PRIu32 "h %02" PRIu32 "m %02" PRIu32 "s", days, hours, minutes, uptime);
|
||||
|
||||
pos = get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>(device_info_buffer), pos);
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
@@ -155,6 +155,12 @@ def validate_wakeup_pin(
|
||||
return processed_pins
|
||||
|
||||
|
||||
_WAKEUP_PIN_MODE_BOTH_LEVELS = (
|
||||
"Specify 'wakeup_pin_mode' either at the top level under 'deep_sleep' "
|
||||
"or under the pin entry, not both"
|
||||
)
|
||||
|
||||
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
wakeup_pins = config.get(CONF_WAKEUP_PIN)
|
||||
has_pin_mode = CONF_WAKEUP_PIN_MODE in config
|
||||
@@ -170,6 +176,14 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
"ESP32 does not support multiple entries for 'wakeup_pin'",
|
||||
path=[CONF_WAKEUP_PIN],
|
||||
)
|
||||
if wakeup_pins and CONF_WAKEUP_PIN_MODE in (pin := wakeup_pins[0]):
|
||||
if has_pin_mode:
|
||||
raise cv.Invalid(
|
||||
_WAKEUP_PIN_MODE_BOTH_LEVELS,
|
||||
path=[CONF_WAKEUP_PIN, 0, CONF_WAKEUP_PIN_MODE],
|
||||
)
|
||||
# ESP32 has one global mode, which to_code() reads from the top level
|
||||
config[CONF_WAKEUP_PIN_MODE] = pin.pop(CONF_WAKEUP_PIN_MODE)
|
||||
elif has_pin_mode:
|
||||
if multiple_pins:
|
||||
raise cv.Invalid(
|
||||
@@ -178,8 +192,7 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
)
|
||||
if CONF_WAKEUP_PIN_MODE in (pin := wakeup_pins[0]):
|
||||
raise cv.Invalid(
|
||||
"Specify 'wakeup_pin_mode' either at the top level under 'deep_sleep' "
|
||||
"or under the pin entry, not both",
|
||||
_WAKEUP_PIN_MODE_BOTH_LEVELS,
|
||||
path=[CONF_WAKEUP_PIN, 0, CONF_WAKEUP_PIN_MODE],
|
||||
)
|
||||
# Move the global mode into the single pin entry, which to_code() reads
|
||||
|
||||
@@ -5,7 +5,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.components.const import CONF_LOOP
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DEVICE, CONF_FILE, CONF_ID, CONF_VOLUME
|
||||
from esphome.const import CONF_DEVICE, CONF_FILE, CONF_VOLUME
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
CODEOWNERS = ["@glmnet"]
|
||||
@@ -51,9 +51,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
var = await uart.new_uart_component(config)
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@@ -124,10 +124,7 @@ void DFPlayer::play_folder(uint16_t folder, uint16_t file) {
|
||||
|
||||
void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
||||
uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t) (argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef};
|
||||
uint16_t checksum = 0;
|
||||
for (uint8_t i = 1; i < 7; i++)
|
||||
checksum += buffer[i];
|
||||
checksum = -checksum;
|
||||
const uint16_t checksum = static_cast<uint16_t>(-sum16_bytes(buffer + 1, 6));
|
||||
buffer[7] = checksum >> 8;
|
||||
buffer[8] = (uint8_t) checksum;
|
||||
|
||||
|
||||
@@ -42,9 +42,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
await uart.new_uart_component(config)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MODEL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TEMPERATURE_COMPENSATION,
|
||||
CONF_THRESHOLD,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_CARBON_MONOXIDE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ICON_GAS_CYLINDER,
|
||||
ICON_MOLECULE_CO,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@tzabbi"]
|
||||
DOMAIN = "dfrobot_sen0465_base"
|
||||
|
||||
CONF_ALARM = "alarm"
|
||||
CONF_CONCENTRATION = "concentration"
|
||||
|
||||
dfrobot_sen0465_base_ns = cg.esphome_ns.namespace("dfrobot_sen0465_base")
|
||||
DFRobotSEN0465Component = dfrobot_sen0465_base_ns.class_(
|
||||
"DFRobotSEN0465Component", cg.PollingComponent
|
||||
)
|
||||
AlarmType = dfrobot_sen0465_base_ns.enum("AlarmType", is_class=True)
|
||||
ALARM_TYPES = {
|
||||
"LOW": AlarmType.ALARM_TYPE_LOW,
|
||||
"HIGH": AlarmType.ALARM_TYPE_HIGH,
|
||||
}
|
||||
|
||||
# One piece of the temperature compensation from the DFRobot_MultiGasSensor library:
|
||||
# for low < T <= high, compensated = raw / (a * T + b) - (c * T + d)
|
||||
CompensationRange = tuple[int, int, float, float, float, float]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Model:
|
||||
"""One gas probe of the series."""
|
||||
|
||||
gas: str
|
||||
gas_type: int # Gas type code the sensor reports
|
||||
unit: str
|
||||
# Also the resolution of the alarm threshold the sensor expects
|
||||
accuracy_decimals: int
|
||||
compensation: tuple[CompensationRange, ...] = ()
|
||||
device_class: str = cv.UNDEFINED
|
||||
icon: str = ICON_GAS_CYLINDER
|
||||
|
||||
|
||||
MODELS: dict[str, Model] = {
|
||||
"SEN0465": Model("O2", 0x05, UNIT_PERCENT, 1),
|
||||
"SEN0466": Model(
|
||||
"CO",
|
||||
0x04,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
0,
|
||||
(
|
||||
(-20, 20, 0.005, 0.9, 0, 0),
|
||||
(20, 40, 0.005, 0.9, 0.3, -6),
|
||||
),
|
||||
device_class=DEVICE_CLASS_CARBON_MONOXIDE,
|
||||
icon=ICON_MOLECULE_CO,
|
||||
),
|
||||
"SEN0467": Model(
|
||||
"H2S",
|
||||
0x03,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
0,
|
||||
(
|
||||
(-20, 20, 0.005, 0.92, 0, 0),
|
||||
(20, 60, 0.015, -0.3, 0, 0),
|
||||
),
|
||||
),
|
||||
"SEN0468": Model(
|
||||
"Cl2",
|
||||
0x31,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
1,
|
||||
(
|
||||
(-20, 0, 0.015, 1.1, -0.0025, 0),
|
||||
(0, 20, 0, 1.1, 0.005, 0),
|
||||
(20, 40, 0, 1.1, -0.005, 0.3),
|
||||
),
|
||||
),
|
||||
"SEN0469": Model(
|
||||
"NH3",
|
||||
0x02,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
0,
|
||||
(
|
||||
(-20, 0, 0.006, 0.95, -0.006, 0.25),
|
||||
(0, 20, 0.006, 0.95, -0.012, 0.25),
|
||||
(20, 40, 0.005, 1.08, -0.1, 2),
|
||||
),
|
||||
),
|
||||
"SEN0470": Model(
|
||||
"SO2",
|
||||
0x2B,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
1,
|
||||
(
|
||||
(-40, 40, 0.006, 0.95, 0, 0),
|
||||
(40, 60, 0.006, 0.95, 0.05, -2),
|
||||
),
|
||||
),
|
||||
"SEN0471": Model(
|
||||
"NO2",
|
||||
0x2C,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
1,
|
||||
(
|
||||
(-20, 0, 0.005, 0.9, -0.0025, 0.005),
|
||||
(0, 20, 0.005, 0.9, 0.005, 0.005),
|
||||
(20, 40, 0.005, 0.9, 0.0025, 0.1),
|
||||
),
|
||||
),
|
||||
"SEN0472": Model(
|
||||
"O3",
|
||||
0x2A,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
1,
|
||||
(
|
||||
(-20, 0, 0.015, 1.1, 0, 0.05),
|
||||
(0, 20, 0, 1.1, 0.01, 0),
|
||||
(20, 40, 0, 1.1, -0.005, 0.3),
|
||||
),
|
||||
),
|
||||
# The library's H2 compensation divides by (0.74 * T + 0.007) below 20 °C, which gives
|
||||
# nonsense results, so it is not offered.
|
||||
"SEN0473": Model("H2", 0x06, UNIT_PARTS_PER_MILLION, 0),
|
||||
"SEN0474": Model(
|
||||
"HCl",
|
||||
0x2E,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
1,
|
||||
(
|
||||
(-20, 0, 0, 1, -0.0075, -0.1),
|
||||
(0, 20, 0, 1, 0, -0.1),
|
||||
(20, 50, 0, 1, -0.01, 0.1),
|
||||
),
|
||||
),
|
||||
"SEN0475": Model(
|
||||
"HF",
|
||||
0x33,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
1,
|
||||
(
|
||||
(-20, 0, 0, 1, -0.0025, 0),
|
||||
(0, 20, 0, 1, 0, -0.1),
|
||||
(20, 40, 0, 1, 0.0375, -0.85),
|
||||
),
|
||||
),
|
||||
"SEN0476": Model(
|
||||
"PH3",
|
||||
0x45,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
1,
|
||||
((-20, 40, 0.005, 0.9, 0, 0),),
|
||||
),
|
||||
}
|
||||
|
||||
# Sizes the C++ range list; the largest table entry sets it
|
||||
COMPENSATION_RANGES = max(len(model.compensation) for model in MODELS.values())
|
||||
|
||||
|
||||
def _model_schema(
|
||||
model: Model, component_class: MockObjClass, bus_schema: cv.Schema
|
||||
) -> cv.Schema:
|
||||
schema = {
|
||||
cv.GenerateID(): cv.declare_id(component_class),
|
||||
cv.Optional(CONF_CONCENTRATION): sensor.sensor_schema(
|
||||
unit_of_measurement=model.unit,
|
||||
icon=model.icon,
|
||||
accuracy_decimals=model.accuracy_decimals,
|
||||
device_class=model.device_class,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
if model.compensation:
|
||||
schema[cv.Optional(CONF_TEMPERATURE_COMPENSATION, default=False)] = cv.boolean
|
||||
# The sensor sends the threshold as a 16-bit count of its last decimal place
|
||||
max_threshold = 0xFFFF / 10**model.accuracy_decimals
|
||||
schema[cv.Optional(CONF_ALARM)] = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_THRESHOLD): cv.float_range(min=0, max=max_threshold),
|
||||
cv.Optional(CONF_TYPE, default="HIGH"): cv.enum(ALARM_TYPES, upper=True),
|
||||
}
|
||||
)
|
||||
return (
|
||||
cv.Schema(schema).extend(cv.polling_component_schema("60s")).extend(bus_schema)
|
||||
)
|
||||
|
||||
|
||||
def dfrobot_sen0465_schema(
|
||||
component_class: MockObjClass, bus_schema: cv.Schema
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
"""Return the config schema for one bus variant, with one schema per model."""
|
||||
return cv.typed_schema(
|
||||
{
|
||||
name: _model_schema(model, component_class, bus_schema)
|
||||
for name, model in MODELS.items()
|
||||
},
|
||||
key=CONF_MODEL,
|
||||
upper=True,
|
||||
)
|
||||
|
||||
|
||||
async def to_code_base(config: ConfigType) -> MockObj:
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
var = cg.new_Pvariable(config[CONF_ID], model.gas_type, model.gas)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if config.get(CONF_TEMPERATURE_COMPENSATION):
|
||||
cg.add_define("USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION")
|
||||
cg.add_define("DFROBOT_SEN0465_COMPENSATION_RANGES", COMPENSATION_RANGES)
|
||||
for low, high, a, b, c, d in model.compensation:
|
||||
cg.add(var.add_compensation_range(low, high, a, b, c, d))
|
||||
|
||||
if (alarm := config.get(CONF_ALARM)) is not None:
|
||||
decimals = model.accuracy_decimals
|
||||
threshold = round(alarm[CONF_THRESHOLD] * 10**decimals)
|
||||
cg.add(var.set_alarm(alarm[CONF_TYPE], threshold, decimals))
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_CONCENTRATION, var.set_concentration_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
return var
|
||||
@@ -0,0 +1,276 @@
|
||||
#include "dfrobot_sen0465_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::dfrobot_sen0465_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0465");
|
||||
|
||||
// Protocol from https://github.com/DFRobot/DFRobot_MultiGasSensor
|
||||
// Request: FF 01 command argument 00 00 00 00 checksum
|
||||
// Response: FF command conc_high conc_low gas_type decimals temp_high temp_low checksum
|
||||
static constexpr uint8_t FRAME_ADDRESS = 0x01;
|
||||
|
||||
static constexpr uint8_t NO_COMMAND = 0x00;
|
||||
static constexpr uint8_t CMD_CHANGE_ACQUIRE_MODE = 0x78;
|
||||
static constexpr uint8_t CMD_GET_ALL_DATA = 0x88;
|
||||
static constexpr uint8_t CMD_SET_THRESHOLD_ALARM = 0x89;
|
||||
static constexpr uint8_t ACQUIRE_MODE_PASSIVE = 0x04;
|
||||
static constexpr uint8_t ALARM_SWITCH_OFF = 0x00;
|
||||
static constexpr uint8_t ALARM_SWITCH_ON = 0x01;
|
||||
static constexpr uint8_t ALARM_LOW_THRESHOLD = 0x00;
|
||||
static constexpr uint8_t ALARM_HIGH_THRESHOLD = 0x01;
|
||||
// Byte 2 of the answer to a setting command is 1 when the sensor accepted it
|
||||
static constexpr uint8_t RESULT_OK = 0x01;
|
||||
|
||||
// The DFRobot examples wait this long after changing the acquire mode
|
||||
static constexpr uint32_t MODE_CHANGE_DELAY_MS = 1000;
|
||||
static constexpr uint32_t RETRY_DELAY_MS = 1000;
|
||||
static constexpr uint8_t MAX_ATTEMPTS = 3;
|
||||
|
||||
// Like FucCheckSum() in the DFRobot library, the checksum covers bytes 1 to 6 only, not byte 7
|
||||
static uint8_t frame_checksum(const uint8_t *frame) {
|
||||
// Two's complement of the byte sum between the head and the checksum
|
||||
return static_cast<uint8_t>(~sum16_bytes(frame + 1, FRAME_SIZE - 3) + 1);
|
||||
}
|
||||
|
||||
static const LogString *step_name(SetupStep step) {
|
||||
return step == SetupStep::SETUP_STEP_ACQUIRE_MODE ? LOG_STR("Passive mode") : LOG_STR("Alarm setting");
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::setup() {
|
||||
// Answer only when asked, instead of sending a reading every second
|
||||
this->step_ = SetupStep::SETUP_STEP_ACQUIRE_MODE;
|
||||
if (!this->send_step_request_())
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::update() {
|
||||
// The first reading is requested when the setup steps are done
|
||||
if (this->step_ != SetupStep::SETUP_STEP_DONE)
|
||||
return;
|
||||
// A new request would replace the wait for the answer to the previous one
|
||||
if (this->pending_command_ != NO_COMMAND) {
|
||||
ESP_LOGV(TAG, "Previous request still waiting for an answer, skipping update");
|
||||
return;
|
||||
}
|
||||
if (!this->send_command_(CMD_GET_ALL_DATA)) {
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"DFRobot SEN0465 series gas sensor:\n"
|
||||
" Gas: %s (type 0x%02X)",
|
||||
this->gas_name_, this->gas_type_);
|
||||
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
|
||||
ESP_LOGCONFIG(TAG, " Temperature compensation: %s", YESNO(!this->compensation_.empty()));
|
||||
#endif
|
||||
if (this->alarm_type_ == AlarmType::ALARM_TYPE_OFF) {
|
||||
ESP_LOGCONFIG(TAG, " Alarm: off");
|
||||
} else {
|
||||
float scale = 1.0f;
|
||||
for (uint8_t i = 0; i < this->alarm_decimals_; i++)
|
||||
scale *= 10.0f;
|
||||
const float threshold = this->alarm_threshold_ / scale;
|
||||
ESP_LOGCONFIG(TAG, " Alarm: %s %.*f",
|
||||
this->alarm_type_ == AlarmType::ALARM_TYPE_HIGH ? LOG_STR_LITERAL("above") : LOG_STR_LITERAL("below"),
|
||||
this->alarm_decimals_, threshold);
|
||||
}
|
||||
if (this->setup_incomplete_) {
|
||||
ESP_LOGCONFIG(TAG, " Setup incomplete: yes");
|
||||
}
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Concentration", this->concentration_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
}
|
||||
|
||||
bool DFRobotSEN0465Component::send_command_(uint8_t command, uint8_t arg0, uint8_t arg1, uint8_t arg2, uint8_t arg3) {
|
||||
uint8_t frame[FRAME_SIZE] = {FRAME_HEAD, FRAME_ADDRESS, command, arg0, arg1, arg2, arg3};
|
||||
frame[FRAME_SIZE - 1] = frame_checksum(frame);
|
||||
if (!this->write_frame(frame))
|
||||
return false;
|
||||
this->pending_command_ = command;
|
||||
this->read_response();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DFRobotSEN0465Component::send_step_request_() {
|
||||
if (this->step_ == SetupStep::SETUP_STEP_ACQUIRE_MODE)
|
||||
return this->send_command_(CMD_CHANGE_ACQUIRE_MODE, ACQUIRE_MODE_PASSIVE);
|
||||
if (this->alarm_type_ == AlarmType::ALARM_TYPE_OFF) {
|
||||
// Switch the alarm off explicitly, the sensor may still have one from an earlier configuration
|
||||
return this->send_command_(CMD_SET_THRESHOLD_ALARM, ALARM_SWITCH_OFF);
|
||||
}
|
||||
return this->send_command_(
|
||||
CMD_SET_THRESHOLD_ALARM, ALARM_SWITCH_ON, static_cast<uint8_t>(this->alarm_threshold_ >> 8),
|
||||
static_cast<uint8_t>(this->alarm_threshold_),
|
||||
this->alarm_type_ == AlarmType::ALARM_TYPE_HIGH ? ALARM_HIGH_THRESHOLD : ALARM_LOW_THRESHOLD);
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::retry_step_request_() {
|
||||
if (!this->send_step_request_())
|
||||
this->step_failed_(LOG_STR("request not sent"));
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::step_failed_(const LogString *reason) {
|
||||
this->attempts_++;
|
||||
ESP_LOGW(TAG, "%s failed: %s (attempt %u of %u)", LOG_STR_ARG(step_name(this->step_)), LOG_STR_ARG(reason),
|
||||
this->attempts_, MAX_ATTEMPTS);
|
||||
if (this->attempts_ < MAX_ATTEMPTS) {
|
||||
this->set_timeout(STEP_TIMEOUT_ID, RETRY_DELAY_MS, [this]() { this->retry_step_request_(); });
|
||||
return;
|
||||
}
|
||||
// Carry on: readings may still work, but the warning stays because the sensor is not set up as configured
|
||||
this->setup_incomplete_ = true;
|
||||
this->status_set_warning();
|
||||
this->next_step_();
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::next_step_() {
|
||||
this->attempts_ = 0;
|
||||
if (this->step_ == SetupStep::SETUP_STEP_ACQUIRE_MODE) {
|
||||
this->step_ = SetupStep::SETUP_STEP_ALARM;
|
||||
this->set_timeout(STEP_TIMEOUT_ID, MODE_CHANGE_DELAY_MS, [this]() { this->retry_step_request_(); });
|
||||
return;
|
||||
}
|
||||
this->step_ = SetupStep::SETUP_STEP_DONE;
|
||||
this->update();
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::handle_no_response_() {
|
||||
this->pending_command_ = NO_COMMAND;
|
||||
if (this->step_ != SetupStep::SETUP_STEP_DONE) {
|
||||
this->step_failed_(LOG_STR("no response"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "No response from the sensor");
|
||||
this->fail_reading_();
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::fail_reading_() {
|
||||
// A frozen gas reading would hide a rising concentration, so the entities go unknown
|
||||
if (this->concentration_sensor_ != nullptr)
|
||||
this->concentration_sensor_->publish_state(NAN);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(NAN);
|
||||
this->status_set_warning();
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::handle_response_(const uint8_t *frame) {
|
||||
// Reject a late answer to an earlier request or a reading sent before passive mode was set
|
||||
const bool expected = frame[0] == FRAME_HEAD && frame[1] == this->pending_command_;
|
||||
this->pending_command_ = NO_COMMAND;
|
||||
|
||||
if (this->step_ == SetupStep::SETUP_STEP_DONE) {
|
||||
if (!expected) {
|
||||
char hex[format_hex_pretty_size(FRAME_SIZE)];
|
||||
ESP_LOGW(TAG, "Unexpected response: %s", format_hex_pretty_to(hex, frame, FRAME_SIZE));
|
||||
this->fail_reading_();
|
||||
return;
|
||||
}
|
||||
this->handle_data_(frame);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD_HEX(TAG, frame, FRAME_SIZE, FRAME_SIZE, "%s response: %s", LOG_STR_ARG(step_name(this->step_)));
|
||||
if (!expected) {
|
||||
this->step_failed_(LOG_STR("unexpected response"));
|
||||
return;
|
||||
}
|
||||
// Like the DFRobot library, the answer to the mode change is accepted without a checksum check
|
||||
if (this->step_ == SetupStep::SETUP_STEP_ALARM && frame[FRAME_SIZE - 1] != frame_checksum(frame)) {
|
||||
this->step_failed_(LOG_STR("invalid checksum"));
|
||||
return;
|
||||
}
|
||||
if (frame[2] != RESULT_OK) {
|
||||
this->step_failed_(LOG_STR("not confirmed"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "%s confirmed", LOG_STR_ARG(step_name(this->step_)));
|
||||
this->next_step_();
|
||||
}
|
||||
|
||||
void DFRobotSEN0465Component::handle_data_(const uint8_t *frame) {
|
||||
if (frame[FRAME_SIZE - 1] != frame_checksum(frame)) {
|
||||
char hex[format_hex_pretty_size(FRAME_SIZE)];
|
||||
ESP_LOGW(TAG, "Invalid checksum: %s", format_hex_pretty_to(hex, frame, FRAME_SIZE));
|
||||
this->fail_reading_();
|
||||
return;
|
||||
}
|
||||
|
||||
// On-board 10 kOhm NTC (B = 3380) read by a 10-bit ADC at 3 V, as converted by the DFRobot library
|
||||
float temperature = NAN;
|
||||
const uint16_t temperature_raw = encode_uint16(frame[6], frame[7]);
|
||||
if (temperature_raw > 0 && temperature_raw < 1024) {
|
||||
const float voltage = 3.0f * temperature_raw / 1024.0f;
|
||||
const float resistance = voltage * 10000.0f / (3.0f - voltage);
|
||||
temperature = 1.0f / (1.0f / 298.15f + std::log(resistance / 10000.0f) / 3380.13f) - 273.15f;
|
||||
} else {
|
||||
// 0 is a disconnected thermistor, 1024 and above a bad frame
|
||||
ESP_LOGW(TAG, "Temperature reading %u is out of range", temperature_raw);
|
||||
}
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
|
||||
if (frame[4] != this->gas_type_) {
|
||||
ESP_LOGW(TAG, "Probe reports gas type 0x%02X, expected %s (0x%02X); check the configured model", frame[4],
|
||||
this->gas_name_, this->gas_type_);
|
||||
if (this->concentration_sensor_ != nullptr)
|
||||
this->concentration_sensor_->publish_state(NAN);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
float concentration = encode_uint16(frame[2], frame[3]);
|
||||
switch (frame[5]) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
concentration *= 0.1f;
|
||||
break;
|
||||
case 2:
|
||||
concentration *= 0.01f;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Invalid number of decimal places: %u", frame[5]);
|
||||
if (this->concentration_sensor_ != nullptr)
|
||||
this->concentration_sensor_->publish_state(NAN);
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
|
||||
if (!this->compensation_.empty())
|
||||
concentration = this->compensate_(concentration, temperature);
|
||||
#endif
|
||||
if (concentration < 0.0f)
|
||||
concentration = 0.0f;
|
||||
|
||||
if (this->concentration_sensor_ != nullptr)
|
||||
this->concentration_sensor_->publish_state(concentration);
|
||||
if (std::isnan(concentration) || std::isnan(temperature) || this->setup_incomplete_) {
|
||||
this->status_set_warning();
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
|
||||
float DFRobotSEN0465Component::compensate_(float concentration, float temperature) const {
|
||||
for (const auto &range : this->compensation_) {
|
||||
if (temperature > range.low && temperature <= range.high)
|
||||
return concentration / (range.a * temperature + range.b) - (range.c * temperature + range.d);
|
||||
}
|
||||
ESP_LOGW(TAG, "Temperature %.1f C is outside the compensation range", temperature);
|
||||
return NAN;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_base
|
||||
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0465_base {
|
||||
|
||||
static constexpr uint8_t FRAME_SIZE = 9;
|
||||
static constexpr uint8_t FRAME_HEAD = 0xFF;
|
||||
|
||||
// Scheduler ids, shared with the bus variants
|
||||
static constexpr uint32_t STEP_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t RESPONSE_TIMEOUT_ID = 1;
|
||||
|
||||
enum class AlarmType : uint8_t {
|
||||
ALARM_TYPE_OFF,
|
||||
ALARM_TYPE_LOW,
|
||||
ALARM_TYPE_HIGH,
|
||||
};
|
||||
|
||||
/// Settings sent once after boot, before the first reading
|
||||
enum class SetupStep : uint8_t {
|
||||
SETUP_STEP_ACQUIRE_MODE,
|
||||
SETUP_STEP_ALARM,
|
||||
SETUP_STEP_DONE,
|
||||
};
|
||||
|
||||
/// DFRobot "Factory Calibrated Electrochemical Gas Sensor" series (SEN0465 to SEN0476).
|
||||
/// The bus variants only send request frames and deliver the answers.
|
||||
class DFRobotSEN0465Component : public PollingComponent {
|
||||
SUB_SENSOR(concentration)
|
||||
SUB_SENSOR(temperature)
|
||||
|
||||
public:
|
||||
/// @param gas_type gas type code the configured probe reports
|
||||
/// @param gas_name name of that gas, used in log messages
|
||||
DFRobotSEN0465Component(uint8_t gas_type, const char *gas_name) : gas_name_(gas_name), gas_type_(gas_type) {}
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// @param threshold alarm threshold as sent to the sensor, in units of the last decimal place
|
||||
/// @param decimals decimal places of the threshold, used for logging
|
||||
void set_alarm(AlarmType type, uint16_t threshold, uint8_t decimals) {
|
||||
this->alarm_type_ = type;
|
||||
this->alarm_threshold_ = threshold;
|
||||
this->alarm_decimals_ = decimals;
|
||||
}
|
||||
|
||||
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
|
||||
/// For low < T <= high: compensated = raw / (a * T + b) - (c * T + d)
|
||||
void add_compensation_range(int8_t low, int8_t high, float a, float b, float c, float d) {
|
||||
this->compensation_.push_back({low, high, a, b, c, d});
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
/// Sends one complete request frame.
|
||||
virtual bool write_frame(const uint8_t *frame) = 0;
|
||||
/// Waits for the answer to a request, then calls either handle_response_() or handle_no_response_(), once.
|
||||
virtual void read_response() = 0;
|
||||
|
||||
void handle_response_(const uint8_t *frame);
|
||||
void handle_no_response_();
|
||||
/// Publish unknown on both sensors and set the warning when a reading cannot be used
|
||||
void fail_reading_();
|
||||
|
||||
/// Sends the request of the current step.
|
||||
bool send_step_request_();
|
||||
/// Sends the request again from a scheduled callback; a failed write counts as a failed attempt.
|
||||
void retry_step_request_();
|
||||
void step_failed_(const LogString *reason);
|
||||
void next_step_();
|
||||
void handle_data_(const uint8_t *frame);
|
||||
/// Sends a request and waits for its answer; returns false when the request could not be sent.
|
||||
bool send_command_(uint8_t command, uint8_t arg0 = 0, uint8_t arg1 = 0, uint8_t arg2 = 0, uint8_t arg3 = 0);
|
||||
|
||||
const char *gas_name_;
|
||||
uint16_t alarm_threshold_{0};
|
||||
uint8_t gas_type_;
|
||||
AlarmType alarm_type_{AlarmType::ALARM_TYPE_OFF};
|
||||
uint8_t alarm_decimals_{0};
|
||||
SetupStep step_{SetupStep::SETUP_STEP_ACQUIRE_MODE};
|
||||
uint8_t attempts_{0};
|
||||
/// Command whose answer is awaited, 0 when no request is outstanding
|
||||
uint8_t pending_command_{0};
|
||||
/// A setup step gave up, so the alarm or the mode may not be what was configured
|
||||
bool setup_incomplete_{false};
|
||||
|
||||
#ifdef USE_DFROBOT_SEN0465_TEMPERATURE_COMPENSATION
|
||||
struct CompensationRange {
|
||||
int8_t low;
|
||||
int8_t high;
|
||||
float a;
|
||||
float b;
|
||||
float c;
|
||||
float d;
|
||||
};
|
||||
|
||||
/// Returns NAN when the temperature is outside every range
|
||||
float compensate_(float concentration, float temperature) const;
|
||||
|
||||
StaticVector<CompensationRange, DFROBOT_SEN0465_COMPENSATION_RANGES> compensation_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_base
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@tzabbi"]
|
||||
DOMAIN = "dfrobot_sen0465_i2c"
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "dfrobot_sen0465_i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0465_i2c {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0465_i2c");
|
||||
|
||||
using dfrobot_sen0465_base::FRAME_SIZE;
|
||||
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
|
||||
|
||||
static constexpr uint8_t DATA_REGISTER = 0x00;
|
||||
// The DFRobot library waits 10 ms, but on an ESP32-C6 reading the answer to the alarm command
|
||||
// 10 ms after sending it timed out
|
||||
static constexpr uint32_t RESPONSE_DELAY_MS = 100;
|
||||
|
||||
void DFRobotSEN0465I2CComponent::dump_config() {
|
||||
DFRobotSEN0465Component::dump_config();
|
||||
LOG_I2C_DEVICE(this);
|
||||
}
|
||||
|
||||
bool DFRobotSEN0465I2CComponent::write_frame(const uint8_t *frame) {
|
||||
return this->write_register(DATA_REGISTER, frame, FRAME_SIZE) == i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
void DFRobotSEN0465I2CComponent::read_response() {
|
||||
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_DELAY_MS, [this]() {
|
||||
uint8_t frame[FRAME_SIZE];
|
||||
// Like the DFRobot library, end the register write with a STOP before reading
|
||||
if (this->write(&DATA_REGISTER, 1) != i2c::ERROR_OK || this->read(frame, FRAME_SIZE) != i2c::ERROR_OK) {
|
||||
this->handle_no_response_();
|
||||
return;
|
||||
}
|
||||
this->handle_response_(frame);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_i2c
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0465_i2c {
|
||||
|
||||
class DFRobotSEN0465I2CComponent final : public dfrobot_sen0465_base::DFRobotSEN0465Component, public i2c::I2CDevice {
|
||||
public:
|
||||
using DFRobotSEN0465Component::DFRobotSEN0465Component;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
bool write_frame(const uint8_t *frame) override;
|
||||
void read_response() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_i2c
|
||||
@@ -0,0 +1,26 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.dfrobot_sen0465_base import (
|
||||
DFRobotSEN0465Component,
|
||||
dfrobot_sen0465_schema,
|
||||
to_code_base,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["dfrobot_sen0465_base"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
dfrobot_sen0465_i2c_ns = cg.esphome_ns.namespace("dfrobot_sen0465_i2c")
|
||||
DFRobotSEN0465I2CComponent = dfrobot_sen0465_i2c_ns.class_(
|
||||
"DFRobotSEN0465I2CComponent", DFRobotSEN0465Component, i2c.I2CDevice
|
||||
)
|
||||
|
||||
# 0x77 is the factory setting of the A0/A1 switches according to the DFRobot examples
|
||||
CONFIG_SCHEMA = dfrobot_sen0465_schema(
|
||||
DFRobotSEN0465I2CComponent, i2c.i2c_device_schema(0x77)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await to_code_base(config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@tzabbi"]
|
||||
DOMAIN = "dfrobot_sen0465_uart"
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "dfrobot_sen0465_uart.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::dfrobot_sen0465_uart {
|
||||
|
||||
using dfrobot_sen0465_base::FRAME_HEAD;
|
||||
using dfrobot_sen0465_base::FRAME_SIZE;
|
||||
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
|
||||
|
||||
// The DFRobot library waits up to this long for an answer
|
||||
static constexpr uint32_t RESPONSE_TIMEOUT_MS = 3000;
|
||||
|
||||
void DFRobotSEN0465UARTComponent::setup() {
|
||||
// loop() only runs while an answer is expected
|
||||
this->disable_loop();
|
||||
DFRobotSEN0465Component::setup();
|
||||
}
|
||||
|
||||
void DFRobotSEN0465UARTComponent::loop() {
|
||||
// The tail of a reading the probe sent on its own may precede the answer; start at the next frame head.
|
||||
uint8_t byte;
|
||||
while (this->rx_count_ == 0 && this->peek_byte(&byte) && byte != FRAME_HEAD)
|
||||
this->read_byte(&byte);
|
||||
const size_t available = this->available();
|
||||
if (available == 0)
|
||||
return;
|
||||
const size_t len = std::min<size_t>(available, FRAME_SIZE - this->rx_count_);
|
||||
if (!this->read_array(this->rx_buffer_ + this->rx_count_, len))
|
||||
return;
|
||||
this->rx_count_ += len;
|
||||
if (this->rx_count_ < FRAME_SIZE)
|
||||
return;
|
||||
|
||||
this->rx_count_ = 0;
|
||||
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
|
||||
this->disable_loop();
|
||||
this->handle_response_(this->rx_buffer_);
|
||||
}
|
||||
|
||||
bool DFRobotSEN0465UARTComponent::write_frame(const uint8_t *frame) {
|
||||
// Drop anything left over, such as the answer to the mode change or readings sent before passive mode was set
|
||||
uint8_t byte;
|
||||
while (this->available() > 0) {
|
||||
if (!this->read_byte(&byte))
|
||||
break;
|
||||
}
|
||||
this->rx_count_ = 0;
|
||||
this->write_array(frame, FRAME_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DFRobotSEN0465UARTComponent::read_response() {
|
||||
this->enable_loop();
|
||||
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
|
||||
this->disable_loop();
|
||||
this->rx_count_ = 0;
|
||||
this->handle_no_response_();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_uart
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0465_uart {
|
||||
|
||||
class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSEN0465Component,
|
||||
public uart::UARTDevice {
|
||||
public:
|
||||
using DFRobotSEN0465Component::DFRobotSEN0465Component;
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
bool write_frame(const uint8_t *frame) override;
|
||||
void read_response() override;
|
||||
|
||||
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
|
||||
uint8_t rx_count_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_uart
|
||||
@@ -0,0 +1,38 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
from esphome.components.dfrobot_sen0465_base import (
|
||||
DFRobotSEN0465Component,
|
||||
dfrobot_sen0465_schema,
|
||||
to_code_base,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
AUTO_LOAD = ["dfrobot_sen0465_base"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
dfrobot_sen0465_uart_ns = cg.esphome_ns.namespace("dfrobot_sen0465_uart")
|
||||
DFRobotSEN0465UARTComponent = dfrobot_sen0465_uart_ns.class_(
|
||||
"DFRobotSEN0465UARTComponent", DFRobotSEN0465Component, uart.UARTDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = dfrobot_sen0465_schema(
|
||||
DFRobotSEN0465UARTComponent, uart.UART_DEVICE_SCHEMA
|
||||
)
|
||||
|
||||
# The sensor's baud rate is fixed
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
DOMAIN,
|
||||
baud_rate=9600,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await to_code_base(config)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -79,6 +79,5 @@ def final_validate_mode(keys: dict[str, str]) -> Callable[[ConfigType], None]:
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], RUN_MODES[config[CONF_MODE]])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
cg.add_define("USE_LINE_READER")
|
||||
await uart.new_uart_component(config, RUN_MODES[config[CONF_MODE]])
|
||||
@@ -64,15 +64,6 @@ static bool starts_with(const char *str, ESPHOME_PGM_P prefix) {
|
||||
return ESPHOME_strncmp_P(str, prefix, ESPHOME_strlen_P(prefix)) == 0;
|
||||
}
|
||||
|
||||
static bool contains(const char *str, ESPHOME_PGM_P needle) {
|
||||
const size_t len = ESPHOME_strlen_P(needle);
|
||||
for (; *str != '\0'; str++) {
|
||||
if (ESPHOME_strncmp_P(str, needle, len) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Splits "a,b,c*" in place; returns the number of fields stored in `fields`
|
||||
static uint8_t split_fields(char *str, char **fields, uint8_t max_fields) {
|
||||
uint8_t count = 0;
|
||||
@@ -94,9 +85,9 @@ static uint8_t split_fields(char *str, char **fields, uint8_t max_fields) {
|
||||
static bool answer_means_done(Operation operation, const char *line) {
|
||||
switch (operation) {
|
||||
case Operation::OPERATION_STOP:
|
||||
return contains(line, ESPHOME_PSTR("stopped already"));
|
||||
return ESPHOME_strstr_P(line, ESPHOME_PSTR("stopped already")) != nullptr;
|
||||
case Operation::OPERATION_START:
|
||||
return contains(line, ESPHOME_PSTR("started already"));
|
||||
return ESPHOME_strstr_P(line, ESPHOME_PSTR("started already")) != nullptr;
|
||||
case Operation::OPERATION_SAVE:
|
||||
return starts_with(line, ESPHOME_PSTR("no parameter"));
|
||||
default:
|
||||
@@ -210,30 +201,15 @@ void DFRobotSEN0609Component::loop() {
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::readline_(uint8_t c) {
|
||||
if (c == '\r')
|
||||
return;
|
||||
if (c == '\n') {
|
||||
if (!this->line_overflow_) {
|
||||
this->line_[this->line_pos_] = '\0';
|
||||
this->handle_line_(this->line_);
|
||||
}
|
||||
this->line_overflow_ = false;
|
||||
this->line_pos_ = 0;
|
||||
char *line = this->line_reader_.feed(c);
|
||||
if (line != nullptr) {
|
||||
this->handle_line_(line);
|
||||
return;
|
||||
}
|
||||
if (this->line_overflow_)
|
||||
return;
|
||||
if (this->line_pos_ >= MAX_LINE_LENGTH - 1) {
|
||||
ESP_LOGW(TAG, "Line too long, dropped");
|
||||
this->line_overflow_ = true;
|
||||
return;
|
||||
}
|
||||
this->line_[this->line_pos_++] = static_cast<char>(c);
|
||||
this->line_reader_.log_if_overflowed(TAG);
|
||||
// The prompt has no line end, and the echo of the next command follows it on the same line
|
||||
if (c == '>') {
|
||||
this->line_[this->line_pos_] = '\0';
|
||||
if (ESPHOME_strcmp_P(this->line_, ESPHOME_PSTR("DFRobot:/>")) == 0)
|
||||
this->line_pos_ = 0;
|
||||
if (c == '>' && ESPHOME_strcmp_P(this->line_reader_.partial(), ESPHOME_PSTR("DFRobot:/>")) == 0) {
|
||||
this->line_reader_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,11 +252,12 @@ void DFRobotSEN0609Component::handle_line_(char *line) {
|
||||
this->finish_operation_();
|
||||
} else if (starts_with(line, ESPHOME_PSTR("Response "))) {
|
||||
this->parse_response_(line + 9);
|
||||
} else if (contains(line, ESPHOME_PSTR("not stopped"))) {
|
||||
} else if (ESPHOME_strstr_P(line, ESPHOME_PSTR("not stopped")) != nullptr) {
|
||||
// Stop the sensor before the next attempt
|
||||
this->stopped_ = false;
|
||||
this->fail_operation_();
|
||||
} else if (starts_with(line, ESPHOME_PSTR("Error")) || contains(line, ESPHOME_PSTR("not recognized"))) {
|
||||
} else if (starts_with(line, ESPHOME_PSTR("Error")) ||
|
||||
ESPHOME_strstr_P(line, ESPHOME_PSTR("not recognized")) != nullptr) {
|
||||
ESP_LOGW(TAG, "Command '%s' rejected: %s", this->command_, line);
|
||||
this->fail_operation_();
|
||||
} else {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#ifdef USE_BUTTON
|
||||
#include "esphome/components/button/button.h"
|
||||
#endif
|
||||
#include "esphome/core/line_reader.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
@@ -188,7 +189,7 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
|
||||
std::array<bool, SWITCH_COUNT> switch_states_{};
|
||||
#endif
|
||||
|
||||
char line_[MAX_LINE_LENGTH];
|
||||
LineReader<MAX_LINE_LENGTH> line_reader_;
|
||||
char command_[MAX_COMMAND_LENGTH]{};
|
||||
float response_[MAX_RESPONSE_VALUES]{};
|
||||
uint32_t sent_ms_{0};
|
||||
@@ -210,7 +211,6 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
|
||||
RunMode mode_;
|
||||
Operation operation_{Operation::OPERATION_NONE};
|
||||
Param param_{PARAM_COUNT};
|
||||
uint8_t line_pos_{0};
|
||||
uint8_t response_count_{0};
|
||||
uint8_t out_pin_level_{0};
|
||||
uint8_t attempts_{0};
|
||||
@@ -221,7 +221,6 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
|
||||
uint8_t led_on_{0};
|
||||
bool sent_led_on_{false};
|
||||
#endif
|
||||
bool line_overflow_{false};
|
||||
bool read_failed_{false};
|
||||
bool bad_frame_logged_{false};
|
||||
bool echo_seen_{false};
|
||||
|
||||
@@ -3,7 +3,6 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -42,9 +41,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -2,7 +2,6 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
@@ -50,9 +49,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -2,7 +2,7 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SLEEP_PIN, CONF_TYPE
|
||||
from esphome.const import CONF_SLEEP_PIN, CONF_TYPE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@tomwellnitz"]
|
||||
@@ -86,9 +86,7 @@ def get_channel_count(config: ConfigType) -> int:
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
cg.add(var.set_active_pullup(config[CONF_ACTIVE_PULLUP]))
|
||||
cg.add(var.set_channel_count(get_channel_count(config)))
|
||||
|
||||
@@ -2,7 +2,6 @@ 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.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@linkedupbits"]
|
||||
@@ -23,9 +22,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
await i2c.new_i2c_component(config)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
|
||||
@@ -3,7 +3,6 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CO2,
|
||||
CONF_ID,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
@@ -53,9 +52,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -2,7 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import CONF_CONVERSION_RATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INVERTED, CONF_RESOLUTION
|
||||
from esphome.const import CONF_INVERTED, CONF_RESOLUTION
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@ellull"]
|
||||
@@ -71,9 +71,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
if pwm_config := config.get(CONF_PWM):
|
||||
cg.add(var.set_dac_mode(False))
|
||||
|
||||
@@ -132,6 +132,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add_define("USE_LINE_READER")
|
||||
|
||||
# Initialize sensor storage with count from final_validate before any
|
||||
# await, so platform to_code() calls always see it initialized
|
||||
|
||||
@@ -55,7 +55,6 @@ static bool supports_lock(const char *version, const char *end) {
|
||||
#endif
|
||||
|
||||
void EmonTx::setup() {
|
||||
this->buffer_pos_ = 0;
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
// The line feed ends any boot noise
|
||||
this->write_byte('\n');
|
||||
@@ -111,11 +110,14 @@ void EmonTx::loop() {
|
||||
if (this->was_paused_) {
|
||||
// Drop what arrived during the pause and resync at the next line start
|
||||
this->was_paused_ = false;
|
||||
this->buffer_pos_ = 0;
|
||||
this->discarding_line_ = true;
|
||||
uint8_t dropped;
|
||||
while (this->available() > 0 && this->read_byte(&dropped))
|
||||
this->discarding_line_ = dropped != '\n';
|
||||
uint8_t dropped = 0;
|
||||
while (this->available() > 0 && this->read_byte(&dropped)) {
|
||||
}
|
||||
if (dropped == '\n') {
|
||||
this->line_reader_.reset();
|
||||
} else {
|
||||
this->line_reader_.discard_line();
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The emonTx may have been flashed, or a request dropped, during the pause
|
||||
if (this->wants_version_())
|
||||
@@ -129,43 +131,24 @@ void EmonTx::loop() {
|
||||
if (!this->read_byte(&received))
|
||||
break;
|
||||
|
||||
if (received == '\n') {
|
||||
// End of line - process the buffer
|
||||
if (this->discarding_line_) {
|
||||
this->discarding_line_ = false;
|
||||
} else if (this->buffer_pos_ > 0) {
|
||||
// Null-terminate for safe logging and c_str() use
|
||||
size_t len = this->buffer_pos_;
|
||||
this->buffer_[len] = '\0';
|
||||
this->buffer_pos_ = 0;
|
||||
|
||||
StringRef line(this->buffer_.data(), len);
|
||||
ESP_LOGD(TAG, "Received line: %s", line.c_str());
|
||||
|
||||
// Fire data callbacks for all received lines
|
||||
this->data_callbacks_.call(line);
|
||||
|
||||
// Check if this line is JSON (starts with '{')
|
||||
if (this->buffer_[0] == '{') {
|
||||
ESP_LOGV(TAG, "Line is JSON, parsing...");
|
||||
this->parse_json_(this->buffer_.data(), len);
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->wants_version_()) {
|
||||
this->parse_msg_number_(this->buffer_.data(), len);
|
||||
this->parse_version_line_(this->buffer_.data(), len);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else if (received == '\r' || this->discarding_line_) {
|
||||
// Ignore CR, and the rest of a line that overflowed or was interrupted by a pause
|
||||
} else if (this->buffer_pos_ >= MAX_LINE_LENGTH) {
|
||||
ESP_LOGW(TAG, "Buffer overflow (>%zu bytes), discarding line", MAX_LINE_LENGTH);
|
||||
this->buffer_pos_ = 0;
|
||||
this->discarding_line_ = true;
|
||||
} else {
|
||||
this->buffer_[this->buffer_pos_++] = static_cast<char>(received);
|
||||
size_t len;
|
||||
char *line = this->line_reader_.feed(received, len);
|
||||
this->line_reader_.log_if_overflowed(TAG);
|
||||
if (line == nullptr || len == 0) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGD(TAG, "Received line: %s", line);
|
||||
this->data_callbacks_.call(StringRef(line, len));
|
||||
if (line[0] == '{') {
|
||||
ESP_LOGV(TAG, "Line is JSON, parsing...");
|
||||
this->parse_json_(line, len);
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->wants_version_()) {
|
||||
this->parse_msg_number_(line, len);
|
||||
this->parse_version_line_(line, len);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/line_reader.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
/// Maximum line length in bytes (plus one byte reserved for null terminator)
|
||||
/// Maximum line length in characters
|
||||
static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
|
||||
enum class LockOnReboot : uint8_t {
|
||||
@@ -92,18 +92,16 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
#endif
|
||||
LazyCallbackManager<void(JsonObject, StringRef)> json_callbacks_;
|
||||
LazyCallbackManager<void(StringRef)> data_callbacks_;
|
||||
uint16_t buffer_pos_{0};
|
||||
std::atomic<bool> paused_{false};
|
||||
bool was_paused_{false};
|
||||
LockOnReboot lock_on_reboot_{LockOnReboot::LOCK_ON_REBOOT_NEVER};
|
||||
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool lock_supported_{false}; // The last version reported is v1.2.0 or later
|
||||
bool unlock_sent_{false}; // AUTO has sent 'emonunlock' since boot
|
||||
uint8_t version_frames_left_{0}; // Data lines still to wait for a version reply; 0 when none is owed
|
||||
uint32_t last_msg_number_{0};
|
||||
#endif
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
LineReader<MAX_LINE_LENGTH + 1> line_reader_;
|
||||
};
|
||||
|
||||
} // namespace esphome::emontx
|
||||
@@ -3,7 +3,6 @@ from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -46,9 +45,7 @@ CONFIG_SCHEMA = (
|
||||
|
||||
|
||||
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)
|
||||
var = await i2c.new_i2c_component(config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
|
||||
@@ -30,6 +30,7 @@ from esphome.const import (
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_OFFSET_HEIGHT,
|
||||
CONF_PAGES,
|
||||
CONF_RESET_DURATION,
|
||||
CONF_RESET_PIN,
|
||||
@@ -73,12 +74,17 @@ for module_info in pkgutil.iter_modules(models.__path__):
|
||||
|
||||
MODELS = models.EpaperModel.models
|
||||
|
||||
DIMENSION_SCHEMA = cv.Schema(
|
||||
{
|
||||
|
||||
def dimension_schema(model: models.EpaperModel) -> cv.Schema:
|
||||
schema = {
|
||||
cv.Required(CONF_WIDTH): cv.int_,
|
||||
cv.Required(CONF_HEIGHT): cv.int_,
|
||||
}
|
||||
)
|
||||
# Only models with an offset_height default use it, and the default must survive explicit dimensions
|
||||
if (offset_height := model.get_default(CONF_OFFSET_HEIGHT, None)) is not None:
|
||||
schema[cv.Optional(CONF_OFFSET_HEIGHT, default=offset_height)] = cv.positive_int
|
||||
return cv.Schema(schema)
|
||||
|
||||
|
||||
TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
|
||||
|
||||
@@ -135,7 +141,7 @@ def model_schema(config):
|
||||
model.option(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
cv.GenerateID(): cv.declare_id(class_name),
|
||||
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
|
||||
cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA,
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(model),
|
||||
model.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema),
|
||||
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
|
||||
map_sequence
|
||||
|
||||
@@ -90,7 +90,9 @@ bool EPaperBase::is_idle_() const {
|
||||
return !this->busy_pin_->digital_read();
|
||||
}
|
||||
|
||||
bool EPaperBase::reset() {
|
||||
bool EPaperBase::reset() { return this->hardware_reset_(); }
|
||||
|
||||
bool EPaperBase::hardware_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
if (this->state_ == EPaperState::RESET) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
@@ -131,7 +133,9 @@ void EPaperBase::update() {
|
||||
}
|
||||
|
||||
void EPaperBase::wait_for_idle_(bool should_wait) {
|
||||
this->waiting_for_idle_start_ = millis();
|
||||
// The wait starts when the step's delay ends, or now for a wait in the middle of a step
|
||||
const uint32_t now = millis();
|
||||
this->waiting_for_idle_start_ = static_cast<int32_t>(this->delay_until_ - now) > 0 ? this->delay_until_ : now;
|
||||
this->waiting_for_idle_ = should_wait;
|
||||
}
|
||||
|
||||
@@ -145,19 +149,30 @@ void EPaperBase::loop() {
|
||||
auto now = millis();
|
||||
// using modulus arithmetic to handle wrap-around
|
||||
int diff = now - this->delay_until_;
|
||||
if (diff < 0)
|
||||
if (diff < 0) {
|
||||
// Running the loop continuously cannot shorten a delay longer than one loop interval
|
||||
if (-diff > static_cast<int>(App.get_loop_interval())) {
|
||||
this->high_frequency_loop_.stop();
|
||||
} else {
|
||||
this->high_frequency_loop_.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this->waiting_for_idle_) {
|
||||
if (this->is_idle_()) {
|
||||
this->waiting_for_idle_ = false;
|
||||
this->high_frequency_loop_.start();
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
|
||||
#endif
|
||||
} else {
|
||||
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
|
||||
const uint32_t busy_for = now - this->waiting_for_idle_start_;
|
||||
if (busy_for > this->busy_timeout_seconds_ * 1000u) {
|
||||
this->abandon_update_();
|
||||
return;
|
||||
}
|
||||
if (busy_for > FAST_BUSY_POLL_MS)
|
||||
this->high_frequency_loop_.stop();
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
if (now - this->waiting_for_idle_last_print_ >= 1000) {
|
||||
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
|
||||
@@ -168,6 +183,8 @@ void EPaperBase::loop() {
|
||||
}
|
||||
}
|
||||
this->process_state_();
|
||||
if (this->is_failed())
|
||||
this->high_frequency_loop_.stop();
|
||||
}
|
||||
|
||||
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
|
||||
@@ -225,10 +242,12 @@ void EPaperBase::process_state_() {
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
return;
|
||||
}
|
||||
// A full update resets update_count_ to zero, so a non-zero count marks a partial one
|
||||
this->partial_ = this->update_count_ != 0;
|
||||
this->set_state_(EPaperState::RESET);
|
||||
break;
|
||||
case EPaperState::INITIALISE:
|
||||
if (!this->initialise(this->update_count_ != 0)) {
|
||||
if (!this->initialise(this->partial_)) {
|
||||
return; // Not done yet, come back next loop
|
||||
}
|
||||
this->set_state_(EPaperState::TRANSFER_DATA);
|
||||
@@ -248,11 +267,14 @@ void EPaperBase::process_state_() {
|
||||
this->set_state_(EPaperState::REFRESH_SCREEN);
|
||||
break;
|
||||
case EPaperState::REFRESH_SCREEN:
|
||||
this->refresh_screen(this->update_count_ != 0);
|
||||
this->refresh_screen(this->partial_);
|
||||
this->update_count_ = (this->update_count_ + 1) % this->full_update_every_;
|
||||
this->set_state_(EPaperState::POWER_OFF);
|
||||
break;
|
||||
case EPaperState::POWER_OFF:
|
||||
if (!this->after_refresh()) {
|
||||
return; // Not done yet, come back next loop
|
||||
}
|
||||
this->power_off();
|
||||
this->set_state_(EPaperState::DEEP_SLEEP);
|
||||
break;
|
||||
@@ -268,16 +290,20 @@ void EPaperBase::process_state_() {
|
||||
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
|
||||
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
|
||||
this->state_ = state;
|
||||
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
|
||||
// allow subclasses to nominate delays
|
||||
if (delay == 0)
|
||||
delay = this->next_delay_;
|
||||
this->next_delay_ = 0;
|
||||
this->delay_until_ = millis() + delay;
|
||||
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
|
||||
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
|
||||
TRUEFALSE(this->waiting_for_idle_));
|
||||
// Each step of an update otherwise waits for the next loop iteration; loop() slows down again for long waits
|
||||
if (state == EPaperState::IDLE) {
|
||||
this->high_frequency_loop_.stop();
|
||||
this->disable_loop();
|
||||
} else {
|
||||
this->high_frequency_loop_.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/split_buffer/split_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
using namespace display;
|
||||
@@ -31,6 +32,8 @@ static constexpr uint8_t SWAP_XY = 4;
|
||||
|
||||
static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
|
||||
static constexpr size_t MAX_TRANSFER_SIZE = 128;
|
||||
// Longer busy waits are the waveform or a stuck pin, and are polled at the normal loop pace
|
||||
static constexpr uint32_t FAST_BUSY_POLL_MS = 50;
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
|
||||
class EPaperBase : public Display,
|
||||
@@ -129,6 +132,8 @@ class EPaperBase : public Display,
|
||||
bool is_idle_() const;
|
||||
void setup_pins_() const;
|
||||
virtual bool reset();
|
||||
/// The reset pin part of reset(), for a driver that must skip what its parent adds to it
|
||||
bool hardware_reset_();
|
||||
virtual bool initialise(bool partial);
|
||||
void send_init_sequence_(const uint8_t *sequence, size_t length);
|
||||
void wait_for_idle_(bool should_wait);
|
||||
@@ -165,6 +170,13 @@ class EPaperBase : public Display,
|
||||
*/
|
||||
virtual void power_off() = 0;
|
||||
|
||||
/**
|
||||
* Called after the refresh, before power off, repeatedly until it returns true.
|
||||
* Lets a driver do extra work spread over several loops, such as rewriting the previous image.
|
||||
* @return true when done, false to be called again next loop
|
||||
*/
|
||||
virtual bool after_refresh() { return true; }
|
||||
|
||||
/**
|
||||
* Place the display into deep sleep
|
||||
*/
|
||||
@@ -193,12 +205,14 @@ class EPaperBase : public Display,
|
||||
GPIOPin *reset_pin_{};
|
||||
std::vector<GPIOPin *> enable_pins_{};
|
||||
bool waiting_for_idle_{};
|
||||
HighFrequencyLoopRequester high_frequency_loop_;
|
||||
uint32_t delay_until_{}; // timestamp until which to delay processing
|
||||
uint16_t next_delay_{}; // milliseconds to delay before next state
|
||||
uint8_t transform_{};
|
||||
uint8_t effective_transform_{};
|
||||
uint8_t update_count_{};
|
||||
bool full_update_requested_{};
|
||||
bool partial_{}; // the update in progress is a partial one; set when UPDATE moves to RESET
|
||||
// these values represent the bounds of the updated buffer. Note that x_high and y_high
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
|
||||
|
||||
@@ -68,7 +68,7 @@ bool HOT EPaperMono::transfer_data() {
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
|
||||
for (size_t i = 0; i != row_length; i++) {
|
||||
bytes_to_send[i] = this->send_red_ ? 0 : this->buffer_[data_idx++];
|
||||
bytes_to_send[i] = this->send_red_ && !this->send_red_as_image_ ? 0 : this->buffer_[data_idx++];
|
||||
}
|
||||
++this->current_data_index_;
|
||||
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
|
||||
|
||||
@@ -21,6 +21,8 @@ class EPaperMono : public EPaperBase {
|
||||
virtual void set_window();
|
||||
bool transfer_data() override;
|
||||
bool send_red_{true};
|
||||
// When set, the red RAM pass writes the image rather than blank data.
|
||||
bool send_red_as_image_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -10,12 +10,11 @@ ESPHOME_LOG_TAG(TAG, "epaper_spi.uc8179");
|
||||
|
||||
bool EPaperUC8179::initialise(bool partial) {
|
||||
EPaperBase::initialise(partial); // send the model init sequence
|
||||
this->partial_ = partial;
|
||||
ESP_LOGV(TAG, "Power on");
|
||||
// POWER ON must precede the waveform/mode registers and the data transfer
|
||||
// (the original driver powers on and busy-waits before writing them).
|
||||
// The state machine busy-waits before entering TRANSFER_DATA.
|
||||
this->command(0x04);
|
||||
this->command(CMD_POWER_ON);
|
||||
// Give the busy line time to assert before the state machine polls it
|
||||
this->next_delay_ = 100;
|
||||
return true;
|
||||
@@ -27,21 +26,17 @@ void EPaperUC8179::set_refresh_mode_() {
|
||||
return; // plain full refresh uses the mode set by the init sequence
|
||||
}
|
||||
// Fast and partial refresh use flipped data polarity and a floating border
|
||||
this->cmd_data(0x50, {0xA9, 0x07});
|
||||
this->cmd_data(CMD_CDI, {0xA9, 0x07});
|
||||
// Force the waveform via the temperature registers: 0x5A selects the fast
|
||||
// full-refresh waveform, 0x6E the partial-refresh waveform
|
||||
this->cmd_data(0xE0, {0x02});
|
||||
this->cmd_data(CMD_CCSET, {0x02});
|
||||
if (this->partial_) {
|
||||
this->cmd_data(0xE5, {0x6E});
|
||||
this->command(0x91); // enter partial mode
|
||||
// Set the partial window to the full screen
|
||||
const uint16_t x_end = this->width_ - 1;
|
||||
const uint16_t y_end = this->height_ - 1;
|
||||
this->cmd_data(0x90, {0x00, 0x00, static_cast<uint8_t>(x_end >> 8), static_cast<uint8_t>(x_end & 0xFF), 0x00, 0x00,
|
||||
static_cast<uint8_t>(y_end >> 8), static_cast<uint8_t>(y_end & 0xFF), 0x01});
|
||||
this->cmd_data(CMD_TSSET, {0x6E});
|
||||
// Partial mode, with the window set to the full screen
|
||||
this->set_partial_window_(0, this->width_ - 1, 0, this->height_ - 1);
|
||||
} else {
|
||||
this->cmd_data(0xE5, {0x5A});
|
||||
this->command(0x92); // exit partial mode
|
||||
this->cmd_data(CMD_TSSET, {0x5A});
|
||||
this->command(CMD_PARTIAL_OUT); // exit partial mode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +58,7 @@ bool HOT EPaperUC8179::transfer_data() {
|
||||
// Phase 1 (fast full refresh only): previous image via 0x10 (DTM1), inverse of the new image
|
||||
if (two_pass && this->current_data_index_ < buffer_length) {
|
||||
if (this->current_data_index_ == 0) {
|
||||
this->command(0x10); // DATA START TRANSMISSION 1 (previous image)
|
||||
this->command(CMD_DTM1); // DATA START TRANSMISSION 1 (previous image)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < buffer_length) {
|
||||
@@ -86,7 +81,7 @@ bool HOT EPaperUC8179::transfer_data() {
|
||||
const size_t total = offset + buffer_length;
|
||||
if (this->current_data_index_ < total) {
|
||||
if (this->current_data_index_ == offset) {
|
||||
this->command(0x13); // DATA START TRANSMISSION 2 (new image)
|
||||
this->command(CMD_DTM2); // DATA START TRANSMISSION 2 (new image)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < total) {
|
||||
@@ -117,7 +112,7 @@ void EPaperUC8179::power_on() {
|
||||
|
||||
void EPaperUC8179::refresh_screen(bool /*partial*/) {
|
||||
ESP_LOGV(TAG, "Refresh");
|
||||
this->command(0x12); // DISPLAY REFRESH
|
||||
this->command(CMD_REFRESH); // DISPLAY REFRESH
|
||||
// Delay the next busy poll: the busy line takes a short time to assert after
|
||||
// the refresh command, and polling too early would read it as already idle
|
||||
this->next_delay_ = 100;
|
||||
@@ -125,15 +120,7 @@ void EPaperUC8179::refresh_screen(bool /*partial*/) {
|
||||
|
||||
void EPaperUC8179::power_off() {
|
||||
ESP_LOGV(TAG, "Power off");
|
||||
this->command(0x02); // POWER OFF
|
||||
}
|
||||
|
||||
void EPaperUC8179::deep_sleep() {
|
||||
// Deep sleep loses the previous-image RAM that partial refresh compares against
|
||||
if (!this->is_using_partial_update_()) {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
|
||||
}
|
||||
this->command(CMD_POWER_OFF); // POWER OFF
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi.h"
|
||||
#include "epaper_spi_ultrachip.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
@@ -28,11 +28,11 @@ namespace esphome::epaper_spi {
|
||||
* waveform is forced, partial mode is entered with a full-screen window and
|
||||
* only DTM2 is sent; the controller compares against its previous-image RAM.
|
||||
*/
|
||||
class EPaperUC8179 final : public EPaperBase {
|
||||
class EPaperUC8179 final : public EPaperUltraChip {
|
||||
public:
|
||||
EPaperUC8179(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {}
|
||||
: EPaperUltraChip(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {}
|
||||
|
||||
protected:
|
||||
bool initialise(bool partial) override;
|
||||
@@ -40,11 +40,7 @@ class EPaperUC8179 final : public EPaperBase {
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override;
|
||||
void power_off() override;
|
||||
void deep_sleep() override;
|
||||
void set_refresh_mode_();
|
||||
|
||||
// Set by initialise() so transfer_data() knows which planes to send
|
||||
bool partial_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,144 @@
|
||||
#include "epaper_spi_uc8279.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.uc8279");
|
||||
|
||||
static constexpr uint8_t CMD_PSR = 0x00;
|
||||
static constexpr uint8_t CMD_PFS = 0x03;
|
||||
static constexpr uint8_t CMD_GATE_SCAN = 0xE1;
|
||||
|
||||
static constexpr uint8_t PSR0_OTP_LUT = 0x17;
|
||||
static constexpr uint8_t PSR1 = 0x4D;
|
||||
static constexpr uint8_t PFS = 0x20;
|
||||
static constexpr uint8_t GATE_SCAN = 0x02;
|
||||
static constexpr uint8_t CCSET = 0x02;
|
||||
static constexpr uint8_t TSSET_FULL = 0x1E;
|
||||
static constexpr uint8_t TSSET_PARTIAL = 0x5A;
|
||||
static constexpr uint8_t CDI_FULL = 0x97;
|
||||
static constexpr uint8_t CDI_PARTIAL = 0xD7;
|
||||
|
||||
static constexpr uint16_t RESET_SETTLE_MS = 50;
|
||||
static constexpr uint16_t BUSY_ASSERT_MS = 10;
|
||||
static constexpr uint16_t REFRESH_START_MS = 50;
|
||||
|
||||
bool EPaperUC8279::reset() {
|
||||
// A partial update follows one that left the controller configured, in standby, with DTM1 holding the image
|
||||
if (this->partial_)
|
||||
return true;
|
||||
if (!EPaperBase::reset())
|
||||
return false;
|
||||
this->next_delay_ = RESET_SETTLE_MS;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EPaperUC8279::initialise(bool partial) {
|
||||
if (partial)
|
||||
return true;
|
||||
return EPaperBase::initialise(partial);
|
||||
}
|
||||
|
||||
void EPaperUC8279::write_gate_(size_t gate, bool white) {
|
||||
const size_t row_width = this->row_width_;
|
||||
if (white || gate < this->offset_height_) {
|
||||
uint8_t white_bytes[MAX_TRANSFER_SIZE];
|
||||
memset(white_bytes, 0xFF, sizeof(white_bytes));
|
||||
for (size_t written = 0; written != row_width;) {
|
||||
const size_t length = std::min(row_width - written, MAX_TRANSFER_SIZE);
|
||||
this->write_array(white_bytes, length);
|
||||
written += length;
|
||||
}
|
||||
return;
|
||||
}
|
||||
size_t index = (gate - this->offset_height_) * row_width;
|
||||
for (size_t remaining = row_width; remaining != 0;) {
|
||||
size_t length;
|
||||
const uint8_t *data = this->buffer_.get_span(index, length);
|
||||
length = std::min(length, remaining);
|
||||
this->write_array(data, length);
|
||||
index += length;
|
||||
remaining -= length;
|
||||
}
|
||||
}
|
||||
|
||||
// One plane of one gate's row per step, time-sliced; `base` is the value of current_data_index_ at its first gate
|
||||
bool HOT EPaperUC8279::write_plane_(uint8_t command, bool white, size_t base) {
|
||||
const uint32_t start_time = millis();
|
||||
const size_t gates = this->gate_count_();
|
||||
if (this->current_data_index_ == base)
|
||||
this->command(command);
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ - base != gates) {
|
||||
this->write_gate_(this->current_data_index_ - base, white);
|
||||
++this->current_data_index_;
|
||||
if (this->current_data_index_ - base != gates && millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
return true;
|
||||
}
|
||||
|
||||
// DTM2 gets the new image; a full refresh also writes a white DTM1, so every pixel transitions
|
||||
bool HOT EPaperUC8279::transfer_data() {
|
||||
const size_t gates = this->gate_count_();
|
||||
if (this->current_data_index_ < gates) {
|
||||
if (!this->write_plane_(CMD_DTM2, false, 0))
|
||||
return false;
|
||||
// Start DTM1 on the next pass, so this one stays within its time slice
|
||||
if (!this->partial_)
|
||||
return false;
|
||||
}
|
||||
if (!this->partial_ && !this->write_plane_(CMD_DTM1, true, gates))
|
||||
return false;
|
||||
this->current_data_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperUC8279::power_on() {
|
||||
ESP_LOGV(TAG, "Power on");
|
||||
this->cmd_data(CMD_CDI, {this->partial_ ? CDI_PARTIAL : CDI_FULL});
|
||||
this->cmd_data(CMD_CCSET, {CCSET});
|
||||
this->cmd_data(CMD_TSSET, {this->partial_ ? TSSET_PARTIAL : TSSET_FULL});
|
||||
if (this->partial_) {
|
||||
this->cmd_data(CMD_PFS, {PFS});
|
||||
this->cmd_data(CMD_GATE_SCAN, {GATE_SCAN});
|
||||
}
|
||||
this->command(CMD_POWER_ON);
|
||||
this->next_delay_ = BUSY_ASSERT_MS;
|
||||
}
|
||||
|
||||
void EPaperUC8279::refresh_screen(bool partial) {
|
||||
ESP_LOGV(TAG, "Refresh screen (%s)", partial ? LOG_STR_LITERAL("partial") : LOG_STR_LITERAL("full"));
|
||||
if (partial)
|
||||
this->set_partial_window_(0, this->width_ - 1, this->offset_height_, this->gate_count_() - 1);
|
||||
// Power-on reloads the panel's stored settings, so the panel setting only takes when written after it
|
||||
this->cmd_data(CMD_PSR, {PSR0_OTP_LUT, PSR1});
|
||||
this->command(CMD_REFRESH);
|
||||
this->next_delay_ = REFRESH_START_MS;
|
||||
}
|
||||
|
||||
bool HOT EPaperUC8279::after_refresh() {
|
||||
// While partial updates are in use, DTM1 holds the image on the panel for the next partial comparison
|
||||
if (!this->is_using_partial_update_())
|
||||
return true;
|
||||
if (this->current_data_index_ == 0 && this->partial_)
|
||||
this->command(CMD_PARTIAL_OUT);
|
||||
if (!this->write_plane_(CMD_DTM1, false, 0))
|
||||
return false;
|
||||
this->current_data_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperUC8279::power_off() {
|
||||
this->command(CMD_POWER_OFF);
|
||||
this->next_delay_ = BUSY_ASSERT_MS;
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi_ultrachip.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Monochrome e-paper displays using the UltraChip UC8279 controller, such as the Xteink X4 Pro.
|
||||
*
|
||||
* Buffer layout: 1 bit per pixel, 1=white, 0=black (the base class default).
|
||||
*
|
||||
* The controller scans offset_height + height gates; the panel is bonded to the last height of them,
|
||||
* so every image plane is written for all gates, white outside the panel.
|
||||
*
|
||||
* A full refresh writes the new image to DTM2 and white to DTM1. A partial refresh writes only DTM2
|
||||
* and drives the pixels that differ from DTM1, which therefore has to hold the image on the panel:
|
||||
* while partial updates are in use it is rewritten after every refresh, in time slices like the
|
||||
* image transfer. The panel loses both planes in its own deep sleep, so it then waits in standby
|
||||
* between updates instead.
|
||||
*/
|
||||
class EPaperUC8279 : public EPaperUltraChip {
|
||||
public:
|
||||
EPaperUC8279(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length, uint16_t offset_height)
|
||||
: EPaperUltraChip(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY),
|
||||
offset_height_(offset_height) {}
|
||||
|
||||
protected:
|
||||
bool reset() override;
|
||||
bool initialise(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
void power_on() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_off() override;
|
||||
bool after_refresh() override;
|
||||
|
||||
uint16_t gate_count_() const { return this->offset_height_ + this->height_; }
|
||||
// Writes one gate's row of the image, or white for a gate outside the panel or when white is true
|
||||
void write_gate_(size_t gate, bool white);
|
||||
// Writes one plane in time slices, sending `command` at its first gate; true once all gates are written
|
||||
bool write_plane_(uint8_t command, bool white, size_t base);
|
||||
|
||||
uint16_t offset_height_;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "epaper_spi_ultrachip.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.ultrachip");
|
||||
|
||||
static constexpr uint8_t hi(uint16_t v) { return static_cast<uint8_t>(v >> 8); }
|
||||
static constexpr uint8_t lo(uint16_t v) { return static_cast<uint8_t>(v & 0xFF); }
|
||||
|
||||
void EPaperUltraChip::deep_sleep() {
|
||||
// Partial updates compare against the previous image, which deep sleep would lose; standby keeps it
|
||||
if (this->is_using_partial_update_())
|
||||
return;
|
||||
this->send_deep_sleep_();
|
||||
}
|
||||
|
||||
void EPaperUltraChip::send_deep_sleep_() {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
this->cmd_data(CMD_DEEP_SLEEP, {DEEP_SLEEP_CHECK});
|
||||
}
|
||||
|
||||
void EPaperUltraChip::on_safe_shutdown() { this->send_deep_sleep_(); }
|
||||
|
||||
void EPaperUltraChip::set_partial_window_(uint16_t x_start, uint16_t x_end, uint16_t y_start, uint16_t y_end) {
|
||||
this->command(CMD_PARTIAL_IN);
|
||||
this->cmd_data(CMD_PARTIAL_WINDOW,
|
||||
{hi(x_start), static_cast<uint8_t>(lo(x_start) & 0xF8), hi(x_end),
|
||||
static_cast<uint8_t>(lo(x_end) | 0x07), hi(y_start), lo(y_start), hi(y_end), lo(y_end), 0x01});
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Shared base for the UltraChip UC81xx mono controllers (UC8179, UC8279).
|
||||
*
|
||||
* They use the same command set and the same deep-sleep and partial-window handling; the
|
||||
* concrete drivers differ only in how each refresh is configured and which planes are sent.
|
||||
*/
|
||||
class EPaperUltraChip : public EPaperBase {
|
||||
public:
|
||||
using EPaperBase::EPaperBase;
|
||||
|
||||
// Nothing carries the image across a restart or the controller's deep sleep, so sleep the panel rather than
|
||||
// leaving it waiting in standby
|
||||
void on_safe_shutdown() override;
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t CMD_POWER_OFF = 0x02;
|
||||
static constexpr uint8_t CMD_POWER_ON = 0x04;
|
||||
static constexpr uint8_t CMD_DEEP_SLEEP = 0x07;
|
||||
static constexpr uint8_t CMD_DTM1 = 0x10;
|
||||
static constexpr uint8_t CMD_REFRESH = 0x12;
|
||||
static constexpr uint8_t CMD_DTM2 = 0x13;
|
||||
static constexpr uint8_t CMD_CDI = 0x50;
|
||||
static constexpr uint8_t CMD_PARTIAL_WINDOW = 0x90;
|
||||
static constexpr uint8_t CMD_PARTIAL_IN = 0x91;
|
||||
static constexpr uint8_t CMD_PARTIAL_OUT = 0x92;
|
||||
static constexpr uint8_t CMD_CCSET = 0xE0;
|
||||
static constexpr uint8_t CMD_TSSET = 0xE5;
|
||||
static constexpr uint8_t DEEP_SLEEP_CHECK = 0xA5;
|
||||
|
||||
void deep_sleep() override;
|
||||
// Sends the deep-sleep command unconditionally; deep_sleep() skips it while partial updates are in use
|
||||
void send_deep_sleep_();
|
||||
// Enters partial mode and sets the refresh window; x is rounded out to whole bytes
|
||||
void set_partial_window_(uint16_t x_start, uint16_t x_end, uint16_t y_start, uint16_t y_end);
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
Loaded 100 of 625 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user