Compare commits

..

71 Commits

Author SHA1 Message Date
Jesse Hills
ec26d31499 Merge pull request #8996 from esphome/bump-2025.5.2
2025.5.2
2025-06-03 20:45:09 +12:00
Jesse Hills
1bbc6db1c3 Bump version to 2025.5.2 2025-06-03 20:04:39 +12:00
J. Nick Koston
162472bdc2 Fix logger stack overflow (#8988) 2025-06-03 20:04:39 +12:00
Clyde Stubbs
aecac15809 [debug] Make sensors work without logger debug level (#8980) 2025-06-03 20:04:39 +12:00
Samuel Sieb
6554af21b9 [esp8266] fix isr pin (#8981)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-06-03 20:04:39 +12:00
Samuel Sieb
8583466c6a [rp2040] use low-level control for ISR gpio and add IRAM_ATTR (#8950)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-06-03 20:04:39 +12:00
Kevin Ahrendt
6666604069 [i2s-audio] ensure mic task isn't pinned to a core (#8879) 2025-06-03 20:04:39 +12:00
Jesse Hills
f74f89c6b5 Merge pull request #8913 from esphome/bump-2025.5.1
2025.5.1
2025-05-27 21:01:19 +12:00
Jesse Hills
42390faf4a Bump version to 2025.5.1 2025-05-27 14:31:38 +12:00
Jesse Hills
fdc6c4a219 [web_server] Fix download list where external_components has a substitution value (#8911)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-27 14:31:38 +12:00
Jesse Hills
6c08f5e343 [api] Fix crash with gcc compiler on host (#8902) 2025-05-27 14:31:38 +12:00
Keith Burzinski
e0e4ba9592 [esp32] Fix building on IDF 4 (#8892) 2025-05-27 14:31:38 +12:00
Jesse Hills
ad20825f31 [logger] Fix options in select (#8875)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2025-05-27 14:31:38 +12:00
Kevin Ahrendt
e4f3a952d5 [speaker] ensure the pipeline returns an error state before returning its stopped (#8878) 2025-05-27 14:31:38 +12:00
Kevin Ahrendt
90e3c5bba2 [micro_wake_word] avoid duplicated detections from same event (#8877) 2025-05-27 14:31:38 +12:00
Clyde Stubbs
b1d5ad27f3 [lvgl] Improve error messages from text validation (#8872) 2025-05-27 14:31:38 +12:00
Jesse Hills
5c54f75b7a [online_image] Allocate pngle manually to potentially use psram (#8354)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-05-27 14:31:38 +12:00
Cossid
a5f85b4437 [tuya_select] - Fix datapoint config error. (#8871) 2025-05-27 14:31:38 +12:00
Jesse Hills
da4e710249 [core] Add some missing includes (#8864) 2025-05-27 14:31:38 +12:00
Jesse Hills
cdcd1cd292 Merge pull request #8863 from esphome/bump-2025.5.0
2025.5.0
2025-05-21 20:32:40 +12:00
Jesse Hills
1cba22175f Bump version to 2025.5.0 2025-05-21 15:26:55 +12:00
Jesse Hills
801138da27 Merge pull request #8862 from esphome/bump-2025.5.0b6
2025.5.0b6
2025-05-21 13:09:04 +12:00
Jesse Hills
51740a2e99 Bump version to 2025.5.0b6 2025-05-21 11:54:08 +12:00
Jesse Hills
d68a391e67 [api-docs] Move netlify.toml to root (#8861) 2025-05-21 11:54:07 +12:00
Jesse Hills
756aa13779 Merge pull request #8860 from esphome/bump-2025.5.0b5
2025.5.0b5
2025-05-21 11:25:48 +12:00
Jesse Hills
25bbc0c221 Bump version to 2025.5.0b5 2025-05-21 10:05:54 +12:00
Gustavo Ambrozio
220a14e1f8 [at581x] Fix issue with methods not being public (#8852) 2025-05-21 10:05:53 +12:00
Clyde Stubbs
ac74b25c46 Fix #ifdefs (#8853) 2025-05-21 10:05:53 +12:00
Jesse Hills
937fe393a1 Merge pull request #8845 from esphome/bump-2025.5.0b4
2025.5.0b4
2025-05-20 01:19:01 +12:00
Jesse Hills
4b552d9fba Bump version to 2025.5.0b4 2025-05-19 20:01:40 +12:00
Jesse Hills
aa53d8f1ee [api-docs] Run using netlify builders (#8842)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-19 20:01:40 +12:00
Jesse Hills
a28932bc29 [docker] Update pip on build (#8835)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-05-19 20:01:40 +12:00
J. Nick Koston
afa7414ee1 Fix ethernet connection timeout issue caused by incorrect time value during setup (#8841) 2025-05-19 20:01:40 +12:00
J. Nick Koston
aed7ef481e Fix API connection sending ping too early after connection establishment (#8840) 2025-05-19 20:01:40 +12:00
Jesse Hills
c820fee1f6 [release] Don't wait for docker to be finished before deploying schema (#8838) 2025-05-19 20:01:40 +12:00
Jesse Hills
5244ac4ff6 [release] Fix output value (#8839) 2025-05-19 20:01:40 +12:00
Jesse Hills
89d283eee4 Deploy doxygen docs to netlify (#8837) 2025-05-19 20:01:40 +12:00
Jesse Hills
ef053d23b4 Fix api doc homepage (#8836)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-19 20:01:39 +12:00
Fexiven
98470d32f0 Update esp32-camera library version (#8832) 2025-05-19 20:01:39 +12:00
J. Nick Koston
cab6edd800 Avoid protobuf message construction when tx buffer is full (#8787)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-19 20:01:39 +12:00
Jesse Hills
aaaf9b2b62 Merge pull request #8834 from esphome/bump-2025.5.0b3
2025.5.0b3
2025-05-19 16:02:46 +12:00
Jesse Hills
38cfd32382 Bump version to 2025.5.0b3 2025-05-19 09:24:53 +12:00
dependabot[bot]
1b9ae57b9d Bump docker/build-push-action from 6.16.0 to 6.17.0 in /.github/actions/build-image (#8810)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 09:24:53 +12:00
J. Nick Koston
4d54cb9b31 Refactor API frame helpers to enable buffer reuse (#8825) 2025-05-19 09:24:53 +12:00
J. Nick Koston
15d0b4355e Reduce number of calls to fetch time in the main loop (#8804) 2025-05-19 09:24:53 +12:00
J. Nick Koston
316fe2f06c Fix ESP32 console logging corruption and message loss in multi-task (#8806) 2025-05-19 09:24:53 +12:00
Kent Gibson
f8681adec4 Fix misspelling of climate in climate_ir.climate_ir_with_receiver_schema (#8829) 2025-05-19 09:24:53 +12:00
Clyde Stubbs
868f5ff20c Revert "[binary_sensor] initial state refactor" (#8828) 2025-05-19 09:24:53 +12:00
Anton Sergunov
59295a615e Fix the case of single error (#8824) 2025-05-19 09:24:53 +12:00
Keith Burzinski
d8516cfabb [sen5x] Fix validation for values read from hardware (#8769) 2025-05-19 09:24:53 +12:00
J. Nick Koston
d847b345b8 Fix ESP32 Camera class inheritance (#8811) 2025-05-19 09:24:53 +12:00
Thomas Rupprecht
c50e33f531 [gps] update lib, improve code/tests/config (#8768) 2025-05-19 09:24:53 +12:00
Thomas Rupprecht
5a84bab9ec [log] improve/refactor log (#8708) 2025-05-19 09:24:53 +12:00
J. Nick Koston
41f860c2a3 Logger Recursion Guard per Task on ESP32 (#8765) 2025-05-19 09:24:53 +12:00
J. Nick Koston
c7e62d1279 Optimize protobuf varint decoder for ESPHome use case (#8791) 2025-05-19 09:24:53 +12:00
J. Nick Koston
2341ff651a Use fixed buffer for plaintext protocol like noise protocol (#8800) 2025-05-19 09:24:53 +12:00
Jesse Hills
9704de6647 Update some sensor schemas to be Optional (#8803) 2025-05-19 09:24:52 +12:00
Jesse Hills
97fb8c2cdf Merge pull request #8792 from esphome/bump-2025.5.0b2
2025.5.0b2
2025-05-15 06:50:46 +12:00
Jesse Hills
d9839f3a5c Bump version to 2025.5.0b2 2025-05-14 21:29:00 +12:00
Jesse Hills
498e3904a9 Bump esphome-dashboard to 20250514.0 (#8790) 2025-05-14 21:28:33 +12:00
Jesse Hills
7cb01bf842 [climate] Update components to use `climate_schema(...)` (#8788) 2025-05-14 21:27:05 +12:00
Jesse Hills
c050e8d0fb Fix release to pypi (#8789) 2025-05-14 21:27:05 +12:00
J. Nick Koston
4f2643e6e9 Improve batching of BLE advertisements for better airtime efficiency (#8778) 2025-05-14 21:27:05 +12:00
Jesse Hills
7d0262dd1a [fan] Update components to use `fan_schema(...)` (#8786) 2025-05-14 21:27:05 +12:00
Jesse Hills
c30ffd0098 [schema] Get component name if available for deprecation warning (#8785) 2025-05-14 21:27:05 +12:00
Jesse Hills
ea31122979 [media_player] Deprecate `MEDIA_PLAYER_SCHEMA` (#8784) 2025-05-14 21:27:05 +12:00
Jesse Hills
1e20440c8e Merge pull request #8783 from esphome/bump-2025.5.0b1
2025.5.0b1
2025-05-14 15:54:44 +12:00
Jesse Hills
0630244195 Bump version to 2025.5.0b1 2025-05-14 09:54:26 +12:00
Clyde Stubbs
183659f527 [mipi_spi] New display driver for MIPI DBI devices (#8383)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-14 09:22:58 +12:00
Jesse Hills
4ea63af796 [online_image] Support 24 bit bmp images (#8612) 2025-05-14 09:21:19 +12:00
Samuel Sieb
0aa7911b1b [esp32][esp8266] use low-level pin control for ISR gpio (#8743)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-05-14 08:58:15 +12:00
190 changed files with 8233 additions and 2015 deletions

View File

@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v6.16.0
uses: docker/build-push-action@v6.17.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v6.16.0
uses: docker/build-push-action@v6.17.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -18,6 +18,7 @@ jobs:
outputs:
tag: ${{ steps.tag.outputs.tag }}
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@v4.1.7
- name: Get tag
@@ -27,6 +28,11 @@ jobs:
if [[ "${{ github.event_name }}" = "release" ]]; then
TAG="${{ github.event.release.tag_name}}"
BRANCH_BUILD="false"
if [[ "${{ github.event.release.prerelease }}" = "true" ]]; then
ENVIRONMENT="beta"
else
ENVIRONMENT="production"
fi
else
TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
today="$(date --utc '+%Y%m%d')"
@@ -35,12 +41,15 @@ jobs:
if [[ "$BRANCH" != "dev" ]]; then
TAG="${TAG}-${BRANCH}"
BRANCH_BUILD="true"
ENVIRONMENT=""
else
BRANCH_BUILD="false"
ENVIRONMENT="dev"
fi
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT
echo "deploy_env=${ENVIRONMENT}" >> $GITHUB_OUTPUT
# yamllint enable rule:line-length
deploy-pypi:
@@ -56,16 +65,14 @@ jobs:
uses: actions/setup-python@v5.6.0
with:
python-version: "3.x"
- name: Set up python environment
env:
ESPHOME_NO_VENV: 1
run: script/setup
- name: Build
run: |-
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
skip-existing: true
deploy-docker:
name: Build ESPHome ${{ matrix.platform.arch }}
@@ -235,9 +242,8 @@ jobs:
deploy-esphome-schema:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs:
- init
- deploy-manifest
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Trigger Workflow
uses: actions/github-script@v7.0.1

1
.gitignore vendored
View File

@@ -143,3 +143,4 @@ sdkconfig.*
/components
/managed_components
api-docs/

View File

@@ -169,7 +169,7 @@ esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle
esphome/components/gps/* @coogle @ximex
esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers
@@ -282,6 +282,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov
esphome/components/mipi_spi/* @clydebarrow
esphome/components/mitsubishi/* @RubyBailey
esphome/components/mixer/speaker/* @kahrendt
esphome/components/mlx90393/* @functionpointer

2877
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,9 @@ FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*"
RUN pip install uv==0.6.14
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14
COPY requirements.txt /

View File

@@ -43,7 +43,7 @@ from esphome.const import (
)
from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import Fore, color, setup_log
from esphome.log import AnsiFore, color, setup_log
from esphome.util import (
get_serial_ports,
list_yaml_files,
@@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None):
raise ValueError
break
except ValueError:
safe_print(color(Fore.RED, f"Invalid option: '{opt}'"))
safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'"))
return options[opt - 1][1]
@@ -596,30 +596,30 @@ def command_update_all(args):
click.echo(f"{half_line}{middle_text}{half_line}")
for f in files:
print(f"Updating {color(Fore.CYAN, f)}")
print(f"Updating {color(AnsiFore.CYAN, f)}")
print("-" * twidth)
print()
rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
)
if rc == 0:
print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}")
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
success[f] = True
else:
print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}")
print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
success[f] = False
print()
print()
print()
print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]")
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0
for f in files:
if success[f]:
print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}")
print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else:
print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}")
print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
failed += 1
return failed
@@ -645,7 +645,7 @@ def command_rename(args, config):
if c not in ALLOWED_NAME_CHARS:
print(
color(
Fore.BOLD_RED,
AnsiFore.BOLD_RED,
f"'{c}' is an invalid character for names. Valid characters are: "
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
)
@@ -658,7 +658,9 @@ def command_rename(args, config):
yaml = yaml_util.load_yaml(CORE.config_path)
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
print(
color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
color(
AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
)
)
return 1
old_name = yaml[CONF_ESPHOME][CONF_NAME]
@@ -681,7 +683,7 @@ def command_rename(args, config):
)
> 1
):
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename"))
return 1
new_raw = re.sub(
@@ -693,7 +695,7 @@ def command_rename(args, config):
new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
print(
f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
)
print()
@@ -702,7 +704,7 @@ def command_rename(args, config):
rc = run_external_process("esphome", "config", new_path)
if rc != 0:
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
os.remove(new_path)
return 1
@@ -728,7 +730,7 @@ def command_rename(args, config):
if CORE.config_path != new_path:
os.remove(CORE.config_path)
print(color(Fore.BOLD_GREEN, "SUCCESS"))
print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
print()
return 0

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import ble_client, climate
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT
from esphome.const import CONF_UNIT_OF_MEASUREMENT
UNITS = {
"f": "f",
@@ -17,9 +17,9 @@ Anova = anova_ns.class_(
)
CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
climate.climate_schema(Anova)
.extend(
{
cv.GenerateID(): cv.declare_id(Anova),
cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS),
}
)
@@ -29,8 +29,7 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
var = await climate.new_climate(config)
await cg.register_component(var, config)
await climate.register_climate(var, config)
await ble_client.register_ble_node(var, config)
cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT]))

File diff suppressed because it is too large Load Diff

View File

@@ -8,13 +8,17 @@
#include "api_server.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include <vector>
namespace esphome {
namespace api {
using send_message_t = bool(APIConnection *, void *);
// Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
using send_message_t = bool (APIConnection::*)(void *);
/*
This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that
@@ -30,10 +34,10 @@ class DeferredMessageQueue {
protected:
void *source_;
send_message_t *send_message_;
send_message_t send_message_;
public:
DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {}
DeferredMessage(void *source, send_message_t send_message) : source_(source), send_message_(send_message) {}
bool operator==(const DeferredMessage &test) const {
return (source_ == test.source_ && send_message_ == test.send_message_);
}
@@ -46,12 +50,13 @@ class DeferredMessageQueue {
APIConnection *api_connection_;
// helper for allowing only unique entries in the queue
void dmq_push_back_with_dedup_(void *source, send_message_t *send_message);
void dmq_push_back_with_dedup_(void *source, send_message_t send_message);
public:
DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {}
void process_queue();
void defer(void *source, send_message_t *send_message);
void defer(void *source, send_message_t send_message);
bool empty() const { return deferred_queue_.empty(); }
};
class APIConnection : public APIServerConnection {
@@ -69,137 +74,213 @@ class APIConnection : public APIServerConnection {
#ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state);
static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor);
protected:
bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor);
bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor, bool state);
bool try_send_binary_sensor_info_(binary_sensor::BinarySensor *binary_sensor);
public:
#endif
#ifdef USE_COVER
bool send_cover_state(cover::Cover *cover);
void send_cover_info(cover::Cover *cover);
static bool try_send_cover_state(APIConnection *api, void *v_cover);
static bool try_send_cover_info(APIConnection *api, void *v_cover);
void cover_command(const CoverCommandRequest &msg) override;
protected:
bool try_send_cover_state_(cover::Cover *cover);
bool try_send_cover_info_(cover::Cover *cover);
public:
#endif
#ifdef USE_FAN
bool send_fan_state(fan::Fan *fan);
void send_fan_info(fan::Fan *fan);
static bool try_send_fan_state(APIConnection *api, void *v_fan);
static bool try_send_fan_info(APIConnection *api, void *v_fan);
void fan_command(const FanCommandRequest &msg) override;
protected:
bool try_send_fan_state_(fan::Fan *fan);
bool try_send_fan_info_(fan::Fan *fan);
public:
#endif
#ifdef USE_LIGHT
bool send_light_state(light::LightState *light);
void send_light_info(light::LightState *light);
static bool try_send_light_state(APIConnection *api, void *v_light);
static bool try_send_light_info(APIConnection *api, void *v_light);
void light_command(const LightCommandRequest &msg) override;
protected:
bool try_send_light_state_(light::LightState *light);
bool try_send_light_info_(light::LightState *light);
public:
#endif
#ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor, float state);
void send_sensor_info(sensor::Sensor *sensor);
static bool try_send_sensor_state(APIConnection *api, void *v_sensor);
static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state);
static bool try_send_sensor_info(APIConnection *api, void *v_sensor);
protected:
bool try_send_sensor_state_(sensor::Sensor *sensor);
bool try_send_sensor_state_(sensor::Sensor *sensor, float state);
bool try_send_sensor_info_(sensor::Sensor *sensor);
public:
#endif
#ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch, bool state);
void send_switch_info(switch_::Switch *a_switch);
static bool try_send_switch_state(APIConnection *api, void *v_a_switch);
static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state);
static bool try_send_switch_info(APIConnection *api, void *v_a_switch);
void switch_command(const SwitchCommandRequest &msg) override;
protected:
bool try_send_switch_state_(switch_::Switch *a_switch);
bool try_send_switch_state_(switch_::Switch *a_switch, bool state);
bool try_send_switch_info_(switch_::Switch *a_switch);
public:
#endif
#ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state);
static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor);
protected:
bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor);
bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor, std::string state);
bool try_send_text_sensor_info_(text_sensor::TextSensor *text_sensor);
public:
#endif
#ifdef USE_ESP32_CAMERA
void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
void send_camera_info(esp32_camera::ESP32Camera *camera);
static bool try_send_camera_info(APIConnection *api, void *v_camera);
void camera_image(const CameraImageRequest &msg) override;
protected:
bool try_send_camera_info_(esp32_camera::ESP32Camera *camera);
public:
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void send_climate_info(climate::Climate *climate);
static bool try_send_climate_state(APIConnection *api, void *v_climate);
static bool try_send_climate_info(APIConnection *api, void *v_climate);
void climate_command(const ClimateCommandRequest &msg) override;
protected:
bool try_send_climate_state_(climate::Climate *climate);
bool try_send_climate_info_(climate::Climate *climate);
public:
#endif
#ifdef USE_NUMBER
bool send_number_state(number::Number *number, float state);
void send_number_info(number::Number *number);
static bool try_send_number_state(APIConnection *api, void *v_number);
static bool try_send_number_state(APIConnection *api, number::Number *number, float state);
static bool try_send_number_info(APIConnection *api, void *v_number);
void number_command(const NumberCommandRequest &msg) override;
protected:
bool try_send_number_state_(number::Number *number);
bool try_send_number_state_(number::Number *number, float state);
bool try_send_number_info_(number::Number *number);
public:
#endif
#ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date);
void send_date_info(datetime::DateEntity *date);
static bool try_send_date_state(APIConnection *api, void *v_date);
static bool try_send_date_info(APIConnection *api, void *v_date);
void date_command(const DateCommandRequest &msg) override;
protected:
bool try_send_date_state_(datetime::DateEntity *date);
bool try_send_date_info_(datetime::DateEntity *date);
public:
#endif
#ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time);
void send_time_info(datetime::TimeEntity *time);
static bool try_send_time_state(APIConnection *api, void *v_time);
static bool try_send_time_info(APIConnection *api, void *v_time);
void time_command(const TimeCommandRequest &msg) override;
protected:
bool try_send_time_state_(datetime::TimeEntity *time);
bool try_send_time_info_(datetime::TimeEntity *time);
public:
#endif
#ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime);
void send_datetime_info(datetime::DateTimeEntity *datetime);
static bool try_send_datetime_state(APIConnection *api, void *v_datetime);
static bool try_send_datetime_info(APIConnection *api, void *v_datetime);
void datetime_command(const DateTimeCommandRequest &msg) override;
protected:
bool try_send_datetime_state_(datetime::DateTimeEntity *datetime);
bool try_send_datetime_info_(datetime::DateTimeEntity *datetime);
public:
#endif
#ifdef USE_TEXT
bool send_text_state(text::Text *text, std::string state);
void send_text_info(text::Text *text);
static bool try_send_text_state(APIConnection *api, void *v_text);
static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state);
static bool try_send_text_info(APIConnection *api, void *v_text);
void text_command(const TextCommandRequest &msg) override;
protected:
bool try_send_text_state_(text::Text *text);
bool try_send_text_state_(text::Text *text, std::string state);
bool try_send_text_info_(text::Text *text);
public:
#endif
#ifdef USE_SELECT
bool send_select_state(select::Select *select, std::string state);
void send_select_info(select::Select *select);
static bool try_send_select_state(APIConnection *api, void *v_select);
static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state);
static bool try_send_select_info(APIConnection *api, void *v_select);
void select_command(const SelectCommandRequest &msg) override;
protected:
bool try_send_select_state_(select::Select *select);
bool try_send_select_state_(select::Select *select, std::string state);
bool try_send_select_info_(select::Select *select);
public:
#endif
#ifdef USE_BUTTON
void send_button_info(button::Button *button);
static bool try_send_button_info(APIConnection *api, void *v_button);
void button_command(const ButtonCommandRequest &msg) override;
protected:
bool try_send_button_info_(button::Button *button);
public:
#endif
#ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
void send_lock_info(lock::Lock *a_lock);
static bool try_send_lock_state(APIConnection *api, void *v_a_lock);
static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state);
static bool try_send_lock_info(APIConnection *api, void *v_a_lock);
void lock_command(const LockCommandRequest &msg) override;
protected:
bool try_send_lock_state_(lock::Lock *a_lock);
bool try_send_lock_state_(lock::Lock *a_lock, lock::LockState state);
bool try_send_lock_info_(lock::Lock *a_lock);
public:
#endif
#ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve);
void send_valve_info(valve::Valve *valve);
static bool try_send_valve_state(APIConnection *api, void *v_valve);
static bool try_send_valve_info(APIConnection *api, void *v_valve);
void valve_command(const ValveCommandRequest &msg) override;
protected:
bool try_send_valve_state_(valve::Valve *valve);
bool try_send_valve_info_(valve::Valve *valve);
public:
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
void send_media_player_info(media_player::MediaPlayer *media_player);
static bool try_send_media_player_state(APIConnection *api, void *v_media_player);
static bool try_send_media_player_info(APIConnection *api, void *v_media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override;
protected:
bool try_send_media_player_state_(media_player::MediaPlayer *media_player);
bool try_send_media_player_info_(media_player::MediaPlayer *media_player);
public:
#endif
bool try_send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
@@ -246,25 +327,37 @@ class APIConnection : public APIServerConnection {
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel);
static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
protected:
bool try_send_alarm_control_panel_state_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
bool try_send_alarm_control_panel_info_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
public:
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, std::string event_type);
void send_event_info(event::Event *event);
static bool try_send_event(APIConnection *api, void *v_event);
static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type);
static bool try_send_event_info(APIConnection *api, void *v_event);
protected:
bool try_send_event_(event::Event *event);
bool try_send_event_(event::Event *event, std::string event_type);
bool try_send_event_info_(event::Event *event);
public:
#endif
#ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update);
void send_update_info(update::UpdateEntity *update);
static bool try_send_update_state(APIConnection *api, void *v_update);
static bool try_send_update_info(APIConnection *api, void *v_update);
void update_command(const UpdateCommandRequest &msg) override;
protected:
bool try_send_update_state_(update::UpdateEntity *update);
bool try_send_update_info_(update::UpdateEntity *update);
public:
#endif
void on_disconnect_response(const DisconnectResponse &value) override;
@@ -315,9 +408,17 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
// FIXME: ensure no recursive writes can happen
this->proto_write_buffer_.clear();
this->proto_write_buffer_.reserve(reserve_size);
// Get header padding size - used for both reserve and insert
uint8_t header_padding = this->helper_->frame_header_padding();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position
this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0);
return {&this->proto_write_buffer_};
}
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
std::string get_client_combined_info() const { return this->client_combined_info_; }
@@ -325,6 +426,99 @@ class APIConnection : public APIServerConnection {
protected:
friend APIServer;
/**
* Generic send entity state method to reduce code duplication.
* Only attempts to build and send the message if the transmit buffer is available.
*
* This is the base version for entities that use their current state.
*
* @param entity The entity to send state for
* @param try_send_func The function that tries to send the state
* @return True on success or message deferred, false if subscription check failed
*/
bool send_state_(esphome::EntityBase *entity, send_message_t try_send_func) {
if (!this->state_subscription_)
return false;
if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
return true;
}
this->deferred_message_queue_.defer(entity, try_send_func);
return true;
}
/**
* Send entity state method that handles explicit state values.
* Only attempts to build and send the message if the transmit buffer is available.
*
* This method accepts a state parameter to be used instead of the entity's current state.
* It attempts to send the state with the provided value first, and if that fails due to buffer constraints,
* it defers the entity for later processing using the entity-only function.
*
* @tparam EntityT The entity type
* @tparam StateT Type of the state parameter
* @tparam Args Additional argument types (if any)
* @param entity The entity to send state for
* @param try_send_entity_func The function that tries to send the state with entity pointer only
* @param try_send_state_func The function that tries to send the state with entity and state parameters
* @param state The state value to send
* @param args Additional arguments to pass to the try_send_state_func
* @return True on success or message deferred, false if subscription check failed
*/
template<typename EntityT, typename StateT, typename... Args>
bool send_state_with_value_(EntityT *entity, bool (APIConnection::*try_send_entity_func)(EntityT *),
bool (APIConnection::*try_send_state_func)(EntityT *, StateT, Args...), StateT state,
Args... args) {
if (!this->state_subscription_)
return false;
if (this->try_to_clear_buffer(true) && (this->*try_send_state_func)(entity, state, args...)) {
return true;
}
this->deferred_message_queue_.defer(entity, reinterpret_cast<send_message_t>(try_send_entity_func));
return true;
}
/**
* Generic send entity info method to reduce code duplication.
* Only attempts to build and send the message if the transmit buffer is available.
*
* @param entity The entity to send info for
* @param try_send_func The function that tries to send the info
*/
void send_info_(esphome::EntityBase *entity, send_message_t try_send_func) {
if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
return;
}
this->deferred_message_queue_.defer(entity, try_send_func);
}
/**
* Generic function for generating entity info response messages.
* This is used to reduce duplication in the try_send_*_info functions.
*
* @param entity The entity to generate info for
* @param response The response object
* @param send_response_func Function pointer to send the response
* @return True if the message was sent successfully
*/
template<typename ResponseT>
bool try_send_entity_info_(esphome::EntityBase *entity, ResponseT &response,
bool (APIServerConnectionBase::*send_response_func)(const ResponseT &)) {
// Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id();
if (entity->has_own_name())
response.name = entity->get_name();
// Set common EntityBase properties
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
// Send the response using the provided send method
return (this->*send_response_func)(response);
}
bool send_(const void *buf, size_t len, bool force);
enum class ConnectionState {

View File

@@ -493,9 +493,12 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
std::vector<uint8_t> data;
data.resize(reason.length() + 1);
data[0] = 0x01; // failure
for (size_t i = 0; i < reason.length(); i++) {
data[i + 1] = (uint8_t) reason[i];
// Copy error message in bulk
if (!reason.empty()) {
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
}
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
@@ -557,7 +560,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK;
}
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
int err;
APIError aerr;
aerr = state_action_();
@@ -569,31 +572,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
// Message data starts after padding
size_t payload_len = raw_buffer->size() - frame_header_padding_;
size_t padding = 0;
size_t msg_len = 4 + payload_len + padding;
size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
if (tmpbuf == nullptr) {
HELPER_LOG("Could not allocate for writing packet");
return APIError::OUT_OF_MEMORY;
}
tmpbuf[0] = 0x01; // indicator
// tmpbuf[1], tmpbuf[2] to be set later
// We need to resize to include MAC space, but we already reserved it in create_buffer
raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
// Write the noise header in the padded area
// Buffer layout:
// [0] - 0x01 indicator byte
// [1-2] - Size of encrypted payload (filled after encryption)
// [3-4] - Message type (encrypted)
// [5-6] - Payload length (encrypted)
// [7...] - Actual payload data (encrypted)
uint8_t *buf_start = raw_buffer->data();
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set later after encryption
const uint8_t msg_offset = 3;
const uint8_t payload_offset = msg_offset + 4;
tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type
tmpbuf[msg_offset + 1] = (uint8_t) type;
tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len
tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
// copy data
std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
// fill padding with zeros
std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
// payload data is already in the buffer starting at position 7
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
// The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption
noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) {
state_ = State::FAILED;
@@ -602,11 +610,13 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
}
size_t total_len = 3 + mbuf.size;
tmpbuf[1] = (uint8_t) (mbuf.size >> 8);
tmpbuf[2] = (uint8_t) mbuf.size;
buf_start[1] = (uint8_t) (mbuf.size >> 8);
buf_start[2] = (uint8_t) mbuf.size;
struct iovec iov;
iov.iov_base = &tmpbuf[0];
// Point iov_base to the beginning of the buffer (no unused padding in Noise)
// We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC)
iov.iov_base = buf_start;
iov.iov_len = total_len;
// write raw to not have two packets sent if NAGLE disabled
@@ -718,6 +728,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
return APIError::HANDSHAKESTATE_SPLIT_FAILED;
}
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
@@ -830,6 +842,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// read header
while (!rx_header_parsed_) {
uint8_t data;
// Reading one byte at a time is fastest in practice for ESP32 when
// there is no data on the wire (which is the common case).
// This results in faster failure detection compared to
// attempting to read multiple bytes at once.
ssize_t received = socket_->read(&data, 1);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
@@ -843,27 +859,60 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
}
rx_header_buf_.push_back(data);
// try parse header
if (rx_header_buf_[0] != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
// Successfully read a byte
// Process byte according to current buffer position
if (rx_header_buf_pos_ == 0) { // Case 1: First byte (indicator byte)
if (data != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", data);
return APIError::BAD_INDICATOR;
}
// We don't store the indicator byte, just increment position
rx_header_buf_pos_ = 1; // Set to 1 directly
continue; // Need more bytes before we can parse
}
size_t i = 1;
// Check buffer overflow before storing
if (rx_header_buf_pos_ == 5) { // Case 2: Buffer would overflow (5 bytes is max allowed)
state_ = State::FAILED;
HELPER_LOG("Header buffer overflow");
return APIError::BAD_DATA_PACKET;
}
// Store byte in buffer (adjust index to account for skipped indicator byte)
rx_header_buf_[rx_header_buf_pos_ - 1] = data;
// Increment position after storing
rx_header_buf_pos_++;
// Case 3: If we only have one varint byte, we need more
if (rx_header_buf_pos_ == 2) { // Have read indicator + 1 byte
continue; // Need more bytes before we can parse
}
// At this point, we have at least 3 bytes total:
// - Validated indicator byte (0x00) but not stored
// - At least 2 bytes in the buffer for the varints
// Buffer layout:
// First 1-3 bytes: Message size varint (variable length)
// - 2 bytes would only allow up to 16383, which is less than noise's 65535
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
// Remaining 1-2 bytes: Message type varint (variable length)
// We now attempt to parse both varints. If either is incomplete,
// we'll continue reading more bytes.
uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
i += consumed;
rx_header_parsed_len_ = msg_size_varint->as_uint32();
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
@@ -909,7 +958,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_.clear();
rx_header_buf_pos_ = 0;
rx_header_parsed_ = false;
return APIError::OK;
}
@@ -953,28 +1002,66 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK;
}
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
std::vector<uint8_t> header;
header.reserve(1 + api::ProtoSize::varint(static_cast<uint32_t>(payload_len)) +
api::ProtoSize::varint(static_cast<uint32_t>(type)));
header.push_back(0x00);
ProtoVarInt(payload_len).encode(header);
ProtoVarInt(type).encode(header);
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
// Message data starts after padding (frame_header_padding_ = 6)
size_t payload_len = raw_buffer->size() - frame_header_padding_;
struct iovec iov[2];
iov[0].iov_base = &header[0];
iov[0].iov_len = header.size();
if (payload_len == 0) {
return write_raw_(iov, 1);
// Calculate varint sizes for header components
size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
size_t total_header_len = 1 + size_varint_len + type_varint_len;
if (total_header_len > frame_header_padding_) {
// Header is too large to fit in the padding
return APIError::BAD_ARG;
}
iov[1].iov_base = const_cast<uint8_t *>(payload);
iov[1].iov_len = payload_len;
return write_raw_(iov, 2);
// Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [6...] - Actual payload data
uint8_t *buf_start = raw_buffer->data();
size_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode size varint directly into buffer
ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
// Encode type varint directly into buffer
ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
struct iovec iov;
// Point iov_base to the beginning of our header (skip unused padding)
// This ensures we only send the actual header and payload, not the empty padding bytes
iov.iov_base = buf_start + header_offset;
iov.iov_len = total_header_len + payload_len;
return write_raw_(&iov, 1);
}
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
// try send from tx_buf

View File

@@ -16,6 +16,8 @@
namespace esphome {
namespace api {
class ProtoWriteBuffer;
struct ReadPacketBuffer {
std::vector<uint8_t> container;
uint16_t type;
@@ -65,32 +67,46 @@ class APIFrameHelper {
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
virtual bool can_write_without_blocking() = 0;
virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
virtual std::string getpeername() = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual APIError close() = 0;
virtual APIError shutdown(int how) = 0;
// Give this helper a name for logging
virtual void set_log_info(std::string info) = 0;
// Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol
virtual uint8_t frame_footer_size() = 0;
protected:
// Common implementation for writing raw data to socket
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
};
#ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {}
: socket_(std::move(socket)), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
~APINoiseFrameHelper() override;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen);
@@ -99,6 +115,10 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
struct ParsedFrame {
@@ -119,6 +139,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
std::unique_ptr<socket::Socket> socket_;
std::string info_;
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_;
@@ -149,13 +172,20 @@ class APINoiseFrameHelper : public APIFrameHelper {
#ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
~APIPlaintextFrameHelper() override = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen);
@@ -164,6 +194,10 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
struct ParsedFrame {
@@ -179,7 +213,16 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
std::unique_ptr<socket::Socket> socket_;
std::string info_;
std::vector<uint8_t> rx_header_buf_;
// Fixed-size header buffer for plaintext protocol:
// We only need space for the two varints since we validate the indicator byte separately.
// To match noise protocol's maximum message size (65535), we need:
// 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// While varints could theoretically be up to 10 bytes each for 64-bit values,
// attempting to process messages with headers that large would likely crash the
// ESP32 due to memory constraints.
uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
uint32_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0;

View File

@@ -20,16 +20,26 @@ class ProtoVarInt {
explicit ProtoVarInt(uint64_t value) : value_(value) {}
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
if (consumed != nullptr)
*consumed = 0;
if (len == 0)
if (len == 0) {
if (consumed != nullptr)
*consumed = 0;
return {};
}
uint64_t result = 0;
uint8_t bitpos = 0;
// Most common case: single-byte varint (values 0-127)
if ((buffer[0] & 0x80) == 0) {
if (consumed != nullptr)
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
for (uint32_t i = 0; i < len; i++) {
// General case for multi-byte varints
// Since we know buffer[0]'s high bit is set, initialize with its value
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < len; i++) {
uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
@@ -40,7 +50,9 @@ class ProtoVarInt {
}
}
return {};
if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint
}
uint32_t as_uint32() const { return this->value_; }
@@ -71,6 +83,34 @@ class ProtoVarInt {
return static_cast<int64_t>(this->value_ >> 1);
}
}
/**
* Encode the varint value to a pre-allocated buffer without bounds checking.
*
* @param buffer The pre-allocated buffer to write the encoded varint to
* @param len The size of the buffer in bytes
*
* @note The caller is responsible for ensuring the buffer is large enough
* to hold the encoded value. Use ProtoSize::varint() to calculate
* the exact size needed before calling this method.
* @note No bounds checking is performed for performance reasons.
*/
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
uint64_t val = this->value_;
if (val <= 0x7F) {
buffer[0] = val;
return;
}
size_t i = 0;
while (val && i < len) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
buffer[i++] = temp | 0x80;
} else {
buffer[i++] = temp;
}
}
}
void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_;
if (val <= 0x7F) {

View File

@@ -14,11 +14,8 @@ namespace esphome {
namespace at581x {
class AT581XComponent : public Component, public i2c::I2CDevice {
#ifdef USE_SWITCH
protected:
switch_::Switch *rf_power_switch_{nullptr};
public:
#ifdef USE_SWITCH
void set_rf_power_switch(switch_::Switch *s) {
this->rf_power_switch_ = s;
s->turn_on();
@@ -48,6 +45,9 @@ class AT581XComponent : public Component, public i2c::I2CDevice {
bool i2c_read_reg(uint8_t addr, uint8_t &data);
protected:
#ifdef USE_SWITCH
switch_::Switch *rf_power_switch_{nullptr};
#endif
int freq_;
int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */
int protect_time_ms_; /*!< Protection time, recommended 1000 ms */

View File

@@ -1,7 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@bazuchan"]
@@ -9,13 +7,8 @@ CODEOWNERS = ["@bazuchan"]
ballu_ns = cg.esphome_ns.namespace("ballu")
BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BalluClimate),
}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -9,7 +9,6 @@ from esphome.const import (
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
CONF_HEAT_ACTION,
CONF_HUMIDITY_SENSOR,
CONF_ID,
CONF_IDLE_ACTION,
CONF_SENSOR,
)
@@ -19,9 +18,9 @@ BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Com
BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig")
CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend(
climate.climate_schema(BangBangClimate)
.extend(
{
cv.GenerateID(): cv.declare_id(BangBangClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
@@ -36,15 +35,15 @@ CONFIG_SCHEMA = cv.All(
}
),
}
).extend(cv.COMPONENT_SCHEMA),
)
.extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
var = await climate.new_climate(config)
await cg.register_component(var, config)
await climate.register_climate(var, config)
sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens))

View File

@@ -3,6 +3,7 @@
#include "bedjet_hub.h"
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/core/application.h"
#include <cinttypes>
namespace esphome {

View File

@@ -1,11 +1,8 @@
import logging
import esphome.codegen as cg
from esphome.components import ble_client, climate
import esphome.config_validation as cv
from esphome.const import (
CONF_HEAT_MODE,
CONF_ID,
CONF_RECEIVE_TIMEOUT,
CONF_TEMPERATURE_SOURCE,
CONF_TIME_ID,
@@ -13,7 +10,6 @@ from esphome.const import (
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jhansche"]
DEPENDENCIES = ["bedjet"]
@@ -30,9 +26,9 @@ BEDJET_TEMPERATURE_SOURCES = {
}
CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
climate.climate_schema(BedJetClimate)
.extend(
{
cv.GenerateID(): cv.declare_id(BedJetClimate),
cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
BEDJET_HEAT_MODES, lower=True
),
@@ -63,9 +59,8 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
var = await climate.new_climate(config)
await cg.register_component(var, config)
await climate.register_climate(var, config)
await register_bedjet_child(var, config)
cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))

View File

@@ -1,31 +1,22 @@
import logging
import esphome.codegen as cg
from esphome.components import fan
import esphome.config_validation as cv
from esphome.const import CONF_ID
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jhansche"]
DEPENDENCIES = ["bedjet"]
BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent)
CONFIG_SCHEMA = (
fan.FAN_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BedJetFan),
}
)
fan.fan_schema(BedJetFan)
.extend(cv.polling_component_schema("60s"))
.extend(BEDJET_CLIENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
var = await fan.new_fan(config)
await cg.register_component(var, config)
await fan.register_fan(var, config)
await register_bedjet_child(var, config)

View File

@@ -1,31 +1,28 @@
import esphome.codegen as cg
from esphome.components import fan, output
import esphome.config_validation as cv
from esphome.const import (
CONF_DIRECTION_OUTPUT,
CONF_OSCILLATION_OUTPUT,
CONF_OUTPUT,
CONF_OUTPUT_ID,
)
from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT
from .. import binary_ns
BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan),
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
}
).extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = (
fan.fan_schema(BinaryFan)
.extend(
{
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
var = await fan.new_fan(config)
await cg.register_component(var, config)
await fan.register_fan(var, config)
output_ = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(output_))

View File

@@ -15,17 +15,21 @@ void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state);
this->send_state_internal(state, false);
} else {
this->filter_list_->input(state);
this->filter_list_->input(state, false);
}
}
void BinarySensor::publish_initial_state(bool state) {
this->has_state_ = false;
this->publish_state(state);
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
}
}
void BinarySensor::send_state_internal(bool state) {
bool is_initial = !this->has_state_;
void BinarySensor::send_state_internal(bool state, bool is_initial) {
if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else {

View File

@@ -67,7 +67,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void send_state_internal(bool state);
void send_state_internal(bool state, bool is_initial);
/// Return whether this binary sensor has outputted a state.
virtual bool has_state() const;

View File

@@ -9,37 +9,37 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter";
void Filter::output(bool value) {
void Filter::output(bool value, bool is_initial) {
if (!this->dedup_.next(value))
return;
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value);
this->parent_->send_state_internal(value, is_initial);
} else {
this->next_->input(value);
this->next_->input(value, is_initial);
}
}
void Filter::input(bool value) {
auto b = this->new_value(value);
void Filter::input(bool value, bool is_initial) {
auto b = this->new_value(value, is_initial);
if (b.has_value()) {
this->output(*b);
this->output(*b, is_initial);
}
}
optional<bool> DelayedOnOffFilter::new_value(bool value) {
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
} else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
}
return {};
}
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value) {
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
if (value) {
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
return {};
} else {
this->cancel_timeout("ON");
@@ -49,9 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value) {
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
return {};
} else {
this->cancel_timeout("OFF");
@@ -61,11 +61,11 @@ optional<bool> DelayedOffFilter::new_value(bool value) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value) { return !value; }
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value) {
optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
if (value) {
// Ignore if already running
if (this->active_timing_ != 0)
@@ -101,7 +101,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val);
this->output(val, false); // This is at least the second one so not initial
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
}
@@ -109,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value) {
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->steady_ = true;
this->output(value);
this->output(value, is_initial);
});
return {};
} else {
this->steady_ = false;
this->output(value);
this->output(value, is_initial);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value;
}

View File

@@ -14,11 +14,11 @@ class BinarySensor;
class Filter {
public:
virtual optional<bool> new_value(bool value) = 0;
virtual optional<bool> new_value(bool value, bool is_initial) = 0;
void input(bool value);
void input(bool value, bool is_initial);
void output(bool value);
void output(bool value, bool is_initial);
protected:
friend BinarySensor;
@@ -30,7 +30,7 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
@@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
@@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
@@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
};
struct AutorepeatFilterTiming {
@@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
@@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
protected:
std::function<optional<bool>(bool)> f_;
@@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;

View File

@@ -2,6 +2,7 @@
#include "esphome/core/log.h"
#include "esphome/core/macros.h"
#include "esphome/core/application.h"
#ifdef USE_ESP32
@@ -51,14 +52,19 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
return true;
}
// Static buffer to store advertisements between batches
static constexpr size_t MAX_BATCH_SIZE = 8;
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
static constexpr size_t FLUSH_BATCH_SIZE = 8;
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
return batch_buffer;
}
bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
return false;
// Get the batch buffer reference
auto &batch_buffer = get_batch_buffer();
// Reserve additional capacity if needed
size_t new_size = batch_buffer.size() + count;
if (batch_buffer.capacity() < new_size) {
@@ -83,7 +89,7 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
// Only send if we've accumulated a good batch size to maximize batching efficiency
// https://github.com/esphome/backlog/issues/21
if (batch_buffer.size() >= MAX_BATCH_SIZE) {
if (batch_buffer.size() >= FLUSH_BATCH_SIZE) {
this->flush_pending_advertisements();
}
@@ -91,6 +97,7 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
}
void BluetoothProxy::flush_pending_advertisements() {
auto &batch_buffer = get_batch_buffer();
if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return;
@@ -171,7 +178,7 @@ void BluetoothProxy::loop() {
// Flush any pending BLE advertisements that have been accumulated but not yet sent
if (this->raw_advertisements_) {
static uint32_t last_flush_time = 0;
uint32_t now = millis();
uint32_t now = App.get_loop_component_start_time();
// Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) {

View File

@@ -32,14 +32,14 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CCS811Component),
cv.Required(CONF_ECO2): sensor.sensor_schema(
cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
@@ -64,10 +64,13 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sens = await sensor.new_sensor(config[CONF_ECO2])
cg.add(var.set_co2(sens))
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
if eco2_config := config.get(CONF_ECO2):
sens = await sensor.new_sensor(eco2_config)
cg.add(var.set_co2(sens))
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc(sens))
if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config)

View File

@@ -1,7 +1,13 @@
import logging
from esphome import core
import esphome.codegen as cg
from esphome.components import climate, remote_base, sensor
import esphome.config_validation as cv
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
from esphome.cpp_generator import MockObjClass
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["remote_transmitter"]
AUTO_LOAD = ["sensor", "remote_base"]
@@ -16,30 +22,58 @@ ClimateIR = climate_ir_ns.class_(
remote_base.RemoteTransmittable,
)
CLIMATE_IR_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
def climate_ir_schema(
class_: MockObjClass,
) -> cv.Schema:
return (
climate.climate_schema(class_)
.extend(
{
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
)
def climate_ir_with_receiver_schema(
class_: MockObjClass,
) -> cv.Schema:
return climate_ir_schema(class_).extend(
{
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
remote_base.RemoteReceiverBase
),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
)
CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
{
cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
remote_base.RemoteReceiverBase
),
}
)
# Remove before 2025.11.0
def deprecated_schema_constant(config):
type: str = "unknown"
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
type = str(id.type).split("::", maxsplit=1)[0]
_LOGGER.warning(
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s",
type,
)
return config
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
async def register_climate_ir(var, config):
await cg.register_component(var, config)
await climate.register_climate(var, config)
await remote_base.register_transmittable(var, config)
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
@@ -48,3 +82,9 @@ async def register_climate_ir(var, config):
if sensor_id := config.get(CONF_SENSOR):
sens = await cg.get_variable(sensor_id)
cg.add(var.set_sensor(sens))
async def new_climate_ir(config, *args):
var = await climate.new_climate(config, *args)
await register_climate_ir(var, config)
return var

View File

@@ -1,7 +1,6 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
@@ -14,9 +13,8 @@ CONF_BIT_HIGH = "bit_high"
CONF_BIT_ONE_LOW = "bit_one_low"
CONF_BIT_ZERO_LOW = "bit_zero_low"
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
{
cv.GenerateID(): cv.declare_id(LgIrClimate),
cv.Optional(
CONF_HEADER_HIGH, default="8000us"
): cv.positive_time_period_microseconds,
@@ -37,8 +35,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_header_high(config[CONF_HEADER_HIGH]))
cg.add(var.set_header_low(config[CONF_HEADER_LOW]))

View File

@@ -1,7 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@glmnet"]
@@ -9,13 +7,8 @@ CODEOWNERS = ["@glmnet"]
coolix_ns = cg.esphome_ns.namespace("coolix")
CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(CoolixClimate),
}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import fan
import esphome.config_validation as cv
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
from esphome.core.entity_helpers import inherit_property_from
from .. import copy_ns
@@ -9,12 +9,15 @@ from .. import copy_ns
CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(CopyFan),
cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan),
}
).extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = (
fan.fan_schema(CopyFan)
.extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
@@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await fan.register_fan(var, config)
var = await fan.new_fan(config)
await cg.register_component(var, config)
source = await cg.get_variable(config[CONF_SOURCE_ID])

View File

@@ -1,5 +1,6 @@
#include "cse7766.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace cse7766 {
@@ -7,7 +8,7 @@ namespace cse7766 {
static const char *const TAG = "cse7766";
void CSE7766Component::loop() {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index.
this->raw_data_index_ = 0;

View File

@@ -1,6 +1,7 @@
#include "current_based_cover.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cfloat>
namespace esphome {
@@ -60,7 +61,7 @@ void CurrentBasedCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
if (this->current_operation == COVER_OPERATION_OPENING) {
if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction

View File

@@ -1,20 +1,13 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
daikin_ns = cg.esphome_ns.namespace("daikin")
DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DaikinClimate),
}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -1,18 +1,13 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc")
DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(DaikinArcClimate)}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinArcClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT
from esphome.const import CONF_USE_FAHRENHEIT
AUTO_LOAD = ["climate_ir"]
@@ -9,15 +9,13 @@ daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc")
DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinBrcClimate).extend(
{
cv.GenerateID(): cv.declare_id(DaikinBrcClimate),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))

View File

@@ -1,6 +1,7 @@
#include "daly_bms.h"
#include <vector>
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace daly_bms {
@@ -32,7 +33,7 @@ void DalyBmsComponent::update() {
}
void DalyBmsComponent::loop() {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
if (this->receiving_ && (now - this->last_transmission_ >= 200)) {
// last transmission too long ago. Reset RX index.
ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index.");

View File

@@ -15,10 +15,6 @@ namespace debug {
static const char *const TAG = "debug";
void DebugComponent::dump_config() {
#ifndef ESPHOME_LOG_HAS_DEBUG
return; // Can't log below if debug logging is disabled
#endif
ESP_LOGCONFIG(TAG, "Debug component:");
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Device info", this->device_info_);
@@ -70,7 +66,7 @@ void DebugComponent::loop() {
#ifdef USE_SENSOR
// calculate loop time - from last call to this one
if (this->loop_time_sensor_ != nullptr) {
uint32_t now = millis();
uint32_t now = App.get_loop_component_start_time();
uint32_t loop_time = now - this->last_loop_timetag_;
this->max_loop_time_ = std::max(this->max_loop_time_, loop_time);
this->last_loop_timetag_ = now;

View File

@@ -34,13 +34,15 @@ class DebugComponent : public PollingComponent {
#endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32
void on_shutdown() override;
void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; }
#endif // USE_ESP32
void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) {
this->cpu_frequency_sensor_ = cpu_frequency_sensor;
}
#endif // USE_SENSOR
#ifdef USE_ESP32
void on_shutdown() override;
#endif // USE_ESP32
protected:
uint32_t free_heap_{};

View File

@@ -1,20 +1,13 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
delonghi_ns = cg.esphome_ns.namespace("delonghi")
DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(DelonghiClimate),
}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DelonghiClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -27,14 +27,14 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DPS310Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_PRESSURE): sensor.sensor_schema(
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
icon=ICON_GAUGE,
accuracy_decimals=1,
@@ -53,10 +53,10 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens))

View File

@@ -26,19 +26,19 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(EE895Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
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,
),
cv.Required(CONF_CO2): sensor.sensor_schema(
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_PRESSURE): sensor.sensor_schema(
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
@@ -56,14 +56,14 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_CO2 in config:
sens = await sensor.new_sensor(config[CONF_CO2])
if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens))

View File

@@ -1,7 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@E440QF"]
AUTO_LOAD = ["climate_ir"]
@@ -9,13 +7,8 @@ AUTO_LOAD = ["climate_ir"]
emmeti_ns = cg.esphome_ns.namespace("emmeti")
EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(EmmetiClimate),
}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(EmmetiClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -1,6 +1,7 @@
#include "endstop_cover.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
namespace esphome {
namespace endstop {
@@ -65,7 +66,7 @@ void EndstopCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
if (this->current_operation == COVER_OPERATION_OPENING && this->is_open_()) {
float dur = (now - this->start_dir_time_) / 1e3f;

View File

@@ -28,21 +28,21 @@ UNIT_INDEX = "index"
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.Required(CONF_ECO2): sensor.sensor_schema(
cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_AQI): sensor.sensor_schema(
cv.Optional(CONF_AQI): sensor.sensor_schema(
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
@@ -62,12 +62,15 @@ async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
sens = await sensor.new_sensor(config[CONF_ECO2])
cg.add(var.set_co2(sens))
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
sens = await sensor.new_sensor(config[CONF_AQI])
cg.add(var.set_aqi(sens))
if eco2_config := config.get(CONF_ECO2):
sens = await sensor.new_sensor(eco2_config)
cg.add(var.set_co2(sens))
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc(sens))
if aqi_config := config.get(CONF_AQI):
sens = await sensor.new_sensor(aqi_config)
cg.add(var.set_aqi(sens))
if compensation_config := config.get(CONF_COMPENSATION):
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE])

View File

@@ -15,8 +15,9 @@
#ifdef USE_ARDUINO
#include <Esp.h>
#else
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_clk_tree.h>
#endif
void setup();
void loop();
#endif
@@ -63,7 +64,13 @@ uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
#ifdef USE_ESP_IDF
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
#else
rtc_cpu_freq_config_t config;
rtc_clk_cpu_freq_get_config(&config);
freq = config.freq_mhz * 1000000U;
#endif
#elif defined(USE_ARDUINO)
freq = ESP.getCpuFreqMHz() * 1000000;
#endif

View File

@@ -2,42 +2,66 @@
#include "gpio.h"
#include "esphome/core/log.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#include "hal/gpio_hal.h"
#include "soc/soc_caps.h"
#include "soc/gpio_periph.h"
#include <cinttypes>
#if (SOC_RTCIO_PIN_COUNT > 0)
#include "hal/rtc_io_hal.h"
#endif
#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT
#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT
#endif
namespace esphome {
namespace esp32 {
static const char *const TAG = "esp32";
static const gpio_hal_context_t GPIO_HAL = {.dev = GPIO_HAL_GET_HW(GPIO_PORT_0)};
bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) {
static gpio_mode_t flags_to_mode(gpio::Flags flags) {
flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN));
if (flags == gpio::FLAG_INPUT) {
if (flags == gpio::FLAG_INPUT)
return GPIO_MODE_INPUT;
} else if (flags == gpio::FLAG_OUTPUT) {
if (flags == gpio::FLAG_OUTPUT)
return GPIO_MODE_OUTPUT;
} else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) {
if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN))
return GPIO_MODE_OUTPUT_OD;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) {
if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN))
return GPIO_MODE_INPUT_OUTPUT_OD;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) {
if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT))
return GPIO_MODE_INPUT_OUTPUT;
} else {
// unsupported or gpio::FLAG_NONE
return GPIO_MODE_DISABLE;
}
// unsupported or gpio::FLAG_NONE
return GPIO_MODE_DISABLE;
}
struct ISRPinArg {
gpio_num_t pin;
gpio::Flags flags;
bool inverted;
#if defined(USE_ESP32_VARIANT_ESP32)
bool use_rtc;
int rtc_pin;
#endif
};
ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_;
arg->pin = this->pin_;
arg->flags = gpio::FLAG_NONE;
arg->inverted = inverted_;
#if defined(USE_ESP32_VARIANT_ESP32)
arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_);
if (arg->use_rtc)
arg->rtc_pin = rtc_io_number_get(this->pin_);
#endif
return ISRInternalGPIOPin((void *) arg);
}
@@ -90,6 +114,7 @@ void ESP32InternalGPIOPin::setup() {
if (flags_ & gpio::FLAG_OUTPUT) {
gpio_set_drive_capability(pin_, drive_strength_);
}
ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT);
}
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
@@ -115,28 +140,65 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); }
using namespace esp32;
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
return bool(gpio_get_level(arg->pin)) != arg->inverted;
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted;
}
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
gpio_set_level(arg->pin, value != arg->inverted ? 1 : 0);
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted);
}
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
// not supported
}
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
gpio_set_direction(arg->pin, flags_to_mode(flags));
gpio_pull_mode_t pull_mode = GPIO_FLOATING;
if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) {
pull_mode = GPIO_PULLUP_PULLDOWN;
} else if (flags & gpio::FLAG_PULLUP) {
pull_mode = GPIO_PULLUP_ONLY;
} else if (flags & gpio::FLAG_PULLDOWN) {
pull_mode = GPIO_PULLDOWN_ONLY;
gpio::Flags diff = (gpio::Flags)(flags ^ arg->flags);
if (diff & gpio::FLAG_OUTPUT) {
if (flags & gpio::FLAG_OUTPUT) {
gpio_hal_output_enable(&GPIO_HAL, arg->pin);
if (flags & gpio::FLAG_OPEN_DRAIN)
gpio_hal_od_enable(&GPIO_HAL, arg->pin);
} else {
gpio_hal_output_disable(&GPIO_HAL, arg->pin);
}
}
gpio_set_pull_mode(arg->pin, pull_mode);
if (diff & gpio::FLAG_INPUT) {
if (flags & gpio::FLAG_INPUT) {
gpio_hal_input_enable(&GPIO_HAL, arg->pin);
#if defined(USE_ESP32_VARIANT_ESP32)
if (arg->use_rtc) {
if (flags & gpio::FLAG_PULLUP) {
rtcio_hal_pullup_enable(arg->rtc_pin);
} else {
rtcio_hal_pullup_disable(arg->rtc_pin);
}
if (flags & gpio::FLAG_PULLDOWN) {
rtcio_hal_pulldown_enable(arg->rtc_pin);
} else {
rtcio_hal_pulldown_disable(arg->rtc_pin);
}
} else
#endif
{
if (flags & gpio::FLAG_PULLUP) {
gpio_hal_pullup_en(&GPIO_HAL, arg->pin);
} else {
gpio_hal_pullup_dis(&GPIO_HAL, arg->pin);
}
if (flags & gpio::FLAG_PULLDOWN) {
gpio_hal_pulldown_en(&GPIO_HAL, arg->pin);
} else {
gpio_hal_pulldown_dis(&GPIO_HAL, arg->pin);
}
}
} else {
gpio_hal_input_disable(&GPIO_HAL, arg->pin);
}
}
arg->flags = flags;
}
} // namespace esphome

View File

@@ -6,6 +6,7 @@
#include <cstring>
#include "ble_uuid.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace esp32_ble {
@@ -143,7 +144,7 @@ void BLEAdvertising::loop() {
if (this->raw_advertisements_callbacks_.empty()) {
return;
}
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
this->stop();
this->current_adv_index_ += 1;

View File

@@ -296,7 +296,7 @@ async def to_code(config):
add_idf_component(
name="esp32-camera",
repo="https://github.com/espressif/esp32-camera.git",
ref="v2.0.9",
ref="v2.0.15",
)
for conf in config.get(CONF_ON_STREAM_START, []):

View File

@@ -3,6 +3,7 @@
#include "esp32_camera.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include <freertos/task.h>
@@ -54,11 +55,7 @@ void ESP32Camera::dump_config() {
ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href);
ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk);
ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz);
#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated
ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sscb_sda, conf.pin_sscb_scl);
#else
ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl);
#endif
ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset);
switch (this->config_.frame_size) {
case FRAMESIZE_QQVGA:
@@ -162,7 +159,7 @@ void ESP32Camera::loop() {
}
// request idle image every idle_update_interval
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now;
this->request_image(IDLE);
@@ -238,13 +235,8 @@ void ESP32Camera::set_external_clock(uint8_t pin, uint32_t frequency) {
this->config_.xclk_freq_hz = frequency;
}
void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) {
#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated
this->config_.pin_sscb_sda = sda;
this->config_.pin_sscb_scl = scl;
#else
this->config_.pin_sccb_sda = sda;
this->config_.pin_sccb_scl = scl;
#endif
}
void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; }
void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; }

View File

@@ -106,7 +106,7 @@ class CameraImageReader {
};
/* ---------------- ESP32Camera class ---------------- */
class ESP32Camera : public Component, public EntityBase {
class ESP32Camera : public EntityBase, public Component {
public:
ESP32Camera();

View File

@@ -92,7 +92,7 @@ void ESP32ImprovComponent::loop() {
if (!this->incoming_data_.empty())
this->process_incoming_data_();
uint32_t now = millis();
uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case improv::STATE_STOPPED:

View File

@@ -288,7 +288,7 @@ uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) {
}
void ESP32TouchComponent::loop() {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250;
for (auto *child : this->children_) {
child->value_ = this->component_touch_pad_read(child->get_touch_pad());

View File

@@ -8,7 +8,7 @@ namespace esp8266 {
static const char *const TAG = "esp8266";
static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) {
static int flags_to_mode(gpio::Flags flags, uint8_t pin) {
if (flags == gpio::FLAG_INPUT) { // NOLINT(bugprone-branch-clone)
return INPUT;
} else if (flags == gpio::FLAG_OUTPUT) {
@@ -34,12 +34,39 @@ static int IRAM_ATTR flags_to_mode(gpio::Flags flags, uint8_t pin) {
struct ISRPinArg {
uint8_t pin;
bool inverted;
volatile uint32_t *in_reg;
volatile uint32_t *out_set_reg;
volatile uint32_t *out_clr_reg;
volatile uint32_t *mode_set_reg;
volatile uint32_t *mode_clr_reg;
volatile uint32_t *func_reg;
volatile uint32_t *control_reg;
uint32_t mask;
};
ISRInternalGPIOPin ESP8266GPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_;
arg->inverted = inverted_;
arg->pin = this->pin_;
arg->inverted = this->inverted_;
if (this->pin_ < 16) {
arg->in_reg = &GPI;
arg->out_set_reg = &GPOS;
arg->out_clr_reg = &GPOC;
arg->mode_set_reg = &GPES;
arg->mode_clr_reg = &GPEC;
arg->func_reg = &GPF(this->pin_);
arg->control_reg = &GPC(this->pin_);
arg->mask = 1 << this->pin_;
} else {
arg->in_reg = &GP16I;
arg->out_set_reg = &GP16O;
arg->out_clr_reg = nullptr;
arg->mode_set_reg = &GP16E;
arg->mode_clr_reg = nullptr;
arg->func_reg = &GPF16;
arg->control_reg = nullptr;
arg->mask = 1;
}
return ISRInternalGPIOPin((void *) arg);
}
@@ -88,20 +115,63 @@ void ESP8266GPIOPin::detach_interrupt() const { detachInterrupt(pin_); }
using namespace esp8266;
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
return bool(digitalRead(arg->pin)) != arg->inverted; // NOLINT
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
return bool(*arg->in_reg & arg->mask) != arg->inverted;
}
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
digitalWrite(arg->pin, value != arg->inverted ? 1 : 0); // NOLINT
if (arg->pin < 16) {
if (value != arg->inverted) {
*arg->out_set_reg = arg->mask;
} else {
*arg->out_clr_reg = arg->mask;
}
} else {
if (value != arg->inverted) {
*arg->out_set_reg |= 1;
} else {
*arg->out_set_reg &= ~1;
}
}
}
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin);
}
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
pinMode(arg->pin, flags_to_mode(flags, arg->pin)); // NOLINT
auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
if (arg->pin < 16) {
if (flags & gpio::FLAG_OUTPUT) {
*arg->mode_set_reg = arg->mask;
if (flags & gpio::FLAG_OPEN_DRAIN) {
*arg->control_reg |= 1 << GPCD;
} else {
*arg->control_reg &= ~(1 << GPCD);
}
} else if (flags & gpio::FLAG_INPUT) {
*arg->mode_clr_reg = arg->mask;
}
if (flags & gpio::FLAG_PULLUP) {
*arg->func_reg |= 1 << GPFPU;
*arg->control_reg |= 1 << GPCD;
} else {
*arg->func_reg &= ~(1 << GPFPU);
}
} else {
if (flags & gpio::FLAG_OUTPUT) {
*arg->mode_set_reg |= 1;
} else {
*arg->mode_set_reg &= ~1;
}
if (flags & gpio::FLAG_PULLDOWN) {
*arg->func_reg |= 1 << GP16FPD;
} else {
*arg->func_reg &= ~(1 << GP16FPD);
}
}
}
} // namespace esphome

View File

@@ -240,7 +240,7 @@ void EthernetComponent::setup() {
}
void EthernetComponent::loop() {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case EthernetComponentState::STOPPED:

View File

@@ -1,6 +1,7 @@
#include "feedback_cover.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace feedback {
@@ -220,7 +221,7 @@ void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_o
void FeedbackCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
// Recompute position every loop cycle
this->recompute_position_();

View File

@@ -1,7 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
@@ -10,13 +8,8 @@ FujitsuGeneralClimate = fujitsu_general_ns.class_(
"FujitsuGeneralClimate", climate_ir.ClimateIR
)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate),
}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(FujitsuGeneralClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -6,6 +6,7 @@
*/
#include "gcja5.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cstring>
namespace esphome {
@@ -16,7 +17,7 @@ static const char *const TAG = "gcja5";
void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); }
void GCJA5Component::loop() {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index.
this->rx_message_.clear();

View File

@@ -9,23 +9,32 @@ from esphome.const import (
CONF_LONGITUDE,
CONF_SATELLITES,
CONF_SPEED,
DEVICE_CLASS_SPEED,
STATE_CLASS_MEASUREMENT,
UNIT_DEGREES,
UNIT_KILOMETER_PER_HOUR,
UNIT_METER,
)
CONF_GPS_ID = "gps_id"
CONF_HDOP = "hdop"
ICON_ALTIMETER = "mdi:altimeter"
ICON_COMPASS = "mdi:compass"
ICON_LATITUDE = "mdi:latitude"
ICON_LONGITUDE = "mdi:longitude"
ICON_SATELLITE = "mdi:satellite-variant"
ICON_SPEEDOMETER = "mdi:speedometer"
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["sensor"]
CODEOWNERS = ["@coogle"]
CODEOWNERS = ["@coogle", "@ximex"]
gps_ns = cg.esphome_ns.namespace("gps")
GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice)
GPSListener = gps_ns.class_("GPSListener")
CONF_GPS_ID = "gps_id"
CONF_HDOP = "hdop"
MULTI_CONF = True
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -33,25 +42,37 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(GPS),
cv.Optional(CONF_LATITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
icon=ICON_LATITUDE,
accuracy_decimals=6,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_LONGITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
icon=ICON_LONGITUDE,
accuracy_decimals=6,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SPEED): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOMETER_PER_HOUR,
icon=ICON_SPEEDOMETER,
accuracy_decimals=3,
device_class=DEVICE_CLASS_SPEED,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_COURSE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
icon=ICON_COMPASS,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ALTITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
icon=ICON_ALTIMETER,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SATELLITES): sensor.sensor_schema(
icon=ICON_SATELLITE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
@@ -73,28 +94,28 @@ async def to_code(config):
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_LATITUDE in config:
sens = await sensor.new_sensor(config[CONF_LATITUDE])
if latitude_config := config.get(CONF_LATITUDE):
sens = await sensor.new_sensor(latitude_config)
cg.add(var.set_latitude_sensor(sens))
if CONF_LONGITUDE in config:
sens = await sensor.new_sensor(config[CONF_LONGITUDE])
if longitude_config := config.get(CONF_LONGITUDE):
sens = await sensor.new_sensor(longitude_config)
cg.add(var.set_longitude_sensor(sens))
if CONF_SPEED in config:
sens = await sensor.new_sensor(config[CONF_SPEED])
if speed_config := config.get(CONF_SPEED):
sens = await sensor.new_sensor(speed_config)
cg.add(var.set_speed_sensor(sens))
if CONF_COURSE in config:
sens = await sensor.new_sensor(config[CONF_COURSE])
if course_config := config.get(CONF_COURSE):
sens = await sensor.new_sensor(course_config)
cg.add(var.set_course_sensor(sens))
if CONF_ALTITUDE in config:
sens = await sensor.new_sensor(config[CONF_ALTITUDE])
if altitude_config := config.get(CONF_ALTITUDE):
sens = await sensor.new_sensor(altitude_config)
cg.add(var.set_altitude_sensor(sens))
if CONF_SATELLITES in config:
sens = await sensor.new_sensor(config[CONF_SATELLITES])
if satellites_config := config.get(CONF_SATELLITES):
sens = await sensor.new_sensor(satellites_config)
cg.add(var.set_satellites_sensor(sens))
if hdop_config := config.get(CONF_HDOP):
@@ -102,4 +123,4 @@ async def to_code(config):
cg.add(var.set_hdop_sensor(sens))
# https://platformio.org/lib/show/1655/TinyGPSPlus
cg.add_library("mikalhart/TinyGPSPlus", "1.0.2")
cg.add_library("mikalhart/TinyGPSPlus", "1.1.0")

View File

@@ -10,6 +10,17 @@ static const char *const TAG = "gps";
TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); }
void GPS::dump_config() {
ESP_LOGCONFIG(TAG, "GPS:");
LOG_SENSOR(" ", "Latitude", this->latitude_sensor_);
LOG_SENSOR(" ", "Longitude", this->longitude_sensor_);
LOG_SENSOR(" ", "Speed", this->speed_sensor_);
LOG_SENSOR(" ", "Course", this->course_sensor_);
LOG_SENSOR(" ", "Altitude", this->altitude_sensor_);
LOG_SENSOR(" ", "Satellites", this->satellites_sensor_);
LOG_SENSOR(" ", "HDOP", this->hdop_sensor_);
}
void GPS::update() {
if (this->latitude_sensor_ != nullptr)
this->latitude_sensor_->publish_state(this->latitude_);
@@ -34,40 +45,45 @@ void GPS::update() {
}
void GPS::loop() {
while (this->available() && !this->has_time_) {
while (this->available() > 0 && !this->has_time_) {
if (this->tiny_gps_.encode(this->read())) {
if (tiny_gps_.location.isUpdated()) {
this->latitude_ = tiny_gps_.location.lat();
this->longitude_ = tiny_gps_.location.lng();
if (this->tiny_gps_.location.isUpdated()) {
this->latitude_ = this->tiny_gps_.location.lat();
this->longitude_ = this->tiny_gps_.location.lng();
ESP_LOGD(TAG, "Location:");
ESP_LOGD(TAG, " Lat: %f", this->latitude_);
ESP_LOGD(TAG, " Lon: %f", this->longitude_);
ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_);
ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_);
}
if (tiny_gps_.speed.isUpdated()) {
this->speed_ = tiny_gps_.speed.kmph();
if (this->tiny_gps_.speed.isUpdated()) {
this->speed_ = this->tiny_gps_.speed.kmph();
ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_);
}
if (tiny_gps_.course.isUpdated()) {
this->course_ = tiny_gps_.course.deg();
if (this->tiny_gps_.course.isUpdated()) {
this->course_ = this->tiny_gps_.course.deg();
ESP_LOGD(TAG, "Course: %.2f °", this->course_);
}
if (tiny_gps_.altitude.isUpdated()) {
this->altitude_ = tiny_gps_.altitude.meters();
if (this->tiny_gps_.altitude.isUpdated()) {
this->altitude_ = this->tiny_gps_.altitude.meters();
ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_);
}
if (tiny_gps_.satellites.isUpdated()) {
this->satellites_ = tiny_gps_.satellites.value();
if (this->tiny_gps_.satellites.isUpdated()) {
this->satellites_ = this->tiny_gps_.satellites.value();
ESP_LOGD(TAG, "Satellites: %d", this->satellites_);
}
if (tiny_gps_.hdop.isUpdated()) {
this->hdop_ = tiny_gps_.hdop.hdop();
if (this->tiny_gps_.hdop.isUpdated()) {
this->hdop_ = this->tiny_gps_.hdop.hdop();
ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_);
}
for (auto *listener : this->listeners_)
for (auto *listener : this->listeners_) {
listener->on_update(this->tiny_gps_);
}
}
}
}

View File

@@ -5,7 +5,7 @@
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h"
#include <TinyGPS++.h>
#include <TinyGPSPlus.h>
#include <vector>
@@ -27,13 +27,13 @@ class GPSListener {
class GPS : public PollingComponent, public uart::UARTDevice {
public:
void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; }
void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; }
void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; }
void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; }
void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; }
void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; }
void set_hdop_sensor(sensor::Sensor *hdop_sensor) { hdop_sensor_ = hdop_sensor; }
void set_latitude_sensor(sensor::Sensor *latitude_sensor) { this->latitude_sensor_ = latitude_sensor; }
void set_longitude_sensor(sensor::Sensor *longitude_sensor) { this->longitude_sensor_ = longitude_sensor; }
void set_speed_sensor(sensor::Sensor *speed_sensor) { this->speed_sensor_ = speed_sensor; }
void set_course_sensor(sensor::Sensor *course_sensor) { this->course_sensor_ = course_sensor; }
void set_altitude_sensor(sensor::Sensor *altitude_sensor) { this->altitude_sensor_ = altitude_sensor; }
void set_satellites_sensor(sensor::Sensor *satellites_sensor) { this->satellites_sensor_ = satellites_sensor; }
void set_hdop_sensor(sensor::Sensor *hdop_sensor) { this->hdop_sensor_ = hdop_sensor; }
void register_listener(GPSListener *listener) {
listener->parent_ = this;
@@ -41,19 +41,20 @@ class GPS : public PollingComponent, public uart::UARTDevice {
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void dump_config() override;
void loop() override;
void update() override;
TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; }
protected:
float latitude_ = NAN;
float longitude_ = NAN;
float speed_ = NAN;
float course_ = NAN;
float altitude_ = NAN;
int satellites_ = 0;
double hdop_ = NAN;
float latitude_{NAN};
float longitude_{NAN};
float speed_{NAN};
float course_{NAN};
float altitude_{NAN};
uint16_t satellites_{0};
float hdop_{NAN};
sensor::Sensor *latitude_sensor_{nullptr};
sensor::Sensor *longitude_sensor_{nullptr};

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODEL
from esphome.const import CONF_MODEL
CODEOWNERS = ["@orestismers"]
@@ -21,16 +21,13 @@ MODELS = {
"yag": Model.GREE_YAG,
}
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
{
cv.GenerateID(): cv.declare_id(GreeClimate),
cv.Required(CONF_MODEL): cv.enum(MODELS),
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_model(config[CONF_MODEL]))
await climate_ir.register_climate_ir(var, config)

View File

@@ -1,5 +1,6 @@
#include "growatt_solar.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace growatt_solar {
@@ -18,7 +19,7 @@ void GrowattSolar::loop() {
void GrowattSolar::update() {
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_send_ < this->get_update_interval() / 2) {
return;
}

View File

@@ -30,6 +30,7 @@ from esphome.const import (
CONF_VISUAL,
CONF_WIFI,
)
from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
@@ -185,42 +186,46 @@ def validate_visual(config):
return config
BASE_CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend(
{
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
),
cv.Optional(
CONF_SUPPORTED_SWING_MODES,
default=[
"VERTICAL",
"HORIZONTAL",
"BOTH",
],
): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)),
cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean,
cv.Optional(CONF_DISPLAY): cv.boolean,
cv.Optional(
CONF_ANSWER_TIMEOUT,
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StatusMessageTrigger),
}
),
}
def _base_config_schema(class_: MockObjClass) -> cv.Schema:
return (
climate.climate_schema(class_)
.extend(
{
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
),
cv.Optional(
CONF_SUPPORTED_SWING_MODES,
default=[
"VERTICAL",
"HORIZONTAL",
"BOTH",
],
): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)),
cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean,
cv.Optional(CONF_DISPLAY): cv.boolean,
cv.Optional(
CONF_ANSWER_TIMEOUT,
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
StatusMessageTrigger
),
}
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
PROTOCOL_SMARTAIR2: _base_config_schema(Smartair2Climate).extend(
{
cv.GenerateID(): cv.declare_id(Smartair2Climate),
cv.Optional(
CONF_ALTERNATIVE_SWING_CONTROL, default=False
): cv.boolean,
@@ -232,9 +237,8 @@ CONFIG_SCHEMA = cv.All(
),
}
),
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
PROTOCOL_HON: _base_config_schema(HonClimate).extend(
{
cv.GenerateID(): cv.declare_id(HonClimate),
cv.Optional(
CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS"
): cv.ensure_list(
@@ -464,10 +468,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
cg.add(haier_ns.init_haier_protocol_logging())
var = cg.new_Pvariable(config[CONF_ID])
var = await climate.new_climate(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
await climate.register_climate(var, config)
cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
if CONF_CONTROL_METHOD in config:

View File

@@ -30,25 +30,28 @@ DECAY_MODE_OPTIONS = {
# Actions
BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan),
cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput),
cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput),
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
).extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = (
fan.fan_schema(HBridgeFan)
.extend(
{
cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput),
cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput),
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)
.extend(cv.COMPONENT_SCHEMA)
)
@automation.register_action(
"fan.hbridge.brake",
BrakeAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(HBridgeFan)}),
maybe_simple_id({cv.GenerateID(): cv.use_id(HBridgeFan)}),
)
async def fan_hbridge_brake_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -56,13 +59,12 @@ async def fan_hbridge_brake_to_code(config, action_id, template_arg, args):
async def to_code(config):
var = cg.new_Pvariable(
config[CONF_ID],
var = await fan.new_fan(
config,
config[CONF_SPEED_COUNT],
config[CONF_DECAY_MODE],
)
await cg.register_component(var, config)
await fan.register_fan(var, config)
pin_a_ = await cg.get_variable(config[CONF_PIN_A])
cg.add(var.set_pin_a(pin_a_))
pin_b_ = await cg.get_variable(config[CONF_PIN_B])

View File

@@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE,
CONF_PROTOCOL,
@@ -98,9 +97,8 @@ VERTICAL_DIRECTIONS = {
}
CONFIG_SCHEMA = cv.All(
climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
climate_ir.climate_ir_with_receiver_schema(HeatpumpIRClimate).extend(
{
cv.GenerateID(): cv.declare_id(HeatpumpIRClimate),
cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS),
cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS),
cv.Required(CONF_VERTICAL_DEFAULT): cv.enum(VERTICAL_DIRECTIONS),
@@ -112,8 +110,8 @@ CONFIG_SCHEMA = cv.All(
)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
async def to_code(config):
var = await climate_ir.new_climate_ir(config)
if CONF_VISUAL not in config:
config[CONF_VISUAL] = {}
visual = config[CONF_VISUAL]
@@ -121,7 +119,6 @@ def to_code(config):
visual[CONF_MAX_TEMPERATURE] = config[CONF_MAX_TEMPERATURE]
if CONF_MIN_TEMPERATURE not in visual:
visual[CONF_MIN_TEMPERATURE] = config[CONF_MIN_TEMPERATURE]
yield climate_ir.register_climate_ir(var, config)
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))

View File

@@ -1,20 +1,13 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344")
HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(HitachiClimate),
}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -1,20 +1,13 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"]
hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424")
HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(HitachiClimate),
}
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
await climate_ir.new_climate_ir(config)

View File

@@ -25,13 +25,13 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HTE501Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
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,
),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
@@ -49,10 +49,10 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity_sensor(sens))

View File

@@ -23,13 +23,13 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HYT271Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
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,
),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
@@ -47,10 +47,10 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity(sens))

View File

@@ -2,7 +2,7 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32, media_player
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE
from esphome.const import CONF_MODE
from .. import (
CONF_I2S_AUDIO_ID,
@@ -57,16 +57,17 @@ def validate_esp32_variant(config):
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"internal": media_player.MEDIA_PLAYER_SCHEMA.extend(
"internal": media_player.media_player_schema(I2SAudioMediaPlayer)
.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
}
).extend(cv.COMPONENT_SCHEMA),
"external": media_player.MEDIA_PLAYER_SCHEMA.extend(
)
.extend(cv.COMPONENT_SCHEMA),
"external": media_player.media_player_schema(I2SAudioMediaPlayer)
.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(
CONF_I2S_DOUT_PIN
@@ -79,7 +80,8 @@ CONFIG_SCHEMA = cv.All(
*I2C_COMM_FMT_OPTIONS, lower=True
),
}
).extend(cv.COMPONENT_SCHEMA),
)
.extend(cv.COMPONENT_SCHEMA),
},
key=CONF_DAC_TYPE,
),
@@ -97,9 +99,8 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
var = await media_player.new_media_player(config)
await cg.register_component(var, config)
await media_player.register_media_player(var, config)
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])

