mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			38 Commits
		
	
	
		
			2025.5.0b1
			...
			2025.5.0b4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					937fe393a1 | ||
| 
						 | 
					4b552d9fba | ||
| 
						 | 
					aa53d8f1ee | ||
| 
						 | 
					a28932bc29 | ||
| 
						 | 
					afa7414ee1 | ||
| 
						 | 
					aed7ef481e | ||
| 
						 | 
					c820fee1f6 | ||
| 
						 | 
					5244ac4ff6 | ||
| 
						 | 
					89d283eee4 | ||
| 
						 | 
					ef053d23b4 | ||
| 
						 | 
					98470d32f0 | ||
| 
						 | 
					cab6edd800 | ||
| 
						 | 
					aaaf9b2b62 | ||
| 
						 | 
					38cfd32382 | ||
| 
						 | 
					1b9ae57b9d | ||
| 
						 | 
					4d54cb9b31 | ||
| 
						 | 
					15d0b4355e | ||
| 
						 | 
					316fe2f06c | ||
| 
						 | 
					f8681adec4 | ||
| 
						 | 
					868f5ff20c | ||
| 
						 | 
					59295a615e | ||
| 
						 | 
					d8516cfabb | ||
| 
						 | 
					d847b345b8 | ||
| 
						 | 
					c50e33f531 | ||
| 
						 | 
					5a84bab9ec | ||
| 
						 | 
					41f860c2a3 | ||
| 
						 | 
					c7e62d1279 | ||
| 
						 | 
					2341ff651a | ||
| 
						 | 
					9704de6647 | ||
| 
						 | 
					97fb8c2cdf | ||
| 
						 | 
					d9839f3a5c | ||
| 
						 | 
					498e3904a9 | ||
| 
						 | 
					7cb01bf842 | ||
| 
						 | 
					c050e8d0fb | ||
| 
						 | 
					4f2643e6e9 | ||
| 
						 | 
					7d0262dd1a | ||
| 
						 | 
					c30ffd0098 | ||
| 
						 | 
					ea31122979 | 
							
								
								
									
										4
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -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
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -143,3 +143,4 @@ sdkconfig.*
 | 
			
		||||
/components
 | 
			
		||||
/managed_components
 | 
			
		||||
 | 
			
		||||
api-docs/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.netlify/netlify.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.netlify/netlify.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
[build]
 | 
			
		||||
  command = "script/build-api-docs"
 | 
			
		||||
  publish = "api-docs"
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 /
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
											
										
									
								
							@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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_))
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/macros.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -51,35 +52,60 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
  api::BluetoothLERawAdvertisementsResponse resp;
 | 
			
		||||
  // Pre-allocate the advertisements vector to avoid reallocations
 | 
			
		||||
  resp.advertisements.reserve(count);
 | 
			
		||||
  // 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) {
 | 
			
		||||
    batch_buffer.reserve(new_size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add new advertisements to the batch buffer
 | 
			
		||||
  for (size_t i = 0; i < count; i++) {
 | 
			
		||||
    auto &result = advertisements[i];
 | 
			
		||||
    api::BluetoothLERawAdvertisement adv;
 | 
			
		||||
    uint8_t length = result.adv_data_len + result.scan_rsp_len;
 | 
			
		||||
 | 
			
		||||
    batch_buffer.emplace_back();
 | 
			
		||||
    auto &adv = batch_buffer.back();
 | 
			
		||||
    adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
 | 
			
		||||
    adv.rssi = result.rssi;
 | 
			
		||||
    adv.address_type = result.ble_addr_type;
 | 
			
		||||
    adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]);
 | 
			
		||||
 | 
			
		||||
    uint8_t length = result.adv_data_len + result.scan_rsp_len;
 | 
			
		||||
    adv.data.reserve(length);
 | 
			
		||||
    // Use a bulk insert instead of individual push_backs
 | 
			
		||||
    adv.data.insert(adv.data.end(), &result.ble_adv[0], &result.ble_adv[length]);
 | 
			
		||||
 | 
			
		||||
    resp.advertisements.push_back(std::move(adv));
 | 
			
		||||
 | 
			
		||||
    ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
 | 
			
		||||
    ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
 | 
			
		||||
             result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Proxying %d packets", count);
 | 
			
		||||
  this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp);
 | 
			
		||||
 | 
			
		||||
  // 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() >= FLUSH_BATCH_SIZE) {
 | 
			
		||||
    this->flush_pending_advertisements();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
  api::BluetoothLERawAdvertisementsResponse resp;
 | 
			
		||||
  resp.advertisements.swap(batch_buffer);
 | 
			
		||||
  this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  api::BluetoothLEAdvertisementResponse resp;
 | 
			
		||||
  resp.address = device.address_uint64();
 | 
			
		||||
