Compare commits

...
Author SHA1 Message Date
J. Nick Koston 9cd7228bb8 Merge branch 'dev' into i2c-write-registers-migrate 2026-10-17 15:28:30 -10:00
J. Nick Koston 5df6c65cc2 [api] Test api.connected without options and with a lambda (#20835) 2026-10-17 15:23:35 -10:00
J. Nick Koston af5efabdc6 [light] Test light.toggle with a transition length (#20832) 2026-10-17 15:23:22 -10:00
J. Nick Koston a2754f655c [dfrobot_sen0609][http_request] Use ESPHOME_strstr_P (#20825) 2026-10-17 15:23:06 -10:00
J. Nick Koston 34e6d3ec9c [pylontech] Restore the CONF_ID import (#21013) 2026-10-17 15:21:36 -10:00
J. Nick Koston 21362f8c92 [ethernet] Test the ethernet.connected condition (#20844) 2026-10-17 15:21:18 -10:00
J. Nick Koston 626d84bc64 [sensor][number] Test in_range with every bound combination (#20847) 2026-10-17 15:20:52 -10:00
J. Nick Koston 68fa7b9847 [wifi] Test the wifi.connected and wifi.enabled conditions (#20841) 2026-10-17 15:20:00 -10:00
J. Nick Koston 32dc3b8535 [http_request] Test ota.http_request.flash with every field (#20829) 2026-10-17 15:19:24 -10:00
J. Nick Koston 74bcec34ad [i2c] Add I2CDevice::update_register for register read-modify-write (#20854) 2026-10-17 15:18:36 -10:00
J. Nick Koston c1687aba4c [components] Let core report communication failures in dump_config (5/5) (#20874) 2026-10-17 15:16:21 -10:00
J. Nick Koston ec642f928f [i2c] Add little-endian and signed byte decoding helpers (#20938) 2026-10-17 15:15:36 -10:00
J. Nick Koston ec9a207f42 [components] Create UART devices with new_uart_component (#20919) 2026-10-17 15:15:21 -10:00
J. Nick Koston 3b7847d951 [components] Create SPI devices with new_spi_component (#20920) 2026-10-17 21:13:49 -04:00
J. Nick Koston 1197f22f00 [fusb302b] Expose the sensors as platforms (#21004) 2026-10-17 15:09:49 -10:00
J. Nick Koston e034662dc6 [uart_tcp] Expose the connection entities as platforms (#21008) 2026-10-17 15:09:32 -10:00
J. Nick Koston 42e849967a [bq76972] Expose the sensors as a sensor platform (#21006) 2026-10-17 15:09:14 -10:00
J. Nick Koston e6f1ed8d51 [tcp_uart] Expose the connection entities as platforms (#21009) 2026-10-17 15:07:42 -10:00
Igor StadnykandJ. Nick Koston bb9067acd7 [tfluna] Recover from transient runtime reconfiguration failures (#20992)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 15:07:16 -10:00
Michele Porelli 928cf7d696 [libretiny] Emulate rtc preference storage on the Beken BK7231T, BK7231N and BK7238 with flash and the reset reason (#21007) 2026-10-17 14:59:40 -10:00
89afb111ad [mt6701] Add MT6701 (MagnTek magnetic encoder) component (#17643)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 14:06:46 -10:00
Brandon HarveyandJ. Nick Koston d1ec52e95a [sen6x] Add device status binary sensors (#18784)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 23:34:15 +00:00
Bonne Egglestonandclydebarrow 818d43f742 [epaper_spi] Run the loop continuously between the steps of an update (#20554)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-10-18 10:25:36 +11:00
1a28a9220b [uart] Add a binary sensor that fires on a byte sequence (#14032)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 22:35:46 +00:00
Brandon HarveyandJ. Nick Koston 022733c46b [sen6x] Add PM number concentration sensors (#18782)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 12:16:45 -10:00
0f6fc503bd [sen6x] Add temperature compensation, acceleration and startup delay (#18781)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 11:01:14 -10:00
Bonne Egglestonandclydebarrow ae62960193 [epaper_spi] Add the UC8279 controller and the Xteink X4 Pro (#20545)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-10-18 07:51:07 +11:00
325c747dcd [sen6x] Add CO2 configuration options (#18780)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 10:41:13 -10:00
Brandon HarveyandJ. Nick Koston 5759c6b8e6 [deep_sleep] Honor nested wakeup_pin_mode on ESP32 single-pin configs (#16618)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 10:25:40 -10:00
c9ba6c355c [hoermann_hcp] Add an actuator error binary sensor (#20299)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 10:00:31 -10:00
Brandon HarveyandJ. Nick Koston d02da3c8de [wifi] Support hidden option in single-network shorthand (#18157)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 09:54:55 -10:00
Bascht74andJ. Nick Koston a576092f25 [remote_base] Send the 72-bit Jarolift TDEF group frame with transmit_keeloq (#20927)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 09:52:01 -10:00
Simonas Kazlauskas 792d650fb2 [remote_base] Add a protocol for 433MHz A-OK remotes (#20991) 2026-10-17 09:44:25 -10:00
f72857151a [pylontech] Add individual cell voltage and temperature sensors (#16852)
Co-authored-by: Michael <michael_e@gmx-topmail.com>
Co-authored-by: ritschys <117527311+ritschys@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 19:28:52 +00:00
5f8f075e76 [zephyr_coredump] Store a coredump in flash on nRF52 and print it to the next log reader (#13122)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 19:22:28 +00:00
Matias FernandezandJ. Nick Koston c2c8cb5eb3 [climate_ir_lg] Fix swing command to not include climate state (#17417)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 09:12:35 -10:00
J. Nick Koston 5c2b05d0eb [ci] Run the C++ unit tests on macOS without a sanitizer whose runtime aborts (#21000) 2026-10-17 09:00:51 -10:00
Igor StadnykandJ. Nick Koston e110613ef1 [core] Construct App once in the C++ unit test main (#21001)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 19:00:37 +00:00
luar123 4324b7d084 [zigbee] bump esp-zigbee-sdk to 2.0.5 (#20998) 2026-10-17 08:58:49 -10:00
37d6140193 [epaper_spi] Fix partial refresh and port waveshare 2.90inv2 (#18959)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: luar123 <49960470+luar123@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 08:55:37 -10:00
4f192130f8 [water_heater] Hand the component to the core team (#20999)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-17 18:51:06 +00:00
Hasan Khalilandpre-commit-ci-lite[bot] 4ef17cebdb [api] Pre-reserve Home Assistant state subscriptions vector (#20986)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-17 08:01:01 -10:00
35276a38bc [rtsp_server] Add an RTSP server that streams the esp32_camera picture (#17990)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 07:18:52 -10:00
J. Nick Koston 635df9c6ad [socket] Share the allow-list reject log and config lines through Ipv4Allow (#20983) 2026-10-17 06:51:29 -10:00
Jonathan Swoboda d16d8c111e [debug] Log uptime in dump_config (#20989) 2026-10-17 06:50:54 -10:00
Jonathan Swoboda 1ead61f11f [esp32] Remove trailing comment from ECDSA test signing key (#20988) 2026-10-17 06:50:11 -10:00
J. Nick Koston bd06d37d1a [components] Check I2C chip IDs with check_chip_id (#20943) 2026-10-17 06:49:46 -10:00
Jonathan Swoboda 7c6fac91ff [esp32] Bump recommended ESP-IDF to 5.5.6 (#20987) 2026-10-17 06:49:15 -10:00
J. Nick Koston 052a6096e6 [components] Log hex dumps with the ESP_LOG*_HEX macros (#20932) 2026-10-17 16:46:16 +00:00
J. Nick Koston 25b684f762 [pylontech] Parse each line as it arrives with one fixed buffer (#20959) 2026-10-17 06:44:41 -10:00
J. Nick Koston 62f8cc3436 [uart] Add read_available helpers to drain available bytes in chunks (#20947) 2026-10-17 06:41:51 -10:00
J. Nick Koston 66f15df7be [es8311] Leave es8311 out: with the shared failure log its small tables no longer save flash 2026-10-17 06:41:24 -10:00
J. Nick Koston d1a237500c [as734x] Make the init tables constexpr like the codec tables 2026-10-17 06:41:24 -10:00
J. Nick Koston 87350f7d6d [fusb302b] Keep the write_byte chains; the tables were larger 2026-10-17 06:41:24 -10:00
J. Nick Koston 5357d7f9f8 [i2c] Use I2CRegValue in as734x, one es7243e entry per line, drop the es8156 macro 2026-10-17 06:41:24 -10:00
J. Nick Koston 5397546ecd [i2c] Write constant codec register runs from PROGMEM tables 2026-10-17 06:41:24 -10:00
J. Nick Koston 34f9152d61 [i2c] Add write_registers for constant register tables (#20945) 2026-10-17 06:41:20 -10:00
luar123 73af6b0500 [core][scheduler] run process_to_add after component phase (#20993) 2026-10-17 06:40:39 -10:00
J. Nick Koston a384aae5b1 [button] Add factory reset, restart and query button schema presets (#20915) 2026-10-17 06:38:45 -10:00
J. Nick Koston 905267a9c3 [esp32_ble] Build ble.enabled, ble.enable and ble.disable with the apply helpers (#20839) 2026-10-17 06:38:25 -10:00
J. Nick Koston d08c845c65 [components] Create I2C devices with new_i2c_component (#20909) 2026-10-17 06:36:49 -10:00
J. Nick Koston 91dfb92c60 [esp32_ble] Test ble.enabled, ble.enable and ble.disable (#20838) 2026-10-17 09:22:29 -04:00
J. Nick Koston cef9418a0f [i2c][uart][spi] Add new_i2c_component, new_uart_component and new_spi_component (#20908) 2026-10-17 09:20:28 -04:00
J. Nick Koston c0a693dfce [components] Use xor_bytes and sum16_bytes for checksums (#20922) 2026-10-17 09:18:24 -04:00
Clyde Stubbs 1b4721d445 [core] Script improvements including choose (#20547) 2026-10-17 21:23:12 +11:00
f8d31c3e54 [dfrobot_sen0465] Add DFRobot SEN0465-SEN0476 electrochemical gas sensors (#20880)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-16 22:47:50 -10:00
eafcca8274 [mics_5524] Add datasheet model, diagnostics and advanced wiring (2/2) (#20521)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-16 22:47:28 -10:00
Remco van Essen 06b585d373 [i2s_audio] Fix speaker restart loop with full_duplex (#20984) 2026-10-16 22:05:21 -10:00
HugoPeters1024andJ. Nick Koston 885f2fe5d8 [kamstrup_kmp] Fix Kamstrup KMP reads with echoing optical heads (#20897)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-16 22:04:01 -10:00
J. Nick Koston 4e27b2fc12 [components] Collect UART lines with LineReader (#20956) 2026-10-16 19:54:11 -10:00
J. Nick Koston 375900bfcd [core] Add LineReader to collect lines in a fixed buffer (#20952) 2026-10-16 19:54:11 -10:00
Zhaoqi Xu 6d664d3fef [web_server] Report numbers without a state as NA (#20973) 2026-10-16 19:15:04 -10:00
J. Nick Koston 4dc2b5911b [core] Move CONF_DRIVE_STRENGTH to the shared component constants (#20975) 2026-10-16 18:21:04 -10:00
tomaszduda23 687466b6cd [logger][usb_cdc_acm][core] Add CDC uart to NRF52 (#11634) 2026-10-16 18:14:51 -10:00
J. Nick Koston dcdc042a4b [host] Define a no-op InterruptLock (#20974) 2026-10-16 17:53:33 -10:00
d2a1f7ee31 [zephyr] Fix preference log levels and report settings read/save errors (#20810)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-17 03:02:54 +00:00
625 changed files with 15683 additions and 3075 deletions

No files matched your search

+9 -1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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 -4
View File
@@ -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)
+1 -3
View File
@@ -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 -4
View File
@@ -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 -4
View File
@@ -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)
+1 -4
View File
@@ -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)
+1 -4
View File
@@ -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)
+73 -73
View File
@@ -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]() {
+2 -4
View File
@@ -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)
+1 -4
View File
@@ -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)
+1 -4
View File
@@ -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)
+2 -11
View File
@@ -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);
+1 -4
View File
@@ -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};
};
+2 -9
View File
@@ -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);
+1 -9
View File
@@ -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 |
+1 -4
View File
@@ -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 -4
View File
@@ -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]))
+3
View File
@@ -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
+1 -4
View File
@@ -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]))
+1 -7
View File
@@ -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)) {
+1 -4
View File
@@ -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]))
+1 -4
View File
@@ -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]))
+7 -6
View File
@@ -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);
}
+1 -6
View File
@@ -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
+1 -3
View File
@@ -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
+1 -4
View File
@@ -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)
+1 -4
View File
@@ -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]))
+1 -8
View File
@@ -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) {
+1 -3
View File
@@ -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)
+1 -4
View File
@@ -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))
+1 -4
View File
@@ -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)
+1 -4
View File
@@ -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 -4
View File
@@ -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)
+1 -4
View File
@@ -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):
+1 -3
View File
@@ -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]))
+1 -4
View File
@@ -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)
+1 -10
View File
@@ -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)) {
+1 -4
View File
@@ -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)
+1 -3
View File
@@ -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))))
+1 -11
View File
@@ -79,19 +79,9 @@ bool BQ25186Component::apply_config(const BQ25186ConfigRegisters &regs) {
}
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
+1 -4
View File
@@ -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)
+4 -61
View File
@@ -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(
{
+8
View File
@@ -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
+8 -1
View File
@@ -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
+73
View File
@@ -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))
+39
View File
@@ -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"),
)
+2 -1
View File
@@ -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 {
+1 -3
View File
@@ -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
+1 -4
View File
@@ -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) {
+1 -4
View File
@@ -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)
+1
View File
@@ -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"
+1 -3
View File
@@ -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]))
+1 -4
View File
@@ -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)
+1 -4
View File
@@ -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)
+2 -4
View File
@@ -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
+15 -2
View File
@@ -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
+2 -4
View File
@@ -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)
+1 -4
View File
@@ -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};
+1 -4
View File
@@ -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)
+1 -4
View File
@@ -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 -4
View File
@@ -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)))
+1 -4
View File
@@ -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(
+1 -4
View File
@@ -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 -4
View File
@@ -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))
+1
View File
@@ -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
+25 -42
View File
@@ -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
}
}
+3 -5
View File
@@ -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
+1 -4
View File
@@ -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)
+10 -4
View File
@@ -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
+33 -7
View File
@@ -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