View File

@@ -30,11 +30,11 @@ static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000;
static const char *const TAG = "i2s_audio.microphone";
enum MicrophoneEventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // stops the microphone task
TASK_STARTING = (1 << 10),
TASK_RUNNING = (1 << 11),
TASK_STOPPING = (1 << 12),
TASK_STOPPED = (1 << 13),
COMMAND_STOP = (1 << 0), // stops the microphone task, set and cleared by ``loop``
TASK_STARTING = (1 << 10), // set by mic task, cleared by ``loop``
TASK_RUNNING = (1 << 11), // set by mic task, cleared by ``loop``
TASK_STOPPED = (1 << 13), // set by mic task, cleared by ``loop``
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
@@ -151,24 +151,21 @@ bool I2SAudioMicrophone::start_driver_() {
config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN);
err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err));
this->status_set_error();
ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err));
return false;
}
err = i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error setting ADC mode: %s", esp_err_to_name(err));
this->status_set_error();
return false;
}
err = i2s_adc_enable(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error enabling ADC: %s", esp_err_to_name(err));
this->status_set_error();
ESP_LOGE(TAG, "Error setting ADC mode: %s", esp_err_to_name(err));
return false;
}
err = i2s_adc_enable(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error enabling ADC: %s", esp_err_to_name(err));
return false;
}
} else
#endif
{
@@ -177,8 +174,7 @@ bool I2SAudioMicrophone::start_driver_() {
err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error installing I2S driver: %s", esp_err_to_name(err));
this->status_set_error();
ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err));
return false;
}
@@ -187,8 +183,7 @@ bool I2SAudioMicrophone::start_driver_() {
err = i2s_set_pin(this->parent_->get_port(), &pin_config);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error setting I2S pin: %s", esp_err_to_name(err));
this->status_set_error();
ESP_LOGE(TAG, "Error setting I2S pin: %s", esp_err_to_name(err));
return false;
}
}
@@ -203,8 +198,7 @@ bool I2SAudioMicrophone::start_driver_() {
/* Allocate a new RX channel and get the handle of this channel */
err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err));
this->status_set_error();
ESP_LOGE(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err));
return false;
}
@@ -276,22 +270,20 @@ bool I2SAudioMicrophone::start_driver_() {
err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg);
}
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err));
this->status_set_error();
ESP_LOGE(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err));
return false;
}
/* Before reading data, start the RX channel first */
i2s_channel_enable(this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err));
this->status_set_error();
ESP_LOGE(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err));
return false;
}
#endif
this->status_clear_error();
this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation
return true;
}
@@ -303,71 +295,55 @@ void I2SAudioMicrophone::stop() {
}
void I2SAudioMicrophone::stop_driver_() {
// There is no harm continuing to unload the driver if an error is ever returned by the various functions. This
// ensures that we stop/unload the driver when it only partially starts.
esp_err_t err;
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC
if (this->adc_) {
err = i2s_adc_disable(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err));
this->status_set_error();
return;
ESP_LOGW(TAG, "Error disabling ADC - it may not have started: %s", esp_err_to_name(err));
}
}
#endif
err = i2s_stop(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error stopping I2S microphone: %s", esp_err_to_name(err));
this->status_set_error();
return;
ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err));
}
err = i2s_driver_uninstall(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error uninstalling I2S driver: %s", esp_err_to_name(err));
this->status_set_error();
return;
ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err));
}
#else
/* Have to stop the channel before deleting it */
err = i2s_channel_disable(this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error stopping I2S microphone: %s", esp_err_to_name(err));
this->status_set_error();
return;
ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err));
}
/* If the handle is not needed any more, delete it to release the channel resources */
err = i2s_del_channel(this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error deleting I2S channel: %s", esp_err_to_name(err));
this->status_set_error();
return;
ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err));
}
#endif
this->parent_->unlock();
this->status_clear_error();
}
void I2SAudioMicrophone::mic_task(void *params) {
I2SAudioMicrophone *this_microphone = (I2SAudioMicrophone *) params;
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STARTING);
uint8_t start_counter = 0;
bool started = this_microphone->start_driver_();
while (!started && start_counter < 10) {
// Attempt to load the driver again in 100 ms. Doesn't slow down main loop since its in a task.
vTaskDelay(pdMS_TO_TICKS(100));
++start_counter;
started = this_microphone->start_driver_();
}
{ // Ensures the samples vector is freed when the task stops
if (started) {
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
const size_t bytes_to_read = this_microphone->audio_stream_info_.ms_to_bytes(READ_DURATION_MS);
std::vector<uint8_t> samples;
samples.reserve(bytes_to_read);
while (!(xEventGroupGetBits(this_microphone->event_group_) & COMMAND_STOP)) {
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
while (!(xEventGroupGetBits(this_microphone->event_group_) & MicrophoneEventGroupBits::COMMAND_STOP)) {
if (this_microphone->data_callbacks_.size() > 0) {
samples.resize(bytes_to_read);
size_t bytes_read = this_microphone->read_(samples.data(), bytes_to_read, 2 * pdMS_TO_TICKS(READ_DURATION_MS));
@@ -382,9 +358,6 @@ void I2SAudioMicrophone::mic_task(void *params) {
}
}
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPING);
this_microphone->stop_driver_();
xEventGroupSetBits(this_microphone->event_group_, MicrophoneEventGroupBits::TASK_STOPPED);
while (true) {
// Continuously delay until the loop method deletes the task
@@ -425,7 +398,10 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w
#endif
if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (ticks_to_wait != 0))) {
// Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call
ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err));
if (!this->status_has_warning()) {
// Avoid spamming the logs with the error message if its repeated
ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err));
}
this->status_set_warning();
return 0;
}
@@ -452,7 +428,7 @@ void I2SAudioMicrophone::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) {
ESP_LOGD(TAG, "Task has started, attempting to setup I2S audio driver");
ESP_LOGD(TAG, "Task started, attempting to allocate buffer");
xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING);
}
@@ -463,23 +439,25 @@ void I2SAudioMicrophone::loop() {
this->state_ = microphone::STATE_RUNNING;
}
if (event_group_bits & MicrophoneEventGroupBits::TASK_STOPPING) {
ESP_LOGD(TAG, "Task is stopping, attempting to unload the I2S audio driver");
xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STOPPING);
}
if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) {
ESP_LOGD(TAG, "Task is finished, freeing resources");
ESP_LOGD(TAG, "Task finished, freeing resources and uninstalling I2S driver");
vTaskDelete(this->task_handle_);
this->task_handle_ = nullptr;
this->stop_driver_();
xEventGroupClearBits(this->event_group_, ALL_BITS);
this->status_clear_error();
this->state_ = microphone::STATE_STOPPED;
}
// Start the microphone if any semaphores are taken
if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) < MAX_LISTENERS) &&
(this->state_ == microphone::STATE_STOPPED)) {
this->state_ = microphone::STATE_STARTING;
}
// Stop the microphone if all semaphores are returned
if ((uxSemaphoreGetCount(this->active_listeners_semaphore_) == MAX_LISTENERS) &&
(this->state_ == microphone::STATE_RUNNING)) {
this->state_ = microphone::STATE_STOPPING;
@@ -487,14 +465,26 @@ void I2SAudioMicrophone::loop() {
switch (this->state_) {
case microphone::STATE_STARTING:
if ((this->task_handle_ == nullptr) && !this->status_has_error()) {
if (this->status_has_error()) {
break;
}
if (!this->start_driver_()) {
this->status_momentary_error("I2S driver failed to start, unloading it and attempting again in 1 second", 1000);
this->stop_driver_(); // Stop/frees whatever possibly started
break;
}
if (this->task_handle_ == nullptr) {
xTaskCreate(I2SAudioMicrophone::mic_task, "mic_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->task_handle_);
if (this->task_handle_ == nullptr) {
this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000);
this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
}
}
break;
case microphone::STATE_RUNNING:
break;

View File

@@ -43,7 +43,11 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
#endif
protected:
/// @brief Starts the I2S driver. Updates the ``audio_stream_info_`` member variable with the current setttings.
/// @return True if succesful, false otherwise
bool start_driver_();
/// @brief Stops the I2S driver.
void stop_driver_();
/// @brief Attempts to correct a microphone DC offset; e.g., a microphones silent level is offset from 0. Applies a

View File

@@ -1,5 +1,6 @@
#include "kuntze.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace kuntze {
@@ -60,7 +61,7 @@ void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
}
void Kuntze::loop() {
uint32_t now = millis();
uint32_t now = App.get_loop_component_start_time();
// timeout after 15 seconds
if (this->waiting_ && (now - this->last_send_ > 15000)) {
ESP_LOGW(TAG, "timed out waiting for response");

View File

@@ -24,6 +24,7 @@ from esphome.const import (
CONF_HARDWARE_UART,
CONF_ID,
CONF_LEVEL,
CONF_LOGGER,
CONF_LOGS,
CONF_ON_MESSAGE,
CONF_TAG,
@@ -247,6 +248,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
baud_rate = config[CONF_BAUD_RATE]
level = config[CONF_LEVEL]
CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
log = cg.new_Pvariable(
config[CONF_ID],
@@ -254,6 +256,7 @@ async def to_code(config):
config[CONF_TX_BUFFER_SIZE],
)
if CORE.is_esp32:
cg.add(log.create_pthread_key())
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
if task_log_buffer_size > 0:
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")

View File

@@ -14,25 +14,47 @@ namespace logger {
static const char *const TAG = "logger";
#ifdef USE_ESP32
// Implementation for ESP32 (multi-core with atomic support)
// Main thread: synchronous logging with direct buffer access
// Other threads: console output with stack buffer, callbacks via async buffer
// Implementation for ESP32 (multi-task platform with task-specific tracking)
// Main task always uses direct buffer access for console output and callbacks
//
// For non-main tasks:
// - WITH task log buffer: Prefer sending to ring buffer for async processing
// - Avoids allocating stack memory for console output in normal operation
// - Prevents console corruption from concurrent writes by multiple tasks
// - Messages are serialized through main loop for proper console output
// - Fallback to emergency console logging only if ring buffer is full
// - WITHOUT task log buffer: Only emergency console output, no callbacks
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_.load(std::memory_order_relaxed))
if (level > this->level_for(tag))
return;
recursion_guard_.store(true, std::memory_order_relaxed);
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
bool is_main_task = (current_task == main_task_);
// For main task: call log_message_to_buffer_and_send_ which does console and callback logging
if (current_task == main_task_) {
// Check and set recursion guard - uses pthread TLS for per-task state
if (this->check_and_set_task_log_recursion_(is_main_task)) {
return; // Recursion detected
}
// Main task uses the shared buffer for efficiency
if (is_main_task) {
this->log_message_to_buffer_and_send_(level, tag, line, format, args);
recursion_guard_.store(false, std::memory_order_release);
this->reset_task_log_recursion_(is_main_task);
return;
}
// For non-main tasks: use stack-allocated buffer only for console output
if (this->baud_rate_ > 0) { // If logging is enabled, write to console
bool message_sent = false;
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
message_sent = this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag,
static_cast<uint16_t>(line), current_task, format, args);
#endif // USE_ESPHOME_TASK_LOG_BUFFER
// Emergency console logging for non-main tasks when ring buffer is full or disabled
// This is a fallback mechanism to ensure critical log messages are visible
// Note: This may cause interleaved/corrupted console output if multiple tasks
// log simultaneously, but it's better than losing important messages entirely
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
// Maximum size for console log messages (includes null terminator)
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
@@ -42,32 +64,21 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *
this->write_msg_(console_buffer);
}
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
if (this->log_callback_.size() > 0) {
// This will be processed in the main loop
this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag, static_cast<uint16_t>(line),
current_task, format, args);
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER
recursion_guard_.store(false, std::memory_order_release);
// Reset the recursion guard for this task
this->reset_task_log_recursion_(is_main_task);
}
#endif // USE_ESP32
#ifndef USE_ESP32
// Implementation for platforms that do not support atomic operations
// or have to consider logging in other tasks
#else
// Implementation for all other platforms
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_)
if (level > this->level_for(tag) || global_recursion_guard_)
return;
recursion_guard_ = true;
global_recursion_guard_ = true;
// Format and send to both console and callbacks
this->log_message_to_buffer_and_send_(level, tag, line, format, args);
recursion_guard_ = false;
global_recursion_guard_ = false;
}
#endif // !USE_ESP32
@@ -76,10 +87,10 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_)
if (level > this->level_for(tag) || global_recursion_guard_)
return;
recursion_guard_ = true;
global_recursion_guard_ = true;
this->tx_buffer_at_ = 0;
// Copy format string from progmem
@@ -91,7 +102,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
// Buffer full from copying format
if (this->tx_buffer_at_ >= this->tx_buffer_size_) {
recursion_guard_ = false; // Make sure to reset the recursion guard before returning
global_recursion_guard_ = false; // Make sure to reset the recursion guard before returning
return;
}
@@ -107,7 +118,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
}
this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start);
recursion_guard_ = false;
global_recursion_guard_ = false;
}
#endif // USE_STORE_LOG_STR_IN_FLASH
@@ -179,7 +190,17 @@ void Logger::loop() {
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
this->tx_buffer_[this->tx_buffer_at_] = '\0';
this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_);
// At this point all the data we need from message has been transferred to the tx_buffer
// so we can release the message to allow other tasks to use it as soon as possible.
this->log_buffer_->release_message_main_loop(received_token);
// Write to console from the main loop to prevent corruption from concurrent writes
// This ensures all log messages appear on the console in a clean, serialized manner
// Note: Messages may appear slightly out of order due to async processing, but
// this is preferred over corrupted/interleaved console output
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_);
}
}
}
#endif