@@ -91,28 +117,28 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
 | 
			
		||||
  // Pre-allocate vectors based on known sizes
 | 
			
		||||
  auto service_uuids = device.get_service_uuids();
 | 
			
		||||
  resp.service_uuids.reserve(service_uuids.size());
 | 
			
		||||
  for (auto uuid : service_uuids) {
 | 
			
		||||
    resp.service_uuids.push_back(uuid.to_string());
 | 
			
		||||
  for (auto &uuid : service_uuids) {
 | 
			
		||||
    resp.service_uuids.emplace_back(uuid.to_string());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Pre-allocate service data vector
 | 
			
		||||
  auto service_datas = device.get_service_datas();
 | 
			
		||||
  resp.service_data.reserve(service_datas.size());
 | 
			
		||||
  for (auto &data : service_datas) {
 | 
			
		||||
    api::BluetoothServiceData service_data;
 | 
			
		||||
    resp.service_data.emplace_back();
 | 
			
		||||
    auto &service_data = resp.service_data.back();
 | 
			
		||||
    service_data.uuid = data.uuid.to_string();
 | 
			
		||||
    service_data.data.assign(data.data.begin(), data.data.end());
 | 
			
		||||
    resp.service_data.push_back(std::move(service_data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Pre-allocate manufacturer data vector
 | 
			
		||||
  auto manufacturer_datas = device.get_manufacturer_datas();
 | 
			
		||||
  resp.manufacturer_data.reserve(manufacturer_datas.size());
 | 
			
		||||
  for (auto &data : manufacturer_datas) {
 | 
			
		||||
    api::BluetoothServiceData manufacturer_data;
 | 
			
		||||
    resp.manufacturer_data.emplace_back();
 | 
			
		||||
    auto &manufacturer_data = resp.manufacturer_data.back();
 | 
			
		||||
    manufacturer_data.uuid = data.uuid.to_string();
 | 
			
		||||
    manufacturer_data.data.assign(data.data.begin(), data.data.end());
 | 
			
		||||
    resp.manufacturer_data.push_back(std::move(manufacturer_data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->api_connection_->send_bluetooth_le_advertisement(resp);
 | 
			
		||||
@@ -148,6 +174,18 @@ void BluetoothProxy::loop() {
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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 = App.get_loop_component_start_time();
 | 
			
		||||
 | 
			
		||||
    // Flush accumulated advertisements every 100ms
 | 
			
		||||
    if (now - last_flush_time >= 100) {
 | 
			
		||||
      this->flush_pending_advertisements();
 | 
			
		||||
      last_flush_time = now;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  for (auto *connection : this->connections_) {
 | 
			
		||||
    if (connection->send_service_ == connection->service_count_) {
 | 
			
		||||
      connection->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void flush_pending_advertisements();
 | 
			
		||||
  esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
 | 
			
		||||
 | 
			
		||||
  void register_connection(BluetoothConnection *connection) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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])
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 
 | 
			
		||||
@@ -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.");
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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])
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -122,7 +122,7 @@ void ESP32BLETracker::loop() {
 | 
			
		||||
 | 
			
		||||
  if (this->scanner_state_ == ScannerState::RUNNING &&
 | 
			
		||||
      this->scan_result_index_ &&  // if it looks like we have a scan result we will take the lock
 | 
			
		||||
      xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) {
 | 
			
		||||
      xSemaphoreTake(this->scan_result_lock_, 0)) {
 | 
			
		||||
    uint32_t index = this->scan_result_index_;
 | 
			
		||||
    if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
 | 
			
		||||
      ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
 | 
			
		||||
@@ -447,7 +447,7 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
 | 
			
		||||
void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) {
 | 
			
		||||
  ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt);
 | 
			
		||||
  if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
 | 
			
		||||
    if (xSemaphoreTake(this->scan_result_lock_, 0L)) {
 | 
			
		||||
    if (xSemaphoreTake(this->scan_result_lock_, 0)) {
 | 
			
		||||
      if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
 | 
			
		||||
        this->scan_result_buffer_[this->scan_result_index_++] = param;
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -290,7 +290,7 @@ class ESP32BLETracker : public Component,
 | 
			
		||||
#ifdef USE_PSRAM
 | 
			
		||||
  const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32;
 | 
			
		||||
#else
 | 
			
		||||
  const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 16;
 | 
			
		||||
  const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20;
 | 
			
		||||
#endif  // USE_PSRAM
 | 
			
		||||
  esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_;
 | 
			
		||||
  esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
 | 
			
		||||
 
 | 
			
		||||
@@ -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, []):
 | 
			
		||||
 
 | 
			
		||||
@@ -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; }
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,7 @@ class CameraImageReader {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* ---------------- ESP32Camera class ---------------- */
 | 
			
		||||
class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
class ESP32Camera : public EntityBase, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  ESP32Camera();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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());
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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_();
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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_);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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};
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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])
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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])
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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");
 | 
			
		||||
 
 | 
			
		||||
@@ -254,6 +254,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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -345,7 +345,7 @@ void MQTTClientComponent::loop() {
 | 
			
		||||
    this->disconnect_reason_.reset();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
 | 
			
		||||
  switch (this->state_) {
 | 
			
		||||
    case MQTT_CLIENT_DISABLED:
 | 
			
		||||
 
 | 
			
		||||
@@ -24,13 +24,13 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(MS5611Component),
 | 
			
		||||
            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_PRESSURE): sensor.sensor_schema(
 | 
			
		||||
            cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_HECTOPASCAL,
 | 
			
		||||
                icon=ICON_GAUGE,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
@@ -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_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))
 | 
			
		||||
 
 | 
			
		||||
@@ -29,19 +29,19 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(MS8607Component),
 | 
			
		||||
            cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=2,  # Resolution: 0.01
 | 
			
		||||
                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,
 | 
			
		||||
                accuracy_decimals=2,  # Resolution: 0.016
 | 
			
		||||
                device_class=DEVICE_CLASS_PRESSURE,
 | 
			
		||||
                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=2,  # Resolution: 0.04
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
 
 | 
			
		||||
@@ -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"]
 | 
			
		||||
 | 
			
		||||
noblex_ns = cg.esphome_ns.namespace("noblex")
 | 
			
		||||
NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(NoblexClimate),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(NoblexClimate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 
 | 
			
		||||
@@ -41,9 +41,8 @@ CONF_KI_MULTIPLIER = "ki_multiplier"
 | 
			
		||||
CONF_KD_MULTIPLIER = "kd_multiplier"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
    climate.climate_schema(PIDClimate).extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(PIDClimate),
 | 
			
		||||
            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): cv.temperature,
 | 
			
		||||
@@ -80,9 +79,8 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "pmsx003.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace pmsx003 {
 | 
			
		||||
@@ -42,7 +43,7 @@ void PMSX003Component::dump_config() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PMSX003Component::loop() {
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
 | 
			
		||||
  // If we update less often than it takes the device to stabilise, spin the fan down
 | 
			
		||||
  // rather than running it constantly. It does take some time to stabilise, so we
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "pzem004t.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -16,7 +17,7 @@ void PZEM004T::setup() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PZEM004T::loop() {
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  if (now - this->last_read_ > 500 && this->available() < 7) {
 | 
			
		||||
    while (this->available())
 | 
			
		||||
      this->read();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "rf_bridge.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
@@ -128,7 +129,7 @@ void RFBridgeComponent::write_byte_str_(const std::string &codes) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RFBridgeComponent::loop() {
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  if (now - this->last_bridge_byte_ > 50) {
 | 
			
		||||
    this->rx_buffer_.clear();
 | 
			
		||||
    this->last_bridge_byte_ = now;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "sds011.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace sds011 {
 | 
			
		||||
@@ -75,7 +76,7 @@ void SDS011Component::dump_config() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SDS011Component::loop() {
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  if ((now - this->last_transmission_ >= 500) && this->data_index_) {
 | 
			
		||||
    // last transmission too long ago. Reset RX index.
 | 
			
		||||
    ESP_LOGV(TAG, "Last transmission too long ago. Reset RX index.");
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,10 @@ static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
 | 
			
		||||
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181;
 | 
			
		||||
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
 | 
			
		||||
 | 
			
		||||
static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10;                            // used for VOC and NOx index values
 | 
			
		||||
static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR;     // must be adjusted by the scale factor
 | 
			
		||||
static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR;  // must be adjusted by the scale factor
 | 
			
		||||
 | 
			
		||||
void SEN5XComponent::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up sen5x...");
 | 
			
		||||
 | 
			
		||||
@@ -88,8 +92,9 @@ void SEN5XComponent::setup() {
 | 
			
		||||
          product_name_.push_back(current_char);
 | 
			
		||||
          // second char
 | 
			
		||||
          current_char = *current_int & 0xFF;
 | 
			
		||||
          if (current_char)
 | 
			
		||||
          if (current_char) {
 | 
			
		||||
            product_name_.push_back(current_char);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        current_int++;
 | 
			
		||||
      } while (current_char && --max);
 | 
			
		||||
@@ -271,10 +276,10 @@ void SEN5XComponent::dump_config() {
 | 
			
		||||
        ESP_LOGCONFIG(TAG, "  Low RH/T acceleration mode");
 | 
			
		||||
        break;
 | 
			
		||||
      case MEDIUM_ACCELERATION:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, "  Medium RH/T accelertion mode");
 | 
			
		||||
        ESP_LOGCONFIG(TAG, "  Medium RH/T acceleration mode");
 | 
			
		||||
        break;
 | 
			
		||||
      case HIGH_ACCELERATION:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, "  High RH/T accelertion mode");
 | 
			
		||||
        ESP_LOGCONFIG(TAG, "  High RH/T acceleration mode");
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -337,47 +342,61 @@ void SEN5XComponent::update() {
 | 
			
		||||
      ESP_LOGD(TAG, "read data error (%d)", this->last_error_);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    float pm_1_0 = measurements[0] / 10.0;
 | 
			
		||||
    if (measurements[0] == 0xFFFF)
 | 
			
		||||
      pm_1_0 = NAN;
 | 
			
		||||
    float pm_2_5 = measurements[1] / 10.0;
 | 
			
		||||
    if (measurements[1] == 0xFFFF)
 | 
			
		||||
      pm_2_5 = NAN;
 | 
			
		||||
    float pm_4_0 = measurements[2] / 10.0;
 | 
			
		||||
    if (measurements[2] == 0xFFFF)
 | 
			
		||||
      pm_4_0 = NAN;
 | 
			
		||||
    float pm_10_0 = measurements[3] / 10.0;
 | 
			
		||||
    if (measurements[3] == 0xFFFF)
 | 
			
		||||
      pm_10_0 = NAN;
 | 
			
		||||
    float humidity = measurements[4] / 100.0;
 | 
			
		||||
    if (measurements[4] == 0xFFFF)
 | 
			
		||||
      humidity = NAN;
 | 
			
		||||
    float temperature = (int16_t) measurements[5] / 200.0;
 | 
			
		||||
    if (measurements[5] == 0xFFFF)
 | 
			
		||||
      temperature = NAN;
 | 
			
		||||
    float voc = measurements[6] / 10.0;
 | 
			
		||||
    if (measurements[6] == 0xFFFF)
 | 
			
		||||
      voc = NAN;
 | 
			
		||||
    float nox = measurements[7] / 10.0;
 | 
			
		||||
    if (measurements[7] == 0xFFFF)
 | 
			
		||||
      nox = NAN;
 | 
			
		||||
 | 
			
		||||
    if (this->pm_1_0_sensor_ != nullptr)
 | 
			
		||||
    ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]);
 | 
			
		||||
    float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]);
 | 
			
		||||
    float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]);
 | 
			
		||||
    float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]);
 | 
			
		||||
    float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]);
 | 
			
		||||
    float humidity = measurements[4] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[4]) / 100.0f;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]);
 | 
			
		||||
    float temperature = measurements[5] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[5]) / 200.0f;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]);
 | 
			
		||||
    int16_t voc_idx = static_cast<int16_t>(measurements[6]);
 | 
			
		||||
    float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE)
 | 
			
		||||
                    ? NAN
 | 
			
		||||
                    : static_cast<float>(voc_idx) / 10.0f;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]);
 | 
			
		||||
    int16_t nox_idx = static_cast<int16_t>(measurements[7]);
 | 
			
		||||
    float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE)
 | 
			
		||||
                    ? NAN
 | 
			
		||||
                    : static_cast<float>(nox_idx) / 10.0f;
 | 
			
		||||
 | 
			
		||||
    if (this->pm_1_0_sensor_ != nullptr) {
 | 
			
		||||
      this->pm_1_0_sensor_->publish_state(pm_1_0);
 | 
			
		||||
    if (this->pm_2_5_sensor_ != nullptr)
 | 
			
		||||
    }
 | 
			
		||||
    if (this->pm_2_5_sensor_ != nullptr) {
 | 
			
		||||
      this->pm_2_5_sensor_->publish_state(pm_2_5);
 | 
			
		||||
    if (this->pm_4_0_sensor_ != nullptr)
 | 
			
		||||
    }
 | 
			
		||||
    if (this->pm_4_0_sensor_ != nullptr) {
 | 
			
		||||
      this->pm_4_0_sensor_->publish_state(pm_4_0);
 | 
			
		||||
    if (this->pm_10_0_sensor_ != nullptr)
 | 
			
		||||
    }
 | 
			
		||||
    if (this->pm_10_0_sensor_ != nullptr) {
 | 
			
		||||
      this->pm_10_0_sensor_->publish_state(pm_10_0);
 | 
			
		||||
    if (this->temperature_sensor_ != nullptr)
 | 
			
		||||
    }
 | 
			
		||||
    if (this->temperature_sensor_ != nullptr) {
 | 
			
		||||
      this->temperature_sensor_->publish_state(temperature);
 | 
			
		||||
    if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    }
 | 
			
		||||
    if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
      this->humidity_sensor_->publish_state(humidity);
 | 
			
		||||
    if (this->voc_sensor_ != nullptr)
 | 
			
		||||
    }
 | 
			
		||||
    if (this->voc_sensor_ != nullptr) {
 | 
			
		||||
      this->voc_sensor_->publish_state(voc);
 | 
			
		||||
    if (this->nox_sensor_ != nullptr)
 | 
			
		||||
    }
 | 
			
		||||
    if (this->nox_sensor_ != nullptr) {
 | 
			
		||||
      this->nox_sensor_->publish_state(nox);
 | 
			
		||||
    }
 | 
			
		||||
    this->status_clear_warning();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(SenseAirComponent),
 | 
			
		||||
            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,
 | 
			
		||||