View File

@@ -3,7 +3,7 @@
#include <cstdarg>
#include <map>
#ifdef USE_ESP32
#include <atomic>
#include <pthread.h>
#endif
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
@@ -84,6 +84,23 @@ enum UARTSelection {
};
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY
/**
* @brief Logger component for all ESPHome logging.
*
* This class implements a multi-platform logging system with protection against recursion.
*
* Recursion Protection Strategy:
* - On ESP32: Uses task-specific recursion guards
* * Main task: Uses a dedicated boolean member variable for efficiency
* * Other tasks: Uses pthread TLS with a dynamically allocated key for task-specific state
* - On other platforms: Uses a simple global recursion guard
*
* We use pthread TLS via pthread_key_create to create a unique key for storing
* task-specific recursion state, which:
* 1. Efficiently handles multiple tasks without locks or mutexes
* 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables
* 3. Avoids the limitations of the fixed FreeRTOS task local storage slots
*/
class Logger : public Component {
public:
explicit Logger(uint32_t baud_rate, size_t tx_buffer_size);
@@ -102,6 +119,9 @@ class Logger : public Component {
#ifdef USE_ESP_IDF
uart_port_t get_uart_num() const { return uart_num_; }
#endif
#ifdef USE_ESP32
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger.
@@ -192,9 +212,9 @@ class Logger : public Component {
}
// Format string to explicit buffer with varargs
inline void printf_to_buffer_(const char *format, char *buffer, int *buffer_at, int buffer_size, ...) {
inline void printf_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format, ...) {
va_list arg;
va_start(arg, buffer_size);
va_start(arg, format);
this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, arg);
va_end(arg);
}
@@ -222,18 +242,22 @@ class Logger : public Component {
std::map<std::string, int> log_levels_{};
CallbackManager<void(int, const char *, const char *)> log_callback_{};
int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
#ifdef USE_ESP32
std::atomic<bool> recursion_guard_{false};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
#endif
#ifdef USE_ESP32
// Task-specific recursion guards:
// - Main task uses a dedicated member variable for efficiency
// - Other tasks use pthread TLS with a dynamically created key via pthread_key_create
bool main_task_recursion_guard_{false};
pthread_key_t log_recursion_key_;
#else
bool recursion_guard_{false};
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif
void *main_task_ = nullptr;
CallbackManager<void(int)> level_callback_{};
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void *main_task_ = nullptr; // Only used for thread name identification
const char *HOT get_thread_name_() {
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
if (current_task == main_task_) {
@@ -248,6 +272,32 @@ class Logger : public Component {
}
#endif
#ifdef USE_ESP32
inline bool HOT check_and_set_task_log_recursion_(bool is_main_task) {
if (is_main_task) {
const bool was_recursive = main_task_recursion_guard_;
main_task_recursion_guard_ = true;
return was_recursive;
}
intptr_t current = (intptr_t) pthread_getspecific(log_recursion_key_);
if (current != 0)
return true;
pthread_setspecific(log_recursion_key_, (void *) 1);
return false;
}
inline void HOT reset_task_log_recursion_(bool is_main_task) {
if (is_main_task) {
main_task_recursion_guard_ = false;
return;
}
pthread_setspecific(log_recursion_key_, (void *) 0);
}
#endif
inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer,
int *buffer_at, int buffer_size) {
// Format header
@@ -262,13 +312,13 @@ class Logger : public Component {
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
if (thread_name != nullptr) {
// Non-main task with thread name
this->printf_to_buffer_("%s[%s][%s:%03u]%s[%s]%s: ", buffer, buffer_at, buffer_size, color, letter, tag, line,
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]%s[%s]%s: ", color, letter, tag, line,
ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), thread_name, color);
return;
}
#endif
// Main task or non ESP32/LibreTiny platform
this->printf_to_buffer_("%s[%s][%s:%03u]: ", buffer, buffer_at, buffer_size, color, letter, tag, line);
this->printf_to_buffer_(buffer, buffer_at, buffer_size, "%s[%s][%s:%03u]: ", color, letter, tag, line);
}
inline void HOT format_body_to_buffer_(char *buffer, int *buffer_at, int buffer_size, const char *format,

View File

@@ -5,7 +5,7 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_
from esphome.core import CORE
from esphome.cpp_helpers import register_component, register_parented
from .. import CONF_LOGGER_ID, LOG_LEVEL_SEVERITY, Logger, logger_ns
from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns
CODEOWNERS = ["@clydebarrow"]
@@ -21,9 +21,10 @@ CONFIG_SCHEMA = select.select_schema(
async def to_code(config):
levels = LOG_LEVEL_SEVERITY
index = levels.index(CORE.config[CONF_LOGGER][CONF_LEVEL])
parent = await cg.get_variable(config[CONF_LOGGER_ID])
levels = list(LOG_LEVELS)
index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL])
levels = levels[: index + 1]
var = await select.new_select(config, options=levels)
await register_parented(var, config[CONF_LOGGER_ID])
await register_parented(var, parent)
await register_component(var, config)

View File

@@ -36,29 +36,43 @@ from .types import (
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
TIME_TEXT_SCHEMA = cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
)
PRINTF_TEXT_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
},
),
validate_printf,
)
def _validate_text(value):
"""
Do some sanity checking of the format to get better error messages
than using cv.Any
"""
if value is None:
raise cv.Invalid("No text specified")
if isinstance(value, dict):
if CONF_TIME_FORMAT in value:
return TIME_TEXT_SCHEMA(value)
return PRINTF_TEXT_SCHEMA(value)
return cv.templatable(cv.string)(value)
# A schema for text properties
TEXT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TEXT): cv.Any(
cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(
cv.lambda_
),
},
),
validate_printf,
),
cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
),
cv.templatable(cv.string),
)
cv.Optional(CONF_TEXT): _validate_text,
}
)

View File

@@ -1,5 +1,6 @@
#include "matrix_keypad.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace matrix_keypad {
@@ -28,7 +29,7 @@ void MatrixKeypad::setup() {
void MatrixKeypad::loop() {
static uint32_t active_start = 0;
static int active_key = -1;
uint32_t now = millis();
uint32_t now = App.get_loop_component_start_time();
int key = -1;
bool error = false;
int pos = 0, row, col;

View File

@@ -2,6 +2,7 @@
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "max7219font.h"
#include <algorithm>
@@ -63,7 +64,7 @@ void MAX7219Component::dump_config() {
}
void MAX7219Component::loop() {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
const uint32_t millis_since_last_scroll = now - this->last_scroll_;
const size_t first_line_size = this->max_displaybuffer_[0].size();
// check if the buffer has shrunk past the current position since last update

View File

@@ -2,6 +2,8 @@ from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID,
CONF_ON_IDLE,
CONF_ON_STATE,
@@ -10,6 +12,7 @@ from esphome.const import (
)
from esphome.core import CORE
from esphome.coroutine import coroutine_with_priority
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@jesserockz"]
@@ -103,7 +106,13 @@ async def register_media_player(var, config):
await setup_media_player_core_(var, config)
MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
async def new_media_player(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_media_player(var, config)
return var
_MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
{
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
@@ -134,6 +143,29 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
)
def media_player_schema(
class_: MockObjClass,
*,
entity_category: str = cv.UNDEFINED,
icon: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)}
for key, default, validator in [
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_ICON, icon, cv.icon),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _MEDIA_PLAYER_SCHEMA.extend(schema)
# Remove before 2025.11.0
MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer)
MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player"))
MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema(
{

View File

@@ -32,7 +32,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MHZ19Component),
cv.Required(CONF_CO2): sensor.sensor_schema(
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
@@ -61,16 +61,20 @@ async def to_code(config):
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_CO2 in config:
sens = await sensor.new_sensor(config[CONF_CO2])
if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_AUTOMATIC_BASELINE_CALIBRATION in config:
cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION]))
if (
automatic_baseline_calibration := config.get(
CONF_AUTOMATIC_BASELINE_CALIBRATION
)
) is not None:
cg.add(var.set_abc_enabled(automatic_baseline_calibration))
cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME]))