@@ -57,8 +57,8 @@ 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))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -37,14 +37,14 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(SGP30Component),
 | 
			
		||||
            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,
 | 
			
		||||
@@ -86,32 +86,30 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_ECO2 in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_ECO2])
 | 
			
		||||
    if eco2_config := config.get(CONF_ECO2):
 | 
			
		||||
        sens = await sensor.new_sensor(eco2_config)
 | 
			
		||||
        cg.add(var.set_eco2_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_TVOC in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TVOC])
 | 
			
		||||
    if tvoc_config := config.get(CONF_TVOC):
 | 
			
		||||
        sens = await sensor.new_sensor(tvoc_config)
 | 
			
		||||
        cg.add(var.set_tvoc_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_ECO2_BASELINE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_ECO2_BASELINE])
 | 
			
		||||
    if eco2_baseline_config := config.get(CONF_ECO2_BASELINE):
 | 
			
		||||
        sens = await sensor.new_sensor(eco2_baseline_config)
 | 
			
		||||
        cg.add(var.set_eco2_baseline_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_TVOC_BASELINE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TVOC_BASELINE])
 | 
			
		||||
    if tvoc_baseline_config := config.get(CONF_TVOC_BASELINE):
 | 
			
		||||
        sens = await sensor.new_sensor(tvoc_baseline_config)
 | 
			
		||||
        cg.add(var.set_tvoc_baseline_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_STORE_BASELINE in config:
 | 
			
		||||
        cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
 | 
			
		||||
    if (store_baseline := config.get(CONF_STORE_BASELINE)) is not None:
 | 
			
		||||
        cg.add(var.set_store_baseline(store_baseline))
 | 
			
		||||
 | 
			
		||||
    if CONF_BASELINE in config:
 | 
			
		||||
        baseline_config = config[CONF_BASELINE]
 | 
			
		||||
    if baseline_config := config.get(CONF_BASELINE):
 | 
			
		||||
        cg.add(var.set_eco2_baseline(baseline_config[CONF_ECO2_BASELINE]))
 | 
			
		||||
        cg.add(var.set_tvoc_baseline(baseline_config[CONF_TVOC_BASELINE]))
 | 
			
		||||
 | 
			
		||||
    if CONF_COMPENSATION in config:
 | 
			
		||||
        compensation_config = config[CONF_COMPENSATION]
 | 
			
		||||
    if compensation_config := config.get(CONF_COMPENSATION):
 | 
			
		||||
        sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
        sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])
 | 
			
		||||
 
 | 
			
		||||