View File

@@ -147,7 +147,11 @@ bool StreamingModel::perform_streaming_inference(const int8_t features[PREPROCES
this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0]; // probability;
this->unprocessed_probability_status_ = true;
}
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
if (this->recent_streaming_probabilities_[this->last_n_index_] < this->probability_cutoff_) {
// Only increment ignore windows if less than the probability cutoff; this forces the model to "cool-off" from a
// previous detection and calling ``reset_probabilities`` so it avoids duplicate detections
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
}
}
return true;
}

View File

@@ -104,9 +104,9 @@ validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True)
CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend(
climate.climate_schema(AirConditioner)
.extend(
{
cv.GenerateID(): cv.declare_id(AirConditioner),
cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
@@ -259,10 +259,9 @@ async def power_inv_to_code(var, config, args):
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
var = await climate.new_climate(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
await climate.register_climate(var, config)
cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds))
cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT
from esphome.const import CONF_USE_FAHRENHEIT
AUTO_LOAD = ["climate_ir", "coolix"]
CODEOWNERS = ["@dudanov"]
@@ -10,15 +10,13 @@ midea_ir_ns = cg.esphome_ns.namespace("midea_ir")
MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend(
{
cv.GenerateID(): cv.declare_id(MideaIR),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))

View File

@@ -0,0 +1,15 @@
CODEOWNERS = ["@clydebarrow"]
DOMAIN = "mipi_spi"
CONF_DRAW_FROM_ORIGIN = "draw_from_origin"
CONF_SPI_16 = "spi_16"
CONF_PIXEL_MODE = "pixel_mode"
CONF_COLOR_DEPTH = "color_depth"
CONF_BUS_MODE = "bus_mode"
CONF_USE_AXIS_FLIPS = "use_axis_flips"
CONF_NATIVE_WIDTH = "native_width"
CONF_NATIVE_HEIGHT = "native_height"
MODE_RGB = "RGB"
MODE_BGR = "BGR"

View File

@@ -0,0 +1,474 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import display, spi
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
import esphome.config_validation as cv
from esphome.config_validation import ALLOW_EXTRA
from esphome.const import (
CONF_BRIGHTNESS,
CONF_COLOR_ORDER,
CONF_CS_PIN,
CONF_DATA_RATE,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_ID,
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_WIDTH,
)
from esphome.core import TimePeriod
from ..const import CONF_DRAW_ROUNDING
from ..lvgl.defines import CONF_COLOR_DEPTH
from . import (
CONF_BUS_MODE,
CONF_DRAW_FROM_ORIGIN,
CONF_NATIVE_HEIGHT,
CONF_NATIVE_WIDTH,
CONF_PIXEL_MODE,
CONF_SPI_16,
CONF_USE_AXIS_FLIPS,
DOMAIN,
MODE_BGR,
MODE_RGB,
)
from .models import (
DELAY_FLAG,
MADCTL_BGR,
MADCTL_MV,
MADCTL_MX,
MADCTL_MY,
MADCTL_XFLIP,
MADCTL_YFLIP,
DriverChip,
amoled,
cyd,
ili,
jc,
lanbon,
lilygo,
waveshare,
)
from .models.commands import BRIGHTNESS, DISPON, INVOFF, INVON, MADCTL, PIXFMT, SLPOUT
DEPENDENCIES = ["spi"]
LOGGER = logging.getLogger(DOMAIN)
mipi_spi_ns = cg.esphome_ns.namespace("mipi_spi")
MipiSpi = mipi_spi_ns.class_(
"MipiSpi", display.Display, display.DisplayBuffer, cg.Component, spi.SPIDevice
)
ColorOrder = display.display_ns.enum("ColorMode")
ColorBitness = display.display_ns.enum("ColorBitness")
Model = mipi_spi_ns.enum("Model")
COLOR_ORDERS = {
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
}
COLOR_DEPTHS = {
8: ColorBitness.COLOR_BITNESS_332,
16: ColorBitness.COLOR_BITNESS_565,
}
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
DriverChip("CUSTOM", initsequence={})
MODELS = DriverChip.models
# These statements are noops, but serve to suppress linting of side-effect-only imports
for _ in (ili, jc, amoled, lilygo, lanbon, cyd, waveshare):
pass
PixelMode = mipi_spi_ns.enum("PixelMode")
PIXEL_MODE_18BIT = "18bit"
PIXEL_MODE_16BIT = "16bit"
PIXEL_MODES = {
PIXEL_MODE_16BIT: 0x55,
PIXEL_MODE_18BIT: 0x66,
}
def validate_dimension(rounding):
def validator(value):
value = cv.positive_int(value)
if value % rounding != 0:
raise cv.Invalid(f"Dimensions and offsets must be divisible by {rounding}")
return value
return validator
def map_sequence(value):
"""
The format is a repeated sequence of [CMD, <data>] where <data> is s a sequence of bytes. The length is inferred
from the length of the sequence and should not be explicit.
A delay can be inserted by specifying "- delay N" where N is in ms
"""
if isinstance(value, str) and value.lower().startswith("delay "):
value = value.lower()[6:]
delay = cv.All(
cv.positive_time_period_milliseconds,
cv.Range(TimePeriod(milliseconds=1), TimePeriod(milliseconds=255)),
)(value)
return DELAY_FLAG, delay.total_milliseconds
if isinstance(value, int):
return (value,)
value = cv.All(cv.ensure_list(cv.int_range(0, 255)), cv.Length(1, 254))(value)
return tuple(value)
def power_of_two(value):
value = cv.int_range(1, 128)(value)
if value & (value - 1) != 0:
raise cv.Invalid("value must be a power of two")
return value
def dimension_schema(rounding):
return cv.Any(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): validate_dimension(rounding),
cv.Required(CONF_HEIGHT): validate_dimension(rounding),
cv.Optional(CONF_OFFSET_HEIGHT, default=0): validate_dimension(
rounding
),
cv.Optional(CONF_OFFSET_WIDTH, default=0): validate_dimension(rounding),
}
),
)
def model_schema(bus_mode, model: DriverChip, swapsies: bool):
transform = cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
}
)
if model.get_default(CONF_SWAP_XY, False) == cv.UNDEFINED:
transform = transform.extend(
{
cv.Optional(CONF_SWAP_XY): cv.invalid(
"Axis swapping not supported by this model"
)
}
)
else:
transform = transform.extend(
{
cv.Required(CONF_SWAP_XY): cv.boolean,
}
)
# CUSTOM model will need to provide a custom init sequence
iseqconf = (
cv.Required(CONF_INIT_SEQUENCE)
if model.initsequence is None
else cv.Optional(CONF_INIT_SEQUENCE)
)
# Dimensions are optional if the model has a default width and the transform is not overridden
cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not swapsies else cv.Required
)
pixel_modes = PIXEL_MODES if bus_mode == TYPE_SINGLE else (PIXEL_MODE_16BIT,)
color_depth = (
("16", "8", "16bit", "8bit") if bus_mode == TYPE_SINGLE else ("16", "16bit")
)
schema = (
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE3" if bus_mode == TYPE_OCTAL else "MODE0",
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
mode=bus_mode,
)
)
.extend(
{
model.option(pin, cv.UNDEFINED): pins.gpio_output_pin_schema
for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_DC_PIN)
}
)
.extend(
{
cv.GenerateID(): cv.declare_id(MipiSpi),
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
model.get_default(CONF_DRAW_ROUNDING, 1)
),
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
pins.gpio_output_pin_schema
),
model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(
COLOR_ORDERS, upper=True
),
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.Any(
cv.one_of(*pixel_modes, lower=True),
cv.int_range(0, 255, min_included=True, max_included=True),
),
cv.Optional(CONF_TRANSFORM): transform,
cv.Optional(CONF_BUS_MODE, default=bus_mode): cv.one_of(
bus_mode, lower=True
),
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
iseqconf: cv.ensure_list(map_sequence),
}
)
.extend(
{
model.option(x): cv.boolean
for x in [
CONF_DRAW_FROM_ORIGIN,
CONF_SPI_16,
CONF_INVERT_COLORS,
CONF_USE_AXIS_FLIPS,
]
}
)
)
if brightness := model.get_default(CONF_BRIGHTNESS):
schema = schema.extend(
{
cv.Optional(CONF_BRIGHTNESS, default=brightness): cv.int_range(
0, 0xFF, min_included=True, max_included=True
),
}
)
if bus_mode != TYPE_SINGLE:
return cv.All(schema, cv.only_with_esp_idf)
return schema
def rotation_as_transform(model, config):
"""
Check if a rotation can be implemented in hardware using the MADCTL register.
A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y.
"""
rotation = config.get(CONF_ROTATION, 0)
return rotation and (
model.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180
)
def config_schema(config):
# First get the model and bus mode
config = cv.Schema(
{
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
},
extra=ALLOW_EXTRA,
)(config)
model = MODELS[config[CONF_MODEL]]
bus_modes = model.modes
config = cv.Schema(
{
model.option(CONF_BUS_MODE, TYPE_SINGLE): cv.one_of(*bus_modes, lower=True),
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
},
extra=ALLOW_EXTRA,
)(config)
bus_mode = config.get(CONF_BUS_MODE, model.modes[0])
swapsies = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
config = model_schema(bus_mode, model, swapsies)(config)
# Check for invalid combinations of MADCTL config
if init_sequence := config.get(CONF_INIT_SEQUENCE):
if MADCTL in [x[0] for x in init_sequence] and CONF_TRANSFORM in config:
raise cv.Invalid(
f"transform is not supported when MADCTL ({MADCTL:#X}) is in the init sequence"
)
if bus_mode == TYPE_QUAD and CONF_DC_PIN in config:
raise cv.Invalid("DC pin is not supported in quad mode")
if config[CONF_PIXEL_MODE] == PIXEL_MODE_18BIT and bus_mode != TYPE_SINGLE:
raise cv.Invalid("18-bit pixel mode is not supported on a quad or octal bus")
if bus_mode != TYPE_QUAD and CONF_DC_PIN not in config:
raise cv.Invalid(f"DC pin is required in {bus_mode} mode")
return config
CONFIG_SCHEMA = config_schema
def get_transform(model, config):
can_transform = rotation_as_transform(model, config)
transform = config.get(
CONF_TRANSFORM,
{
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False),
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False),
CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False),
},
)
# Can we use the MADCTL register to set the rotation?
if can_transform and CONF_TRANSFORM not in config:
rotation = config[CONF_ROTATION]
if rotation == 180:
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
elif rotation == 90:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
else:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
transform[CONF_TRANSFORM] = True
return transform
def get_sequence(model, config):
"""
Create the init sequence for the display.
Use the default sequence from the model, if any, and append any custom sequence provided in the config.
Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence
Pixel format, color order, and orientation will be set.
"""
sequence = list(model.initsequence)
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
sequence.extend(custom_sequence)
# Ensure each command is a tuple
sequence = [x if isinstance(x, tuple) else (x,) for x in sequence]
commands = [x[0] for x in sequence]
# Set pixel format if not already in the custom sequence
if PIXFMT not in commands:
pixel_mode = config[CONF_PIXEL_MODE]
if not isinstance(pixel_mode, int):
pixel_mode = PIXEL_MODES[pixel_mode]
sequence.append((PIXFMT, pixel_mode))
# Does the chip use the flipping bits for mirroring rather than the reverse order bits?
use_flip = config[CONF_USE_AXIS_FLIPS]
if MADCTL not in commands:
madctl = 0
transform = get_transform(model, config)
if transform.get(CONF_TRANSFORM):
LOGGER.info("Using hardware transform to implement rotation")
if transform.get(CONF_MIRROR_X):
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
if transform.get(CONF_MIRROR_Y):
madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY
if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined
madctl |= MADCTL_MV
if config[CONF_COLOR_ORDER] == MODE_BGR:
madctl |= MADCTL_BGR
sequence.append((MADCTL, madctl))
if INVON not in commands and INVOFF not in commands:
if config[CONF_INVERT_COLORS]:
sequence.append((INVON,))
else:
sequence.append((INVOFF,))
if BRIGHTNESS not in commands:
if brightness := config.get(
CONF_BRIGHTNESS, model.get_default(CONF_BRIGHTNESS)
):
sequence.append((BRIGHTNESS, brightness))
if SLPOUT not in commands:
sequence.append((SLPOUT,))
sequence.append((DISPON,))
# Flatten the sequence into a list of bytes, with the length of each command
# or the delay flag inserted where needed
return sum(
tuple(
(x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:]
for x in sequence
),
(),
)
async def to_code(config):
model = MODELS[config[CONF_MODEL]]
transform = get_transform(model, config)
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
offset_width = dimensions[CONF_OFFSET_WIDTH]
offset_height = dimensions[CONF_OFFSET_HEIGHT]
else:
(width, height) = dimensions
offset_width = 0
offset_height = 0
else:
# Default dimensions, use model defaults and transform if needed
width = model.get_default(CONF_WIDTH)
height = model.get_default(CONF_HEIGHT)
offset_width = model.get_default(CONF_OFFSET_WIDTH, 0)
offset_height = model.get_default(CONF_OFFSET_HEIGHT, 0)
# if mirroring axes and there are offsets, also mirror the offsets to cater for situations where
# the offset is asymmetric
if transform[CONF_MIRROR_X]:
native_width = model.get_default(
CONF_NATIVE_WIDTH, width + offset_width * 2
)
offset_width = native_width - width - offset_width
if transform[CONF_MIRROR_Y]:
native_height = model.get_default(
CONF_NATIVE_HEIGHT, height + offset_height * 2
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set
if transform[CONF_SWAP_XY] is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
color_depth = config[CONF_COLOR_DEPTH]
if color_depth.endswith("bit"):
color_depth = color_depth[:-3]
color_depth = COLOR_DEPTHS[int(color_depth)]
var = cg.new_Pvariable(
config[CONF_ID], width, height, offset_width, offset_height, color_depth
)
cg.add(var.set_init_sequence(get_sequence(model, config)))
if rotation_as_transform(model, config):
if CONF_TRANSFORM in config:
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
else:
config[CONF_ROTATION] = 0
cg.add(var.set_model(config[CONF_MODEL]))
cg.add(var.set_draw_from_origin(config[CONF_DRAW_FROM_ORIGIN]))
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
cg.add(var.set_spi_16(config[CONF_SPI_16]))
if enable_pin := config.get(CONF_ENABLE_PIN):
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
cg.add(var.set_enable_pins(enable))
if reset_pin := config.get(CONF_RESET_PIN):
reset = await cg.gpio_pin_expression(reset_pin)
cg.add(var.set_reset_pin(reset))
if dc_pin := config.get(CONF_DC_PIN):
dc_pin = await cg.gpio_pin_expression(dc_pin)
cg.add(var.set_dc_pin(dc_pin))
if lamb := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda(
lamb, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
await display.register_display(var, config)
await spi.register_spi_device(var, config)

View File

@@ -0,0 +1,481 @@
#include "mipi_spi.h"
#include "esphome/core/log.h"
namespace esphome {
namespace mipi_spi {
void MipiSpi::setup() {
ESP_LOGCONFIG(TAG, "Setting up MIPI SPI");
this->spi_setup();
if (this->dc_pin_ != nullptr) {
this->dc_pin_->setup();
this->dc_pin_->digital_write(false);
}
for (auto *pin : this->enable_pins_) {
pin->setup();
pin->digital_write(true);
}
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(true);
delay(5);
this->reset_pin_->digital_write(false);
delay(5);
this->reset_pin_->digital_write(true);
}
this->bus_width_ = this->parent_->get_bus_width();
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
auto when = millis() + 120;
delay(10);
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGD(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
auto arg_byte = vec[index];
switch (cmd) {
case SLEEP_OUT: {
// are we ready, boots?
int duration = when - millis();
if (duration > 0) {
ESP_LOGD(TAG, "Sleep %dms", duration);
delay(duration);
}
} break;
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case PIXFMT:
this->pixel_mode_ = arg_byte & 0x11 ? PIXEL_MODE_16 : PIXEL_MODE_18;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
default:
break;
}
const auto *ptr = vec.data() + index;
ESP_LOGD(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
this->setup_complete_ = true;
if (this->draw_from_origin_)
check_buffer_();
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::update() {
if (!this->setup_complete_ || this->is_failed()) {
return;
}
this->do_update_();
if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_)
return;
ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
if (this->draw_from_origin_) {
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_ - 1;
}
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_, this->y_low_,
this->width_ - w - this->x_low_);
// invalidate watermarks
this->x_low_ = this->width_;
this->y_low_ = this->height_;
this->x_high_ = 0;
this->y_high_ = 0;
}
void MipiSpi::fill(Color color) {
if (!this->check_buffer_())
return;
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->get_width_internal() - 1;
this->y_high_ = this->get_height_internal() - 1;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
auto new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
break;
}
default: {
auto new_color = display::ColorUtil::color_to_565(color);
if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) {
// Upper and lower is equal can use quicker memset operation. Takes ~20ms.
memset(this->buffer_, (uint8_t) new_color, this->buffer_bytes_);
} else {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
auto len = this->buffer_bytes_ / 2;
while (len--) {
*ptr_16++ = new_color;
}
}
}
}
}
void MipiSpi::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) {
return;
}
if (!this->check_buffer_())
return;
size_t pos = (y * this->width_) + x;
switch (this->color_depth_) {
case display::COLOR_BITNESS_332: {
uint8_t new_color = display::ColorUtil::color_to_332(color);
if (this->buffer_[pos] == new_color)
return;
this->buffer_[pos] = new_color;
break;
}
case display::COLOR_BITNESS_565: {
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
uint16_t new_color = hi_byte | (lo_byte << 8); // big endian
if (ptr_16[pos] == new_color)
return;
ptr_16[pos] = new_color;
break;
}
default:
return;
}
// low and high watermark may speed up drawing from buffer
if (x < this->x_low_)
this->x_low_ = x;
if (y < this->y_low_)
this->y_low_ = y;
if (x > this->x_high_)
this->x_high_ = x;
if (y > this->y_high_)
this->y_high_ = y;
}
void MipiSpi::reset_params_() {
if (!this->is_ready())
return;
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
}
void MipiSpi::write_init_sequence_() {
size_t index = 0;
auto &vec = this->init_sequence_;
while (index != vec.size()) {
if (vec.size() - index < 2) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
uint8_t cmd = vec[index++];
uint8_t x = vec[index++];
if (x == DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
uint8_t num_args = x & 0x7F;
if (vec.size() - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence");
this->mark_failed();
return;
}
const auto *ptr = vec.data() + index;
this->write_command_(cmd, ptr, num_args);
index += num_args;
}
}
this->setup_complete_ = true;
ESP_LOGCONFIG(TAG, "MIPI SPI setup complete");
}
void MipiSpi::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
ESP_LOGVV(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += this->offset_width_;
x2 += this->offset_width_;
y1 += this->offset_height_;
y2 += this->offset_height_;
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
put16_be(buf, x1);
put16_be(buf + 2, x2);
this->write_command_(CASET, buf, sizeof buf);
}
void MipiSpi::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (!this->setup_complete_ || this->is_failed())
return;
if (w <= 0 || h <= 0)
return;
if (bitness != this->color_depth_ || big_endian != (this->bit_order_ == spi::BIT_ORDER_MSB_FIRST)) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
if (this->draw_from_origin_) {
auto stride = x_offset + w + x_pad;
for (int y = 0; y != h; y++) {
memcpy(this->buffer_ + ((y + y_start) * this->width_ + x_start) * 2,
ptr + ((y + y_offset) * stride + x_offset) * 2, w * 2);
}
ptr = this->buffer_;
w = this->width_;
h += y_start;
x_start = 0;
y_start = 0;
x_offset = 0;
y_offset = 0;
}
this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad);
}
void MipiSpi::write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
// deal with byte swapping
transfer_buffer[idx++] = (color_val & 0xF8); // Blue
transfer_buffer[idx++] = ((color_val & 0x7) << 5) | ((color_val & 0xE000) >> 11); // Green
transfer_buffer[idx++] = (color_val >> 5) & 0xF8; // Red
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
transfer_buffer[idx++] = color_val & 0xE0; // Red
transfer_buffer[idx++] = (color_val << 3) & 0xE0; // Green
transfer_buffer[idx++] = color_val << 6; // Blue
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride) {
stride -= w;
uint8_t transfer_buffer[6 * 256];
size_t idx = 0; // index into transfer_buffer
while (h-- != 0) {
for (auto x = w; x-- != 0;) {
auto color_val = *ptr++;
transfer_buffer[idx++] = (color_val & 0xE0) | ((color_val & 0x1C) >> 2);
transfer_buffer[idx++] = (color_val & 0x3) << 3;
if (idx == sizeof(transfer_buffer)) {
this->write_array(transfer_buffer, idx);
idx = 0;
}
}
ptr += stride;
}
if (idx != 0)
this->write_array(transfer_buffer, idx);
}
void MipiSpi::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad) {
this->set_addr_window_(x_start, y_start, x_start + w - 1, y_start + h - 1);
auto stride = x_offset + w + x_pad;
const auto *offset_ptr = ptr;
if (this->color_depth_ == display::COLOR_BITNESS_332) {
offset_ptr += y_offset * stride + x_offset;
} else {
stride *= 2;
offset_ptr += y_offset * stride + x_offset * 2;
}
switch (this->bus_width_) {
case 4:
this->enable();
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
// we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't
// bother
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, ptr, w * h * 2, 4);
} else {
this->write_cmd_addr_data(8, 0x32, 24, WDATA << 8, nullptr, 0, 4);
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 4);
offset_ptr += stride;
}
}
break;
case 8:
this->write_command_(WDATA);
this->enable();
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
this->write_cmd_addr_data(0, 0, 0, 0, ptr, w * h * 2, 8);
} else {
for (int y = 0; y != h; y++) {
this->write_cmd_addr_data(0, 0, 0, 0, offset_ptr, w * 2, 8);
offset_ptr += stride;
}
}
break;
default:
this->write_command_(WDATA);
this->enable();
if (this->color_depth_ == display::COLOR_BITNESS_565) {
// Source buffer is 16-bit RGB565
if (this->pixel_mode_ == PIXEL_MODE_18) {
// Convert RGB565 to RGB666
this->write_18_from_16_bit_(reinterpret_cast<const uint16_t *>(offset_ptr), w, h, stride / 2);
} else {
// Direct RGB565 output
if (x_offset == 0 && x_pad == 0 && y_offset == 0) {
this->write_array(ptr, w * h * 2);
} else {
for (int y = 0; y != h; y++) {
this->write_array(offset_ptr, w * 2);
offset_ptr += stride;
}
}
}
} else {
// Source buffer is 8-bit RGB332
if (this->pixel_mode_ == PIXEL_MODE_18) {
// Convert RGB332 to RGB666
this->write_18_from_8_bit_(offset_ptr, w, h, stride);
} else {
this->write_16_from_8_bit_(offset_ptr, w, h, stride);
}
break;
}
}
this->disable();
}
void MipiSpi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
if (this->bus_width_ == 4) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
this->disable();
} else if (this->bus_width_ == 8) {
this->dc_pin_->digital_write(false);
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, &cmd, 1, 8);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
this->enable();
this->write_cmd_addr_data(0, 0, 0, 0, bytes, len, 8);
this->disable();
}
} else {
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(cmd);
this->disable();
this->dc_pin_->digital_write(true);
if (len != 0) {
if (this->spi_16_) {
for (size_t i = 0; i != len; i++) {
this->enable();
this->write_byte(0);
this->write_byte(bytes[i]);
this->disable();
}
} else {
this->enable();
this->write_array(bytes, len);
this->disable();
}
}
}
}
void MipiSpi::dump_config() {
ESP_LOGCONFIG(TAG, "MIPI_SPI Display");
ESP_LOGCONFIG(TAG, " Model: %s", this->model_);
ESP_LOGCONFIG(TAG, " Width: %u", this->width_);
ESP_LOGCONFIG(TAG, " Height: %u", this->height_);
if (this->offset_width_ != 0)
ESP_LOGCONFIG(TAG, " Offset width: %u", this->offset_width_);
if (this->offset_height_ != 0)
ESP_LOGCONFIG(TAG, " Offset height: %u", this->offset_height_);
ESP_LOGCONFIG(TAG, " Swap X/Y: %s", YESNO(this->madctl_ & MADCTL_MV));
ESP_LOGCONFIG(TAG, " Mirror X: %s", YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)));
ESP_LOGCONFIG(TAG, " Mirror Y: %s", YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)));
ESP_LOGCONFIG(TAG, " Color depth: %d bits", this->color_depth_ == display::COLOR_BITNESS_565 ? 16 : 8);
ESP_LOGCONFIG(TAG, " Invert colors: %s", YESNO(this->invert_colors_));
ESP_LOGCONFIG(TAG, " Color order: %s", this->madctl_ & MADCTL_BGR ? "BGR" : "RGB");
ESP_LOGCONFIG(TAG, " Pixel mode: %s", this->pixel_mode_ == PIXEL_MODE_18 ? "18bit" : "16bit");
if (this->brightness_.has_value())
ESP_LOGCONFIG(TAG, " Brightness: %u", this->brightness_.value());
if (this->spi_16_)
ESP_LOGCONFIG(TAG, " SPI 16bit: YES");
ESP_LOGCONFIG(TAG, " Draw rounding: %u", this->draw_rounding_);
if (this->draw_from_origin_)
ESP_LOGCONFIG(TAG, " Draw from origin: YES");
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
ESP_LOGCONFIG(TAG, " SPI Mode: %d", this->mode_);
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", static_cast<unsigned>(this->data_rate_ / 1000000));
ESP_LOGCONFIG(TAG, " SPI Bus width: %d", this->bus_width_);
}
} // namespace mipi_spi
} // namespace esphome

View File

@@ -0,0 +1,171 @@
#pragma once
#include <utility>
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display_color_utils.h"
namespace esphome {
namespace mipi_spi {
constexpr static const char *const TAG = "display.mipi_spi";
static const uint8_t SW_RESET_CMD = 0x01;
static const uint8_t SLEEP_OUT = 0x11;
static const uint8_t NORON = 0x13;
static const uint8_t INVERT_OFF = 0x20;
static const uint8_t INVERT_ON = 0x21;
static const uint8_t ALL_ON = 0x23;
static const uint8_t WRAM = 0x24;
static const uint8_t MIPI = 0x26;
static const uint8_t DISPLAY_ON = 0x29;
static const uint8_t RASET = 0x2B;
static const uint8_t CASET = 0x2A;
static const uint8_t WDATA = 0x2C;
static const uint8_t TEON = 0x35;
static const uint8_t MADCTL_CMD = 0x36;
static const uint8_t PIXFMT = 0x3A;
static const uint8_t BRIGHTNESS = 0x51;
static const uint8_t SWIRE1 = 0x5A;
static const uint8_t SWIRE2 = 0x5B;
static const uint8_t PAGESEL = 0xFE;
static const uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static const uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static const uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static const uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static const uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static const uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static const uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static const uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian.
static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
buf[1] = value;
}
enum PixelMode {
PIXEL_MODE_16,
PIXEL_MODE_18,
};
class MipiSpi : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> {
public:
MipiSpi(size_t width, size_t height, int16_t offset_width, int16_t offset_height, display::ColorBitness color_depth)
: width_(width),
height_(height),
offset_width_(offset_width),
offset_height_(offset_height),
color_depth_(color_depth) {}
void set_model(const char *model) { this->model_ = model; }
void update() override;
void setup() override;
display::ColorOrder get_color_mode() {
return this->madctl_ & MADCTL_BGR ? display::COLOR_ORDER_BGR : display::COLOR_ORDER_RGB;
}
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void set_enable_pins(std::vector<GPIOPin *> enable_pins) { this->enable_pins_ = std::move(enable_pins); }
void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; }
void set_invert_colors(bool invert_colors) {
this->invert_colors_ = invert_colors;
this->reset_params_();
}
void set_brightness(uint8_t brightness) {
this->brightness_ = brightness;
this->reset_params_();
}
void set_draw_from_origin(bool draw_from_origin) { this->draw_from_origin_ = draw_from_origin; }
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void dump_config() override;
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
bool can_proceed() override { return this->setup_complete_; }
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
void set_spi_16(bool spi_16) { this->spi_16_ = spi_16; }
protected:
bool check_buffer_() {
if (this->is_failed())
return false;
if (this->buffer_ != nullptr)
return true;
auto bytes_per_pixel = this->color_depth_ == display::COLOR_BITNESS_565 ? 2 : 1;
this->init_internal_(this->width_ * this->height_ * bytes_per_pixel);
if (this->buffer_ == nullptr) {
this->mark_failed();
return false;
}
this->buffer_bytes_ = this->width_ * this->height_ * bytes_per_pixel;
return true;
}
void fill(Color color) override;
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
void write_18_from_16_bit_(const uint16_t *ptr, size_t w, size_t h, size_t stride);
void write_18_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_16_from_8_bit_(const uint8_t *ptr, size_t w, size_t h, size_t stride);
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
int x_pad);
/**
* the RM67162 in quad SPI mode seems to work like this (not in the datasheet, this is deduced from the
* sample code.)
*
* Immediately after enabling /CS send 4 bytes in single-dataline SPI mode:
* 0: either 0x2 or 0x32. The first indicates that any subsequent data bytes after the initial 4 will be
* sent in 1-dataline SPI. The second indicates quad mode.
* 1: 0x00
* 2: The command (register address) byte.
* 3: 0x00
*
* This is followed by zero or more data bytes in either 1-wire or 4-wire mode, depending on the first byte.
* At the conclusion of the write, de-assert /CS.
*
* @param cmd
* @param bytes
* @param len
*/
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len);
void write_command_(uint8_t cmd, uint8_t data) { this->write_command_(cmd, &data, 1); }
void write_command_(uint8_t cmd) { this->write_command_(cmd, &cmd, 0); }
void reset_params_();
void write_init_sequence_();
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
GPIOPin *reset_pin_{nullptr};
std::vector<GPIOPin *> enable_pins_{};
GPIOPin *dc_pin_{nullptr};
uint16_t x_low_{1};
uint16_t y_low_{1};
uint16_t x_high_{0};
uint16_t y_high_{0};
bool setup_complete_{};
bool invert_colors_{};
size_t width_;
size_t height_;
int16_t offset_width_;
int16_t offset_height_;
size_t buffer_bytes_{0};
display::ColorBitness color_depth_;
PixelMode pixel_mode_{PIXEL_MODE_16};
uint8_t bus_width_{};
bool spi_16_{};
uint8_t madctl_{};
bool draw_from_origin_{false};
unsigned draw_rounding_{2};
optional<uint8_t> brightness_{};
const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{};
};
} // namespace mipi_spi
} // namespace esphome