@@ -26,13 +26,13 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(SHTCXComponent),
 | 
			
		||||
            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,
 | 
			
		||||
@@ -50,10 +50,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))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "slow_pwm_output.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace slow_pwm {
 | 
			
		||||
@@ -39,7 +40,7 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SlowPWMOutput::loop() {
 | 
			
		||||
  uint32_t now = millis();
 | 
			
		||||
  uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  float scaled_state = this->state_ * this->period_;
 | 
			
		||||
 | 
			
		||||
  if (now - this->period_start_time_ >= this->period_) {
 | 
			
		||||
 
 | 
			
		||||
@@ -271,9 +271,8 @@ PIPELINE_SCHEMA = cv.Schema(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    media_player.MEDIA_PLAYER_SCHEMA.extend(
 | 
			
		||||
    media_player.media_player_schema(SpeakerMediaPlayer).extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(SpeakerMediaPlayer),
 | 
			
		||||
            cv.Required(CONF_ANNOUNCEMENT_PIPELINE): PIPELINE_SCHEMA,
 | 
			
		||||
            cv.Optional(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA,
 | 
			
		||||
            cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
 | 
			
		||||
@@ -343,9 +342,8 @@ async def to_code(config):
 | 
			
		||||
        # Allocate wifi buffers in PSRAM
 | 
			
		||||
        esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_OTA_STATE_CALLBACK")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_DIRECTION_OUTPUT,
 | 
			
		||||
    CONF_OSCILLATION_OUTPUT,
 | 
			
		||||
    CONF_OUTPUT,
 | 
			
		||||
    CONF_OUTPUT_ID,
 | 
			
		||||
    CONF_PRESET_MODES,
 | 
			
		||||
    CONF_SPEED,
 | 
			
		||||
    CONF_SPEED_COUNT,
 | 
			
		||||
@@ -16,25 +15,27 @@ from .. import speed_ns
 | 
			
		||||
 | 
			
		||||
SpeedFan = speed_ns.class_("SpeedFan", cg.Component, fan.Fan)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan),
 | 
			
		||||
        cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput),
 | 
			
		||||
        cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
        cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
        cv.Optional(CONF_SPEED): cv.invalid(
 | 
			
		||||
            "Configuring individual speeds is deprecated."
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
 | 
			
		||||
        cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    fan.fan_schema(SpeedFan)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput),
 | 
			
		||||
            cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
            cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
            cv.Optional(CONF_SPEED): cv.invalid(
 | 
			
		||||
                "Configuring individual speeds is deprecated."
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
 | 
			
		||||
            cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_SPEED_COUNT])
 | 
			
		||||
    var = await fan.new_fan(config, config[CONF_SPEED_COUNT])
 | 
			
		||||
    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_))
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *o
 | 
			
		||||
bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); }
 | 
			
		||||
 | 
			
		||||
void SprinklerSwitch::loop() {
 | 
			
		||||
  if ((this->pinned_millis_) && (millis() > this->pinned_millis_ + this->pulse_duration_)) {
 | 
			
		||||
  if ((this->pinned_millis_) && (App.get_loop_component_start_time() > this->pinned_millis_ + this->pulse_duration_)) {
 | 
			
		||||
    this->pinned_millis_ = 0;  // reset tracker
 | 
			
		||||
    if (this->off_switch_->state) {
 | 
			
		||||
      this->off_switch_->turn_off();
 | 
			
		||||
@@ -148,22 +148,23 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler
 | 
			
		||||
    : controller_(controller), valve_(valve) {}
 | 
			
		||||
 | 
			
		||||
void SprinklerValveOperator::loop() {
 | 
			
		||||
  if (millis() >= this->start_millis_) {  // dummy check
 | 
			
		||||
  uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  if (now >= this->start_millis_) {  // dummy check
 | 
			
		||||
    switch (this->state_) {
 | 
			
		||||
      case STARTING:
 | 
			
		||||
        if (millis() > (this->start_millis_ + this->start_delay_)) {
 | 
			
		||||
        if (now > (this->start_millis_ + this->start_delay_)) {
 | 
			
		||||
          this->run_();  // start_delay_ has been exceeded, so ensure both valves are on and update the state
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case ACTIVE:
 | 
			
		||||
        if (millis() > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
 | 
			
		||||
        if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
 | 
			
		||||
          this->stop();  // start_delay_ + run_duration_ has been exceeded, start shutting down
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case STOPPING:
 | 
			
		||||
        if (millis() > (this->stop_millis_ + this->stop_delay_)) {
 | 
			
		||||
        if (now > (this->stop_millis_ + this->stop_delay_)) {
 | 
			
		||||
          this->kill_();  // stop_delay_has been exceeded, ensure all valves are off
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(T6615Component),
 | 
			
		||||
            cv.Required(CONF_CO2): sensor.sensor_schema(
 | 
			
		||||
            cv.Optional(CONF_CO2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_MILLION,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
@@ -41,6 +41,6 @@ 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))
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,8 @@ void T6615Component::loop() {
 | 
			
		||||
    case T6615Command::GET_PPM: {
 | 
			
		||||
      const uint16_t ppm = encode_uint16(response_buffer[3], response_buffer[4]);
 | 
			
		||||
      ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm);
 | 
			
		||||
      this->co2_sensor_->publish_state(ppm);
 | 
			
		||||
      if (this->co2_sensor_ != nullptr)
 | 
			
		||||
        this->co2_sensor_->publish_state(ppm);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user