View File

@@ -0,0 +1,65 @@
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
import esphome.config_validation as cv
from esphome.const import CONF_HEIGHT, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, CONF_WIDTH
from .. import CONF_NATIVE_HEIGHT, CONF_NATIVE_WIDTH
MADCTL_MY = 0x80 # Bit 7 Bottom to top
MADCTL_MX = 0x40 # Bit 6 Right to left
MADCTL_MV = 0x20 # Bit 5 Reverse Mode
MADCTL_ML = 0x10 # Bit 4 LCD refresh Bottom to top
MADCTL_RGB = 0x00 # Bit 3 Red-Green-Blue pixel order
MADCTL_BGR = 0x08 # Bit 3 Blue-Green-Red pixel order
MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left
# These bits are used instead of the above bits on some chips, where using MX and MY results in incorrect
# partial updates.
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
MADCTL_YFLIP = 0x01 # Mirror the display vertically
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
def delay(ms):
return DELAY_FLAG, ms
class DriverChip:
models = {}
def __init__(
self,
name: str,
modes=(TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL),
initsequence=None,
**defaults,
):
name = name.upper()
self.name = name
self.modes = modes
self.initsequence = initsequence
self.defaults = defaults
DriverChip.models[name] = self
def extend(self, name, **kwargs):
defaults = self.defaults.copy()
if (
CONF_WIDTH in defaults
and CONF_OFFSET_WIDTH in kwargs
and CONF_NATIVE_WIDTH not in defaults
):
defaults[CONF_NATIVE_WIDTH] = defaults[CONF_WIDTH]
if (
CONF_HEIGHT in defaults
and CONF_OFFSET_HEIGHT in kwargs
and CONF_NATIVE_HEIGHT not in defaults
):
defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT]
defaults.update(kwargs)
return DriverChip(name, self.modes, initsequence=self.initsequence, **defaults)
def get_default(self, key, fallback=False):
return self.defaults.get(key, fallback)
def option(self, name, fallback=False):
return cv.Optional(name, default=self.get_default(name, fallback))

View File

@@ -0,0 +1,72 @@
from esphome.components.spi import TYPE_QUAD
from .. import MODE_RGB
from . import DriverChip, delay
from .commands import MIPI, NORON, PAGESEL, PIXFMT, SLPOUT, SWIRE1, SWIRE2, TEON, WRAM
DriverChip(
"T-DISPLAY-S3-AMOLED",
width=240,
height=536,
cs_pin=6,
reset_pin=17,
enable_pin=38,
bus_mode=TYPE_QUAD,
brightness=0xD0,
color_order=MODE_RGB,
initsequence=(SLPOUT,), # Requires early SLPOUT
)
DriverChip(
name="T-DISPLAY-S3-AMOLED-PLUS",
width=240,
height=536,
cs_pin=6,
reset_pin=17,
dc_pin=7,
enable_pin=38,
data_rate="40MHz",
brightness=0xD0,
color_order=MODE_RGB,
initsequence=(
(PAGESEL, 4),
(0x6A, 0x00),
(PAGESEL, 0x05),
(PAGESEL, 0x07),
(0x07, 0x4F),
(PAGESEL, 0x01),
(0x2A, 0x02),
(0x2B, 0x73),
(PAGESEL, 0x0A),
(0x29, 0x10),
(PAGESEL, 0x00),
(0x53, 0x20),
(TEON, 0x00),
(PIXFMT, 0x75),
(0xC4, 0x80),
),
)
RM690B0 = DriverChip(
"RM690B0",
brightness=0xD0,
color_order=MODE_RGB,
width=480,
height=600,
initsequence=(
(PAGESEL, 0x20),
(MIPI, 0x0A),
(WRAM, 0x80),
(SWIRE1, 0x51),
(SWIRE2, 0x2E),
(PAGESEL, 0x00),
(0xC2, 0x00),
delay(10),
(TEON, 0x00),
(NORON,),
),
)
T4_S3_AMOLED = RM690B0.extend("T4-S3", width=450, offset_width=16, bus_mode=TYPE_QUAD)
models = {}

View File

@@ -0,0 +1,82 @@
# MIPI DBI commands
NOP = 0x00
SWRESET = 0x01
RDDID = 0x04
RDDST = 0x09
RDMODE = 0x0A
RDMADCTL = 0x0B
RDPIXFMT = 0x0C
RDIMGFMT = 0x0D
RDSELFDIAG = 0x0F
SLEEP_IN = 0x10
SLPIN = 0x10
SLEEP_OUT = 0x11
SLPOUT = 0x11
PTLON = 0x12
NORON = 0x13
INVERT_OFF = 0x20
INVOFF = 0x20
INVERT_ON = 0x21
INVON = 0x21
ALL_ON = 0x23
WRAM = 0x24
GAMMASET = 0x26
MIPI = 0x26
DISPOFF = 0x28
DISPON = 0x29
CASET = 0x2A
PASET = 0x2B
RASET = 0x2B
RAMWR = 0x2C
WDATA = 0x2C
RAMRD = 0x2E
PTLAR = 0x30
VSCRDEF = 0x33
TEON = 0x35
MADCTL = 0x36
MADCTL_CMD = 0x36
VSCRSADD = 0x37
IDMOFF = 0x38
IDMON = 0x39
COLMOD = 0x3A
PIXFMT = 0x3A
GETSCANLINE = 0x45
BRIGHTNESS = 0x51
WRDISBV = 0x51
RDDISBV = 0x52
WRCTRLD = 0x53
SWIRE1 = 0x5A
SWIRE2 = 0x5B
IFMODE = 0xB0
FRMCTR1 = 0xB1
FRMCTR2 = 0xB2
FRMCTR3 = 0xB3
INVCTR = 0xB4
DFUNCTR = 0xB6
ETMOD = 0xB7
PWCTR1 = 0xC0
PWCTR2 = 0xC1
PWCTR3 = 0xC2
PWCTR4 = 0xC3
PWCTR5 = 0xC4
VMCTR1 = 0xC5
IFCTR = 0xC6
VMCTR2 = 0xC7
GMCTR = 0xC8
SETEXTC = 0xC8
PWSET = 0xD0
VMCTR = 0xD1
PWSETN = 0xD2
RDID4 = 0xD3
RDINDEX = 0xD9
RDID1 = 0xDA
RDID2 = 0xDB
RDID3 = 0xDC
RDIDX = 0xDD
GMCTRP1 = 0xE0
GMCTRN1 = 0xE1
CSCON = 0xF0
PWCTR6 = 0xF6
ADJCTL3 = 0xF7
PAGESEL = 0xFE

View File

@@ -0,0 +1,10 @@
from .ili import ILI9341
ILI9341.extend(
"ESP32-2432S028",
data_rate="40MHz",
cs_pin=15,
dc_pin=2,
)
models = {}

View File

@@ -0,0 +1,749 @@
from esphome.components.spi import TYPE_OCTAL
from .. import MODE_RGB
from . import DriverChip, delay
from .commands import (
ADJCTL3,
CSCON,
DFUNCTR,
ETMOD,
FRMCTR1,
FRMCTR2,
FRMCTR3,
GAMMASET,
GMCTR,
GMCTRN1,
GMCTRP1,
IDMOFF,
IFCTR,
IFMODE,
INVCTR,
NORON,
PWCTR1,
PWCTR2,
PWCTR3,
PWCTR4,
PWCTR5,
PWSET,
PWSETN,
SETEXTC,
SWRESET,
VMCTR,
VMCTR1,
VMCTR2,
VSCRSADD,
)
DriverChip(
"M5CORE",
width=320,
height=240,
cs_pin=14,
dc_pin=27,
reset_pin=33,
initsequence=(
(SETEXTC, 0xFF, 0x93, 0x42),
(PWCTR1, 0x12, 0x12),
(PWCTR2, 0x03),
(VMCTR1, 0xF2),
(IFMODE, 0xE0),
(0xF6, 0x01, 0x00, 0x00),
(
GMCTRP1,
0x00,
0x0C,
0x11,
0x04,
0x11,
0x08,
0x37,
0x89,
0x4C,
0x06,
0x0C,
0x0A,
0x2E,
0x34,
0x0F,
),
(
GMCTRN1,
0x00,
0x0B,
0x11,
0x05,
0x13,
0x09,
0x33,
0x67,
0x48,
0x07,
0x0E,
0x0B,
0x2E,
0x33,
0x0F,
),
(DFUNCTR, 0x08, 0x82, 0x1D, 0x04),
(IDMOFF,),
),
)
ILI9341 = DriverChip(
"ILI9341",
mirror_x=True,
width=240,
height=320,
initsequence=(
(0xEF, 0x03, 0x80, 0x02),
(0xCF, 0x00, 0xC1, 0x30),
(0xED, 0x64, 0x03, 0x12, 0x81),
(0xE8, 0x85, 0x00, 0x78),
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02),
(0xF7, 0x20),
(0xEA, 0x00, 0x00),
(PWCTR1, 0x23),
(PWCTR2, 0x10),
(VMCTR1, 0x3E, 0x28),
(VMCTR2, 0x86),
(VSCRSADD, 0x00),
(FRMCTR1, 0x00, 0x18),
(DFUNCTR, 0x08, 0x82, 0x27),
(0xF2, 0x00),
(GAMMASET, 0x01),
(
GMCTRP1,
0x0F,
0x31,
0x2B,
0x0C,
0x0E,
0x08,
0x4E,
0xF1,
0x37,
0x07,
0x10,
0x03,
0x0E,
0x09,
0x00,
),
(
GMCTRN1,
0x00,
0x0E,
0x14,
0x03,
0x11,
0x07,
0x31,
0xC1,
0x48,
0x08,
0x0F,
0x0C,
0x31,
0x36,
0x0F,
),
),
)
DriverChip(
"ILI9481",
mirror_x=True,
width=320,
height=480,
use_axis_flips=True,
initsequence=(
(PWSET, 0x07, 0x42, 0x18),
(VMCTR, 0x00, 0x07, 0x10),
(PWSETN, 0x01, 0x02),
(PWCTR1, 0x10, 0x3B, 0x00, 0x02, 0x11),
(VMCTR1, 0x03),
(IFCTR, 0x83),
(GMCTR, 0x32, 0x36, 0x45, 0x06, 0x16, 0x37, 0x75, 0x77, 0x54, 0x0C, 0x00),
),
)
DriverChip(
"ILI9486",
mirror_x=True,
width=320,
height=480,
initsequence=(
(PWCTR3, 0x44),
(VMCTR1, 0x00, 0x00, 0x00, 0x00),
(
GMCTRP1,
0x0F,
0x1F,
0x1C,
0x0C,
0x0F,
0x08,
0x48,
0x98,
0x37,
0x0A,
0x13,
0x04,
0x11,
0x0D,
0x00,
),
(
GMCTRN1,
0x0F,
0x32,
0x2E,
0x0B,
0x0D,
0x05,
0x47,
0x75,
0x37,
0x06,
0x10,
0x03,
0x24,
0x20,
0x00,
),
),
)
DriverChip(
"ILI9488",
width=320,
height=480,
pixel_mode="18bit",
initsequence=(
(
GMCTRP1,
0x0F,
0x24,
0x1C,
0x0A,
0x0F,
0x08,
0x43,
0x88,
0x32,
0x0F,
0x10,
0x06,
0x0F,
0x07,
0x00,
),
(
GMCTRN1,
0x0F,
0x38,
0x30,
0x09,
0x0F,
0x0F,
0x4E,
0x77,
0x3C,
0x07,
0x10,
0x05,
0x23,
0x1B,
0x00,
),
(PWCTR1, 0x17, 0x15),
(PWCTR2, 0x41),
(VMCTR1, 0x00, 0x12, 0x80),
(IFMODE, 0x00),
(FRMCTR1, 0xA0),
(INVCTR, 0x02),
(0xE9, 0x00),
(ADJCTL3, 0xA9, 0x51, 0x2C, 0x82),
),
)
ILI9488_A = DriverChip(
"ILI9488_A",
width=320,
height=480,
invert_colors=False,
pixel_mode="18bit",
mirror_x=True,
initsequence=(
(
GMCTRP1,
0x00,
0x03,
0x09,
0x08,
0x16,
0x0A,
0x3F,
0x78,
0x4C,
0x09,
0x0A,
0x08,
0x16,
0x1A,
0x0F,
),
(
GMCTRN1,
0x00,
0x16,
0x19,
0x03,
0x0F,
0x05,
0x32,
0x45,
0x46,
0x04,
0x0E,
0x0D,
0x35,
0x37,
0x0F,
),
(PWCTR1, 0x17, 0x15),
(PWCTR2, 0x41),
(VMCTR1, 0x00, 0x12, 0x80),
(IFMODE, 0x00),
(FRMCTR1, 0xA0),
(INVCTR, 0x02),
(DFUNCTR, 0x02, 0x02),
(0xE9, 0x00),
(ADJCTL3, 0xA9, 0x51, 0x2C, 0x82),
),
)
ST7796 = DriverChip(
"ST7796",
mirror_x=True,
width=320,
height=480,
initsequence=(
(SWRESET,),
(CSCON, 0xC3),
(CSCON, 0x96),
(VMCTR1, 0x1C),
(IFMODE, 0x80),
(INVCTR, 0x01),
(DFUNCTR, 0x80, 0x02, 0x3B),
(ETMOD, 0xC6),
(CSCON, 0x69),
(CSCON, 0x3C),
),
)
DriverChip(
"S3BOX",
width=320,
height=240,
mirror_x=True,
mirror_y=True,
invert_colors=False,
data_rate="40MHz",
dc_pin=4,
cs_pin=5,
# reset_pin={CONF_INVERTED: True, CONF_NUMBER: 48},
initsequence=(
(0xEF, 0x03, 0x80, 0x02),
(0xCF, 0x00, 0xC1, 0x30),
(0xED, 0x64, 0x03, 0x12, 0x81),
(0xE8, 0x85, 0x00, 0x78),
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02),
(0xF7, 0x20),
(0xEA, 0x00, 0x00),
(PWCTR1, 0x23),
(PWCTR2, 0x10),
(VMCTR1, 0x3E, 0x28),
(VMCTR2, 0x86),
(VSCRSADD, 0x00),
(FRMCTR1, 0x00, 0x18),
(DFUNCTR, 0x08, 0x82, 0x27),
(0xF2, 0x00),
(GAMMASET, 0x01),
(
GMCTRP1,
0x0F,
0x31,
0x2B,
0x0C,
0x0E,
0x08,
0x4E,
0xF1,
0x37,
0x07,
0x10,
0x03,
0x0E,
0x09,
0x00,
),
(
GMCTRN1,
0x00,
0x0E,
0x14,
0x03,
0x11,
0x07,
0x31,
0xC1,
0x48,
0x08,
0x0F,
0x0C,
0x31,
0x36,
0x0F,
),
),
)
DriverChip(
"S3BOXLITE",
mirror_x=True,
color_order=MODE_RGB,
width=320,
height=240,
cs_pin=5,
dc_pin=4,
reset_pin=48,
initsequence=(
(0xEF, 0x03, 0x80, 0x02),
(0xCF, 0x00, 0xC1, 0x30),
(0xED, 0x64, 0x03, 0x12, 0x81),
(0xE8, 0x85, 0x00, 0x78),
(0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02),
(0xF7, 0x20),
(0xEA, 0x00, 0x00),
(PWCTR1, 0x23),
(PWCTR2, 0x10),
(VMCTR1, 0x3E, 0x28),
(VMCTR2, 0x86),
(VSCRSADD, 0x00),
(FRMCTR1, 0x00, 0x18),
(DFUNCTR, 0x08, 0x82, 0x27),
(0xF2, 0x00),
(GAMMASET, 0x01),
(
GMCTRP1,
0xF0,
0x09,
0x0B,
0x06,
0x04,
0x15,
0x2F,
0x54,
0x42,
0x3C,
0x17,
0x14,
0x18,
0x1B,
),
(
GMCTRN1,
0xE0,
0x09,
0x0B,
0x06,
0x04,
0x03,
0x2B,
0x43,
0x42,
0x3B,
0x16,
0x14,
0x17,
0x1B,
),
),
)
ST7789V = DriverChip(
"ST7789V",
width=240,
height=320,
initsequence=(
(DFUNCTR, 0x0A, 0x82),
(FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33),
(ETMOD, 0x35),
(0xBB, 0x28),
(PWCTR1, 0x0C),
(PWCTR3, 0x01, 0xFF),
(PWCTR4, 0x10),
(PWCTR5, 0x20),
(IFCTR, 0x0F),
(PWSET, 0xA4, 0xA1),
(
GMCTRP1,
0xD0,
0x00,
0x02,
0x07,
0x0A,
0x28,
0x32,
0x44,
0x42,
0x06,
0x0E,
0x12,
0x14,
0x17,
),
(
GMCTRN1,
0xD0,
0x00,
0x02,
0x07,
0x0A,
0x28,
0x31,
0x54,
0x47,
0x0E,
0x1C,
0x17,
0x1B,
0x1E,
),
),
)
DriverChip(
"GC9A01A",
mirror_x=True,
width=240,
height=240,
initsequence=(
(0xEF,),
(0xEB, 0x14),
(0xFE,),
(0xEF,),
(0xEB, 0x14),
(0x84, 0x40),
(0x85, 0xFF),
(0x86, 0xFF),
(0x87, 0xFF),
(0x88, 0x0A),
(0x89, 0x21),
(0x8A, 0x00),
(0x8B, 0x80),
(0x8C, 0x01),
(0x8D, 0x01),
(0x8E, 0xFF),
(0x8F, 0xFF),
(0xB6, 0x00, 0x00),
(0x90, 0x08, 0x08, 0x08, 0x08),
(0xBD, 0x06),
(0xBC, 0x00),
(0xFF, 0x60, 0x01, 0x04),
(0xC3, 0x13),
(0xC4, 0x13),
(0xF9, 0x22),
(0xBE, 0x11),
(0xE1, 0x10, 0x0E),
(0xDF, 0x21, 0x0C, 0x02),
(0xF0, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A),
(0xF1, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F),
(0xF2, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A),
(0xF3, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F),
(0xED, 0x1B, 0x0B),
(0xAE, 0x77),
(0xCD, 0x63),
(0xE8, 0x34),
(
0x62,
0x18,
0x0D,
0x71,
0xED,
0x70,
0x70,
0x18,
0x0F,
0x71,
0xEF,
0x70,
0x70,
),
(
0x63,
0x18,
0x11,
0x71,
0xF1,
0x70,
0x70,
0x18,
0x13,
0x71,
0xF3,
0x70,
0x70,
),
(0x64, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07),
(0x66, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00),
(0x67, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98),
(0x74, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00),
(0x98, 0x3E, 0x07),
(0x35,),
),
)
DriverChip(
"GC9D01N",
width=160,
height=160,
initsequence=(
(0xFE,),
(0xEF,),
(0x80, 0xFF),
(0x81, 0xFF),
(0x82, 0xFF),
(0x83, 0xFF),
(0x84, 0xFF),
(0x85, 0xFF),
(0x86, 0xFF),
(0x87, 0xFF),
(0x88, 0xFF),
(0x89, 0xFF),
(0x8A, 0xFF),
(0x8B, 0xFF),
(0x8C, 0xFF),
(0x8D, 0xFF),
(0x8E, 0xFF),
(0x8F, 0xFF),
(0x3A, 0x05),
(0xEC, 0x01),
(0x74, 0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00),
(0x98, 0x3E),
(0x99, 0x3E),
(0xB5, 0x0D, 0x0D),
(0x60, 0x38, 0x0F, 0x79, 0x67),
(0x61, 0x38, 0x11, 0x79, 0x67),
(0x64, 0x38, 0x17, 0x71, 0x5F, 0x79, 0x67),
(0x65, 0x38, 0x13, 0x71, 0x5B, 0x79, 0x67),
(0x6A, 0x00, 0x00),
(0x6C, 0x22, 0x02, 0x22, 0x02, 0x22, 0x22, 0x50),
(
0x6E,
0x03,
0x03,
0x01,
0x01,
0x00,
0x00,
0x0F,
0x0F,
0x0D,
0x0D,
0x0B,
0x0B,
0x09,
0x09,
0x00,
0x00,
0x00,
0x00,
0x0A,
0x0A,
0x0C,
0x0C,
0x0E,
0x0E,
0x10,
0x10,
0x00,
0x00,
0x02,
0x02,
0x04,
0x04,
),
(0xBF, 0x01),
(0xF9, 0x40),
(0x9B, 0x3B, 0x93, 0x33, 0x7F, 0x00),
(0x7E, 0x30),
(0x70, 0x0D, 0x02, 0x08, 0x0D, 0x02, 0x08),
(0x71, 0x0D, 0x02, 0x08),
(0x91, 0x0E, 0x09),
(0xC3, 0x19, 0xC4, 0x19, 0xC9, 0x3C),
(0xF0, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3E),
(0xF1, 0x56, 0xA8, 0x7F, 0x33, 0x34, 0x5F),
(0xF2, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3A),
(0xF3, 0x52, 0xA4, 0x7F, 0x33, 0x34, 0xDF),
),
)
DriverChip(
"ST7735",
color_order=MODE_RGB,
width=128,
height=160,
initsequence=(
SWRESET,
delay(10),
(FRMCTR1, 0x01, 0x2C, 0x2D),
(FRMCTR2, 0x01, 0x2C, 0x2D),
(FRMCTR3, 0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D),
(INVCTR, 0x07),
(PWCTR1, 0xA2, 0x02, 0x84),
(PWCTR2, 0xC5),
(PWCTR3, 0x0A, 0x00),
(PWCTR4, 0x8A, 0x2A),
(PWCTR5, 0x8A, 0xEE),
(VMCTR1, 0x0E),
(
GMCTRP1,
0x02,
0x1C,
0x07,
0x12,
0x37,
0x32,
0x29,
0x2D,
0x29,
0x25,
0x2B,
0x39,
0x00,
0x01,
0x03,
0x10,
),
(
GMCTRN1,
0x03,
0x1D,
0x07,
0x06,
0x2E,
0x2C,
0x29,
0x2D,
0x2E,
0x2E,
0x37,
0x3F,
0x00,
0x00,
0x02,
0x10,
),
NORON,
),
)
ST7796.extend(
"WT32-SC01-PLUS",
bus_mode=TYPE_OCTAL,
mirror_x=True,
reset_pin=4,
dc_pin=0,
invert_colors=True,
)
models = {}

View File

@@ -0,0 +1,260 @@
from esphome.components.spi import TYPE_QUAD
import esphome.config_validation as cv
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
from .. import MODE_RGB
from . import DriverChip
AXS15231 = DriverChip(
"AXS15231",
draw_rounding=8,
swap_xy=cv.UNDEFINED,
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
initsequence=(
(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A, 0xA5),
(0xC1, 0x33),
(0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
),
)
AXS15231.extend(
"JC3248W535",
width=320,
height=480,
cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True},
data_rate="40MHz",
)
DriverChip(
"JC3636W518",
height=360,
width=360,
offset_height=1,
draw_rounding=1,
cs_pin=10,
reset_pin=47,
invert_colors=True,
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
data_rate="40MHz",
initsequence=(
(0xF0, 0x08),
(0xF2, 0x08),
(0x9B, 0x51),
(0x86, 0x53),
(0xF2, 0x80),
(0xF0, 0x00),
(0xF0, 0x01),
(0xF1, 0x01),
(0xB0, 0x54),
(0xB1, 0x3F),
(0xB2, 0x2A),
(0xB4, 0x46),
(0xB5, 0x34),
(0xB6, 0xD5),
(0xB7, 0x30),
(0xBA, 0x00),
(0xBB, 0x08),
(0xBC, 0x08),
(0xBD, 0x00),
(0xC0, 0x80),
(0xC1, 0x10),
(0xC2, 0x37),
(0xC3, 0x80),
(0xC4, 0x10),
(0xC5, 0x37),
(0xC6, 0xA9),
(0xC7, 0x41),
(0xC8, 0x51),
(0xC9, 0xA9),
(0xCA, 0x41),
(0xCB, 0x51),
(0xD0, 0x91),
(0xD1, 0x68),
(0xD2, 0x69),
(0xF5, 0x00, 0xA5),
(0xDD, 0x3F),
(0xDE, 0x3F),
(0xF1, 0x10),
(0xF0, 0x00),
(0xF0, 0x02),
(
0xE0,
0x70,
0x09,
0x12,
0x0C,
0x0B,
0x27,
0x38,
0x54,
0x4E,
0x19,
0x15,
0x15,
0x2C,
0x2F,
),
(
0xE1,
0x70,
0x08,
0x11,
0x0C,
0x0B,
0x27,
0x38,
0x43,
0x4C,
0x18,
0x14,
0x14,
0x2B,
0x2D,
),
(0xF0, 0x10),
(0xF3, 0x10),
(0xE0, 0x08),
(0xE1, 0x00),
(0xE2, 0x00),
(0xE3, 0x00),
(0xE4, 0xE0),
(0xE5, 0x06),
(0xE6, 0x21),
(0xE7, 0x00),
(0xE8, 0x05),
(0xE9, 0x82),
(0xEA, 0xDF),
(0xEB, 0x89),
(0xEC, 0x20),
(0xED, 0x14),
(0xEE, 0xFF),
(0xEF, 0x00),
(0xF8, 0xFF),
(0xF9, 0x00),
(0xFA, 0x00),
(0xFB, 0x30),
(0xFC, 0x00),
(0xFD, 0x00),
(0xFE, 0x00),
(0xFF, 0x00),
(0x60, 0x42),
(0x61, 0xE0),
(0x62, 0x40),
(0x63, 0x40),
(0x64, 0x02),
(0x65, 0x00),
(0x66, 0x40),
(0x67, 0x03),
(0x68, 0x00),
(0x69, 0x00),
(0x6A, 0x00),
(0x6B, 0x00),
(0x70, 0x42),
(0x71, 0xE0),
(0x72, 0x40),
(0x73, 0x40),
(0x74, 0x02),
(0x75, 0x00),
(0x76, 0x40),
(0x77, 0x03),
(0x78, 0x00),
(0x79, 0x00),
(0x7A, 0x00),
(0x7B, 0x00),
(0x80, 0x48),
(0x81, 0x00),
(0x82, 0x05),
(0x83, 0x02),
(0x84, 0xDD),
(0x85, 0x00),
(0x86, 0x00),
(0x87, 0x00),
(0x88, 0x48),
(0x89, 0x00),
(0x8A, 0x07),
(0x8B, 0x02),
(0x8C, 0xDF),
(0x8D, 0x00),
(0x8E, 0x00),
(0x8F, 0x00),
(0x90, 0x48),
(0x91, 0x00),
(0x92, 0x09),
(0x93, 0x02),
(0x94, 0xE1),
(0x95, 0x00),
(0x96, 0x00),
(0x97, 0x00),
(0x98, 0x48),
(0x99, 0x00),
(0x9A, 0x0B),
(0x9B, 0x02),
(0x9C, 0xE3),
(0x9D, 0x00),
(0x9E, 0x00),
(0x9F, 0x00),
(0xA0, 0x48),
(0xA1, 0x00),
(0xA2, 0x04),
(0xA3, 0x02),
(0xA4, 0xDC),
(0xA5, 0x00),
(0xA6, 0x00),
(0xA7, 0x00),
(0xA8, 0x48),
(0xA9, 0x00),
(0xAA, 0x06),
(0xAB, 0x02),
(0xAC, 0xDE),
(0xAD, 0x00),
(0xAE, 0x00),
(0xAF, 0x00),
(0xB0, 0x48),
(0xB1, 0x00),
(0xB2, 0x08),
(0xB3, 0x02),
(0xB4, 0xE0),
(0xB5, 0x00),
(0xB6, 0x00),
(0xB7, 0x00),
(0xB8, 0x48),
(0xB9, 0x00),
(0xBA, 0x0A),
(0xBB, 0x02),
(0xBC, 0xE2),
(0xBD, 0x00),
(0xBE, 0x00),
(0xBF, 0x00),
(0xC0, 0x12),
(0xC1, 0xAA),
(0xC2, 0x65),
(0xC3, 0x74),
(0xC4, 0x47),
(0xC5, 0x56),
(0xC6, 0x00),
(0xC7, 0x88),
(0xC8, 0x99),
(0xC9, 0x33),
(0xD0, 0x21),
(0xD1, 0xAA),
(0xD2, 0x65),
(0xD3, 0x74),
(0xD4, 0x47),
(0xD5, 0x56),
(0xD6, 0x00),
(0xD7, 0x88),
(0xD8, 0x99),
(0xD9, 0x33),
(0xF3, 0x01),
(0xF0, 0x00),
(0xF0, 0x01),
(0xF1, 0x01),
(0xA0, 0x0B),
(0xA3, 0x2A),
(0xA5, 0xC3),
),
)
models = {}

View File

@@ -0,0 +1,15 @@
from .ili import ST7789V
ST7789V.extend(
"LANBON-L8",
width=240,
height=320,
mirror_x=True,
mirror_y=True,
data_rate="80MHz",
cs_pin=22,
dc_pin=21,
reset_pin=18,
)
models = {}

View File

@@ -0,0 +1,60 @@
from esphome.components.spi import TYPE_OCTAL
from .. import MODE_BGR
from .ili import ST7789V, ST7796
ST7789V.extend(
"T-EMBED",
width=170,
height=320,
offset_width=35,
color_order=MODE_BGR,
invert_colors=True,
draw_rounding=1,
cs_pin=10,
dc_pin=13,
reset_pin=9,
data_rate="80MHz",
)
ST7789V.extend(
"T-DISPLAY",
height=240,
width=135,
offset_width=52,
offset_height=40,
draw_rounding=1,
cs_pin=5,
dc_pin=16,
invert_colors=True,
)
ST7789V.extend(
"T-DISPLAY-S3",
height=320,
width=170,
offset_width=35,
color_order=MODE_BGR,
invert_colors=True,
draw_rounding=1,
dc_pin=7,
cs_pin=6,
reset_pin=5,
enable_pin=[9, 15],
data_rate="10MHz",
bus_mode=TYPE_OCTAL,
)
ST7796.extend(
"T-DISPLAY-S3-PRO",
width=222,
height=480,
offset_width=49,
draw_rounding=1,
cs_pin=39,
reset_pin=47,
dc_pin=9,
backlight_pin=48,
invert_colors=True,
)
models = {}

View File

@@ -0,0 +1,139 @@
from . import DriverChip
from .ili import ILI9488_A
DriverChip(
"WAVESHARE-4-TFT",
width=320,
height=480,
invert_colors=True,
spi_16=True,
initsequence=(
(
0xF9,
0x00,
0x08,
),
(
0xC0,
0x19,
0x1A,
),
(
0xC1,
0x45,
0x00,
),
(
0xC2,
0x33,
),
(
0xC5,
0x00,
0x28,
),
(
0xB1,
0xA0,
0x11,
),
(
0xB4,
0x02,
),
(
0xB6,
0x00,
0x42,
0x3B,
),
(
0xB7,
0x07,
),
(
0xE0,
0x1F,
0x25,
0x22,
0x0B,
0x06,
0x0A,
0x4E,
0xC6,
0x39,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
),
(
0xE1,
0x1F,
0x3F,
0x3F,
0x0F,
0x1F,
0x0F,
0x46,
0x49,
0x31,
0x05,
0x09,
0x03,
0x1C,
0x1A,
0x00,
),
(
0xF1,
0x36,
0x04,
0x00,
0x3C,
0x0F,
0x0F,
0xA4,
0x02,
),
(
0xF2,
0x18,
0xA3,
0x12,
0x02,
0x32,
0x12,
0xFF,
0x32,
0x00,
),
(
0xF4,
0x40,
0x00,
0x08,
0x91,
0x04,
),
(
0xF8,
0x21,
0x04,
),
),
)
ILI9488_A.extend(
"PICO-RESTOUCH-LCD-3.5",
spi_16=True,
pixel_mode="16bit",
mirror_x=True,
dc_pin=33,
cs_pin=34,
reset_pin=40,
data_rate="20MHz",
invert_colors=True,
)

View File

@@ -1,7 +1,6 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@RubyBailey"]
AUTO_LOAD = ["climate_ir"]
@@ -44,9 +43,8 @@ VERTICAL_DIRECTIONS = {
}
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MitsubishiClimate).extend(
{
cv.GenerateID(): cv.declare_id(MitsubishiClimate),
cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE),
cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean,
cv.Optional(CONF_SUPPORTS_FAN_ONLY, default=False): cv.boolean,
@@ -61,8 +59,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await climate_ir.register_climate_ir(var, config)
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_fan_mode(config[CONF_SET_FAN_MODE]))
cg.add(var.set_supports_dry(config[CONF_SUPPORTS_DRY]))

View File

@@ -1,6 +1,7 @@
#include "modbus.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/application.h"
namespace esphome {
namespace modbus {
@@ -13,7 +14,7 @@ void Modbus::setup() {
}
}
void Modbus::loop() {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
while (this->available()) {
uint8_t byte;

Some files were not shown because too many files have changed in this diff Show More