mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 08:31:47 +00:00 
			
		
		
		
	Compare commits
	
		
			113 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					321504cf29 | ||
| 
						 | 
					0f4a7bf1f5 | ||
| 
						 | 
					711e74a12b | ||
| 
						 | 
					aa8eb2c92a | ||
| 
						 | 
					b422a63b2a | ||
| 
						 | 
					ad5f2cd748 | ||
| 
						 | 
					efae363739 | ||
| 
						 | 
					2d79d21c50 | ||
| 
						 | 
					3b9d126322 | ||
| 
						 | 
					896654aaef | ||
| 
						 | 
					5fad38f65f | ||
| 
						 | 
					89f2ea5725 | ||
| 
						 | 
					a32ad33b4e | ||
| 
						 | 
					a328fff5a7 | ||
| 
						 | 
					233783c76c | ||
| 
						 | 
					39a18fb358 | ||
| 
						 | 
					460a144ca8 | ||
| 
						 | 
					23ead416d5 | ||
| 
						 | 
					1b5f11bbee | ||
| 
						 | 
					0da97289e6 | ||
| 
						 | 
					91f12a50cf | ||
| 
						 | 
					e92a9d1d9e | ||
| 
						 | 
					4eb51ab4d6 | ||
| 
						 | 
					f1a8d957f8 | ||
| 
						 | 
					9821a3442b | ||
| 
						 | 
					87842e097b | ||
| 
						 | 
					7dd40e2014 | ||
| 
						 | 
					a719998220 | ||
| 
						 | 
					09a6fdf1c7 | ||
| 
						 | 
					d2616cbdfc | ||
| 
						 | 
					faf1c8bee8 | ||
| 
						 | 
					f09aca4865 | ||
| 
						 | 
					cc52f37933 | ||
| 
						 | 
					e5051eefbc | ||
| 
						 | 
					9e5cd0da51 | ||
| 
						 | 
					4e120a291e | ||
| 
						 | 
					2790d72bff | ||
| 
						 | 
					e44f447d85 | ||
| 
						 | 
					4356581db0 | ||
| 
						 | 
					f87a701b28 | ||
| 
						 | 
					fa2eb46cd6 | ||
| 
						 | 
					f924e80f43 | ||
| 
						 | 
					6180ee8065 | ||
| 
						 | 
					1be106c0b5 | ||
| 
						 | 
					b0533db2eb | ||
| 
						 | 
					dba502c756 | ||
| 
						 | 
					d9cb64b893 | ||
| 
						 | 
					2d91e6b977 | ||
| 
						 | 
					4a6f1f150a | ||
| 
						 | 
					7f76f3726f | ||
| 
						 | 
					e2d97b6f36 | ||
| 
						 | 
					2a653642f5 | ||
| 
						 | 
					97eba1eecc | ||
| 
						 | 
					ff6bed54c6 | ||
| 
						 | 
					f9b0666adf | ||
| 
						 | 
					ca12b8aa56 | ||
| 
						 | 
					77508f7e44 | ||
| 
						 | 
					54de0ca0da | ||
| 
						 | 
					f364788c03 | ||
| 
						 | 
					00aaf84c37 | ||
| 
						 | 
					b01bc76dc5 | ||
| 
						 | 
					910f812737 | ||
| 
						 | 
					a4d024f43d | ||
| 
						 | 
					9937ad7fa0 | ||
| 
						 | 
					edcd88123d | ||
| 
						 | 
					ea1b5e19f0 | ||
| 
						 | 
					54337befc2 | ||
| 
						 | 
					140ef791aa | ||
| 
						 | 
					58350b6c99 | ||
| 
						 | 
					f186ff8b46 | ||
| 
						 | 
					03190611bb | ||
| 
						 | 
					37f322585e | ||
| 
						 | 
					9218e85bd6 | ||
| 
						 | 
					f923ba87c0 | ||
| 
						 | 
					fac49896df | ||
| 
						 | 
					56225701f9 | ||
| 
						 | 
					b5de43b225 | ||
| 
						 | 
					b955527f6c | ||
| 
						 | 
					94b28102f5 | ||
| 
						 | 
					de871862a8 | ||
| 
						 | 
					3be56fd502 | ||
| 
						 | 
					39cd2838df | ||
| 
						 | 
					565473c90c | ||
| 
						 | 
					ed68a0e773 | ||
| 
						 | 
					e2640c8368 | ||
| 
						 | 
					eff626248f | ||
| 
						 | 
					ce29a3b07a | ||
| 
						 | 
					1b89174558 | ||
| 
						 | 
					1c1ad32610 | ||
| 
						 | 
					71237e2f76 | ||
| 
						 | 
					518c271eba | ||
| 
						 | 
					d71996e58d | ||
| 
						 | 
					2f33cd2db5 | ||
| 
						 | 
					5ec9bb0fb5 | ||
| 
						 | 
					8cc3cbb22e | ||
| 
						 | 
					b0fa317302 | ||
| 
						 | 
					5cb56bc677 | ||
| 
						 | 
					21f8fd9fa5 | ||
| 
						 | 
					2100ef63a9 | ||
| 
						 | 
					29db77c9c9 | ||
| 
						 | 
					f0b14055b6 | ||
| 
						 | 
					fbd9e87b51 | ||
| 
						 | 
					edb3b77916 | ||
| 
						 | 
					ebaa84617f | ||
| 
						 | 
					8eb18995cb | ||
| 
						 | 
					ebabf0e7d8 | ||
| 
						 | 
					607e1f823d | ||
| 
						 | 
					3b52a306cd | ||
| 
						 | 
					0c370d5897 | ||
| 
						 | 
					9b48ff5775 | ||
| 
						 | 
					117b58ebe6 | ||
| 
						 | 
					303b699005 | ||
| 
						 | 
					9173da0416 | 
							
								
								
									
										11
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -14,6 +14,8 @@ esphome/core/* @esphome/core
 | 
			
		||||
esphome/components/ac_dimmer/* @glmnet
 | 
			
		||||
esphome/components/adc/* @esphome/core
 | 
			
		||||
esphome/components/addressable_light/* @justfalter
 | 
			
		||||
esphome/components/airthings_ble/* @jeromelaban
 | 
			
		||||
esphome/components/airthings_wave_plus/* @jeromelaban
 | 
			
		||||
esphome/components/am43/* @buxtronix
 | 
			
		||||
esphome/components/am43/cover/* @buxtronix
 | 
			
		||||
esphome/components/animation/* @syndlex
 | 
			
		||||
@@ -29,6 +31,7 @@ esphome/components/ble_client/* @buxtronix
 | 
			
		||||
esphome/components/bme680_bsec/* @trvrnrth
 | 
			
		||||
esphome/components/canbus/* @danielschramm @mvturnho
 | 
			
		||||
esphome/components/captive_portal/* @OttoWinter
 | 
			
		||||
esphome/components/ccs811/* @habbie
 | 
			
		||||
esphome/components/climate/* @esphome/core
 | 
			
		||||
esphome/components/climate_ir/* @glmnet
 | 
			
		||||
esphome/components/color_temperature/* @jesserockz
 | 
			
		||||
@@ -52,6 +55,8 @@ esphome/components/globals/* @esphome/core
 | 
			
		||||
esphome/components/gpio/* @esphome/core
 | 
			
		||||
esphome/components/gps/* @coogle
 | 
			
		||||
esphome/components/havells_solar/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hbridge/fan/* @WeekendWarrior
 | 
			
		||||
esphome/components/hbridge/light/* @DotNetDann
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
esphome/components/homeassistant/* @OttoWinter
 | 
			
		||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
 | 
			
		||||
@@ -75,8 +80,7 @@ esphome/components/mcp23x17_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp23xxx_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp2515/* @danielschramm @mvturnho
 | 
			
		||||
esphome/components/mcp9808/* @k7hpn
 | 
			
		||||
esphome/components/midea_ac/* @dudanov
 | 
			
		||||
esphome/components/midea_dongle/* @dudanov
 | 
			
		||||
esphome/components/midea/* @dudanov
 | 
			
		||||
esphome/components/mitsubishi/* @RubyBailey
 | 
			
		||||
esphome/components/network/* @esphome/core
 | 
			
		||||
esphome/components/nextion/* @senexcrenshaw
 | 
			
		||||
@@ -90,6 +94,7 @@ esphome/components/ota/* @esphome/core
 | 
			
		||||
esphome/components/output/* @esphome/core
 | 
			
		||||
esphome/components/pid/* @OttoWinter
 | 
			
		||||
esphome/components/pipsolar/* @andreashergert1984
 | 
			
		||||
esphome/components/pm1006/* @habbie
 | 
			
		||||
esphome/components/pmsa003i/* @sjtrny
 | 
			
		||||
esphome/components/pn532/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
 | 
			
		||||
@@ -115,6 +120,7 @@ esphome/components/sht4x/* @sjtrny
 | 
			
		||||
esphome/components/shutdown/* @esphome/core
 | 
			
		||||
esphome/components/sim800l/* @glmnet
 | 
			
		||||
esphome/components/sm2135/* @BoukeHaarsma23
 | 
			
		||||
esphome/components/socket/* @esphome/core
 | 
			
		||||
esphome/components/spi/* @esphome/core
 | 
			
		||||
esphome/components/ssd1322_base/* @kbx81
 | 
			
		||||
esphome/components/ssd1322_spi/* @kbx81
 | 
			
		||||
@@ -129,6 +135,7 @@ esphome/components/ssd1351_base/* @kbx81
 | 
			
		||||
esphome/components/ssd1351_spi/* @kbx81
 | 
			
		||||
esphome/components/st7735/* @SenexCrenshaw
 | 
			
		||||
esphome/components/st7789v/* @kbx81
 | 
			
		||||
esphome/components/st7920/* @marsjan155
 | 
			
		||||
esphome/components/substitutions/* @esphome/core
 | 
			
		||||
esphome/components/sun/* @OttoWinter
 | 
			
		||||
esphome/components/switch/* @esphome/core
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ TYPE_LINT = 'lint'
 | 
			
		||||
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BASE_VERSION = "3.6.0"
 | 
			
		||||
BASE_VERSION = "4.2.0"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
parser = argparse.ArgumentParser()
 | 
			
		||||
 
 | 
			
		||||
@@ -256,7 +256,7 @@ def show_logs(config, args, port):
 | 
			
		||||
        run_miniterm(config, port)
 | 
			
		||||
        return 0
 | 
			
		||||
    if get_port_type(port) == "NETWORK" and "api" in config:
 | 
			
		||||
        from esphome.api.client import run_logs
 | 
			
		||||
        from esphome.components.api.client import run_logs
 | 
			
		||||
 | 
			
		||||
        return run_logs(config, port)
 | 
			
		||||
    if get_port_type(port) == "MQTT" and "mqtt" in config:
 | 
			
		||||
@@ -483,75 +483,9 @@ def parse_args(argv):
 | 
			
		||||
        metavar=("key", "value"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Keep backward compatibility with the old command line format of
 | 
			
		||||
    # esphome <config> <command>.
 | 
			
		||||
    #
 | 
			
		||||
    # Unfortunately this can't be done by adding another configuration argument to the
 | 
			
		||||
    # main config parser, as argparse is greedy when parsing arguments, so in regular
 | 
			
		||||
    # usage it'll eat the command as the configuration argument and error out out
 | 
			
		||||
    # because it can't parse the configuration as a command.
 | 
			
		||||
    #
 | 
			
		||||
    # Instead, construct an ad-hoc parser for the old format that doesn't actually
 | 
			
		||||
    # process the arguments, but parses them enough to let us figure out if the old
 | 
			
		||||
    # format is used. In that case, swap the command and configuration in the arguments
 | 
			
		||||
    # and continue on with the normal parser (after raising a deprecation warning).
 | 
			
		||||
    #
 | 
			
		||||
    # Disable argparse's built-in help option and add it manually to prevent this
 | 
			
		||||
    # parser from printing the help messagefor the old format when invoked with -h.
 | 
			
		||||
    compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
 | 
			
		||||
    compat_parser.add_argument("-h", "--help")
 | 
			
		||||
    compat_parser.add_argument("configuration", nargs="*")
 | 
			
		||||
    compat_parser.add_argument(
 | 
			
		||||
        "command",
 | 
			
		||||
        choices=[
 | 
			
		||||
            "config",
 | 
			
		||||
            "compile",
 | 
			
		||||
            "upload",
 | 
			
		||||
            "logs",
 | 
			
		||||
            "run",
 | 
			
		||||
            "clean-mqtt",
 | 
			
		||||
            "wizard",
 | 
			
		||||
            "mqtt-fingerprint",
 | 
			
		||||
            "version",
 | 
			
		||||
            "clean",
 | 
			
		||||
            "dashboard",
 | 
			
		||||
            "vscode",
 | 
			
		||||
            "update-all",
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # on Python 3.9+ we can simply set exit_on_error=False in the constructor
 | 
			
		||||
    def _raise(x):
 | 
			
		||||
        raise argparse.ArgumentError(None, x)
 | 
			
		||||
 | 
			
		||||
    compat_parser.error = _raise
 | 
			
		||||
 | 
			
		||||
    deprecated_argv_suggestion = None
 | 
			
		||||
 | 
			
		||||
    if ["dashboard", "config"] == argv[1:3] or ["version"] == argv[1:2]:
 | 
			
		||||
        # this is most likely meant in new-style arg format. do not try compat parsing
 | 
			
		||||
        pass
 | 
			
		||||
    else:
 | 
			
		||||
        try:
 | 
			
		||||
            result, unparsed = compat_parser.parse_known_args(argv[1:])
 | 
			
		||||
            last_option = len(argv) - len(unparsed) - 1 - len(result.configuration)
 | 
			
		||||
            unparsed = [
 | 
			
		||||
                "--device" if arg in ("--upload-port", "--serial-port") else arg
 | 
			
		||||
                for arg in unparsed
 | 
			
		||||
            ]
 | 
			
		||||
            argv = (
 | 
			
		||||
                argv[0:last_option] + [result.command] + result.configuration + unparsed
 | 
			
		||||
            )
 | 
			
		||||
            deprecated_argv_suggestion = argv
 | 
			
		||||
        except argparse.ArgumentError:
 | 
			
		||||
            # This is not an old-style command line, so we don't have to do anything.
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    # And continue on with regular parsing
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description=f"ESPHome v{const.__version__}", parents=[options_parser]
 | 
			
		||||
    )
 | 
			
		||||
    parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
 | 
			
		||||
 | 
			
		||||
    mqtt_options = argparse.ArgumentParser(add_help=False)
 | 
			
		||||
    mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.")
 | 
			
		||||
@@ -701,7 +635,83 @@ def parse_args(argv):
 | 
			
		||||
        "configuration", help="Your YAML configuration file directories.", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return parser.parse_args(argv[1:])
 | 
			
		||||
    # Keep backward compatibility with the old command line format of
 | 
			
		||||
    # esphome <config> <command>.
 | 
			
		||||
    #
 | 
			
		||||
    # Unfortunately this can't be done by adding another configuration argument to the
 | 
			
		||||
    # main config parser, as argparse is greedy when parsing arguments, so in regular
 | 
			
		||||
    # usage it'll eat the command as the configuration argument and error out out
 | 
			
		||||
    # because it can't parse the configuration as a command.
 | 
			
		||||
    #
 | 
			
		||||
    # Instead, if parsing using the current format fails, construct an ad-hoc parser
 | 
			
		||||
    # that doesn't actually process the arguments, but parses them enough to let us
 | 
			
		||||
    # figure out if the old format is used. In that case, swap the command and
 | 
			
		||||
    # configuration in the arguments and retry with the normal parser (and raise
 | 
			
		||||
    # a deprecation warning).
 | 
			
		||||
    arguments = argv[1:]
 | 
			
		||||
 | 
			
		||||
    # On Python 3.9+ we can simply set exit_on_error=False in the constructor
 | 
			
		||||
    def _raise(x):
 | 
			
		||||
        raise argparse.ArgumentError(None, x)
 | 
			
		||||
 | 
			
		||||
    # First, try new-style parsing, but don't exit in case of failure
 | 
			
		||||
    try:
 | 
			
		||||
        # duplicate parser so that we can use the original one to raise errors later on
 | 
			
		||||
        current_parser = argparse.ArgumentParser(add_help=False, parents=[parser])
 | 
			
		||||
        current_parser.set_defaults(deprecated_argv_suggestion=None)
 | 
			
		||||
        current_parser.error = _raise
 | 
			
		||||
        return current_parser.parse_args(arguments)
 | 
			
		||||
    except argparse.ArgumentError:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    # Second, try compat parsing and rearrange the command-line if it succeeds
 | 
			
		||||
    # Disable argparse's built-in help option and add it manually to prevent this
 | 
			
		||||
    # parser from printing the help messagefor the old format when invoked with -h.
 | 
			
		||||
    compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False)
 | 
			
		||||
    compat_parser.add_argument("-h", "--help", action="store_true")
 | 
			
		||||
    compat_parser.add_argument("configuration", nargs="*")
 | 
			
		||||
    compat_parser.add_argument(
 | 
			
		||||
        "command",
 | 
			
		||||
        choices=[
 | 
			
		||||
            "config",
 | 
			
		||||
            "compile",
 | 
			
		||||
            "upload",
 | 
			
		||||
            "logs",
 | 
			
		||||
            "run",
 | 
			
		||||
            "clean-mqtt",
 | 
			
		||||
            "wizard",
 | 
			
		||||
            "mqtt-fingerprint",
 | 
			
		||||
            "version",
 | 
			
		||||
            "clean",
 | 
			
		||||
            "dashboard",
 | 
			
		||||
            "vscode",
 | 
			
		||||
            "update-all",
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        compat_parser.error = _raise
 | 
			
		||||
        result, unparsed = compat_parser.parse_known_args(argv[1:])
 | 
			
		||||
        last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration)
 | 
			
		||||
        unparsed = [
 | 
			
		||||
            "--device" if arg in ("--upload-port", "--serial-port") else arg
 | 
			
		||||
            for arg in unparsed
 | 
			
		||||
        ]
 | 
			
		||||
        arguments = (
 | 
			
		||||
            arguments[0:last_option]
 | 
			
		||||
            + [result.command]
 | 
			
		||||
            + result.configuration
 | 
			
		||||
            + unparsed
 | 
			
		||||
        )
 | 
			
		||||
        deprecated_argv_suggestion = arguments
 | 
			
		||||
    except argparse.ArgumentError:
 | 
			
		||||
        # old-style parsing failed, don't suggest any argument
 | 
			
		||||
        deprecated_argv_suggestion = None
 | 
			
		||||
 | 
			
		||||
    # Finally, run the new-style parser again with the possibly swapped arguments,
 | 
			
		||||
    # and let it error out if the command is unparsable.
 | 
			
		||||
    parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion)
 | 
			
		||||
    return parser.parse_args(arguments)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_esphome(argv):
 | 
			
		||||
@@ -715,7 +725,7 @@ def run_esphome(argv):
 | 
			
		||||
            "and will be removed in the future. "
 | 
			
		||||
        )
 | 
			
		||||
        _LOGGER.warning("Please instead use:")
 | 
			
		||||
        _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion[1:]))
 | 
			
		||||
        _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion))
 | 
			
		||||
 | 
			
		||||
    if sys.version_info < (3, 7, 0):
 | 
			
		||||
        _LOGGER.error(
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1,518 +0,0 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import functools
 | 
			
		||||
import logging
 | 
			
		||||
import socket
 | 
			
		||||
import threading
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
# pylint: disable=unused-import
 | 
			
		||||
from typing import Optional  # noqa
 | 
			
		||||
from google.protobuf import message  # noqa
 | 
			
		||||
 | 
			
		||||
from esphome import const
 | 
			
		||||
import esphome.api.api_pb2 as pb
 | 
			
		||||
from esphome.const import CONF_PASSWORD, CONF_PORT
 | 
			
		||||
from esphome.core import EsphomeError
 | 
			
		||||
from esphome.helpers import resolve_ip_address, indent
 | 
			
		||||
from esphome.log import color, Fore
 | 
			
		||||
from esphome.util import safe_print
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIConnectionError(EsphomeError):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MESSAGE_TYPE_TO_PROTO = {
 | 
			
		||||
    1: pb.HelloRequest,
 | 
			
		||||
    2: pb.HelloResponse,
 | 
			
		||||
    3: pb.ConnectRequest,
 | 
			
		||||
    4: pb.ConnectResponse,
 | 
			
		||||
    5: pb.DisconnectRequest,
 | 
			
		||||
    6: pb.DisconnectResponse,
 | 
			
		||||
    7: pb.PingRequest,
 | 
			
		||||
    8: pb.PingResponse,
 | 
			
		||||
    9: pb.DeviceInfoRequest,
 | 
			
		||||
    10: pb.DeviceInfoResponse,
 | 
			
		||||
    11: pb.ListEntitiesRequest,
 | 
			
		||||
    12: pb.ListEntitiesBinarySensorResponse,
 | 
			
		||||
    13: pb.ListEntitiesCoverResponse,
 | 
			
		||||
    14: pb.ListEntitiesFanResponse,
 | 
			
		||||
    15: pb.ListEntitiesLightResponse,
 | 
			
		||||
    16: pb.ListEntitiesSensorResponse,
 | 
			
		||||
    17: pb.ListEntitiesSwitchResponse,
 | 
			
		||||
    18: pb.ListEntitiesTextSensorResponse,
 | 
			
		||||
    19: pb.ListEntitiesDoneResponse,
 | 
			
		||||
    20: pb.SubscribeStatesRequest,
 | 
			
		||||
    21: pb.BinarySensorStateResponse,
 | 
			
		||||
    22: pb.CoverStateResponse,
 | 
			
		||||
    23: pb.FanStateResponse,
 | 
			
		||||
    24: pb.LightStateResponse,
 | 
			
		||||
    25: pb.SensorStateResponse,
 | 
			
		||||
    26: pb.SwitchStateResponse,
 | 
			
		||||
    27: pb.TextSensorStateResponse,
 | 
			
		||||
    28: pb.SubscribeLogsRequest,
 | 
			
		||||
    29: pb.SubscribeLogsResponse,
 | 
			
		||||
    30: pb.CoverCommandRequest,
 | 
			
		||||
    31: pb.FanCommandRequest,
 | 
			
		||||
    32: pb.LightCommandRequest,
 | 
			
		||||
    33: pb.SwitchCommandRequest,
 | 
			
		||||
    34: pb.SubscribeServiceCallsRequest,
 | 
			
		||||
    35: pb.ServiceCallResponse,
 | 
			
		||||
    36: pb.GetTimeRequest,
 | 
			
		||||
    37: pb.GetTimeResponse,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _varuint_to_bytes(value):
 | 
			
		||||
    if value <= 0x7F:
 | 
			
		||||
        return bytes([value])
 | 
			
		||||
 | 
			
		||||
    ret = bytes()
 | 
			
		||||
    while value:
 | 
			
		||||
        temp = value & 0x7F
 | 
			
		||||
        value >>= 7
 | 
			
		||||
        if value:
 | 
			
		||||
            ret += bytes([temp | 0x80])
 | 
			
		||||
        else:
 | 
			
		||||
            ret += bytes([temp])
 | 
			
		||||
 | 
			
		||||
    return ret
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _bytes_to_varuint(value):
 | 
			
		||||
    result = 0
 | 
			
		||||
    bitpos = 0
 | 
			
		||||
    for val in value:
 | 
			
		||||
        result |= (val & 0x7F) << bitpos
 | 
			
		||||
        bitpos += 7
 | 
			
		||||
        if (val & 0x80) == 0:
 | 
			
		||||
            return result
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# pylint: disable=too-many-instance-attributes,not-callable
 | 
			
		||||
class APIClient(threading.Thread):
 | 
			
		||||
    def __init__(self, address, port, password):
 | 
			
		||||
        threading.Thread.__init__(self)
 | 
			
		||||
        self._address = address  # type: str
 | 
			
		||||
        self._port = port  # type: int
 | 
			
		||||
        self._password = password  # type: Optional[str]
 | 
			
		||||
        self._socket = None  # type: Optional[socket.socket]
 | 
			
		||||
        self._socket_open_event = threading.Event()
 | 
			
		||||
        self._socket_write_lock = threading.Lock()
 | 
			
		||||
        self._connected = False
 | 
			
		||||
        self._authenticated = False
 | 
			
		||||
        self._message_handlers = []
 | 
			
		||||
        self._keepalive = 5
 | 
			
		||||
        self._ping_timer = None
 | 
			
		||||
 | 
			
		||||
        self.on_disconnect = None
 | 
			
		||||
        self.on_connect = None
 | 
			
		||||
        self.on_login = None
 | 
			
		||||
        self.auto_reconnect = False
 | 
			
		||||
        self._running_event = threading.Event()
 | 
			
		||||
        self._stop_event = threading.Event()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def stopped(self):
 | 
			
		||||
        return self._stop_event.is_set()
 | 
			
		||||
 | 
			
		||||
    def _refresh_ping(self):
 | 
			
		||||
        if self._ping_timer is not None:
 | 
			
		||||
            self._ping_timer.cancel()
 | 
			
		||||
            self._ping_timer = None
 | 
			
		||||
 | 
			
		||||
        def func():
 | 
			
		||||
            self._ping_timer = None
 | 
			
		||||
 | 
			
		||||
            if self._connected:
 | 
			
		||||
                try:
 | 
			
		||||
                    self.ping()
 | 
			
		||||
                except APIConnectionError as err:
 | 
			
		||||
                    self._fatal_error(err)
 | 
			
		||||
                else:
 | 
			
		||||
                    self._refresh_ping()
 | 
			
		||||
 | 
			
		||||
        self._ping_timer = threading.Timer(self._keepalive, func)
 | 
			
		||||
        self._ping_timer.start()
 | 
			
		||||
 | 
			
		||||
    def _cancel_ping(self):
 | 
			
		||||
        if self._ping_timer is not None:
 | 
			
		||||
            self._ping_timer.cancel()
 | 
			
		||||
            self._ping_timer = None
 | 
			
		||||
 | 
			
		||||
    def _close_socket(self):
 | 
			
		||||
        self._cancel_ping()
 | 
			
		||||
        if self._socket is not None:
 | 
			
		||||
            self._socket.close()
 | 
			
		||||
            self._socket = None
 | 
			
		||||
        self._socket_open_event.clear()
 | 
			
		||||
        self._connected = False
 | 
			
		||||
        self._authenticated = False
 | 
			
		||||
        self._message_handlers = []
 | 
			
		||||
 | 
			
		||||
    def stop(self, force=False):
 | 
			
		||||
        if self.stopped:
 | 
			
		||||
            raise ValueError
 | 
			
		||||
 | 
			
		||||
        if self._connected and not force:
 | 
			
		||||
            try:
 | 
			
		||||
                self.disconnect()
 | 
			
		||||
            except APIConnectionError:
 | 
			
		||||
                pass
 | 
			
		||||
        self._close_socket()
 | 
			
		||||
 | 
			
		||||
        self._stop_event.set()
 | 
			
		||||
        if not force:
 | 
			
		||||
            self.join()
 | 
			
		||||
 | 
			
		||||
    def connect(self):
 | 
			
		||||
        if not self._running_event.wait(0.1):
 | 
			
		||||
            raise APIConnectionError("You need to call start() first!")
 | 
			
		||||
 | 
			
		||||
        if self._connected:
 | 
			
		||||
            self.disconnect(on_disconnect=False)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            ip = resolve_ip_address(self._address)
 | 
			
		||||
        except EsphomeError as err:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Error resolving IP address of %s. Is it connected to WiFi?",
 | 
			
		||||
                self._address,
 | 
			
		||||
            )
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "(If this error persists, please set a static IP address: "
 | 
			
		||||
                "https://esphome.io/components/wifi.html#manual-ips)"
 | 
			
		||||
            )
 | 
			
		||||
            raise APIConnectionError(err) from err
 | 
			
		||||
 | 
			
		||||
        _LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip)
 | 
			
		||||
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 | 
			
		||||
        self._socket.settimeout(10.0)
 | 
			
		||||
        self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
 | 
			
		||||
        try:
 | 
			
		||||
            self._socket.connect((ip, self._port))
 | 
			
		||||
        except OSError as err:
 | 
			
		||||
            err = APIConnectionError(f"Error connecting to {ip}: {err}")
 | 
			
		||||
            self._fatal_error(err)
 | 
			
		||||
            raise err
 | 
			
		||||
        self._socket.settimeout(0.1)
 | 
			
		||||
 | 
			
		||||
        self._socket_open_event.set()
 | 
			
		||||
 | 
			
		||||
        hello = pb.HelloRequest()
 | 
			
		||||
        hello.client_info = f"ESPHome v{const.__version__}"
 | 
			
		||||
        try:
 | 
			
		||||
            resp = self._send_message_await_response(hello, pb.HelloResponse)
 | 
			
		||||
        except APIConnectionError as err:
 | 
			
		||||
            self._fatal_error(err)
 | 
			
		||||
            raise err
 | 
			
		||||
        _LOGGER.debug(
 | 
			
		||||
            "Successfully connected to %s ('%s' API=%s.%s)",
 | 
			
		||||
            self._address,
 | 
			
		||||
            resp.server_info,
 | 
			
		||||
            resp.api_version_major,
 | 
			
		||||
            resp.api_version_minor,
 | 
			
		||||
        )
 | 
			
		||||
        self._connected = True
 | 
			
		||||
        self._refresh_ping()
 | 
			
		||||
        if self.on_connect is not None:
 | 
			
		||||
            self.on_connect()
 | 
			
		||||
 | 
			
		||||
    def _check_connected(self):
 | 
			
		||||
        if not self._connected:
 | 
			
		||||
            err = APIConnectionError("Must be connected!")
 | 
			
		||||
            self._fatal_error(err)
 | 
			
		||||
            raise err
 | 
			
		||||
 | 
			
		||||
    def login(self):
 | 
			
		||||
        self._check_connected()
 | 
			
		||||
        if self._authenticated:
 | 
			
		||||
            raise APIConnectionError("Already logged in!")
 | 
			
		||||
 | 
			
		||||
        connect = pb.ConnectRequest()
 | 
			
		||||
        if self._password is not None:
 | 
			
		||||
            connect.password = self._password
 | 
			
		||||
        resp = self._send_message_await_response(connect, pb.ConnectResponse)
 | 
			
		||||
        if resp.invalid_password:
 | 
			
		||||
            raise APIConnectionError("Invalid password!")
 | 
			
		||||
 | 
			
		||||
        self._authenticated = True
 | 
			
		||||
        if self.on_login is not None:
 | 
			
		||||
            self.on_login()
 | 
			
		||||
 | 
			
		||||
    def _fatal_error(self, err):
 | 
			
		||||
        was_connected = self._connected
 | 
			
		||||
 | 
			
		||||
        self._close_socket()
 | 
			
		||||
 | 
			
		||||
        if was_connected and self.on_disconnect is not None:
 | 
			
		||||
            self.on_disconnect(err)
 | 
			
		||||
 | 
			
		||||
    def _write(self, data):  # type: (bytes) -> None
 | 
			
		||||
        if self._socket is None:
 | 
			
		||||
            raise APIConnectionError("Socket closed")
 | 
			
		||||
 | 
			
		||||
        # _LOGGER.debug("Write: %s", format_bytes(data))
 | 
			
		||||
        with self._socket_write_lock:
 | 
			
		||||
            try:
 | 
			
		||||
                self._socket.sendall(data)
 | 
			
		||||
            except OSError as err:
 | 
			
		||||
                err = APIConnectionError(f"Error while writing data: {err}")
 | 
			
		||||
                self._fatal_error(err)
 | 
			
		||||
                raise err
 | 
			
		||||
 | 
			
		||||
    def _send_message(self, msg):
 | 
			
		||||
        # type: (message.Message) -> None
 | 
			
		||||
        for message_type, klass in MESSAGE_TYPE_TO_PROTO.items():
 | 
			
		||||
            if isinstance(msg, klass):
 | 
			
		||||
                break
 | 
			
		||||
        else:
 | 
			
		||||
            raise ValueError
 | 
			
		||||
 | 
			
		||||
        encoded = msg.SerializeToString()
 | 
			
		||||
        _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg)))
 | 
			
		||||
        req = bytes([0])
 | 
			
		||||
        req += _varuint_to_bytes(len(encoded))
 | 
			
		||||
        req += _varuint_to_bytes(message_type)
 | 
			
		||||
        req += encoded
 | 
			
		||||
        self._write(req)
 | 
			
		||||
 | 
			
		||||
    def _send_message_await_response_complex(
 | 
			
		||||
        self, send_msg, do_append, do_stop, timeout=5
 | 
			
		||||
    ):
 | 
			
		||||
        event = threading.Event()
 | 
			
		||||
        responses = []
 | 
			
		||||
 | 
			
		||||
        def on_message(resp):
 | 
			
		||||
            if do_append(resp):
 | 
			
		||||
                responses.append(resp)
 | 
			
		||||
            if do_stop(resp):
 | 
			
		||||
                event.set()
 | 
			
		||||
 | 
			
		||||
        self._message_handlers.append(on_message)
 | 
			
		||||
        self._send_message(send_msg)
 | 
			
		||||
        ret = event.wait(timeout)
 | 
			
		||||
        try:
 | 
			
		||||
            self._message_handlers.remove(on_message)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        if not ret:
 | 
			
		||||
            raise APIConnectionError("Timeout while waiting for message response!")
 | 
			
		||||
        return responses
 | 
			
		||||
 | 
			
		||||
    def _send_message_await_response(self, send_msg, response_type, timeout=5):
 | 
			
		||||
        def is_response(msg):
 | 
			
		||||
            return isinstance(msg, response_type)
 | 
			
		||||
 | 
			
		||||
        return self._send_message_await_response_complex(
 | 
			
		||||
            send_msg, is_response, is_response, timeout
 | 
			
		||||
        )[0]
 | 
			
		||||
 | 
			
		||||
    def device_info(self):
 | 
			
		||||
        self._check_connected()
 | 
			
		||||
        return self._send_message_await_response(
 | 
			
		||||
            pb.DeviceInfoRequest(), pb.DeviceInfoResponse
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def ping(self):
 | 
			
		||||
        self._check_connected()
 | 
			
		||||
        return self._send_message_await_response(pb.PingRequest(), pb.PingResponse)
 | 
			
		||||
 | 
			
		||||
    def disconnect(self, on_disconnect=True):
 | 
			
		||||
        self._check_connected()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self._send_message_await_response(
 | 
			
		||||
                pb.DisconnectRequest(), pb.DisconnectResponse
 | 
			
		||||
            )
 | 
			
		||||
        except APIConnectionError:
 | 
			
		||||
            pass
 | 
			
		||||
        self._close_socket()
 | 
			
		||||
 | 
			
		||||
        if self.on_disconnect is not None and on_disconnect:
 | 
			
		||||
            self.on_disconnect(None)
 | 
			
		||||
 | 
			
		||||
    def _check_authenticated(self):
 | 
			
		||||
        if not self._authenticated:
 | 
			
		||||
            raise APIConnectionError("Must login first!")
 | 
			
		||||
 | 
			
		||||
    def subscribe_logs(self, on_log, log_level=7, dump_config=False):
 | 
			
		||||
        self._check_authenticated()
 | 
			
		||||
 | 
			
		||||
        def on_msg(msg):
 | 
			
		||||
            if isinstance(msg, pb.SubscribeLogsResponse):
 | 
			
		||||
                on_log(msg)
 | 
			
		||||
 | 
			
		||||
        self._message_handlers.append(on_msg)
 | 
			
		||||
        req = pb.SubscribeLogsRequest(dump_config=dump_config)
 | 
			
		||||
        req.level = log_level
 | 
			
		||||
        self._send_message(req)
 | 
			
		||||
 | 
			
		||||
    def _recv(self, amount):
 | 
			
		||||
        ret = bytes()
 | 
			
		||||
        if amount == 0:
 | 
			
		||||
            return ret
 | 
			
		||||
 | 
			
		||||
        while len(ret) < amount:
 | 
			
		||||
            if self.stopped:
 | 
			
		||||
                raise APIConnectionError("Stopped!")
 | 
			
		||||
            if not self._socket_open_event.is_set():
 | 
			
		||||
                raise APIConnectionError("No socket!")
 | 
			
		||||
            try:
 | 
			
		||||
                val = self._socket.recv(amount - len(ret))
 | 
			
		||||
            except AttributeError as err:
 | 
			
		||||
                raise APIConnectionError("Socket was closed") from err
 | 
			
		||||
            except socket.timeout:
 | 
			
		||||
                continue
 | 
			
		||||
            except OSError as err:
 | 
			
		||||
                raise APIConnectionError(f"Error while receiving data: {err}") from err
 | 
			
		||||
            ret += val
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    def _recv_varint(self):
 | 
			
		||||
        raw = bytes()
 | 
			
		||||
        while not raw or raw[-1] & 0x80:
 | 
			
		||||
            raw += self._recv(1)
 | 
			
		||||
        return _bytes_to_varuint(raw)
 | 
			
		||||
 | 
			
		||||
    def _run_once(self):
 | 
			
		||||
        if not self._socket_open_event.wait(0.1):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Preamble
 | 
			
		||||
        if self._recv(1)[0] != 0x00:
 | 
			
		||||
            raise APIConnectionError("Invalid preamble")
 | 
			
		||||
 | 
			
		||||
        length = self._recv_varint()
 | 
			
		||||
        msg_type = self._recv_varint()
 | 
			
		||||
 | 
			
		||||
        raw_msg = self._recv(length)
 | 
			
		||||
        if msg_type not in MESSAGE_TYPE_TO_PROTO:
 | 
			
		||||
            _LOGGER.debug("Skipping message type %s", msg_type)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        msg = MESSAGE_TYPE_TO_PROTO[msg_type]()
 | 
			
		||||
        msg.ParseFromString(raw_msg)
 | 
			
		||||
        _LOGGER.debug("Got message: %s:\n%s", type(msg), indent(str(msg)))
 | 
			
		||||
        for msg_handler in self._message_handlers[:]:
 | 
			
		||||
            msg_handler(msg)
 | 
			
		||||
        self._handle_internal_messages(msg)
 | 
			
		||||
 | 
			
		||||
    def run(self):
 | 
			
		||||
        self._running_event.set()
 | 
			
		||||
        while not self.stopped:
 | 
			
		||||
            try:
 | 
			
		||||
                self._run_once()
 | 
			
		||||
            except APIConnectionError as err:
 | 
			
		||||
                if self.stopped:
 | 
			
		||||
                    break
 | 
			
		||||
                if self._connected:
 | 
			
		||||
                    _LOGGER.error("Error while reading incoming messages: %s", err)
 | 
			
		||||
                    self._fatal_error(err)
 | 
			
		||||
        self._running_event.clear()
 | 
			
		||||
 | 
			
		||||
    def _handle_internal_messages(self, msg):
 | 
			
		||||
        if isinstance(msg, pb.DisconnectRequest):
 | 
			
		||||
            self._send_message(pb.DisconnectResponse())
 | 
			
		||||
            if self._socket is not None:
 | 
			
		||||
                self._socket.close()
 | 
			
		||||
                self._socket = None
 | 
			
		||||
            self._connected = False
 | 
			
		||||
            if self.on_disconnect is not None:
 | 
			
		||||
                self.on_disconnect(None)
 | 
			
		||||
        elif isinstance(msg, pb.PingRequest):
 | 
			
		||||
            self._send_message(pb.PingResponse())
 | 
			
		||||
        elif isinstance(msg, pb.GetTimeRequest):
 | 
			
		||||
            resp = pb.GetTimeResponse()
 | 
			
		||||
            resp.epoch_seconds = int(time.time())
 | 
			
		||||
            self._send_message(resp)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_logs(config, address):
 | 
			
		||||
    conf = config["api"]
 | 
			
		||||
    port = conf[CONF_PORT]
 | 
			
		||||
    password = conf[CONF_PASSWORD]
 | 
			
		||||
    _LOGGER.info("Starting log output from %s using esphome API", address)
 | 
			
		||||
 | 
			
		||||
    cli = APIClient(address, port, password)
 | 
			
		||||
    stopping = False
 | 
			
		||||
    retry_timer = []
 | 
			
		||||
 | 
			
		||||
    has_connects = []
 | 
			
		||||
 | 
			
		||||
    def try_connect(err, tries=0):
 | 
			
		||||
        if stopping:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if err:
 | 
			
		||||
            _LOGGER.warning("Disconnected from API: %s", err)
 | 
			
		||||
 | 
			
		||||
        while retry_timer:
 | 
			
		||||
            retry_timer.pop(0).cancel()
 | 
			
		||||
 | 
			
		||||
        error = None
 | 
			
		||||
        try:
 | 
			
		||||
            cli.connect()
 | 
			
		||||
            cli.login()
 | 
			
		||||
        except APIConnectionError as err2:  # noqa
 | 
			
		||||
            error = err2
 | 
			
		||||
 | 
			
		||||
        if error is None:
 | 
			
		||||
            _LOGGER.info("Successfully connected to %s", address)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        wait_time = int(min(1.5 ** min(tries, 100), 30))
 | 
			
		||||
        if not has_connects:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Initial connection failed. The ESP might not be connected "
 | 
			
		||||
                "to WiFi yet (%s). Re-Trying in %s seconds",
 | 
			
		||||
                error,
 | 
			
		||||
                wait_time,
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Couldn't connect to API (%s). Trying to reconnect in %s seconds",
 | 
			
		||||
                error,
 | 
			
		||||
                wait_time,
 | 
			
		||||
            )
 | 
			
		||||
        timer = threading.Timer(
 | 
			
		||||
            wait_time, functools.partial(try_connect, None, tries + 1)
 | 
			
		||||
        )
 | 
			
		||||
        timer.start()
 | 
			
		||||
        retry_timer.append(timer)
 | 
			
		||||
 | 
			
		||||
    def on_log(msg):
 | 
			
		||||
        time_ = datetime.now().time().strftime("[%H:%M:%S]")
 | 
			
		||||
        text = msg.message
 | 
			
		||||
        if msg.send_failed:
 | 
			
		||||
            text = color(
 | 
			
		||||
                Fore.WHITE,
 | 
			
		||||
                "(Message skipped because it was too big to fit in "
 | 
			
		||||
                "TCP buffer - This is only cosmetic)",
 | 
			
		||||
            )
 | 
			
		||||
        safe_print(time_ + text)
 | 
			
		||||
 | 
			
		||||
    def on_login():
 | 
			
		||||
        try:
 | 
			
		||||
            cli.subscribe_logs(on_log, dump_config=not has_connects)
 | 
			
		||||
            has_connects.append(True)
 | 
			
		||||
        except APIConnectionError:
 | 
			
		||||
            cli.disconnect()
 | 
			
		||||
 | 
			
		||||
    cli.on_disconnect = try_connect
 | 
			
		||||
    cli.on_login = on_login
 | 
			
		||||
    cli.start()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        try_connect(None)
 | 
			
		||||
        while True:
 | 
			
		||||
            time.sleep(1)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        stopping = True
 | 
			
		||||
        cli.stop(True)
 | 
			
		||||
        while retry_timer:
 | 
			
		||||
            retry_timer.pop(0).cancel()
 | 
			
		||||
    return 0
 | 
			
		||||
@@ -55,6 +55,7 @@ ESP8266_BOARD_PINS = {
 | 
			
		||||
    "espectro": {"LED": 15, "BUTTON": 2},
 | 
			
		||||
    "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0},
 | 
			
		||||
    "espinotee": {"LED": 16},
 | 
			
		||||
    "espmxdevkit": {},
 | 
			
		||||
    "espresso_lite_v1": {"LED": 16},
 | 
			
		||||
    "espresso_lite_v2": {"LED": 2},
 | 
			
		||||
    "gen4iod": {},
 | 
			
		||||
@@ -105,6 +106,10 @@ ESP8266_BOARD_PINS = {
 | 
			
		||||
    },
 | 
			
		||||
    "phoenix_v1": {"LED": 16},
 | 
			
		||||
    "phoenix_v2": {"LED": 2},
 | 
			
		||||
    "sonoff_basic": {},
 | 
			
		||||
    "sonoff_s20": {},
 | 
			
		||||
    "sonoff_sv": {},
 | 
			
		||||
    "sonoff_th": {},
 | 
			
		||||
    "sparkfunBlynk": "thing",
 | 
			
		||||
    "thing": {"LED": 5, "SDA": 2, "SCL": 14},
 | 
			
		||||
    "thingdev": "thing",
 | 
			
		||||
@@ -166,6 +171,7 @@ ESP8266_FLASH_SIZES = {
 | 
			
		||||
    "espectro": FLASH_SIZE_4_MB,
 | 
			
		||||
    "espino": FLASH_SIZE_4_MB,
 | 
			
		||||
    "espinotee": FLASH_SIZE_4_MB,
 | 
			
		||||
    "espmxdevkit": FLASH_SIZE_1_MB,
 | 
			
		||||
    "espresso_lite_v1": FLASH_SIZE_4_MB,
 | 
			
		||||
    "espresso_lite_v2": FLASH_SIZE_4_MB,
 | 
			
		||||
    "gen4iod": FLASH_SIZE_512_KB,
 | 
			
		||||
@@ -178,6 +184,10 @@ ESP8266_FLASH_SIZES = {
 | 
			
		||||
    "oak": FLASH_SIZE_4_MB,
 | 
			
		||||
    "phoenix_v1": FLASH_SIZE_4_MB,
 | 
			
		||||
    "phoenix_v2": FLASH_SIZE_4_MB,
 | 
			
		||||
    "sonoff_basic": FLASH_SIZE_1_MB,
 | 
			
		||||
    "sonoff_s20": FLASH_SIZE_1_MB,
 | 
			
		||||
    "sonoff_sv": FLASH_SIZE_1_MB,
 | 
			
		||||
    "sonoff_th": FLASH_SIZE_1_MB,
 | 
			
		||||
    "sparkfunBlynk": FLASH_SIZE_4_MB,
 | 
			
		||||
    "thing": FLASH_SIZE_512_KB,
 | 
			
		||||
    "thingdev": FLASH_SIZE_512_KB,
 | 
			
		||||
@@ -291,6 +301,7 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "SW2": 2,
 | 
			
		||||
        "SW3": 0,
 | 
			
		||||
    },
 | 
			
		||||
    "az-delivery-devkit-v4": {},
 | 
			
		||||
    "bpi-bit": {
 | 
			
		||||
        "BUTTON_A": 35,
 | 
			
		||||
        "BUTTON_B": 27,
 | 
			
		||||
@@ -320,6 +331,8 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "RGB_LED": 4,
 | 
			
		||||
        "TEMPERATURE_SENSOR": 34,
 | 
			
		||||
    },
 | 
			
		||||
    "briki_abc_esp32": {},
 | 
			
		||||
    "briki_mbc-wb_esp32": {},
 | 
			
		||||
    "d-duino-32": {
 | 
			
		||||
        "D1": 5,
 | 
			
		||||
        "D10": 1,
 | 
			
		||||
@@ -380,11 +393,58 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
    "esp32cam": {},
 | 
			
		||||
    "esp32dev": {},
 | 
			
		||||
    "esp32doit-devkit-v1": {"LED": 2},
 | 
			
		||||
    "esp32doit-espduino": {"TX0": 1, "RX0": 3, "CMD": 11, "CLK": 6, "SD0": 7, "SD1": 8},
 | 
			
		||||
    "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2},
 | 
			
		||||
    "esp32thing_plus": {
 | 
			
		||||
        "SDA": 23,
 | 
			
		||||
        "SCL": 22,
 | 
			
		||||
        "SS": 33,
 | 
			
		||||
        "MOSI": 18,
 | 
			
		||||
        "MISO": 19,
 | 
			
		||||
        "SCK": 5,
 | 
			
		||||
        "A0": 26,
 | 
			
		||||
        "A1": 25,
 | 
			
		||||
        "A2": 34,
 | 
			
		||||
        "A3": 39,
 | 
			
		||||
        "A4": 36,
 | 
			
		||||
        "A5": 4,
 | 
			
		||||
        "A6": 14,
 | 
			
		||||
        "A7": 32,
 | 
			
		||||
        "A8": 15,
 | 
			
		||||
        "A9": 33,
 | 
			
		||||
        "A10": 27,
 | 
			
		||||
        "A11": 12,
 | 
			
		||||
        "A12": 13,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32vn-iot-uno": {},
 | 
			
		||||
    "espea32": {"BUTTON": 0, "LED": 5},
 | 
			
		||||
    "espectro32": {"LED": 15, "SD_SS": 33},
 | 
			
		||||
    "espino32": {"BUTTON": 0, "LED": 16},
 | 
			
		||||
    "etboard": {
 | 
			
		||||
        "LED_BUILTIN": 5,
 | 
			
		||||
        "TX": 34,
 | 
			
		||||
        "RX": 35,
 | 
			
		||||
        "SS": 29,
 | 
			
		||||
        "MOSI": 37,
 | 
			
		||||
        "MISO": 31,
 | 
			
		||||
        "SCK": 30,
 | 
			
		||||
        "A0": 36,
 | 
			
		||||
        "A1": 39,
 | 
			
		||||
        "A2": 32,
 | 
			
		||||
        "A3": 33,
 | 
			
		||||
        "A4": 34,
 | 
			
		||||
        "A5": 35,
 | 
			
		||||
        "A6": 25,
 | 
			
		||||
        "A7": 26,
 | 
			
		||||
        "D2": 27,
 | 
			
		||||
        "D3": 14,
 | 
			
		||||
        "D4": 12,
 | 
			
		||||
        "D5": 13,
 | 
			
		||||
        "D6": 15,
 | 
			
		||||
        "D7": 16,
 | 
			
		||||
        "D8": 17,
 | 
			
		||||
        "D9": 4,
 | 
			
		||||
    },
 | 
			
		||||
    "featheresp32": {
 | 
			
		||||
        "A0": 26,
 | 
			
		||||
        "A1": 25,
 | 
			
		||||
@@ -434,6 +494,18 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "SW4": 21,
 | 
			
		||||
    },
 | 
			
		||||
    "frogboard": {},
 | 
			
		||||
    "healtypi4": {
 | 
			
		||||
        "KEY_BUILTIN": 17,
 | 
			
		||||
        "ADS1292_DRDY_PIN": 26,
 | 
			
		||||
        "ADS1292_CS_PIN": 13,
 | 
			
		||||
        "ADS1292_START_PIN": 14,
 | 
			
		||||
        "ADS1292_PWDN_PIN": 27,
 | 
			
		||||
        "AFE4490_CS_PIN": 21,
 | 
			
		||||
        "AFE4490_DRDY_PIN": 39,
 | 
			
		||||
        "AFE4490_PWDN_PIN": 4,
 | 
			
		||||
        "PUSH_BUTTON": 17,
 | 
			
		||||
        "SLIDE_SWITCH": 16,
 | 
			
		||||
    },
 | 
			
		||||
    "heltec_wifi_kit_32": {
 | 
			
		||||
        "A1": 37,
 | 
			
		||||
        "A2": 38,
 | 
			
		||||
@@ -444,6 +516,7 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "SDA_OLED": 4,
 | 
			
		||||
        "Vext": 21,
 | 
			
		||||
    },
 | 
			
		||||
    "heltec_wifi_kit_32_v2": "heltec_wifi_kit_32",
 | 
			
		||||
    "heltec_wifi_lora_32": {
 | 
			
		||||
        "BUTTON": 0,
 | 
			
		||||
        "DIO0": 26,
 | 
			
		||||
@@ -489,8 +562,68 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "SS": 18,
 | 
			
		||||
        "Vext": 21,
 | 
			
		||||
    },
 | 
			
		||||
    "heltec_wireless_stick_lite": {
 | 
			
		||||
        "LED_BUILTIN": 25,
 | 
			
		||||
        "KEY_BUILTIN": 0,
 | 
			
		||||
        "SS": 18,
 | 
			
		||||
        "MOSI": 27,
 | 
			
		||||
        "MISO": 19,
 | 
			
		||||
        "SCK": 5,
 | 
			
		||||
        "Vext": 21,
 | 
			
		||||
        "LED": 25,
 | 
			
		||||
        "RST_LoRa": 14,
 | 
			
		||||
        "DIO0": 26,
 | 
			
		||||
        "DIO1": 35,
 | 
			
		||||
        "DIO2": 34,
 | 
			
		||||
    },
 | 
			
		||||
    "honeylemon": {
 | 
			
		||||
        "LED_BUILTIN": 2,
 | 
			
		||||
        "BUILTIN_KEY": 0,
 | 
			
		||||
    },
 | 
			
		||||
    "hornbill32dev": {"BUTTON": 0, "LED": 13},
 | 
			
		||||
    "hornbill32minima": {"SS": 2},
 | 
			
		||||
    "imbrios-logsens-v1p1": {
 | 
			
		||||
        "LED_BUILTIN": 33,
 | 
			
		||||
        "UART2_TX": 17,
 | 
			
		||||
        "UART2_RX": 16,
 | 
			
		||||
        "UART2_RTS": 4,
 | 
			
		||||
        "CAN_TX": 17,
 | 
			
		||||
        "CAN_RX": 16,
 | 
			
		||||
        "CAN_TXDE": 4,
 | 
			
		||||
        "SS": 15,
 | 
			
		||||
        "MOSI": 13,
 | 
			
		||||
        "MISO": 12,
 | 
			
		||||
        "SCK": 14,
 | 
			
		||||
        "SPI_SS1": 23,
 | 
			
		||||
        "BUZZER_CTRL": 19,
 | 
			
		||||
        "SD_CARD_DETECT": 35,
 | 
			
		||||
        "SW2_BUILDIN": 0,
 | 
			
		||||
        "SW3_BUILDIN": 36,
 | 
			
		||||
        "SW4_BUILDIN": 34,
 | 
			
		||||
        "LED1_BUILDIN": 32,
 | 
			
		||||
        "LED2_BUILDIN": 33,
 | 
			
		||||
    },
 | 
			
		||||
    "inex_openkb": {
 | 
			
		||||
        "LED_BUILTIN": 16,
 | 
			
		||||
        "LDR_PIN": 36,
 | 
			
		||||
        "SW1": 16,
 | 
			
		||||
        "SW2": 14,
 | 
			
		||||
        "BT_LED": 17,
 | 
			
		||||
        "WIFI_LED": 2,
 | 
			
		||||
        "NTP_LED": 15,
 | 
			
		||||
        "IOT_LED": 12,
 | 
			
		||||
        "BUZZER": 13,
 | 
			
		||||
        "INPUT1": 32,
 | 
			
		||||
        "INPUT2": 33,
 | 
			
		||||
        "INPUT3": 34,
 | 
			
		||||
        "INPUT4": 35,
 | 
			
		||||
        "OUTPUT1": 26,
 | 
			
		||||
        "OUTPUT2": 27,
 | 
			
		||||
        "SDA0": 21,
 | 
			
		||||
        "SCL0": 22,
 | 
			
		||||
        "SDA1": 4,
 | 
			
		||||
        "SCL1": 5,
 | 
			
		||||
    },
 | 
			
		||||
    "intorobot": {
 | 
			
		||||
        "A1": 39,
 | 
			
		||||
        "A2": 35,
 | 
			
		||||
@@ -528,6 +661,40 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
    "iotaap_magnolia": {},
 | 
			
		||||
    "iotbusio": {},
 | 
			
		||||
    "iotbusproteus": {},
 | 
			
		||||
    "kits-edu": {},
 | 
			
		||||
    "labplus_mpython": {
 | 
			
		||||
        "SDA": 23,
 | 
			
		||||
        "SCL": 22,
 | 
			
		||||
        "P0": 33,
 | 
			
		||||
        "P1": 32,
 | 
			
		||||
        "P2": 35,
 | 
			
		||||
        "P3": 34,
 | 
			
		||||
        "P4": 39,
 | 
			
		||||
        "P5": 0,
 | 
			
		||||
        "P6": 16,
 | 
			
		||||
        "P7": 17,
 | 
			
		||||
        "P8": 26,
 | 
			
		||||
        "P9": 25,
 | 
			
		||||
        "P10": 36,
 | 
			
		||||
        "P11": 2,
 | 
			
		||||
        "P13": 18,
 | 
			
		||||
        "P14": 19,
 | 
			
		||||
        "P15": 21,
 | 
			
		||||
        "P16": 5,
 | 
			
		||||
        "P19": 22,
 | 
			
		||||
        "P20": 23,
 | 
			
		||||
        "P": 27,
 | 
			
		||||
        "Y": 14,
 | 
			
		||||
        "T": 12,
 | 
			
		||||
        "H": 13,
 | 
			
		||||
        "O": 15,
 | 
			
		||||
        "N": 4,
 | 
			
		||||
        "BTN_A": 0,
 | 
			
		||||
        "BTN_B": 2,
 | 
			
		||||
        "SOUND": 36,
 | 
			
		||||
        "LIGHT": 39,
 | 
			
		||||
        "BUZZER": 16,
 | 
			
		||||
    },
 | 
			
		||||
    "lolin32": {"LED": 5},
 | 
			
		||||
    "lolin32_lite": {"LED": 22},
 | 
			
		||||
    "lolin_d32": {"LED": 5, "_VBAT": 35},
 | 
			
		||||
@@ -554,6 +721,16 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "SDA": 12,
 | 
			
		||||
        "SS": 18,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stack-atom": {
 | 
			
		||||
        "SDA": 26,
 | 
			
		||||
        "SCL": 32,
 | 
			
		||||
        "ADC1": 35,
 | 
			
		||||
        "ADC2": 36,
 | 
			
		||||
        "SS": 19,
 | 
			
		||||
        "MOSI": 33,
 | 
			
		||||
        "MISO": 23,
 | 
			
		||||
        "SCK": 22,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stack-core-esp32": {
 | 
			
		||||
        "ADC1": 35,
 | 
			
		||||
        "ADC2": 36,
 | 
			
		||||
@@ -580,6 +757,26 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "RXD2": 16,
 | 
			
		||||
        "TXD2": 17,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stack-core2": {
 | 
			
		||||
        "SDA": 32,
 | 
			
		||||
        "SCL": 33,
 | 
			
		||||
        "SS": 5,
 | 
			
		||||
        "MOSI": 23,
 | 
			
		||||
        "MISO": 38,
 | 
			
		||||
        "SCK": 18,
 | 
			
		||||
        "ADC1": 35,
 | 
			
		||||
        "ADC2": 36,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stack-coreink": {
 | 
			
		||||
        "SDA": 32,
 | 
			
		||||
        "SCL": 33,
 | 
			
		||||
        "SS": 9,
 | 
			
		||||
        "MOSI": 23,
 | 
			
		||||
        "MISO": 34,
 | 
			
		||||
        "SCK": 18,
 | 
			
		||||
        "ADC1": 35,
 | 
			
		||||
        "ADC2": 36,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stack-fire": {
 | 
			
		||||
        "ADC1": 35,
 | 
			
		||||
        "ADC2": 36,
 | 
			
		||||
@@ -630,6 +827,17 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "RXD2": 16,
 | 
			
		||||
        "TXD2": 17,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stack-timer-cam": {
 | 
			
		||||
        "LED_BUILTIN": 2,
 | 
			
		||||
        "SDA": 4,
 | 
			
		||||
        "SCL": 13,
 | 
			
		||||
        "SS": 5,
 | 
			
		||||
        "MOSI": 23,
 | 
			
		||||
        "MISO": 19,
 | 
			
		||||
        "SCK": 18,
 | 
			
		||||
        "ADC1": 35,
 | 
			
		||||
        "ADC2": 36,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stick-c": {
 | 
			
		||||
        "ADC1": 35,
 | 
			
		||||
        "ADC2": 36,
 | 
			
		||||
@@ -664,6 +872,17 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "RIGHT_PUTTON": 34,
 | 
			
		||||
        "YELLOW_LED": 18,
 | 
			
		||||
    },
 | 
			
		||||
    "mgbot-iotik32a": {
 | 
			
		||||
        "LED_BUILTIN": 4,
 | 
			
		||||
        "TX2": 17,
 | 
			
		||||
        "RX2": 16,
 | 
			
		||||
    },
 | 
			
		||||
    "mgbot-iotik32b": {
 | 
			
		||||
        "LED_BUILTIN": 18,
 | 
			
		||||
        "IR": 27,
 | 
			
		||||
        "TX2": 17,
 | 
			
		||||
        "RX2": 16,
 | 
			
		||||
    },
 | 
			
		||||
    "mhetesp32devkit": {"LED": 2},
 | 
			
		||||
    "mhetesp32minikit": {"LED": 2},
 | 
			
		||||
    "microduino-core-esp32": {
 | 
			
		||||
@@ -740,6 +959,7 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
    },
 | 
			
		||||
    "node32s": {},
 | 
			
		||||
    "nodemcu-32s": {"BUTTON": 0, "LED": 2},
 | 
			
		||||
    "nscreen-32": {},
 | 
			
		||||
    "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22},
 | 
			
		||||
    "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5},
 | 
			
		||||
    "oroca_edubot": {
 | 
			
		||||
@@ -766,6 +986,10 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "VBAT": 35,
 | 
			
		||||
    },
 | 
			
		||||
    "pico32": {},
 | 
			
		||||
    "piranha_esp32": {
 | 
			
		||||
        "LED_BUILTIN": 2,
 | 
			
		||||
        "KEY_BUILTIN": 0,
 | 
			
		||||
    },
 | 
			
		||||
    "pocket_32": {"LED": 16},
 | 
			
		||||
    "pycom_gpy": {
 | 
			
		||||
        "A1": 37,
 | 
			
		||||
@@ -778,7 +1002,14 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "SDA": 12,
 | 
			
		||||
        "SS": 17,
 | 
			
		||||
    },
 | 
			
		||||
    "qchip": "heltec_wifi_kit_32",
 | 
			
		||||
    "quantum": {},
 | 
			
		||||
    "s_odi_ultra": {
 | 
			
		||||
        "LED_BUILTIN": 2,
 | 
			
		||||
        "LED_BUILTINB": 4,
 | 
			
		||||
    },
 | 
			
		||||
    "sensesiot_weizen": {},
 | 
			
		||||
    "sg-o_airMon": {},
 | 
			
		||||
    "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16},
 | 
			
		||||
    "tinypico": {},
 | 
			
		||||
    "ttgo-lora32-v1": {
 | 
			
		||||
@@ -790,6 +1021,26 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "SCK": 5,
 | 
			
		||||
        "SS": 18,
 | 
			
		||||
    },
 | 
			
		||||
    "ttgo-lora32-v2": {
 | 
			
		||||
        "LED_BUILTIN": 22,
 | 
			
		||||
        "KEY_BUILTIN": 0,
 | 
			
		||||
        "SS": 18,
 | 
			
		||||
        "MOSI": 27,
 | 
			
		||||
        "MISO": 19,
 | 
			
		||||
        "SCK": 5,
 | 
			
		||||
        "A1": 37,
 | 
			
		||||
        "A2": 38,
 | 
			
		||||
    },
 | 
			
		||||
    "ttgo-lora32-v21": {
 | 
			
		||||
        "LED_BUILTIN": 25,
 | 
			
		||||
        "KEY_BUILTIN": 0,
 | 
			
		||||
        "SS": 18,
 | 
			
		||||
        "MOSI": 27,
 | 
			
		||||
        "MISO": 19,
 | 
			
		||||
        "SCK": 5,
 | 
			
		||||
        "A1": 37,
 | 
			
		||||
        "A2": 38,
 | 
			
		||||
    },
 | 
			
		||||
    "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18},
 | 
			
		||||
    "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13},
 | 
			
		||||
    "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13},
 | 
			
		||||
@@ -855,6 +1106,32 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "T5": 5,
 | 
			
		||||
        "T6": 4,
 | 
			
		||||
    },
 | 
			
		||||
    "wifiduino32": {
 | 
			
		||||
        "LED_BUILTIN": 2,
 | 
			
		||||
        "KEY_BUILTIN": 0,
 | 
			
		||||
        "SDA": 5,
 | 
			
		||||
        "SCL": 16,
 | 
			
		||||
        "A0": 27,
 | 
			
		||||
        "A1": 14,
 | 
			
		||||
        "A2": 12,
 | 
			
		||||
        "A3": 35,
 | 
			
		||||
        "A4": 13,
 | 
			
		||||
        "A5": 4,
 | 
			
		||||
        "D0": 3,
 | 
			
		||||
        "D1": 1,
 | 
			
		||||
        "D2": 17,
 | 
			
		||||
        "D3": 15,
 | 
			
		||||
        "D4": 32,
 | 
			
		||||
        "D5": 33,
 | 
			
		||||
        "D6": 25,
 | 
			
		||||
        "D7": 26,
 | 
			
		||||
        "D8": 23,
 | 
			
		||||
        "D9": 22,
 | 
			
		||||
        "D10": 21,
 | 
			
		||||
        "D11": 19,
 | 
			
		||||
        "D12": 18,
 | 
			
		||||
        "D13": 2,
 | 
			
		||||
    },
 | 
			
		||||
    "xinabox_cw02": {"LED": 27},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,7 @@ void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) {
 | 
			
		||||
  for (int led = it.size(); led-- > 0;) {
 | 
			
		||||
    it[led].set(Color::BLACK);
 | 
			
		||||
  }
 | 
			
		||||
  it.schedule_show();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) {
 | 
			
		||||
@@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
 | 
			
		||||
    it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  it.schedule_show();
 | 
			
		||||
  return CONSUMED;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								esphome/components/airthings_ble/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/airthings_ble/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import esp32_ble_tracker
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32_ble_tracker"]
 | 
			
		||||
CODEOWNERS = ["@jeromelaban"]
 | 
			
		||||
 | 
			
		||||
airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble")
 | 
			
		||||
AirthingsListener = airthings_ble_ns.class_(
 | 
			
		||||
    "AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(AirthingsListener),
 | 
			
		||||
    }
 | 
			
		||||
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    yield esp32_ble_tracker.register_ble_device(var, config)
 | 
			
		||||
							
								
								
									
										33
									
								
								esphome/components/airthings_ble/airthings_listener.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/airthings_ble/airthings_listener.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
#include "airthings_listener.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_ble {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "airthings_ble";
 | 
			
		||||
 | 
			
		||||
bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  for (auto &it : device.get_manufacturer_datas()) {
 | 
			
		||||
    if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) {
 | 
			
		||||
      if (it.data.size() < 4)
 | 
			
		||||
        continue;
 | 
			
		||||
 | 
			
		||||
      uint32_t sn = it.data[0];
 | 
			
		||||
      sn |= ((uint32_t) it.data[1] << 8);
 | 
			
		||||
      sn |= ((uint32_t) it.data[2] << 16);
 | 
			
		||||
      sn |= ((uint32_t) it.data[3] << 24);
 | 
			
		||||
 | 
			
		||||
      ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str());
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_ble
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										20
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include <BLEDevice.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_ble {
 | 
			
		||||
 | 
			
		||||
class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener {
 | 
			
		||||
 public:
 | 
			
		||||
  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_ble
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/airthings_wave_plus/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/airthings_wave_plus/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@jeromelaban"]
 | 
			
		||||
							
								
								
									
										142
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
#include "airthings_wave_plus.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_plus {
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                            esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT: {
 | 
			
		||||
      if (param->open.status == ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGI(TAG, "Connected successfully!");
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      ESP_LOGW(TAG, "Disconnected!");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      this->handle = 0;
 | 
			
		||||
      auto chr = this->parent()->get_characteristic(service_uuid, sensors_data_characteristic_uuid);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid.to_string().c_str(),
 | 
			
		||||
                 sensors_data_characteristic_uuid.to_string().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->handle = chr->handle;
 | 
			
		||||
      this->node_state = espbt::ClientState::Established;
 | 
			
		||||
 | 
			
		||||
      request_read_values_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_READ_CHAR_EVT: {
 | 
			
		||||
      if (param->read.conn_id != this->parent()->conn_id)
 | 
			
		||||
        break;
 | 
			
		||||
      if (param->read.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      if (param->read.handle == this->handle) {
 | 
			
		||||
        read_sensors_(param->read.value, param->read.value_len);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) {
 | 
			
		||||
  auto value = (WavePlusReadings *) raw_value;
 | 
			
		||||
 | 
			
		||||
  if (sizeof(WavePlusReadings) <= value_len) {
 | 
			
		||||
    ESP_LOGD(TAG, "version = %d", value->version);
 | 
			
		||||
 | 
			
		||||
    if (value->version == 1) {
 | 
			
		||||
      ESP_LOGD(TAG, "ambient light = %d", value->ambientLight);
 | 
			
		||||
 | 
			
		||||
      this->humidity_sensor_->publish_state(value->humidity / 2.0f);
 | 
			
		||||
      if (is_valid_radon_value_(value->radon)) {
 | 
			
		||||
        this->radon_sensor_->publish_state(value->radon);
 | 
			
		||||
      }
 | 
			
		||||
      if (is_valid_radon_value_(value->radon_lt)) {
 | 
			
		||||
        this->radon_long_term_sensor_->publish_state(value->radon_lt);
 | 
			
		||||
      }
 | 
			
		||||
      this->temperature_sensor_->publish_state(value->temperature / 100.0f);
 | 
			
		||||
      this->pressure_sensor_->publish_state(value->pressure / 50.0f);
 | 
			
		||||
      if (is_valid_co2_value_(value->co2)) {
 | 
			
		||||
        this->co2_sensor_->publish_state(value->co2);
 | 
			
		||||
      }
 | 
			
		||||
      if (is_valid_voc_value_(value->voc)) {
 | 
			
		||||
        this->tvoc_sensor_->publish_state(value->voc);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // This instance must not stay connected
 | 
			
		||||
      // so other clients can connect to it (e.g. the
 | 
			
		||||
      // mobile app).
 | 
			
		||||
      parent()->set_enabled(false);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_radon_value_(short radon) { return 0 <= radon && radon <= 16383; }
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_voc_value_(short voc) { return 0 <= voc && voc <= 16383; }
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_co2_value_(short co2) { return 0 <= co2 && co2 <= 16383; }
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::loop() {}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::update() {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::Established) {
 | 
			
		||||
    if (!parent()->enabled) {
 | 
			
		||||
      ESP_LOGW(TAG, "Reconnecting to device");
 | 
			
		||||
      parent()->set_enabled(true);
 | 
			
		||||
      parent()->connect();
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Connection in progress");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::request_read_values_() {
 | 
			
		||||
  auto status =
 | 
			
		||||
      esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (status) {
 | 
			
		||||
    ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Radon", this->radon_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "CO2", this->co2_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AirthingsWavePlus::AirthingsWavePlus() : PollingComponent(10000) {
 | 
			
		||||
  auto service_bt = *BLEUUID::fromString(std::string("b42e1c08-ade7-11e4-89d3-123b93f75cba")).getNative();
 | 
			
		||||
  auto characteristic_bt = *BLEUUID::fromString(std::string("b42e2a68-ade7-11e4-89d3-123b93f75cba")).getNative();
 | 
			
		||||
 | 
			
		||||
  service_uuid = espbt::ESPBTUUID::from_uuid(service_bt);
 | 
			
		||||
  sensors_data_characteristic_uuid = espbt::ESPBTUUID::from_uuid(characteristic_bt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::setup() {}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_plus
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // ARDUINO_ARCH_ESP32
 | 
			
		||||
							
								
								
									
										79
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
#include <BLEDevice.h>
 | 
			
		||||
 | 
			
		||||
using namespace esphome::ble_client;
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_plus {
 | 
			
		||||
 | 
			
		||||
static const char *TAG = "airthings_wave_plus";
 | 
			
		||||
 | 
			
		||||
class AirthingsWavePlus : public PollingComponent, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  AirthingsWavePlus();
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
 | 
			
		||||
  void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
 | 
			
		||||
  void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; }
 | 
			
		||||
  void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
 | 
			
		||||
  void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
 | 
			
		||||
  void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
 | 
			
		||||
  void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
 | 
			
		||||
  void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool is_valid_radon_value_(short radon);
 | 
			
		||||
  bool is_valid_voc_value_(short voc);
 | 
			
		||||
  bool is_valid_co2_value_(short co2);
 | 
			
		||||
 | 
			
		||||
  void read_sensors_(uint8_t *value, uint16_t value_len);
 | 
			
		||||
  void request_read_values_();
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *radon_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *radon_long_term_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *pressure_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *co2_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *tvoc_sensor_{nullptr};
 | 
			
		||||
 | 
			
		||||
  uint16_t handle;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid;
 | 
			
		||||
  espbt::ESPBTUUID sensors_data_characteristic_uuid;
 | 
			
		||||
 | 
			
		||||
  struct WavePlusReadings {
 | 
			
		||||
    uint8_t version;
 | 
			
		||||
    uint8_t humidity;
 | 
			
		||||
    uint8_t ambientLight;
 | 
			
		||||
    uint8_t unused01;
 | 
			
		||||
    uint16_t radon;
 | 
			
		||||
    uint16_t radon_lt;
 | 
			
		||||
    uint16_t temperature;
 | 
			
		||||
    uint16_t pressure;
 | 
			
		||||
    uint16_t co2;
 | 
			
		||||
    uint16_t voc;
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_plus
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // ARDUINO_ARCH_ESP32
 | 
			
		||||
							
								
								
									
										116
									
								
								esphome/components/airthings_wave_plus/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								esphome/components/airthings_wave_plus/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_HECTOPASCAL,
 | 
			
		||||
    ICON_RADIOACTIVE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_RADON,
 | 
			
		||||
    CONF_RADON_LONG_TERM,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    CONF_CO2,
 | 
			
		||||
    CONF_PRESSURE,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    UNIT_BECQUEREL_PER_CUBIC_METER,
 | 
			
		||||
    UNIT_PARTS_PER_MILLION,
 | 
			
		||||
    UNIT_PARTS_PER_BILLION,
 | 
			
		||||
    ICON_RADIATOR,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
 | 
			
		||||
airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus")
 | 
			
		||||
AirthingsWavePlus = airthings_wave_plus_ns.class_(
 | 
			
		||||
    "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RADON): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
 | 
			
		||||
                icon=ICON_RADIOACTIVE,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
 | 
			
		||||
                icon=ICON_RADIOACTIVE,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_HECTOPASCAL,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_PRESSURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CO2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_MILLION,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TVOC): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_BILLION,
 | 
			
		||||
                icon=ICON_RADIATOR,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("5mins"))
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_HUMIDITY in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
 | 
			
		||||
        cg.add(var.set_humidity(sens))
 | 
			
		||||
    if CONF_RADON in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_RADON])
 | 
			
		||||
        cg.add(var.set_radon(sens))
 | 
			
		||||
    if CONF_RADON_LONG_TERM in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
 | 
			
		||||
        cg.add(var.set_radon_long_term(sens))
 | 
			
		||||
    if CONF_TEMPERATURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
 | 
			
		||||
        cg.add(var.set_temperature(sens))
 | 
			
		||||
    if CONF_PRESSURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_PRESSURE])
 | 
			
		||||
        cg.add(var.set_pressure(sens))
 | 
			
		||||
    if CONF_CO2 in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_CO2])
 | 
			
		||||
        cg.add(var.set_co2(sens))
 | 
			
		||||
    if CONF_TVOC in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TVOC])
 | 
			
		||||
        cg.add(var.set_tvoc(sens))
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import base64
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
@@ -6,6 +8,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_DATA,
 | 
			
		||||
    CONF_DATA_TEMPLATE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_KEY,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
    CONF_PORT,
 | 
			
		||||
    CONF_REBOOT_TIMEOUT,
 | 
			
		||||
@@ -19,7 +22,7 @@ from esphome.const import (
 | 
			
		||||
from esphome.core import coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
AUTO_LOAD = ["async_tcp"]
 | 
			
		||||
AUTO_LOAD = ["socket"]
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
 | 
			
		||||
api_ns = cg.esphome_ns.namespace("api")
 | 
			
		||||
@@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = {
 | 
			
		||||
    "float[]": cg.std_vector.template(float),
 | 
			
		||||
    "string[]": cg.std_vector.template(cg.std_string),
 | 
			
		||||
}
 | 
			
		||||
CONF_ENCRYPTION = "encryption"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_encryption_key(value):
 | 
			
		||||
    value = cv.string_strict(value)
 | 
			
		||||
    try:
 | 
			
		||||
        decoded = base64.b64decode(value, validate=True)
 | 
			
		||||
    except ValueError as err:
 | 
			
		||||
        raise cv.Invalid("Invalid key format, please check it's using base64") from err
 | 
			
		||||
 | 
			
		||||
    if len(decoded) != 32:
 | 
			
		||||
        raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
 | 
			
		||||
 | 
			
		||||
    # Return original data for roundtrip conversion
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
@@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ENCRYPTION): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Required(CONF_KEY): validate_encryption_key,
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
@@ -92,6 +116,15 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.register_user_service(trigger))
 | 
			
		||||
        await automation.build_automation(trigger, func_args, conf)
 | 
			
		||||
 | 
			
		||||
    if CONF_ENCRYPTION in config:
 | 
			
		||||
        conf = config[CONF_ENCRYPTION]
 | 
			
		||||
        decoded = base64.b64decode(conf[CONF_KEY])
 | 
			
		||||
        cg.add(var.set_noise_psk(list(decoded)))
 | 
			
		||||
        cg.add_define("USE_API_NOISE")
 | 
			
		||||
        cg.add_library("esphome/noise-c", "0.1.1")
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add_define("USE_API_PLAINTEXT")
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_API")
 | 
			
		||||
    cg.add_global(api_ns.using)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -473,7 +473,8 @@ message ListEntitiesSensorResponse {
 | 
			
		||||
  bool force_update = 8;
 | 
			
		||||
  string device_class = 9;
 | 
			
		||||
  SensorStateClass state_class = 10;
 | 
			
		||||
  SensorLastResetType last_reset_type = 11;
 | 
			
		||||
  // Last reset type removed in 2021.9.0
 | 
			
		||||
  SensorLastResetType legacy_last_reset_type = 11;
 | 
			
		||||
  bool disabled_by_default = 12;
 | 
			
		||||
}
 | 
			
		||||
message SensorStateResponse {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DEEP_SLEEP
 | 
			
		||||
#include "esphome/components/deep_sleep/deep_sleep_component.h"
 | 
			
		||||
@@ -18,145 +19,146 @@ namespace api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.connection";
 | 
			
		||||
 | 
			
		||||
APIConnection::APIConnection(AsyncClient *client, APIServer *parent)
 | 
			
		||||
    : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
 | 
			
		||||
  this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this);
 | 
			
		||||
  this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this);
 | 
			
		||||
  this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); },
 | 
			
		||||
                           this);
 | 
			
		||||
  this->client_->onData([](void *s, AsyncClient *c, void *buf,
 | 
			
		||||
                           size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); },
 | 
			
		||||
                        this);
 | 
			
		||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
 | 
			
		||||
    : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
 | 
			
		||||
  this->proto_write_buffer_.reserve(64);
 | 
			
		||||
 | 
			
		||||
  this->send_buffer_.reserve(64);
 | 
			
		||||
  this->recv_buffer_.reserve(32);
 | 
			
		||||
  this->client_info_ = this->client_->remoteIP().toString().c_str();
 | 
			
		||||
#if defined(USE_API_PLAINTEXT)
 | 
			
		||||
  helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
 | 
			
		||||
#elif defined(USE_API_NOISE)
 | 
			
		||||
  helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
 | 
			
		||||
#else
 | 
			
		||||
#error "No frame helper defined"
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::start() {
 | 
			
		||||
  this->last_traffic_ = millis();
 | 
			
		||||
}
 | 
			
		||||
APIConnection::~APIConnection() { delete this->client_; }
 | 
			
		||||
void APIConnection::on_error_(int8_t error) { this->remove_ = true; }
 | 
			
		||||
void APIConnection::on_disconnect_() { this->remove_ = true; }
 | 
			
		||||
void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); }
 | 
			
		||||
void APIConnection::on_data_(uint8_t *buf, size_t len) {
 | 
			
		||||
  if (len == 0 || buf == nullptr)
 | 
			
		||||
 | 
			
		||||
  APIError err = helper_->init();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
    return;
 | 
			
		||||
  this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::parse_recv_buffer_() {
 | 
			
		||||
  if (this->recv_buffer_.empty() || this->remove_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  while (!this->recv_buffer_.empty()) {
 | 
			
		||||
    if (this->recv_buffer_[0] != 0x00) {
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str());
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    uint32_t i = 1;
 | 
			
		||||
    const uint32_t size = this->recv_buffer_.size();
 | 
			
		||||
    uint32_t consumed;
 | 
			
		||||
    auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
 | 
			
		||||
    if (!msg_size_varint.has_value())
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      return;
 | 
			
		||||
    i += consumed;
 | 
			
		||||
    uint32_t msg_size = msg_size_varint->as_uint32();
 | 
			
		||||
 | 
			
		||||
    auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed);
 | 
			
		||||
    if (!msg_type_varint.has_value())
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      return;
 | 
			
		||||
    i += consumed;
 | 
			
		||||
    uint32_t msg_type = msg_type_varint->as_uint32();
 | 
			
		||||
 | 
			
		||||
    if (size - i < msg_size)
 | 
			
		||||
      // message body not fully received
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    uint8_t *msg = &this->recv_buffer_[i];
 | 
			
		||||
    this->read_message(msg_size, msg_type, msg);
 | 
			
		||||
    if (this->remove_)
 | 
			
		||||
      return;
 | 
			
		||||
    // pop front
 | 
			
		||||
    uint32_t total = i + msg_size;
 | 
			
		||||
    this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total);
 | 
			
		||||
    this->last_traffic_ = millis();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::disconnect_client() {
 | 
			
		||||
  this->client_->close();
 | 
			
		||||
  this->remove_ = true;
 | 
			
		||||
  client_info_ = helper_->getpeername();
 | 
			
		||||
  helper_->set_log_info(client_info_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::loop() {
 | 
			
		||||
  if (this->remove_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  if (this->next_close_) {
 | 
			
		||||
    this->disconnect_client();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!network_is_connected()) {
 | 
			
		||||
    // when network is disconnected force disconnect immediately
 | 
			
		||||
    // don't wait for timeout
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->client_->disconnected()) {
 | 
			
		||||
    // failsafe for disconnect logic
 | 
			
		||||
    this->on_disconnect_();
 | 
			
		||||
  if (this->next_close_) {
 | 
			
		||||
    // requested a disconnect
 | 
			
		||||
    this->helper_->close();
 | 
			
		||||
    this->remove_ = true;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->parse_recv_buffer_();
 | 
			
		||||
 | 
			
		||||
  APIError err = helper_->loop();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ReadPacketBuffer buffer;
 | 
			
		||||
  err = helper_->read_packet(&buffer);
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK) {
 | 
			
		||||
    // pass
 | 
			
		||||
  } else if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->last_traffic_ = millis();
 | 
			
		||||
    // read a packet
 | 
			
		||||
    this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
 | 
			
		||||
    if (this->remove_)
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->list_entities_iterator_.advance();
 | 
			
		||||
  this->initial_state_iterator_.advance();
 | 
			
		||||
 | 
			
		||||
  const uint32_t keepalive = 60000;
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  if (this->sent_ping_) {
 | 
			
		||||
    // Disconnect if not responded within 2.5*keepalive
 | 
			
		||||
    if (millis() - this->last_traffic_ > (keepalive * 5) / 2) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
 | 
			
		||||
      this->disconnect_client();
 | 
			
		||||
    if (now - this->last_traffic_ > (keepalive * 5) / 2) {
 | 
			
		||||
      on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  } else if (millis() - this->last_traffic_ > keepalive) {
 | 
			
		||||
  } else if (now - this->last_traffic_ > keepalive) {
 | 
			
		||||
    this->sent_ping_ = true;
 | 
			
		||||
    this->send_ping_request(PingRequest());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
  if (this->image_reader_.available()) {
 | 
			
		||||
    uint32_t space = this->client_->space();
 | 
			
		||||
    // reserve 15 bytes for metadata, and at least 64 bytes of data
 | 
			
		||||
    if (space >= 15 + 64) {
 | 
			
		||||
      uint32_t to_send = std::min(space - 15, this->image_reader_.available());
 | 
			
		||||
      auto buffer = this->create_buffer();
 | 
			
		||||
      // fixed32 key = 1;
 | 
			
		||||
      buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
 | 
			
		||||
      // bytes data = 2;
 | 
			
		||||
      buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
 | 
			
		||||
      // bool done = 3;
 | 
			
		||||
      bool done = this->image_reader_.available() == to_send;
 | 
			
		||||
      buffer.encode_bool(3, done);
 | 
			
		||||
      bool success = this->send_buffer(buffer, 44);
 | 
			
		||||
  if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
 | 
			
		||||
    uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available());
 | 
			
		||||
    auto buffer = this->create_buffer();
 | 
			
		||||
    // fixed32 key = 1;
 | 
			
		||||
    buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash());
 | 
			
		||||
    // bytes data = 2;
 | 
			
		||||
    buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send);
 | 
			
		||||
    // bool done = 3;
 | 
			
		||||
    bool done = this->image_reader_.available() == to_send;
 | 
			
		||||
    buffer.encode_bool(3, done);
 | 
			
		||||
    bool success = this->send_buffer(buffer, 44);
 | 
			
		||||
 | 
			
		||||
      if (success) {
 | 
			
		||||
        this->image_reader_.consume_data(to_send);
 | 
			
		||||
      }
 | 
			
		||||
      if (success && done) {
 | 
			
		||||
        this->image_reader_.return_image();
 | 
			
		||||
      }
 | 
			
		||||
    if (success) {
 | 
			
		||||
      this->image_reader_.consume_data(to_send);
 | 
			
		||||
    }
 | 
			
		||||
    if (success && done) {
 | 
			
		||||
      this->image_reader_.return_image();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (state_subs_at_ != -1) {
 | 
			
		||||
    const auto &subs = this->parent_->get_state_subs();
 | 
			
		||||
    if (state_subs_at_ >= subs.size()) {
 | 
			
		||||
      state_subs_at_ = -1;
 | 
			
		||||
    } else {
 | 
			
		||||
      auto &it = subs[state_subs_at_];
 | 
			
		||||
      SubscribeHomeAssistantStateResponse resp;
 | 
			
		||||
      resp.entity_id = it.entity_id;
 | 
			
		||||
      resp.attribute = it.attribute.value();
 | 
			
		||||
      if (this->send_subscribe_home_assistant_state_response(resp)) {
 | 
			
		||||
        state_subs_at_++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) {
 | 
			
		||||
  return App.get_name() + component_type + nameable->get_object_id();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
 | 
			
		||||
  // remote initiated disconnect_client
 | 
			
		||||
  // don't close yet, we still need to send the disconnect response
 | 
			
		||||
  // close will happen on next loop
 | 
			
		||||
  ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str());
 | 
			
		||||
  this->next_close_ = true;
 | 
			
		||||
  DisconnectResponse resp;
 | 
			
		||||
  return resp;
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
 | 
			
		||||
  // pass
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
@@ -241,6 +243,9 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
bool APIConnection::send_fan_state(fan::FanState *fan) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
    return false;
 | 
			
		||||
@@ -295,6 +300,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
    call.set_direction(static_cast<fan::FanDirection>(msg.direction));
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
@@ -417,7 +423,6 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
 | 
			
		||||
  msg.force_update = sensor->get_force_update();
 | 
			
		||||
  msg.device_class = sensor->get_device_class();
 | 
			
		||||
  msg.state_class = static_cast<enums::SensorStateClass>(sensor->state_class);
 | 
			
		||||
  msg.last_reset_type = static_cast<enums::SensorLastResetType>(sensor->last_reset_type);
 | 
			
		||||
  msg.disabled_by_default = sensor->is_disabled_by_default();
 | 
			
		||||
 | 
			
		||||
  return this->send_list_entities_sensor_response(msg);
 | 
			
		||||
@@ -709,8 +714,8 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HelloResponse APIConnection::hello(const HelloRequest &msg) {
 | 
			
		||||
  this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str();
 | 
			
		||||
  this->client_info_ += ")";
 | 
			
		||||
  this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")";
 | 
			
		||||
  this->helper_->set_log_info(client_info_);
 | 
			
		||||
  ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str());
 | 
			
		||||
 | 
			
		||||
  HelloResponse resp;
 | 
			
		||||
@@ -727,7 +732,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
 | 
			
		||||
  // bool invalid_password = 1;
 | 
			
		||||
  resp.invalid_password = !correct;
 | 
			
		||||
  if (correct) {
 | 
			
		||||
    ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str());
 | 
			
		||||
    ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str());
 | 
			
		||||
    this->connection_state_ = ConnectionState::AUTHENTICATED;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
@@ -745,9 +750,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
 | 
			
		||||
  resp.mac_address = get_mac_address_pretty();
 | 
			
		||||
  resp.esphome_version = ESPHOME_VERSION;
 | 
			
		||||
  resp.compilation_time = App.get_compilation_time();
 | 
			
		||||
#ifdef ARDUINO_BOARD
 | 
			
		||||
  resp.model = ARDUINO_BOARD;
 | 
			
		||||
#endif
 | 
			
		||||
  resp.model = ESPHOME_BOARD;
 | 
			
		||||
#ifdef USE_DEEP_SLEEP
 | 
			
		||||
  resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -775,57 +778,39 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) {
 | 
			
		||||
  for (auto &it : this->parent_->get_state_subs()) {
 | 
			
		||||
    SubscribeHomeAssistantStateResponse resp;
 | 
			
		||||
    resp.entity_id = it.entity_id;
 | 
			
		||||
    resp.attribute = it.attribute.value();
 | 
			
		||||
    if (!this->send_subscribe_home_assistant_state_response(resp)) {
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  state_subs_at_ = 0;
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
 | 
			
		||||
  if (this->remove_)
 | 
			
		||||
    return false;
 | 
			
		||||
  if (!this->helper_->can_write_without_blocking())
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> header;
 | 
			
		||||
  header.push_back(0x00);
 | 
			
		||||
  ProtoVarInt(buffer.get_buffer()->size()).encode(header);
 | 
			
		||||
  ProtoVarInt(message_type).encode(header);
 | 
			
		||||
 | 
			
		||||
  size_t needed_space = buffer.get_buffer()->size() + header.size();
 | 
			
		||||
 | 
			
		||||
  if (needed_space > this->client_->space()) {
 | 
			
		||||
    delay(0);
 | 
			
		||||
    if (needed_space > this->client_->space()) {
 | 
			
		||||
      // SubscribeLogsResponse
 | 
			
		||||
      if (message_type != 29) {
 | 
			
		||||
        ESP_LOGV(TAG, "Cannot send message because of TCP buffer space");
 | 
			
		||||
      }
 | 
			
		||||
      delay(0);
 | 
			
		||||
      return false;
 | 
			
		||||
  APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size());
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return false;
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno);
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->client_->add(reinterpret_cast<char *>(header.data()), header.size(),
 | 
			
		||||
                     ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE);
 | 
			
		||||
  this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size(),
 | 
			
		||||
                     ASYNC_WRITE_FLAG_COPY);
 | 
			
		||||
  bool ret = this->client_->send();
 | 
			
		||||
  return ret;
 | 
			
		||||
  this->last_traffic_ = millis();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_unauthenticated_access() {
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str());
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str());
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_no_setup_connection() {
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str());
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str());
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_fatal_error() {
 | 
			
		||||
  ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str());
 | 
			
		||||
  this->client_->close();
 | 
			
		||||
  this->helper_->close();
 | 
			
		||||
  this->remove_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,16 +5,17 @@
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
#include "api_pb2_service.h"
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#include "api_frame_helper.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
class APIConnection : public APIServerConnection {
 | 
			
		||||
 public:
 | 
			
		||||
  APIConnection(AsyncClient *client, APIServer *parent);
 | 
			
		||||
  virtual ~APIConnection();
 | 
			
		||||
  APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
 | 
			
		||||
  virtual ~APIConnection() = default;
 | 
			
		||||
 | 
			
		||||
  void disconnect_client();
 | 
			
		||||
  void start();
 | 
			
		||||
  void loop();
 | 
			
		||||
 | 
			
		||||
  bool send_list_info_done() {
 | 
			
		||||
@@ -86,10 +87,7 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  void on_disconnect_response(const DisconnectResponse &value) override {
 | 
			
		||||
    // we initiated disconnect_client
 | 
			
		||||
    this->next_close_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  void on_disconnect_response(const DisconnectResponse &value) override;
 | 
			
		||||
  void on_ping_response(const PingResponse &value) override {
 | 
			
		||||
    // we initiated ping
 | 
			
		||||
    this->sent_ping_ = false;
 | 
			
		||||
@@ -100,12 +98,7 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
#endif
 | 
			
		||||
  HelloResponse hello(const HelloRequest &msg) override;
 | 
			
		||||
  ConnectResponse connect(const ConnectRequest &msg) override;
 | 
			
		||||
  DisconnectResponse disconnect(const DisconnectRequest &msg) override {
 | 
			
		||||
    // remote initiated disconnect_client
 | 
			
		||||
    this->next_close_ = true;
 | 
			
		||||
    DisconnectResponse resp;
 | 
			
		||||
    return resp;
 | 
			
		||||
  }
 | 
			
		||||
  DisconnectResponse disconnect(const DisconnectRequest &msg) override;
 | 
			
		||||
  PingResponse ping(const PingRequest &msg) override { return {}; }
 | 
			
		||||
  DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
 | 
			
		||||
  void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
 | 
			
		||||
@@ -135,19 +128,16 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  void on_unauthenticated_access() override;
 | 
			
		||||
  void on_no_setup_connection() override;
 | 
			
		||||
  ProtoWriteBuffer create_buffer() override {
 | 
			
		||||
    this->send_buffer_.clear();
 | 
			
		||||
    return {&this->send_buffer_};
 | 
			
		||||
    // FIXME: ensure no recursive writes can happen
 | 
			
		||||
    this->proto_write_buffer_.clear();
 | 
			
		||||
    return {&this->proto_write_buffer_};
 | 
			
		||||
  }
 | 
			
		||||
  bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  friend APIServer;
 | 
			
		||||
 | 
			
		||||
  void on_error_(int8_t error);
 | 
			
		||||
  void on_disconnect_();
 | 
			
		||||
  void on_timeout_(uint32_t time);
 | 
			
		||||
  void on_data_(uint8_t *buf, size_t len);
 | 
			
		||||
  void parse_recv_buffer_();
 | 
			
		||||
  bool send_(const void *buf, size_t len, bool force);
 | 
			
		||||
 | 
			
		||||
  enum class ConnectionState {
 | 
			
		||||
    WAITING_FOR_HELLO,
 | 
			
		||||
@@ -157,8 +147,10 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
 | 
			
		||||
  bool remove_{false};
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> send_buffer_;
 | 
			
		||||
  std::vector<uint8_t> recv_buffer_;
 | 
			
		||||
  // Buffer used to encode proto messages
 | 
			
		||||
  // Re-use to prevent allocations
 | 
			
		||||
  std::vector<uint8_t> proto_write_buffer_;
 | 
			
		||||
  std::unique_ptr<APIFrameHelper> helper_;
 | 
			
		||||
 | 
			
		||||
  std::string client_info_;
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
@@ -170,12 +162,11 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  uint32_t last_traffic_;
 | 
			
		||||
  bool sent_ping_{false};
 | 
			
		||||
  bool service_call_subscription_{false};
 | 
			
		||||
  bool current_nodelay_{false};
 | 
			
		||||
  bool next_close_{false};
 | 
			
		||||
  AsyncClient *client_;
 | 
			
		||||
  bool next_close_ = false;
 | 
			
		||||
  APIServer *parent_;
 | 
			
		||||
  InitialStateIterator initial_state_iterator_;
 | 
			
		||||
  ListEntitiesIterator list_entities_iterator_;
 | 
			
		||||
  int state_subs_at_ = -1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										961
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										961
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,961 @@
 | 
			
		||||
#include "api_frame_helper.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "proto.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.socket";
 | 
			
		||||
 | 
			
		||||
/// Is the given return value (from read/write syscalls) a wouldblock error?
 | 
			
		||||
bool is_would_block(ssize_t ret) {
 | 
			
		||||
  if (ret == -1) {
 | 
			
		||||
    return errno == EWOULDBLOCK || errno == EAGAIN;
 | 
			
		||||
  }
 | 
			
		||||
  return ret == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *api_error_to_str(APIError err) {
 | 
			
		||||
  // not using switch to ensure compiler doesn't try to build a big table out of it
 | 
			
		||||
  if (err == APIError::OK) {
 | 
			
		||||
    return "OK";
 | 
			
		||||
  } else if (err == APIError::WOULD_BLOCK) {
 | 
			
		||||
    return "WOULD_BLOCK";
 | 
			
		||||
  } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) {
 | 
			
		||||
    return "BAD_HANDSHAKE_PACKET_LEN";
 | 
			
		||||
  } else if (err == APIError::BAD_INDICATOR) {
 | 
			
		||||
    return "BAD_INDICATOR";
 | 
			
		||||
  } else if (err == APIError::BAD_DATA_PACKET) {
 | 
			
		||||
    return "BAD_DATA_PACKET";
 | 
			
		||||
  } else if (err == APIError::TCP_NODELAY_FAILED) {
 | 
			
		||||
    return "TCP_NODELAY_FAILED";
 | 
			
		||||
  } else if (err == APIError::TCP_NONBLOCKING_FAILED) {
 | 
			
		||||
    return "TCP_NONBLOCKING_FAILED";
 | 
			
		||||
  } else if (err == APIError::CLOSE_FAILED) {
 | 
			
		||||
    return "CLOSE_FAILED";
 | 
			
		||||
  } else if (err == APIError::SHUTDOWN_FAILED) {
 | 
			
		||||
    return "SHUTDOWN_FAILED";
 | 
			
		||||
  } else if (err == APIError::BAD_STATE) {
 | 
			
		||||
    return "BAD_STATE";
 | 
			
		||||
  } else if (err == APIError::BAD_ARG) {
 | 
			
		||||
    return "BAD_ARG";
 | 
			
		||||
  } else if (err == APIError::SOCKET_READ_FAILED) {
 | 
			
		||||
    return "SOCKET_READ_FAILED";
 | 
			
		||||
  } else if (err == APIError::SOCKET_WRITE_FAILED) {
 | 
			
		||||
    return "SOCKET_WRITE_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) {
 | 
			
		||||
    return "HANDSHAKESTATE_READ_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) {
 | 
			
		||||
    return "HANDSHAKESTATE_WRITE_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) {
 | 
			
		||||
    return "HANDSHAKESTATE_BAD_STATE";
 | 
			
		||||
  } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) {
 | 
			
		||||
    return "CIPHERSTATE_DECRYPT_FAILED";
 | 
			
		||||
  } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) {
 | 
			
		||||
    return "CIPHERSTATE_ENCRYPT_FAILED";
 | 
			
		||||
  } else if (err == APIError::OUT_OF_MEMORY) {
 | 
			
		||||
    return "OUT_OF_MEMORY";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) {
 | 
			
		||||
    return "HANDSHAKESTATE_SETUP_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) {
 | 
			
		||||
    return "HANDSHAKESTATE_SPLIT_FAILED";
 | 
			
		||||
  } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) {
 | 
			
		||||
    return "BAD_HANDSHAKE_ERROR_BYTE";
 | 
			
		||||
  }
 | 
			
		||||
  return "UNKNOWN";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
 | 
			
		||||
// uncomment to log raw packets
 | 
			
		||||
//#define HELPER_LOG_PACKETS
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
 | 
			
		||||
 | 
			
		||||
/// Convert a noise error code to a readable error
 | 
			
		||||
std::string noise_err_to_str(int err) {
 | 
			
		||||
  if (err == NOISE_ERROR_NO_MEMORY)
 | 
			
		||||
    return "NO_MEMORY";
 | 
			
		||||
  if (err == NOISE_ERROR_UNKNOWN_ID)
 | 
			
		||||
    return "UNKNOWN_ID";
 | 
			
		||||
  if (err == NOISE_ERROR_UNKNOWN_NAME)
 | 
			
		||||
    return "UNKNOWN_NAME";
 | 
			
		||||
  if (err == NOISE_ERROR_MAC_FAILURE)
 | 
			
		||||
    return "MAC_FAILURE";
 | 
			
		||||
  if (err == NOISE_ERROR_NOT_APPLICABLE)
 | 
			
		||||
    return "NOT_APPLICABLE";
 | 
			
		||||
  if (err == NOISE_ERROR_SYSTEM)
 | 
			
		||||
    return "SYSTEM";
 | 
			
		||||
  if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
 | 
			
		||||
    return "REMOTE_KEY_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
 | 
			
		||||
    return "LOCAL_KEY_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_PSK_REQUIRED)
 | 
			
		||||
    return "PSK_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_LENGTH)
 | 
			
		||||
    return "INVALID_LENGTH";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PARAM)
 | 
			
		||||
    return "INVALID_PARAM";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_STATE)
 | 
			
		||||
    return "INVALID_STATE";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_NONCE)
 | 
			
		||||
    return "INVALID_NONCE";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
 | 
			
		||||
    return "INVALID_PRIVATE_KEY";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
 | 
			
		||||
    return "INVALID_PUBLIC_KEY";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_FORMAT)
 | 
			
		||||
    return "INVALID_FORMAT";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_SIGNATURE)
 | 
			
		||||
    return "INVALID_SIGNATURE";
 | 
			
		||||
  return to_string(err);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
APIError APINoiseFrameHelper::init() {
 | 
			
		||||
  if (state_ != State::INITIALIZE || socket_ == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad state for init %d", (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nonblocking failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NONBLOCKING_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nodelay failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NODELAY_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // init prologue
 | 
			
		||||
  prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT));
 | 
			
		||||
 | 
			
		||||
  state_ = State::CLIENT_HELLO;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/// Run through handshake messages (if in that phase)
 | 
			
		||||
APIError APINoiseFrameHelper::loop() {
 | 
			
		||||
  APIError err = state_action_();
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  if (err != APIError::OK)
 | 
			
		||||
    return err;
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
 *
 | 
			
		||||
 * @param frame: The struct to hold the frame information in.
 | 
			
		||||
 *   msg_start: points to the start of the payload - this pointer is only valid until the next
 | 
			
		||||
 *     try_receive_raw_ call
 | 
			
		||||
 *
 | 
			
		||||
 * @return 0 if a full packet is in rx_buf_
 | 
			
		||||
 * @return -1 if error, check errno.
 | 
			
		||||
 *
 | 
			
		||||
 * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
 | 
			
		||||
 * errno ENOMEM: Not enough memory for reading packet.
 | 
			
		||||
 * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
 | 
			
		||||
 * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (frame == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad argument for try_read_frame_");
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // read header
 | 
			
		||||
  if (rx_header_buf_len_ < 3) {
 | 
			
		||||
    // no header information yet
 | 
			
		||||
    size_t to_read = 3 - rx_header_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
 | 
			
		||||
    if (is_would_block(received)) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (received == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_buf_len_ += received;
 | 
			
		||||
    if (received != to_read) {
 | 
			
		||||
      // not a full read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // header reading done
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // read body
 | 
			
		||||
  uint8_t indicator = rx_header_buf_[0];
 | 
			
		||||
  if (indicator != 0x01) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad indicator byte %u", indicator);
 | 
			
		||||
    return APIError::BAD_INDICATOR;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA && msg_size > 128) {
 | 
			
		||||
    // for handshake message only permit up to 128 bytes
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad packet len for handshake: %d", msg_size);
 | 
			
		||||
    return APIError::BAD_HANDSHAKE_PACKET_LEN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != msg_size) {
 | 
			
		||||
    rx_buf_.resize(msg_size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < msg_size) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    size_t to_read = msg_size - rx_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    if (is_would_block(received)) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (received == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += received;
 | 
			
		||||
    if (received != to_read) {
 | 
			
		||||
      // not all read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // uncomment for even more debugging
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
  ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  frame->msg = std::move(rx_buf_);
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_len_ = 0;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** To be called from read/write methods.
 | 
			
		||||
 *
 | 
			
		||||
 * This method runs through the internal handshake methods, if in that state.
 | 
			
		||||
 *
 | 
			
		||||
 * If the handshake is still active when this method returns and a read/write can't take place at
 | 
			
		||||
 * the moment, returns WOULD_BLOCK.
 | 
			
		||||
 * If an error occured, returns that error. Only returns OK if the transport is ready for data
 | 
			
		||||
 * traffic.
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  if (state_ == State::INITIALIZE) {
 | 
			
		||||
    HELPER_LOG("Bad state for method: %d", (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::CLIENT_HELLO) {
 | 
			
		||||
    // waiting for client hello
 | 
			
		||||
    ParsedFrame frame;
 | 
			
		||||
    aerr = try_read_frame_(&frame);
 | 
			
		||||
    if (aerr == APIError::BAD_INDICATOR) {
 | 
			
		||||
      send_explicit_handshake_reject_("Bad indicator byte");
 | 
			
		||||
      return aerr;
 | 
			
		||||
    }
 | 
			
		||||
    if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
 | 
			
		||||
      send_explicit_handshake_reject_("Bad handshake packet len");
 | 
			
		||||
      return aerr;
 | 
			
		||||
    }
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
    // ignore contents, may be used in future for flags
 | 
			
		||||
    prologue_.push_back((uint8_t)(frame.msg.size() >> 8));
 | 
			
		||||
    prologue_.push_back((uint8_t) frame.msg.size());
 | 
			
		||||
    prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
 | 
			
		||||
 | 
			
		||||
    state_ = State::SERVER_HELLO;
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::SERVER_HELLO) {
 | 
			
		||||
    // send server hello
 | 
			
		||||
    uint8_t msg[1];
 | 
			
		||||
    msg[0] = 0x01;  // chosen proto
 | 
			
		||||
    aerr = write_frame_(msg, 1);
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
 | 
			
		||||
    // start handshake
 | 
			
		||||
    aerr = init_handshake_();
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
 | 
			
		||||
    state_ = State::HANDSHAKE;
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::HANDSHAKE) {
 | 
			
		||||
    int action = noise_handshakestate_get_action(handshake_);
 | 
			
		||||
    if (action == NOISE_ACTION_READ_MESSAGE) {
 | 
			
		||||
      // waiting for handshake msg
 | 
			
		||||
      ParsedFrame frame;
 | 
			
		||||
      aerr = try_read_frame_(&frame);
 | 
			
		||||
      if (aerr == APIError::BAD_INDICATOR) {
 | 
			
		||||
        send_explicit_handshake_reject_("Bad indicator byte");
 | 
			
		||||
        return aerr;
 | 
			
		||||
      }
 | 
			
		||||
      if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
 | 
			
		||||
        send_explicit_handshake_reject_("Bad handshake packet len");
 | 
			
		||||
        return aerr;
 | 
			
		||||
      }
 | 
			
		||||
      if (aerr != APIError::OK)
 | 
			
		||||
        return aerr;
 | 
			
		||||
 | 
			
		||||
      if (frame.msg.empty()) {
 | 
			
		||||
        send_explicit_handshake_reject_("Empty handshake message");
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      } else if (frame.msg[0] != 0x00) {
 | 
			
		||||
        HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]);
 | 
			
		||||
        send_explicit_handshake_reject_("Bad handshake error byte");
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      NoiseBuffer mbuf;
 | 
			
		||||
      noise_buffer_init(mbuf);
 | 
			
		||||
      noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1);
 | 
			
		||||
      err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
 | 
			
		||||
      if (err != 0) {
 | 
			
		||||
        state_ = State::FAILED;
 | 
			
		||||
        HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
        if (err == NOISE_ERROR_MAC_FAILURE) {
 | 
			
		||||
          send_explicit_handshake_reject_("Handshake MAC failure");
 | 
			
		||||
        } else {
 | 
			
		||||
          send_explicit_handshake_reject_("Handshake error");
 | 
			
		||||
        }
 | 
			
		||||
        return APIError::HANDSHAKESTATE_READ_FAILED;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      aerr = check_handshake_finished_();
 | 
			
		||||
      if (aerr != APIError::OK)
 | 
			
		||||
        return aerr;
 | 
			
		||||
    } else if (action == NOISE_ACTION_WRITE_MESSAGE) {
 | 
			
		||||
      uint8_t buffer[65];
 | 
			
		||||
      NoiseBuffer mbuf;
 | 
			
		||||
      noise_buffer_init(mbuf);
 | 
			
		||||
      noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
 | 
			
		||||
 | 
			
		||||
      err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
 | 
			
		||||
      if (err != 0) {
 | 
			
		||||
        state_ = State::FAILED;
 | 
			
		||||
        HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
        return APIError::HANDSHAKESTATE_WRITE_FAILED;
 | 
			
		||||
      }
 | 
			
		||||
      buffer[0] = 0x00;  // success
 | 
			
		||||
 | 
			
		||||
      aerr = write_frame_(buffer, mbuf.size + 1);
 | 
			
		||||
      if (aerr != APIError::OK)
 | 
			
		||||
        return aerr;
 | 
			
		||||
      aerr = check_handshake_finished_();
 | 
			
		||||
      if (aerr != APIError::OK)
 | 
			
		||||
        return aerr;
 | 
			
		||||
    } else {
 | 
			
		||||
      // bad state for action
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad action for handshake: %d", action);
 | 
			
		||||
      return APIError::HANDSHAKESTATE_BAD_STATE;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::CLOSED || state_ == State::FAILED) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
 | 
			
		||||
  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];
 | 
			
		||||
  }
 | 
			
		||||
  // temporarily remove failed state
 | 
			
		||||
  auto orig_state = state_;
 | 
			
		||||
  state_ = State::EXPLICIT_REJECT;
 | 
			
		||||
  write_frame_(data.data(), data.size());
 | 
			
		||||
  state_ = orig_state;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ParsedFrame frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
 | 
			
		||||
  NoiseBuffer mbuf;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size());
 | 
			
		||||
  err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::CIPHERSTATE_DECRYPT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t msg_size = mbuf.size;
 | 
			
		||||
  uint8_t *msg_data = frame.msg.data();
 | 
			
		||||
  if (msg_size < 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad data packet: size %d too short", msg_size);
 | 
			
		||||
    return APIError::BAD_DATA_PACKET;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // uint16_t type;
 | 
			
		||||
  // uint16_t data_len;
 | 
			
		||||
  // uint8_t *data;
 | 
			
		||||
  // uint8_t *padding;  zero or more bytes to fill up the rest of the packet
 | 
			
		||||
  uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
 | 
			
		||||
  uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
 | 
			
		||||
  if (data_len > msg_size - 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
 | 
			
		||||
    return APIError::BAD_DATA_PACKET;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buffer->container = std::move(frame.msg);
 | 
			
		||||
  buffer->data_offset = 4;
 | 
			
		||||
  buffer->data_len = data_len;
 | 
			
		||||
  buffer->type = type;
 | 
			
		||||
  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) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
  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);
 | 
			
		||||
 | 
			
		||||
  NoiseBuffer mbuf;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
 | 
			
		||||
  err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::CIPHERSTATE_ENCRYPT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t total_len = 3 + mbuf.size;
 | 
			
		||||
  tmpbuf[1] = (uint8_t)(mbuf.size >> 8);
 | 
			
		||||
  tmpbuf[2] = (uint8_t) mbuf.size;
 | 
			
		||||
  // write raw to not have two packets sent if NAGLE disabled
 | 
			
		||||
  aerr = write_raw_(&tmpbuf[0], total_len);
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // try send from tx_buf
 | 
			
		||||
  while (state_ != State::CLOSED && !tx_buf_.empty()) {
 | 
			
		||||
    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
 | 
			
		||||
    if (sent == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN)
 | 
			
		||||
        break;
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
    } else if (sent == 0) {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
    // replace with deque of buffers
 | 
			
		||||
    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/** Write the data to the socket, or buffer it a write would block
 | 
			
		||||
 *
 | 
			
		||||
 * @param data The data to write
 | 
			
		||||
 * @param len The length of data
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::write_raw_(const uint8_t *data, size_t len) {
 | 
			
		||||
  if (len == 0)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  // uncomment for even more debugging
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
  ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // try to empty tx_buf_ first
 | 
			
		||||
    aerr = try_send_tx_buf_();
 | 
			
		||||
    if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // tx buf not empty, can't write now because then stream would be inconsistent
 | 
			
		||||
    tx_buf_.insert(tx_buf_.end(), data, data + len);
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t sent = socket_->write(data, len);
 | 
			
		||||
  if (is_would_block(sent)) {
 | 
			
		||||
    // operation would block, add buffer to tx_buf
 | 
			
		||||
    tx_buf_.insert(tx_buf_.end(), data, data + len);
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  } else if (sent == -1) {
 | 
			
		||||
    // an error occured
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
    return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
  } else if (sent != len) {
 | 
			
		||||
    // partially sent, add end to tx_buf
 | 
			
		||||
    tx_buf_.insert(tx_buf_.end(), data + sent, data + len);
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
  // fully sent
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  uint8_t header[3];
 | 
			
		||||
  header[0] = 0x01;  // indicator
 | 
			
		||||
  header[1] = (uint8_t)(len >> 8);
 | 
			
		||||
  header[2] = (uint8_t) len;
 | 
			
		||||
 | 
			
		||||
  aerr = write_raw_(header, 3);
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
  aerr = write_raw_(data, len);
 | 
			
		||||
  return aerr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Initiate the data structures for the handshake.
 | 
			
		||||
 *
 | 
			
		||||
 * @return 0 on success, -1 on error (check errno)
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::init_handshake_() {
 | 
			
		||||
  int err;
 | 
			
		||||
  memset(&nid_, 0, sizeof(nid_));
 | 
			
		||||
  // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
 | 
			
		||||
  // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
 | 
			
		||||
  nid_.pattern_id = NOISE_PATTERN_NN;
 | 
			
		||||
  nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
 | 
			
		||||
  nid_.dh_id = NOISE_DH_CURVE25519;
 | 
			
		||||
  nid_.prefix_id = NOISE_PREFIX_STANDARD;
 | 
			
		||||
  nid_.hybrid_id = NOISE_DH_NONE;
 | 
			
		||||
  nid_.hash_id = NOISE_HASH_SHA256;
 | 
			
		||||
  nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
 | 
			
		||||
 | 
			
		||||
  err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SETUP_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const auto &psk = ctx_->get_psk();
 | 
			
		||||
  err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SETUP_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SETUP_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  // set_prologue copies it into handshakestate, so we can get rid of it now
 | 
			
		||||
  prologue_ = {};
 | 
			
		||||
 | 
			
		||||
  err = noise_handshakestate_start(handshake_);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SETUP_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::check_handshake_finished_() {
 | 
			
		||||
  assert(state_ == State::HANDSHAKE);
 | 
			
		||||
 | 
			
		||||
  int action = noise_handshakestate_get_action(handshake_);
 | 
			
		||||
  if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  if (action != NOISE_ACTION_SPLIT) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad action for handshake: %d", action);
 | 
			
		||||
    return APIError::HANDSHAKESTATE_BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SPLIT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  HELPER_LOG("Handshake complete!");
 | 
			
		||||
  noise_handshakestate_free(handshake_);
 | 
			
		||||
  handshake_ = nullptr;
 | 
			
		||||
  state_ = State::DATA;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APINoiseFrameHelper::~APINoiseFrameHelper() {
 | 
			
		||||
  if (handshake_ != nullptr) {
 | 
			
		||||
    noise_handshakestate_free(handshake_);
 | 
			
		||||
    handshake_ = nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  if (send_cipher_ != nullptr) {
 | 
			
		||||
    noise_cipherstate_free(send_cipher_);
 | 
			
		||||
    send_cipher_ = nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  if (recv_cipher_ != nullptr) {
 | 
			
		||||
    noise_cipherstate_free(recv_cipher_);
 | 
			
		||||
    recv_cipher_ = nullptr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::close() {
 | 
			
		||||
  state_ = State::CLOSED;
 | 
			
		||||
  int err = socket_->close();
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::CLOSE_FAILED;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::shutdown(int how) {
 | 
			
		||||
  int err = socket_->shutdown(how);
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::SHUTDOWN_FAILED;
 | 
			
		||||
  if (how == SHUT_RDWR) {
 | 
			
		||||
    state_ = State::CLOSED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
extern "C" {
 | 
			
		||||
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
 | 
			
		||||
void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast<uint8_t *>(output), len); }
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
APIError APIPlaintextFrameHelper::init() {
 | 
			
		||||
  if (state_ != State::INITIALIZE || socket_ == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad state for init %d", (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nonblocking failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NONBLOCKING_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nodelay failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NODELAY_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  state_ = State::DATA;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/// Not used for plaintext
 | 
			
		||||
APIError APIPlaintextFrameHelper::loop() {
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  // try send pending TX data
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    APIError err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
 *
 | 
			
		||||
 * @param frame: The struct to hold the frame information in.
 | 
			
		||||
 *   msg: store the parsed frame in that struct
 | 
			
		||||
 *
 | 
			
		||||
 * @return See APIError
 | 
			
		||||
 *
 | 
			
		||||
 * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
 | 
			
		||||
 */
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (frame == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad argument for try_read_frame_");
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // read header
 | 
			
		||||
  while (!rx_header_parsed_) {
 | 
			
		||||
    uint8_t data;
 | 
			
		||||
    ssize_t received = socket_->read(&data, 1);
 | 
			
		||||
    if (is_would_block(received)) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (received == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    size_t i = 1;
 | 
			
		||||
    uint32_t consumed = 0;
 | 
			
		||||
    auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &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);
 | 
			
		||||
    if (!msg_type_varint.has_value()) {
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_parsed_type_ = msg_type_varint->as_uint32();
 | 
			
		||||
    rx_header_parsed_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  // header reading done
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != rx_header_parsed_len_) {
 | 
			
		||||
    rx_buf_.resize(rx_header_parsed_len_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < rx_header_parsed_len_) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    size_t to_read = rx_header_parsed_len_ - rx_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    if (is_would_block(received)) {
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (received == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket read failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_READ_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += received;
 | 
			
		||||
    if (received != to_read) {
 | 
			
		||||
      // not all read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // uncomment for even more debugging
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
  ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  frame->msg = std::move(rx_buf_);
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_.clear();
 | 
			
		||||
  rx_header_parsed_ = false;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ParsedFrame frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
 | 
			
		||||
  buffer->container = std::move(frame.msg);
 | 
			
		||||
  buffer->data_offset = 0;
 | 
			
		||||
  buffer->data_len = rx_header_parsed_len_;
 | 
			
		||||
  buffer->type = rx_header_parsed_type_;
 | 
			
		||||
  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) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> header;
 | 
			
		||||
  header.push_back(0x00);
 | 
			
		||||
  ProtoVarInt(payload_len).encode(header);
 | 
			
		||||
  ProtoVarInt(type).encode(header);
 | 
			
		||||
 | 
			
		||||
  aerr = write_raw_(&header[0], header.size());
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
  aerr = write_raw_(payload, payload_len);
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // try send from tx_buf
 | 
			
		||||
  while (state_ != State::CLOSED && !tx_buf_.empty()) {
 | 
			
		||||
    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
 | 
			
		||||
    if (is_would_block(sent)) {
 | 
			
		||||
      break;
 | 
			
		||||
    } else if (sent == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
    // replace with deque of buffers
 | 
			
		||||
    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/** Write the data to the socket, or buffer it a write would block
 | 
			
		||||
 *
 | 
			
		||||
 * @param data The data to write
 | 
			
		||||
 * @param len The length of data
 | 
			
		||||
 */
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_raw_(const uint8_t *data, size_t len) {
 | 
			
		||||
  if (len == 0)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  // uncomment for even more debugging
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
  ESP_LOGVV(TAG, "Sending raw: %s", hexencode(data, len).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // try to empty tx_buf_ first
 | 
			
		||||
    aerr = try_send_tx_buf_();
 | 
			
		||||
    if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // tx buf not empty, can't write now because then stream would be inconsistent
 | 
			
		||||
    tx_buf_.insert(tx_buf_.end(), data, data + len);
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t sent = socket_->write(data, len);
 | 
			
		||||
  if (is_would_block(sent)) {
 | 
			
		||||
    // operation would block, add buffer to tx_buf
 | 
			
		||||
    tx_buf_.insert(tx_buf_.end(), data, data + len);
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  } else if (sent == -1) {
 | 
			
		||||
    // an error occured
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
    return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
  } else if (sent != len) {
 | 
			
		||||
    // partially sent, add end to tx_buf
 | 
			
		||||
    tx_buf_.insert(tx_buf_.end(), data + sent, data + len);
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
  // fully sent
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::close() {
 | 
			
		||||
  state_ = State::CLOSED;
 | 
			
		||||
  int err = socket_->close();
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::CLOSE_FAILED;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::shutdown(int how) {
 | 
			
		||||
  int err = socket_->shutdown(how);
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::SHUTDOWN_FAILED;
 | 
			
		||||
  if (how == SHUT_RDWR) {
 | 
			
		||||
    state_ = State::CLOSED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										182
									
								
								esphome/components/api/api_frame_helper.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								esphome/components/api/api_frame_helper.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,182 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <deque>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
#include "noise/protocol.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
#include "api_noise_context.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
struct ReadPacketBuffer {
 | 
			
		||||
  std::vector<uint8_t> container;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  size_t data_offset;
 | 
			
		||||
  size_t data_len;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct PacketBuffer {
 | 
			
		||||
  const std::vector<uint8_t> container;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  uint8_t data_offset;
 | 
			
		||||
  uint8_t data_len;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class APIError : int {
 | 
			
		||||
  OK = 0,
 | 
			
		||||
  WOULD_BLOCK = 1001,
 | 
			
		||||
  BAD_HANDSHAKE_PACKET_LEN = 1002,
 | 
			
		||||
  BAD_INDICATOR = 1003,
 | 
			
		||||
  BAD_DATA_PACKET = 1004,
 | 
			
		||||
  TCP_NODELAY_FAILED = 1005,
 | 
			
		||||
  TCP_NONBLOCKING_FAILED = 1006,
 | 
			
		||||
  CLOSE_FAILED = 1007,
 | 
			
		||||
  SHUTDOWN_FAILED = 1008,
 | 
			
		||||
  BAD_STATE = 1009,
 | 
			
		||||
  BAD_ARG = 1010,
 | 
			
		||||
  SOCKET_READ_FAILED = 1011,
 | 
			
		||||
  SOCKET_WRITE_FAILED = 1012,
 | 
			
		||||
  HANDSHAKESTATE_READ_FAILED = 1013,
 | 
			
		||||
  HANDSHAKESTATE_WRITE_FAILED = 1014,
 | 
			
		||||
  HANDSHAKESTATE_BAD_STATE = 1015,
 | 
			
		||||
  CIPHERSTATE_DECRYPT_FAILED = 1016,
 | 
			
		||||
  CIPHERSTATE_ENCRYPT_FAILED = 1017,
 | 
			
		||||
  OUT_OF_MEMORY = 1018,
 | 
			
		||||
  HANDSHAKESTATE_SETUP_FAILED = 1019,
 | 
			
		||||
  HANDSHAKESTATE_SPLIT_FAILED = 1020,
 | 
			
		||||
  BAD_HANDSHAKE_ERROR_BYTE = 1021,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const char *api_error_to_str(APIError err);
 | 
			
		||||
 | 
			
		||||
class APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual APIError init() = 0;
 | 
			
		||||
  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 std::string getpeername() = 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;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#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_(ctx) {}
 | 
			
		||||
  ~APINoiseFrameHelper();
 | 
			
		||||
  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;
 | 
			
		||||
  std::string getpeername() override { return socket_->getpeername(); }
 | 
			
		||||
  APIError close() override;
 | 
			
		||||
  APIError shutdown(int how) override;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) override { info_ = std::move(info); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  APIError state_action_();
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, size_t len);
 | 
			
		||||
  APIError write_raw_(const uint8_t *data, size_t len);
 | 
			
		||||
  APIError init_handshake_();
 | 
			
		||||
  APIError check_handshake_finished_();
 | 
			
		||||
  void send_explicit_handshake_reject_(const std::string &reason);
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  uint8_t rx_header_buf_[3];
 | 
			
		||||
  size_t rx_header_buf_len_ = 0;
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
  size_t rx_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> tx_buf_;
 | 
			
		||||
  std::vector<uint8_t> prologue_;
 | 
			
		||||
 | 
			
		||||
  std::shared_ptr<APINoiseContext> ctx_;
 | 
			
		||||
  NoiseHandshakeState *handshake_ = nullptr;
 | 
			
		||||
  NoiseCipherState *send_cipher_ = nullptr;
 | 
			
		||||
  NoiseCipherState *recv_cipher_ = nullptr;
 | 
			
		||||
  NoiseProtocolId nid_;
 | 
			
		||||
 | 
			
		||||
  enum class State {
 | 
			
		||||
    INITIALIZE = 1,
 | 
			
		||||
    CLIENT_HELLO = 2,
 | 
			
		||||
    SERVER_HELLO = 3,
 | 
			
		||||
    HANDSHAKE = 4,
 | 
			
		||||
    DATA = 5,
 | 
			
		||||
    CLOSED = 6,
 | 
			
		||||
    FAILED = 7,
 | 
			
		||||
    EXPLICIT_REJECT = 8,
 | 
			
		||||
  } state_ = State::INITIALIZE;
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
 | 
			
		||||
  ~APIPlaintextFrameHelper() = 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;
 | 
			
		||||
  std::string getpeername() override { return socket_->getpeername(); }
 | 
			
		||||
  APIError close() override;
 | 
			
		||||
  APIError shutdown(int how) override;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) override { info_ = std::move(info); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_raw_(const uint8_t *data, size_t len);
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  std::vector<uint8_t> rx_header_buf_;
 | 
			
		||||
  bool rx_header_parsed_ = false;
 | 
			
		||||
  uint32_t rx_header_parsed_type_ = 0;
 | 
			
		||||
  uint32_t rx_header_parsed_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
  size_t rx_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> tx_buf_;
 | 
			
		||||
 | 
			
		||||
  enum class State {
 | 
			
		||||
    INITIALIZE = 1,
 | 
			
		||||
    DATA = 2,
 | 
			
		||||
    CLOSED = 3,
 | 
			
		||||
    FAILED = 4,
 | 
			
		||||
  } state_ = State::INITIALIZE;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										23
									
								
								esphome/components/api/api_noise_context.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/api/api_noise_context.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <array>
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
using psk_t = std::array<uint8_t, 32>;
 | 
			
		||||
 | 
			
		||||
class APINoiseContext {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_psk(psk_t psk) { psk_ = std::move(psk); }
 | 
			
		||||
  const psk_t &get_psk() const { return psk_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  psk_t psk_;
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1817,7 +1817,7 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 11: {
 | 
			
		||||
      this->last_reset_type = value.as_enum<enums::SensorLastResetType>();
 | 
			
		||||
      this->legacy_last_reset_type = value.as_enum<enums::SensorLastResetType>();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 12: {
 | 
			
		||||
@@ -1879,7 +1879,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_bool(8, this->force_update);
 | 
			
		||||
  buffer.encode_string(9, this->device_class);
 | 
			
		||||
  buffer.encode_enum<enums::SensorStateClass>(10, this->state_class);
 | 
			
		||||
  buffer.encode_enum<enums::SensorLastResetType>(11, this->last_reset_type);
 | 
			
		||||
  buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type);
 | 
			
		||||
  buffer.encode_bool(12, this->disabled_by_default);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -1928,8 +1928,8 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append(proto_enum_to_string<enums::SensorStateClass>(this->state_class));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  last_reset_type: ");
 | 
			
		||||
  out.append(proto_enum_to_string<enums::SensorLastResetType>(this->last_reset_type));
 | 
			
		||||
  out.append("  legacy_last_reset_type: ");
 | 
			
		||||
  out.append(proto_enum_to_string<enums::SensorLastResetType>(this->legacy_last_reset_type));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  disabled_by_default: ");
 | 
			
		||||
 
 | 
			
		||||
@@ -510,7 +510,7 @@ class ListEntitiesSensorResponse : public ProtoMessage {
 | 
			
		||||
  bool force_update{false};
 | 
			
		||||
  std::string device_class{};
 | 
			
		||||
  enums::SensorStateClass state_class{};
 | 
			
		||||
  enums::SensorLastResetType last_reset_type{};
 | 
			
		||||
  enums::SensorLastResetType legacy_last_reset_type{};
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#include "api_connection.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
@@ -21,20 +22,45 @@ static const char *const TAG = "api";
 | 
			
		||||
void APIServer::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
 | 
			
		||||
  this->setup_controller();
 | 
			
		||||
  this->server_ = AsyncServer(this->port_);
 | 
			
		||||
  this->server_.setNoDelay(false);
 | 
			
		||||
  this->server_.begin();
 | 
			
		||||
  this->server_.onClient(
 | 
			
		||||
      [](void *s, AsyncClient *client) {
 | 
			
		||||
        if (client == nullptr)
 | 
			
		||||
          return;
 | 
			
		||||
  socket_ = socket::socket(AF_INET, SOCK_STREAM, 0);
 | 
			
		||||
  if (socket_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not create socket.");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
 | 
			
		||||
    // we can still continue
 | 
			
		||||
  }
 | 
			
		||||
  err = socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  struct sockaddr_in server;
 | 
			
		||||
  memset(&server, 0, sizeof(server));
 | 
			
		||||
  server.sin_family = AF_INET;
 | 
			
		||||
  server.sin_addr.s_addr = ESPHOME_INADDR_ANY;
 | 
			
		||||
  server.sin_port = htons(this->port_);
 | 
			
		||||
 | 
			
		||||
  err = socket_->bind((struct sockaddr *) &server, sizeof(server));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = socket_->listen(4);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
        // can't print here because in lwIP thread
 | 
			
		||||
        // ESP_LOGD(TAG, "New client connected from %s", client->remoteIP().toString().c_str());
 | 
			
		||||
        auto *a_this = (APIServer *) s;
 | 
			
		||||
        a_this->clients_.push_back(new APIConnection(client, a_this));
 | 
			
		||||
      },
 | 
			
		||||
      this);
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
  if (logger::global_logger != nullptr) {
 | 
			
		||||
    logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
 | 
			
		||||
@@ -59,12 +85,26 @@ void APIServer::setup() {
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void APIServer::loop() {
 | 
			
		||||
  // Accept new clients
 | 
			
		||||
  while (true) {
 | 
			
		||||
    struct sockaddr_storage source_addr;
 | 
			
		||||
    socklen_t addr_len = sizeof(source_addr);
 | 
			
		||||
    auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
    if (!sock)
 | 
			
		||||
      break;
 | 
			
		||||
    ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
 | 
			
		||||
 | 
			
		||||
    auto *conn = new APIConnection(std::move(sock), this);
 | 
			
		||||
    clients_.push_back(conn);
 | 
			
		||||
    conn->start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Partition clients into remove and active
 | 
			
		||||
  auto new_end =
 | 
			
		||||
      std::partition(this->clients_.begin(), this->clients_.end(), [](APIConnection *conn) { return !conn->remove_; });
 | 
			
		||||
  // print disconnection messages
 | 
			
		||||
  for (auto it = new_end; it != this->clients_.end(); ++it) {
 | 
			
		||||
    ESP_LOGD(TAG, "Disconnecting %s", (*it)->client_info_.c_str());
 | 
			
		||||
    ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  // only then delete the pointers, otherwise log routine
 | 
			
		||||
  // would access freed memory
 | 
			
		||||
 
 | 
			
		||||
@@ -4,19 +4,14 @@
 | 
			
		||||
#include "esphome/core/controller.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
#include "api_pb2_service.h"
 | 
			
		||||
#include "util.h"
 | 
			
		||||
#include "list_entities.h"
 | 
			
		||||
#include "subscribe_state.h"
 | 
			
		||||
#include "user_services.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP32
 | 
			
		||||
#include <AsyncTCP.h>
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ARDUINO_ARCH_ESP8266
 | 
			
		||||
#include <ESPAsyncTCP.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include "api_noise_context.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
@@ -35,6 +30,12 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  void set_port(uint16_t port);
 | 
			
		||||
  void set_password(const std::string &password);
 | 
			
		||||
  void set_reboot_timeout(uint32_t reboot_timeout);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(std::move(psk)); }
 | 
			
		||||
  std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
  void handle_disconnect(APIConnection *conn);
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
 | 
			
		||||
@@ -86,7 +87,7 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  AsyncServer server_{0};
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_ = nullptr;
 | 
			
		||||
  uint16_t port_{6053};
 | 
			
		||||
  uint32_t reboot_timeout_{300000};
 | 
			
		||||
  uint32_t last_connected_{0};
 | 
			
		||||
@@ -94,6 +95,10 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  std::string password_;
 | 
			
		||||
  std::vector<HomeAssistantStateSubscription> state_subs_;
 | 
			
		||||
  std::vector<UserServiceDescriptor *> user_services_;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern APIServer *global_api_server;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										73
									
								
								esphome/components/api/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								esphome/components/api/client.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import logging
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from aioesphomeapi import APIClient, ReconnectLogic, APIConnectionError, LogLevel
 | 
			
		||||
import zeroconf
 | 
			
		||||
 | 
			
		||||
from esphome.const import CONF_KEY, CONF_PORT, CONF_PASSWORD, __version__
 | 
			
		||||
from esphome.util import safe_print
 | 
			
		||||
from . import CONF_ENCRYPTION
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def async_run_logs(config, address):
 | 
			
		||||
    conf = config["api"]
 | 
			
		||||
    port: int = int(conf[CONF_PORT])
 | 
			
		||||
    password: str = conf[CONF_PASSWORD]
 | 
			
		||||
    noise_psk: Optional[str] = None
 | 
			
		||||
    if CONF_ENCRYPTION in conf:
 | 
			
		||||
        noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
 | 
			
		||||
    _LOGGER.info("Starting log output from %s using esphome API", address)
 | 
			
		||||
    zc = zeroconf.Zeroconf()
 | 
			
		||||
    cli = APIClient(
 | 
			
		||||
        asyncio.get_event_loop(),
 | 
			
		||||
        address,
 | 
			
		||||
        port,
 | 
			
		||||
        password,
 | 
			
		||||
        client_info=f"ESPHome Logs {__version__}",
 | 
			
		||||
        noise_psk=noise_psk,
 | 
			
		||||
    )
 | 
			
		||||
    first_connect = True
 | 
			
		||||
 | 
			
		||||
    def on_log(msg):
 | 
			
		||||
        time_ = datetime.now().time().strftime("[%H:%M:%S]")
 | 
			
		||||
        text = msg.message.decode("utf8", "backslashreplace")
 | 
			
		||||
        safe_print(time_ + text)
 | 
			
		||||
 | 
			
		||||
    async def on_connect():
 | 
			
		||||
        nonlocal first_connect
 | 
			
		||||
        try:
 | 
			
		||||
            await cli.subscribe_logs(
 | 
			
		||||
                on_log,
 | 
			
		||||
                log_level=LogLevel.LOG_LEVEL_VERY_VERBOSE,
 | 
			
		||||
                dump_config=first_connect,
 | 
			
		||||
            )
 | 
			
		||||
            first_connect = False
 | 
			
		||||
        except APIConnectionError:
 | 
			
		||||
            cli.disconnect()
 | 
			
		||||
 | 
			
		||||
    async def on_disconnect():
 | 
			
		||||
        _LOGGER.warning("Disconnected from API")
 | 
			
		||||
 | 
			
		||||
    zc = zeroconf.Zeroconf()
 | 
			
		||||
    reconnect = ReconnectLogic(
 | 
			
		||||
        client=cli,
 | 
			
		||||
        on_connect=on_connect,
 | 
			
		||||
        on_disconnect=on_disconnect,
 | 
			
		||||
        zeroconf_instance=zc,
 | 
			
		||||
    )
 | 
			
		||||
    await reconnect.start()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        while True:
 | 
			
		||||
            await asyncio.sleep(60)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        await reconnect.stop()
 | 
			
		||||
        zc.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_logs(config, address):
 | 
			
		||||
    asyncio.run(async_run_logs(config, address))
 | 
			
		||||
@@ -19,8 +19,8 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_LIGHTBULB,
 | 
			
		||||
    ICON_CURRENT_AC,
 | 
			
		||||
    LAST_RESET_TYPE_AUTO,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
@@ -94,15 +94,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
 | 
			
		||||
            unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
            accuracy_decimals=2,
 | 
			
		||||
            device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            last_reset_type=LAST_RESET_TYPE_AUTO,
 | 
			
		||||
            state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
            accuracy_decimals=2,
 | 
			
		||||
            device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            last_reset_type=LAST_RESET_TYPE_AUTO,
 | 
			
		||||
            state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
 | 
			
		||||
        cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,10 @@ void BinaryFan::loop() {
 | 
			
		||||
    ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
float BinaryFan::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
// We need a higher priority than the FanState component to make sure that the traits are set
 | 
			
		||||
// when that component sets itself up.
 | 
			
		||||
float BinaryFan::get_setup_priority() const { return fan_->get_setup_priority() + 1.0f; }
 | 
			
		||||
 | 
			
		||||
}  // namespace binary
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_SAFETY,
 | 
			
		||||
    DEVICE_CLASS_SMOKE,
 | 
			
		||||
    DEVICE_CLASS_SOUND,
 | 
			
		||||
    DEVICE_CLASS_UPDATE,
 | 
			
		||||
    DEVICE_CLASS_VIBRATION,
 | 
			
		||||
    DEVICE_CLASS_WINDOW,
 | 
			
		||||
)
 | 
			
		||||
@@ -79,6 +80,7 @@ DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_SAFETY,
 | 
			
		||||
    DEVICE_CLASS_SMOKE,
 | 
			
		||||
    DEVICE_CLASS_SOUND,
 | 
			
		||||
    DEVICE_CLASS_UPDATE,
 | 
			
		||||
    DEVICE_CLASS_VIBRATION,
 | 
			
		||||
    DEVICE_CLASS_WINDOW,
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,14 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import binary_sensor, esp32_ble_tracker
 | 
			
		||||
from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_MAC_ADDRESS,
 | 
			
		||||
    CONF_SERVICE_UUID,
 | 
			
		||||
    CONF_IBEACON_MAJOR,
 | 
			
		||||
    CONF_IBEACON_MINOR,
 | 
			
		||||
    CONF_IBEACON_UUID,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32_ble_tracker"]
 | 
			
		||||
 | 
			
		||||
@@ -13,17 +20,30 @@ BLEPresenceDevice = ble_presence_ns.class_(
 | 
			
		||||
    esp32_ble_tracker.ESPBTDeviceListener,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate(config):
 | 
			
		||||
    if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config:
 | 
			
		||||
        raise cv.Invalid("iBeacon major identifier requires iBeacon UUID")
 | 
			
		||||
    if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config:
 | 
			
		||||
        raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID")
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLEPresenceDevice),
 | 
			
		||||
            cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
 | 
			
		||||
            cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
 | 
			
		||||
            cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t,
 | 
			
		||||
            cv.Optional(CONF_IBEACON_UUID): cv.uuid,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID),
 | 
			
		||||
    cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID),
 | 
			
		||||
    _validate,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -50,5 +70,15 @@ async def to_code(config):
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
 | 
			
		||||
            uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID])
 | 
			
		||||
            uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
 | 
			
		||||
            cg.add(var.set_service_uuid128(uuid128))
 | 
			
		||||
 | 
			
		||||
    if CONF_IBEACON_UUID in config:
 | 
			
		||||
        ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID]))
 | 
			
		||||
        cg.add(var.set_ibeacon_uuid(ibeacon_uuid))
 | 
			
		||||
 | 
			
		||||
        if CONF_IBEACON_MAJOR in config:
 | 
			
		||||
            cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR]))
 | 
			
		||||
 | 
			
		||||
        if CONF_IBEACON_MINOR in config:
 | 
			
		||||
            cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR]))
 | 
			
		||||
 
 | 
			
		||||
@@ -14,41 +14,78 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
 | 
			
		||||
                          public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_address(uint64_t address) {
 | 
			
		||||
    this->by_address_ = true;
 | 
			
		||||
    this->match_by_ = MATCH_BY_MAC_ADDRESS;
 | 
			
		||||
    this->address_ = address;
 | 
			
		||||
  }
 | 
			
		||||
  void set_service_uuid16(uint16_t uuid) {
 | 
			
		||||
    this->by_address_ = false;
 | 
			
		||||
    this->match_by_ = MATCH_BY_SERVICE_UUID;
 | 
			
		||||
    this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid);
 | 
			
		||||
  }
 | 
			
		||||
  void set_service_uuid32(uint32_t uuid) {
 | 
			
		||||
    this->by_address_ = false;
 | 
			
		||||
    this->match_by_ = MATCH_BY_SERVICE_UUID;
 | 
			
		||||
    this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid);
 | 
			
		||||
  }
 | 
			
		||||
  void set_service_uuid128(uint8_t *uuid) {
 | 
			
		||||
    this->by_address_ = false;
 | 
			
		||||
    this->match_by_ = MATCH_BY_SERVICE_UUID;
 | 
			
		||||
    this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid);
 | 
			
		||||
  }
 | 
			
		||||
  void set_ibeacon_uuid(uint8_t *uuid) {
 | 
			
		||||
    this->match_by_ = MATCH_BY_IBEACON_UUID;
 | 
			
		||||
    this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid);
 | 
			
		||||
  }
 | 
			
		||||
  void set_ibeacon_major(uint16_t major) {
 | 
			
		||||
    this->check_ibeacon_major_ = true;
 | 
			
		||||
    this->ibeacon_major_ = major;
 | 
			
		||||
  }
 | 
			
		||||
  void set_ibeacon_minor(uint16_t minor) {
 | 
			
		||||
    this->check_ibeacon_minor_ = true;
 | 
			
		||||
    this->ibeacon_minor_ = minor;
 | 
			
		||||
  }
 | 
			
		||||
  void on_scan_end() override {
 | 
			
		||||
    if (!this->found_)
 | 
			
		||||
      this->publish_state(false);
 | 
			
		||||
    this->found_ = false;
 | 
			
		||||
  }
 | 
			
		||||
  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
 | 
			
		||||
    if (this->by_address_) {
 | 
			
		||||
      if (device.address_uint64() == this->address_) {
 | 
			
		||||
        this->publish_state(true);
 | 
			
		||||
        this->found_ = true;
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      for (auto uuid : device.get_service_uuids()) {
 | 
			
		||||
        if (this->uuid_ == uuid) {
 | 
			
		||||
          this->publish_state(device.get_rssi());
 | 
			
		||||
    switch (this->match_by_) {
 | 
			
		||||
      case MATCH_BY_MAC_ADDRESS:
 | 
			
		||||
        if (device.address_uint64() == this->address_) {
 | 
			
		||||
          this->publish_state(true);
 | 
			
		||||
          this->found_ = true;
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
        break;
 | 
			
		||||
      case MATCH_BY_SERVICE_UUID:
 | 
			
		||||
        for (auto uuid : device.get_service_uuids()) {
 | 
			
		||||
          if (this->uuid_ == uuid) {
 | 
			
		||||
            this->publish_state(device.get_rssi());
 | 
			
		||||
            this->found_ = true;
 | 
			
		||||
            return true;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case MATCH_BY_IBEACON_UUID:
 | 
			
		||||
        if (!device.get_ibeacon().has_value()) {
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        auto ibeacon = device.get_ibeacon().value();
 | 
			
		||||
 | 
			
		||||
        if (this->ibeacon_uuid_ != ibeacon.get_uuid()) {
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) {
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) {
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this->publish_state(device.get_rssi());
 | 
			
		||||
        this->found_ = true;
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -56,10 +93,20 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  enum MATCH_TYPE { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID };
 | 
			
		||||
  MATCH_TYPE match_by_;
 | 
			
		||||
 | 
			
		||||
  bool found_{false};
 | 
			
		||||
  bool by_address_{false};
 | 
			
		||||
 | 
			
		||||
  uint64_t address_;
 | 
			
		||||
 | 
			
		||||
  esp32_ble_tracker::ESPBTUUID uuid_;
 | 
			
		||||
 | 
			
		||||
  esp32_ble_tracker::ESPBTUUID ibeacon_uuid_;
 | 
			
		||||
  uint16_t ibeacon_major_;
 | 
			
		||||
  bool check_ibeacon_major_;
 | 
			
		||||
  uint16_t ibeacon_minor_;
 | 
			
		||||
  bool check_ibeacon_minor_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ble_presence
 | 
			
		||||
 
 | 
			
		||||
@@ -60,5 +60,5 @@ async def to_code(config):
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
 | 
			
		||||
            uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID])
 | 
			
		||||
            uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
 | 
			
		||||
            cg.add(var.set_service_uuid128(uuid128))
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ static const char *const TAG = "bme680_bsec.sensor";
 | 
			
		||||
 | 
			
		||||
static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"};
 | 
			
		||||
 | 
			
		||||
BME680BSECComponent *BME680BSECComponent::instance;
 | 
			
		||||
BME680BSECComponent *BME680BSECComponent::instance;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC...");
 | 
			
		||||
@@ -359,7 +359,7 @@ void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float va
 | 
			
		||||
  sensor->publish_state(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) {
 | 
			
		||||
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
 | 
			
		||||
  if (!sensor || (sensor->has_state() && sensor->state == value)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,7 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
  int64_t get_time_ns_();
 | 
			
		||||
 | 
			
		||||
  void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
 | 
			
		||||
  void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value);
 | 
			
		||||
  void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
 | 
			
		||||
 | 
			
		||||
  void load_state_();
 | 
			
		||||
  void save_state_(uint8_t accuracy);
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ static const char *const TAG = "ccs811";
 | 
			
		||||
    return; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICAITON_FAILED)
 | 
			
		||||
#define CHECKED_IO(f) CHECK_TRUE(f, COMMUNICATION_FAILED)
 | 
			
		||||
 | 
			
		||||
void CCS811Component::setup() {
 | 
			
		||||
  // page 9 programming guide - hwid is always 0x81
 | 
			
		||||
@@ -38,12 +38,14 @@ void CCS811Component::setup() {
 | 
			
		||||
  // set MEAS_MODE (page 5)
 | 
			
		||||
  uint8_t meas_mode = 0;
 | 
			
		||||
  uint32_t interval = this->get_update_interval();
 | 
			
		||||
  if (interval <= 1000)
 | 
			
		||||
    meas_mode = 1 << 4;
 | 
			
		||||
  else if (interval <= 10000)
 | 
			
		||||
    meas_mode = 2 << 4;
 | 
			
		||||
  if (interval >= 60 * 1000)
 | 
			
		||||
    meas_mode = 3 << 4;  // sensor takes a reading every 60 seconds
 | 
			
		||||
  else if (interval >= 10 * 1000)
 | 
			
		||||
    meas_mode = 2 << 4;  // sensor takes a reading every 10 seconds
 | 
			
		||||
  else if (interval >= 1 * 1000)
 | 
			
		||||
    meas_mode = 1 << 4;  // sensor takes a reading every second
 | 
			
		||||
  else
 | 
			
		||||
    meas_mode = 3 << 4;
 | 
			
		||||
    meas_mode = 4 << 4;  // sensor takes a reading every 250ms
 | 
			
		||||
 | 
			
		||||
  CHECKED_IO(this->write_byte(0x01, meas_mode))
 | 
			
		||||
 | 
			
		||||
@@ -51,6 +53,36 @@ void CCS811Component::setup() {
 | 
			
		||||
    // baseline available, write to sensor
 | 
			
		||||
    this->write_bytes(0x11, decode_uint16(*this->baseline_));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto hardware_version_data = this->read_bytes<1>(0x21);
 | 
			
		||||
  auto bootloader_version_data = this->read_bytes<2>(0x23);
 | 
			
		||||
  auto application_version_data = this->read_bytes<2>(0x24);
 | 
			
		||||
 | 
			
		||||
  uint8_t hardware_version = 0;
 | 
			
		||||
  uint16_t bootloader_version = 0;
 | 
			
		||||
  uint16_t application_version = 0;
 | 
			
		||||
 | 
			
		||||
  if (hardware_version_data.has_value()) {
 | 
			
		||||
    hardware_version = (*hardware_version_data)[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (bootloader_version_data.has_value()) {
 | 
			
		||||
    bootloader_version = encode_uint16((*bootloader_version_data)[0], (*bootloader_version_data)[1]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (application_version_data.has_value()) {
 | 
			
		||||
    application_version = encode_uint16((*application_version_data)[0], (*application_version_data)[1]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "hardware_version=0x%x bootloader_version=0x%x application_version=0x%x\n", hardware_version,
 | 
			
		||||
           bootloader_version, application_version);
 | 
			
		||||
  if (this->version_ != nullptr) {
 | 
			
		||||
    char version[20];  // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room
 | 
			
		||||
    sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15),
 | 
			
		||||
            (application_version >> 4 & 15), application_version);
 | 
			
		||||
    ESP_LOGD(TAG, "publishing version state: %s", version);
 | 
			
		||||
    this->version_->publish_state(version);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void CCS811Component::update() {
 | 
			
		||||
  if (!this->status_has_data_())
 | 
			
		||||
@@ -117,6 +149,7 @@ void CCS811Component::dump_config() {
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this)
 | 
			
		||||
  LOG_SENSOR("  ", "CO2 Sensor", this->co2_)
 | 
			
		||||
  LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_)
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "Firmware Version Sensor", this->version_)
 | 
			
		||||
  if (this->baseline_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Baseline: %04X", *this->baseline_);
 | 
			
		||||
  } else {
 | 
			
		||||
@@ -124,7 +157,7 @@ void CCS811Component::dump_config() {
 | 
			
		||||
  }
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    switch (this->error_code_) {
 | 
			
		||||
      case COMMUNICAITON_FAILED:
 | 
			
		||||
      case COMMUNICATION_FAILED:
 | 
			
		||||
        ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
 | 
			
		||||
        break;
 | 
			
		||||
      case INVALID_ID:
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/text_sensor/text_sensor.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -12,6 +13,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_co2(sensor::Sensor *co2) { co2_ = co2; }
 | 
			
		||||
  void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; }
 | 
			
		||||
  void set_version(text_sensor::TextSensor *version) { version_ = version; }
 | 
			
		||||
  void set_baseline(uint16_t baseline) { baseline_ = baseline; }
 | 
			
		||||
  void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
 | 
			
		||||
  void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
 | 
			
		||||
@@ -34,7 +36,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 | 
			
		||||
  enum ErrorCode {
 | 
			
		||||
    UNKNOWN,
 | 
			
		||||
    COMMUNICAITON_FAILED,
 | 
			
		||||
    COMMUNICATION_FAILED,
 | 
			
		||||
    INVALID_ID,
 | 
			
		||||
    SENSOR_REPORTED_ERROR,
 | 
			
		||||
    APP_INVALID,
 | 
			
		||||
@@ -43,6 +45,7 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *co2_{nullptr};
 | 
			
		||||
  sensor::Sensor *tvoc_{nullptr};
 | 
			
		||||
  text_sensor::TextSensor *version_{nullptr};
 | 
			
		||||
  optional<uint16_t> baseline_{};
 | 
			
		||||
  /// Input sensor for humidity reading.
 | 
			
		||||
  sensor::Sensor *humidity_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,13 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.components import i2c, sensor, text_sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    ICON_RADIATOR,
 | 
			
		||||
    ICON_RESTART,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PARTS_PER_MILLION,
 | 
			
		||||
    UNIT_PARTS_PER_BILLION,
 | 
			
		||||
@@ -12,9 +16,12 @@ from esphome.const import (
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_VERSION,
 | 
			
		||||
    ICON_MOLECULE_CO2,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["text_sensor"]
 | 
			
		||||
CODEOWNERS = ["@habbie"]
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
ccs811_ns = cg.esphome_ns.namespace("ccs811")
 | 
			
		||||
@@ -30,14 +37,22 @@ CONFIG_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(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_BILLION,
 | 
			
		||||
                icon=ICON_RADIATOR,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
 | 
			
		||||
                    cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_BASELINE): cv.hex_uint16_t,
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): cv.use_id(sensor.Sensor),
 | 
			
		||||
@@ -58,6 +73,11 @@ async def to_code(config):
 | 
			
		||||
    sens = await sensor.new_sensor(config[CONF_TVOC])
 | 
			
		||||
    cg.add(var.set_tvoc(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_VERSION in config:
 | 
			
		||||
        sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID])
 | 
			
		||||
        await text_sensor.register_text_sensor(sens, config[CONF_VERSION])
 | 
			
		||||
        cg.add(var.set_version(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_BASELINE in config:
 | 
			
		||||
        cg.add(var.set_baseline(config[CONF_BASELINE]))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -63,6 +63,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True)
 | 
			
		||||
 | 
			
		||||
ClimatePreset = climate_ns.enum("ClimatePreset")
 | 
			
		||||
CLIMATE_PRESETS = {
 | 
			
		||||
    "NONE": ClimatePreset.CLIMATE_PRESET_NONE,
 | 
			
		||||
    "ECO": ClimatePreset.CLIMATE_PRESET_ECO,
 | 
			
		||||
    "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
 | 
			
		||||
    "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
 | 
			
		||||
 
 | 
			
		||||
@@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
 | 
			
		||||
  climate->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
 | 
			
		||||
  bool is_changed = alt.has_value();
 | 
			
		||||
  alt.reset();
 | 
			
		||||
  if (is_changed || dst != src) {
 | 
			
		||||
    dst = src;
 | 
			
		||||
    is_changed = true;
 | 
			
		||||
  }
 | 
			
		||||
  return is_changed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Climate::set_fan_mode_(ClimateFanMode mode) {
 | 
			
		||||
  return set_alternative(this->fan_mode, this->custom_fan_mode, mode);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Climate::set_custom_fan_mode_(const std::string &mode) {
 | 
			
		||||
  return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); }
 | 
			
		||||
 | 
			
		||||
bool Climate::set_custom_preset_(const std::string &preset) {
 | 
			
		||||
  return set_alternative(this->custom_preset, this->preset, preset);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::dump_traits_(const char *tag) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
  ESP_LOGCONFIG(tag, "ClimateTraits:");
 | 
			
		||||
  ESP_LOGCONFIG(tag, "  [x] Visual settings:");
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - Min: %.1f", traits.get_visual_min_temperature());
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - Max: %.1f", traits.get_visual_max_temperature());
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - Step: %.1f", traits.get_visual_temperature_step());
 | 
			
		||||
  if (traits.get_supports_current_temperature())
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports current temperature");
 | 
			
		||||
  if (traits.get_supports_two_point_target_temperature())
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports two-point target temperature");
 | 
			
		||||
  if (traits.get_supports_action())
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports action");
 | 
			
		||||
  if (!traits.get_supported_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported modes:");
 | 
			
		||||
    for (ClimateMode m : traits.get_supported_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", climate_mode_to_string(m));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_fan_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported fan modes:");
 | 
			
		||||
    for (ClimateFanMode m : traits.get_supported_fan_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", climate_fan_mode_to_string(m));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_fan_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported custom fan modes:");
 | 
			
		||||
    for (const std::string &s : traits.get_supported_custom_fan_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", s.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_presets().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported presets:");
 | 
			
		||||
    for (ClimatePreset p : traits.get_supported_presets())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", climate_preset_to_string(p));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_presets().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported custom presets:");
 | 
			
		||||
    for (const std::string &s : traits.get_supported_custom_presets())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", s.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_swing_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported swing modes:");
 | 
			
		||||
    for (ClimateSwingMode m : traits.get_supported_swing_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", climate_swing_mode_to_string(m));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace climate
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -245,6 +245,18 @@ class Climate : public Nameable {
 | 
			
		||||
 protected:
 | 
			
		||||
  friend ClimateCall;
 | 
			
		||||
 | 
			
		||||
  /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
 | 
			
		||||
  bool set_fan_mode_(ClimateFanMode mode);
 | 
			
		||||
 | 
			
		||||
  /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
 | 
			
		||||
  bool set_custom_fan_mode_(const std::string &mode);
 | 
			
		||||
 | 
			
		||||
  /// Set preset. Reset custom preset. Return true if preset has been changed.
 | 
			
		||||
  bool set_preset_(ClimatePreset preset);
 | 
			
		||||
 | 
			
		||||
  /// Set custom preset. Reset primary preset. Return true if preset has been changed.
 | 
			
		||||
  bool set_custom_preset_(const std::string &preset);
 | 
			
		||||
 | 
			
		||||
  /** Get the default traits of this climate device.
 | 
			
		||||
   *
 | 
			
		||||
   * Traits are static data that encode the capabilities and static data for a climate device such as supported
 | 
			
		||||
@@ -270,6 +282,7 @@ class Climate : public Nameable {
 | 
			
		||||
  void save_state_();
 | 
			
		||||
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
  void dump_traits_(const char *tag);
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void()> state_callback_{};
 | 
			
		||||
  ESPPreferenceObject rtc_;
 | 
			
		||||
 
 | 
			
		||||
@@ -72,6 +72,7 @@ class ClimateTraits {
 | 
			
		||||
 | 
			
		||||
  void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); }
 | 
			
		||||
  void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
@@ -104,6 +105,7 @@ class ClimateTraits {
 | 
			
		||||
 | 
			
		||||
  void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); }
 | 
			
		||||
  void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); }
 | 
			
		||||
  void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); }
 | 
			
		||||
  bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); }
 | 
			
		||||
  bool get_supports_presets() const { return !supported_presets_.empty(); }
 | 
			
		||||
  const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; }
 | 
			
		||||
 
 | 
			
		||||
@@ -125,16 +125,19 @@ class Cover : public Nameable {
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("open() is deprecated, use make_call().set_command_open() instead.", "2021.9")
 | 
			
		||||
  void open();
 | 
			
		||||
  /** Close the cover.
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("close() is deprecated, use make_call().set_command_close() instead.", "2021.9")
 | 
			
		||||
  void close();
 | 
			
		||||
  /** Stop the cover.
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9")
 | 
			
		||||
  void stop();
 | 
			
		||||
 | 
			
		||||
  void add_on_state_callback(std::function<void()> &&f);
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_INVERTED,
 | 
			
		||||
    CONF_LAST_RESET_TYPE,
 | 
			
		||||
    CONF_MAX_VALUE,
 | 
			
		||||
    CONF_MIN_VALUE,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
@@ -40,8 +39,8 @@ from esphome.const import (
 | 
			
		||||
    ICON_BLUR,
 | 
			
		||||
    ICON_EMPTY,
 | 
			
		||||
    ICON_THERMOMETER,
 | 
			
		||||
    LAST_RESET_TYPE_AUTO,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_EMPTY,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
@@ -336,8 +335,7 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
                    CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS,
 | 
			
		||||
                    CONF_ACCURACY_DECIMALS: 0,
 | 
			
		||||
                    CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
 | 
			
		||||
                    CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                    CONF_LAST_RESET_TYPE: LAST_RESET_TYPE_AUTO,
 | 
			
		||||
                    CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        ): [
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ class DemoSensor : public sensor::Sensor, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void update() override {
 | 
			
		||||
    float val = random_float();
 | 
			
		||||
    bool is_auto = this->last_reset_type == sensor::LAST_RESET_TYPE_AUTO;
 | 
			
		||||
    if (is_auto) {
 | 
			
		||||
    bool increasing = this->state_class == sensor::STATE_CLASS_TOTAL_INCREASING;
 | 
			
		||||
    if (increasing) {
 | 
			
		||||
      float base = isnan(this->state) ? 0.0f : this->state;
 | 
			
		||||
      this->publish_state(base + val * 10);
 | 
			
		||||
    } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,13 +9,17 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_EMPTY,
 | 
			
		||||
    LAST_RESET_TYPE_NEVER,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_NONE,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_CUBIC_METER,
 | 
			
		||||
    UNIT_EMPTY,
 | 
			
		||||
    UNIT_KILOWATT,
 | 
			
		||||
    UNIT_KILOWATT_HOURS,
 | 
			
		||||
    UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
 | 
			
		||||
    UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
)
 | 
			
		||||
from . import Dsmr, CONF_DSMR_ID
 | 
			
		||||
 | 
			
		||||
@@ -26,70 +30,80 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr),
 | 
			
		||||
        cv.Optional("energy_delivered_lux"): sensor.sensor_schema(
 | 
			
		||||
            "kWh",
 | 
			
		||||
            UNIT_KILOWATT_HOURS,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_ENERGY,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            LAST_RESET_TYPE_NEVER,
 | 
			
		||||
            STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema(
 | 
			
		||||
            "kWh",
 | 
			
		||||
            UNIT_KILOWATT_HOURS,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_ENERGY,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            LAST_RESET_TYPE_NEVER,
 | 
			
		||||
            STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema(
 | 
			
		||||
            "kWh",
 | 
			
		||||
            UNIT_KILOWATT_HOURS,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_ENERGY,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            LAST_RESET_TYPE_NEVER,
 | 
			
		||||
            STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("energy_returned_lux"): sensor.sensor_schema(
 | 
			
		||||
            "kWh",
 | 
			
		||||
            UNIT_KILOWATT_HOURS,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_ENERGY,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            LAST_RESET_TYPE_NEVER,
 | 
			
		||||
            STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("energy_returned_tariff1"): sensor.sensor_schema(
 | 
			
		||||
            "kWh",
 | 
			
		||||
            UNIT_KILOWATT_HOURS,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_ENERGY,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            LAST_RESET_TYPE_NEVER,
 | 
			
		||||
            STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("energy_returned_tariff2"): sensor.sensor_schema(
 | 
			
		||||
            "kWh",
 | 
			
		||||
            UNIT_KILOWATT_HOURS,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_ENERGY,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            LAST_RESET_TYPE_NEVER,
 | 
			
		||||
            STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("total_imported_energy"): sensor.sensor_schema(
 | 
			
		||||
            "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_ENERGY,
 | 
			
		||||
            STATE_CLASS_NONE,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("total_exported_energy"): sensor.sensor_schema(
 | 
			
		||||
            "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_ENERGY,
 | 
			
		||||
            STATE_CLASS_NONE,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("power_delivered"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("power_returned"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("reactive_power_delivered"): sensor.sensor_schema(
 | 
			
		||||
            "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_POWER,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("reactive_power_returned"): sensor.sensor_schema(
 | 
			
		||||
            "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_POWER,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("electricity_threshold"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 | 
			
		||||
@@ -107,13 +121,13 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("electricity_sags_l2"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("electricity_sags_l3"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("electricity_swells_l1"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("electricity_swells_l2"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
 | 
			
		||||
@@ -131,40 +145,64 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("power_delivered_l1"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("power_delivered_l2"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("power_delivered_l3"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("power_returned_l1"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("power_returned_l2"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("power_returned_l3"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_POWER,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_POWER,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_POWER,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_POWER,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_POWER,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
            UNIT_KILOVOLT_AMPS_REACTIVE,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_POWER,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("voltage_l1"): sensor.sensor_schema(
 | 
			
		||||
            UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
 | 
			
		||||
@@ -176,20 +214,18 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("gas_delivered"): sensor.sensor_schema(
 | 
			
		||||
            "m³",
 | 
			
		||||
            UNIT_CUBIC_METER,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_GAS,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            LAST_RESET_TYPE_NEVER,
 | 
			
		||||
            STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("gas_delivered_be"): sensor.sensor_schema(
 | 
			
		||||
            "m³",
 | 
			
		||||
            UNIT_CUBIC_METER,
 | 
			
		||||
            ICON_EMPTY,
 | 
			
		||||
            3,
 | 
			
		||||
            DEVICE_CLASS_GAS,
 | 
			
		||||
            STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            LAST_RESET_TYPE_NEVER,
 | 
			
		||||
            STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  it->schedule_show();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -82,11 +82,9 @@ bool BLEServer::create_device_characteristics_() {
 | 
			
		||||
        this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
 | 
			
		||||
    model->set_value(this->model_.value());
 | 
			
		||||
  } else {
 | 
			
		||||
#ifdef ARDUINO_BOARD
 | 
			
		||||
    BLECharacteristic *model =
 | 
			
		||||
        this->device_information_service_->create_characteristic(MODEL_UUID, BLECharacteristic::PROPERTY_READ);
 | 
			
		||||
    model->set_value(ARDUINO_BOARD);
 | 
			
		||||
#endif
 | 
			
		||||
    model->set_value(ESPHOME_BOARD);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  BLECharacteristic *version =
 | 
			
		||||
 
 | 
			
		||||
@@ -108,6 +108,16 @@ def as_hex(value):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def as_hex_array(value):
 | 
			
		||||
    value = value.replace("-", "")
 | 
			
		||||
    cpp_array = [
 | 
			
		||||
        f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
 | 
			
		||||
    ]
 | 
			
		||||
    return cg.RawExpression(
 | 
			
		||||
        "(uint8_t*)(const uint8_t[16]){{{}}}".format(",".join(cpp_array))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def as_reversed_hex_array(value):
 | 
			
		||||
    value = value.replace("-", "")
 | 
			
		||||
    cpp_array = [
 | 
			
		||||
        f"0x{part}" for part in [value[i : i + 2] for i in range(0, len(value), 2)]
 | 
			
		||||
@@ -193,7 +203,7 @@ async def to_code(config):
 | 
			
		||||
        elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid32_format):
 | 
			
		||||
            cg.add(trigger.set_service_uuid32(as_hex(conf[CONF_SERVICE_UUID])))
 | 
			
		||||
        elif len(conf[CONF_SERVICE_UUID]) == len(bt_uuid128_format):
 | 
			
		||||
            uuid128 = as_hex_array(conf[CONF_SERVICE_UUID])
 | 
			
		||||
            uuid128 = as_reversed_hex_array(conf[CONF_SERVICE_UUID])
 | 
			
		||||
            cg.add(trigger.set_service_uuid128(uuid128))
 | 
			
		||||
        if CONF_MAC_ADDRESS in conf:
 | 
			
		||||
            cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
 | 
			
		||||
@@ -205,7 +215,7 @@ async def to_code(config):
 | 
			
		||||
        elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid32_format):
 | 
			
		||||
            cg.add(trigger.set_manufacturer_uuid32(as_hex(conf[CONF_MANUFACTURER_ID])))
 | 
			
		||||
        elif len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid128_format):
 | 
			
		||||
            uuid128 = as_hex_array(conf[CONF_MANUFACTURER_ID])
 | 
			
		||||
            uuid128 = as_reversed_hex_array(conf[CONF_MANUFACTURER_ID])
 | 
			
		||||
            cg.add(trigger.set_manufacturer_uuid128(uuid128))
 | 
			
		||||
        if CONF_MAC_ADDRESS in conf:
 | 
			
		||||
            cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
 | 
			
		||||
 
 | 
			
		||||
@@ -434,6 +434,14 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
 | 
			
		||||
  }
 | 
			
		||||
  for (auto &data : this->manufacturer_datas_) {
 | 
			
		||||
    ESP_LOGVV(TAG, "  Manufacturer data: %s", hexencode(data.data).c_str());
 | 
			
		||||
    if (this->get_ibeacon().has_value()) {
 | 
			
		||||
      auto ibeacon = this->get_ibeacon().value();
 | 
			
		||||
      ESP_LOGVV(TAG, "    iBeacon data:");
 | 
			
		||||
      ESP_LOGVV(TAG, "      UUID: %s", ibeacon.get_uuid().to_string().c_str());
 | 
			
		||||
      ESP_LOGVV(TAG, "      Major: %u", ibeacon.get_major());
 | 
			
		||||
      ESP_LOGVV(TAG, "      Minor: %u", ibeacon.get_minor());
 | 
			
		||||
      ESP_LOGVV(TAG, "      TXPower: %d", ibeacon.get_signal_power());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  for (auto &data : this->service_datas_) {
 | 
			
		||||
    ESP_LOGVV(TAG, "  Service data:");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
#include "esp8266_pwm.h"
 | 
			
		||||
#include "esphome/core/macros.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
 | 
			
		||||
#error ESP8266 PWM requires at least arduino_core_version 2.4.0
 | 
			
		||||
#if defined(ARDUINO_ARCH_ESP8266) && ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
 | 
			
		||||
#error ESP8266 PWM requires at least arduino_version 2.4.0
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <core_esp8266_waveform.h>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,12 @@
 | 
			
		||||
import re
 | 
			
		||||
import logging
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import subprocess
 | 
			
		||||
import hashlib
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_COMPONENTS,
 | 
			
		||||
    CONF_REF,
 | 
			
		||||
    CONF_REFRESH,
 | 
			
		||||
    CONF_SOURCE,
 | 
			
		||||
    CONF_URL,
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
@@ -15,7 +14,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_PATH,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome import loader
 | 
			
		||||
from esphome import git, loader
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@@ -23,19 +22,11 @@ DOMAIN = CONF_EXTERNAL_COMPONENTS
 | 
			
		||||
 | 
			
		||||
TYPE_GIT = "git"
 | 
			
		||||
TYPE_LOCAL = "local"
 | 
			
		||||
CONF_REFRESH = "refresh"
 | 
			
		||||
CONF_REF = "ref"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_git_ref(value):
 | 
			
		||||
    if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None:
 | 
			
		||||
        raise cv.Invalid("Not a valid git ref")
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
GIT_SCHEMA = {
 | 
			
		||||
    cv.Required(CONF_URL): cv.url,
 | 
			
		||||
    cv.Optional(CONF_REF): validate_git_ref,
 | 
			
		||||
    cv.Optional(CONF_REF): cv.git_ref,
 | 
			
		||||
}
 | 
			
		||||
LOCAL_SCHEMA = {
 | 
			
		||||
    cv.Required(CONF_PATH): cv.directory,
 | 
			
		||||
@@ -68,14 +59,6 @@ def validate_source_shorthand(value):
 | 
			
		||||
    return SOURCE_SCHEMA(conf)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_refresh(value: str):
 | 
			
		||||
    if value.lower() == "always":
 | 
			
		||||
        return validate_refresh("0s")
 | 
			
		||||
    if value.lower() == "never":
 | 
			
		||||
        return validate_refresh("1000y")
 | 
			
		||||
    return cv.positive_time_period_seconds(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SOURCE_SCHEMA = cv.Any(
 | 
			
		||||
    validate_source_shorthand,
 | 
			
		||||
    cv.typed_schema(
 | 
			
		||||
@@ -90,7 +73,7 @@ SOURCE_SCHEMA = cv.Any(
 | 
			
		||||
CONFIG_SCHEMA = cv.ensure_list(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_SOURCE): SOURCE_SCHEMA,
 | 
			
		||||
        cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh),
 | 
			
		||||
        cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, cv.source_refresh),
 | 
			
		||||
        cv.Optional(CONF_COMPONENTS, default="all"): cv.Any(
 | 
			
		||||
            "all", cv.ensure_list(cv.string)
 | 
			
		||||
        ),
 | 
			
		||||
@@ -102,65 +85,13 @@ async def to_code(config):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _compute_destination_path(key: str) -> Path:
 | 
			
		||||
    base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN
 | 
			
		||||
    h = hashlib.new("sha256")
 | 
			
		||||
    h.update(key.encode())
 | 
			
		||||
    return base_dir / h.hexdigest()[:8]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _run_git_command(cmd, cwd=None):
 | 
			
		||||
    try:
 | 
			
		||||
        ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False)
 | 
			
		||||
    except FileNotFoundError as err:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "git is not installed but required for external_components.\n"
 | 
			
		||||
            "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git"
 | 
			
		||||
        ) from err
 | 
			
		||||
 | 
			
		||||
    if ret.returncode != 0 and ret.stderr:
 | 
			
		||||
        err_str = ret.stderr.decode("utf-8")
 | 
			
		||||
        lines = [x.strip() for x in err_str.splitlines()]
 | 
			
		||||
        if lines[-1].startswith("fatal:"):
 | 
			
		||||
            raise cv.Invalid(lines[-1][len("fatal: ") :])
 | 
			
		||||
        raise cv.Invalid(err_str)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _process_git_config(config: dict, refresh) -> str:
 | 
			
		||||
    key = f"{config[CONF_URL]}@{config.get(CONF_REF)}"
 | 
			
		||||
    repo_dir = _compute_destination_path(key)
 | 
			
		||||
    if not repo_dir.is_dir():
 | 
			
		||||
        _LOGGER.info("Cloning %s", key)
 | 
			
		||||
        _LOGGER.debug("Location: %s", repo_dir)
 | 
			
		||||
        cmd = ["git", "clone", "--depth=1"]
 | 
			
		||||
        if CONF_REF in config:
 | 
			
		||||
            cmd += ["--branch", config[CONF_REF]]
 | 
			
		||||
        cmd += ["--", config[CONF_URL], str(repo_dir)]
 | 
			
		||||
        _run_git_command(cmd)
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        # Check refresh needed
 | 
			
		||||
        file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
 | 
			
		||||
        # On first clone, FETCH_HEAD does not exists
 | 
			
		||||
        if not file_timestamp.exists():
 | 
			
		||||
            file_timestamp = Path(repo_dir / ".git" / "HEAD")
 | 
			
		||||
        age = datetime.datetime.now() - datetime.datetime.fromtimestamp(
 | 
			
		||||
            file_timestamp.stat().st_mtime
 | 
			
		||||
        )
 | 
			
		||||
        if age.total_seconds() > refresh.total_seconds:
 | 
			
		||||
            _LOGGER.info("Updating %s", key)
 | 
			
		||||
            _LOGGER.debug("Location: %s", repo_dir)
 | 
			
		||||
            # Stash local changes (if any)
 | 
			
		||||
            _run_git_command(
 | 
			
		||||
                ["git", "stash", "push", "--include-untracked"], str(repo_dir)
 | 
			
		||||
            )
 | 
			
		||||
            # Fetch remote ref
 | 
			
		||||
            cmd = ["git", "fetch", "--", "origin"]
 | 
			
		||||
            if CONF_REF in config:
 | 
			
		||||
                cmd.append(config[CONF_REF])
 | 
			
		||||
            _run_git_command(cmd, str(repo_dir))
 | 
			
		||||
            # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch)
 | 
			
		||||
            _run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir))
 | 
			
		||||
    repo_dir = git.clone_or_update(
 | 
			
		||||
        url=config[CONF_URL],
 | 
			
		||||
        ref=config.get(CONF_REF),
 | 
			
		||||
        refresh=refresh,
 | 
			
		||||
        domain=DOMAIN,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if (repo_dir / "esphome" / "components").is_dir():
 | 
			
		||||
        components_dir = repo_dir / "esphome" / "components"
 | 
			
		||||
 
 | 
			
		||||
@@ -15,9 +15,11 @@ from esphome.const import (
 | 
			
		||||
    CONF_SPEED_COMMAND_TOPIC,
 | 
			
		||||
    CONF_SPEED_STATE_TOPIC,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_ON_SPEED_SET,
 | 
			
		||||
    CONF_ON_TURN_OFF,
 | 
			
		||||
    CONF_ON_TURN_ON,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_DIRECTION,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
@@ -27,6 +29,12 @@ fan_ns = cg.esphome_ns.namespace("fan")
 | 
			
		||||
FanState = fan_ns.class_("FanState", cg.Nameable, cg.Component)
 | 
			
		||||
MakeFan = cg.Application.struct("MakeFan")
 | 
			
		||||
 | 
			
		||||
FanDirection = fan_ns.enum("FanDirection")
 | 
			
		||||
FAN_DIRECTION_ENUM = {
 | 
			
		||||
    "FORWARD": FanDirection.FAN_DIRECTION_FORWARD,
 | 
			
		||||
    "REVERSE": FanDirection.FAN_DIRECTION_REVERSE,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Actions
 | 
			
		||||
TurnOnAction = fan_ns.class_("TurnOnAction", automation.Action)
 | 
			
		||||
TurnOffAction = fan_ns.class_("TurnOffAction", automation.Action)
 | 
			
		||||
@@ -34,6 +42,10 @@ ToggleAction = fan_ns.class_("ToggleAction", automation.Action)
 | 
			
		||||
 | 
			
		||||
FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template())
 | 
			
		||||
FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template())
 | 
			
		||||
FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template())
 | 
			
		||||
 | 
			
		||||
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
 | 
			
		||||
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
 | 
			
		||||
 | 
			
		||||
FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
 | 
			
		||||
    {
 | 
			
		||||
@@ -61,6 +73,11 @@ FAN_SCHEMA = cv.NAMEABLE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanTurnOffTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_SPEED_SET): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -100,6 +117,9 @@ async def setup_fan_core_(var, config):
 | 
			
		||||
    for conf in config.get(CONF_ON_TURN_OFF, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
    for conf in config.get(CONF_ON_SPEED_SET, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_fan(var, config):
 | 
			
		||||
@@ -143,6 +163,9 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(FanState),
 | 
			
		||||
            cv.Optional(CONF_OSCILLATING): cv.templatable(cv.boolean),
 | 
			
		||||
            cv.Optional(CONF_SPEED): cv.templatable(cv.int_range(1)),
 | 
			
		||||
            cv.Optional(CONF_DIRECTION): cv.templatable(
 | 
			
		||||
                cv.enum(FAN_DIRECTION_ENUM, upper=True)
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
@@ -155,9 +178,35 @@ async def fan_turn_on_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    if CONF_SPEED in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_SPEED], args, int)
 | 
			
		||||
        cg.add(var.set_speed(template_))
 | 
			
		||||
    if CONF_DIRECTION in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_DIRECTION], args, FanDirection)
 | 
			
		||||
        cg.add(var.set_direction(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_condition(
 | 
			
		||||
    "fan.is_on",
 | 
			
		||||
    FanIsOnCondition,
 | 
			
		||||
    automation.maybe_simple_id(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(FanState),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
@automation.register_condition(
 | 
			
		||||
    "fan.is_off",
 | 
			
		||||
    FanIsOffCondition,
 | 
			
		||||
    automation.maybe_simple_id(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(FanState),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def fan_is_on_off_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, paren)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(100.0)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_define("USE_FAN")
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
 | 
			
		||||
 | 
			
		||||
  TEMPLATABLE_VALUE(bool, oscillating)
 | 
			
		||||
  TEMPLATABLE_VALUE(int, speed)
 | 
			
		||||
  TEMPLATABLE_VALUE(FanDirection, direction)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    auto call = this->state_->turn_on();
 | 
			
		||||
@@ -22,6 +23,9 @@ template<typename... Ts> class TurnOnAction : public Action<Ts...> {
 | 
			
		||||
    if (this->speed_.has_value()) {
 | 
			
		||||
      call.set_speed(this->speed_.value(x...));
 | 
			
		||||
    }
 | 
			
		||||
    if (this->direction_.has_value()) {
 | 
			
		||||
      call.set_direction(this->direction_.value(x...));
 | 
			
		||||
    }
 | 
			
		||||
    call.perform();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -46,6 +50,23 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
 | 
			
		||||
  FanState *state_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class FanIsOnCondition : public Condition<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit FanIsOnCondition(FanState *state) : state_(state) {}
 | 
			
		||||
  bool check(Ts... x) override { return this->state_->state; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  FanState *state_;
 | 
			
		||||
};
 | 
			
		||||
template<typename... Ts> class FanIsOffCondition : public Condition<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit FanIsOffCondition(FanState *state) : state_(state) {}
 | 
			
		||||
  bool check(Ts... x) override { return !this->state_->state; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  FanState *state_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FanTurnOnTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  FanTurnOnTrigger(FanState *state) {
 | 
			
		||||
@@ -82,5 +103,23 @@ class FanTurnOffTrigger : public Trigger<> {
 | 
			
		||||
  bool last_on_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FanSpeedSetTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  FanSpeedSetTrigger(FanState *state) {
 | 
			
		||||
    state->add_on_state_callback([this, state]() {
 | 
			
		||||
      auto speed = state->speed;
 | 
			
		||||
      auto should_trigger = speed != !this->last_speed_;
 | 
			
		||||
      this->last_speed_ = speed;
 | 
			
		||||
      if (should_trigger) {
 | 
			
		||||
        this->trigger();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this->last_speed_ = state->speed;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int last_speed_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
// This whole file is deprecated, don't warn about usage of deprecated types in here.
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
 | 
			
		||||
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) {
 | 
			
		||||
  const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1);
 | 
			
		||||
  const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,16 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
// Shut-up about usage of deprecated FanSpeed for a bit.
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
 | 
			
		||||
ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9")
 | 
			
		||||
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels);
 | 
			
		||||
ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9")
 | 
			
		||||
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels);
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ void FanState::setup() {
 | 
			
		||||
  call.set_direction(recovered.direction);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
 | 
			
		||||
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
 | 
			
		||||
uint32_t FanState::hash_base() { return 418001110UL; }
 | 
			
		||||
 | 
			
		||||
void FanStateCall::perform() const {
 | 
			
		||||
@@ -67,6 +67,8 @@ void FanStateCall::perform() const {
 | 
			
		||||
  this->state_->state_callback_.call();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
FanStateCall &FanStateCall::set_speed(const char *legacy_speed) {
 | 
			
		||||
  const auto supported_speed_count = this->state_->get_traits().supported_speed_count();
 | 
			
		||||
  if (strcasecmp(legacy_speed, "low") == 0) {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
 | 
			
		||||
enum FanSpeed {
 | 
			
		||||
enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed {
 | 
			
		||||
  FAN_SPEED_LOW = 0,     ///< The fan is running on low speed.
 | 
			
		||||
  FAN_SPEED_MEDIUM = 1,  ///< The fan is running on medium speed.
 | 
			
		||||
  FAN_SPEED_HIGH = 2     ///< The fan is running on high/full speed.
 | 
			
		||||
@@ -45,6 +45,7 @@ class FanStateCall {
 | 
			
		||||
    this->speed_ = speed;
 | 
			
		||||
    return *this;
 | 
			
		||||
  }
 | 
			
		||||
  ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
 | 
			
		||||
  FanStateCall &set_speed(const char *legacy_speed);
 | 
			
		||||
  FanStateCall &set_direction(FanDirection direction) {
 | 
			
		||||
    this->direction_ = direction;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,13 +20,12 @@ void FastLEDLightOutput::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Num LEDs: %u", this->num_leds_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Max refresh rate: %u", *this->max_refresh_rate_);
 | 
			
		||||
}
 | 
			
		||||
void FastLEDLightOutput::loop() {
 | 
			
		||||
  if (!this->should_show_())
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  uint32_t now = micros();
 | 
			
		||||
void FastLEDLightOutput::write_state(light::LightState *state) {
 | 
			
		||||
  // protect from refreshing too often
 | 
			
		||||
  uint32_t now = micros();
 | 
			
		||||
  if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) {
 | 
			
		||||
    // try again next loop iteration, so that this change won't get lost
 | 
			
		||||
    this->schedule_show();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->last_refresh_ = now;
 | 
			
		||||
 
 | 
			
		||||
@@ -213,7 +213,7 @@ class FastLEDLightOutput : public light::AddressableLight {
 | 
			
		||||
  }
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void write_state(light::LightState *state) override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::HARDWARE; }
 | 
			
		||||
 | 
			
		||||
  void clear_effect_data() override {
 | 
			
		||||
 
 | 
			
		||||
@@ -297,12 +297,6 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Validate footer
 | 
			
		||||
  if (!data.expect_mark(FUJITSU_GENERAL_BIT_MARK)) {
 | 
			
		||||
    ESP_LOGV(TAG, "Footer fail");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint8_t byte = 0; byte < recv_message_length; ++byte) {
 | 
			
		||||
    ESP_LOGVV(TAG, "%02X", recv_message[byte]);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,9 +13,8 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_CURRENT_AC,
 | 
			
		||||
    LAST_RESET_TYPE_AUTO,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_NONE,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_DEGREES,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
@@ -143,25 +142,23 @@ CONFIG_SCHEMA = (
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                last_reset_type=LAST_RESET_TYPE_AUTO,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                last_reset_type=LAST_RESET_TYPE_AUTO,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_HOURS,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_NONE,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_MINUTE,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_NONE,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_DEGREES,
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
 | 
			
		||||
hbridge_ns = cg.esphome_ns.namespace("hbridge")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								esphome/components/hbridge/fan/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								esphome/components/hbridge/fan/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.automation import maybe_simple_id
 | 
			
		||||
from esphome.components import fan, output
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_DECAY_MODE,
 | 
			
		||||
    CONF_SPEED_COUNT,
 | 
			
		||||
    CONF_PIN_A,
 | 
			
		||||
    CONF_PIN_B,
 | 
			
		||||
    CONF_ENABLE_PIN,
 | 
			
		||||
)
 | 
			
		||||
from .. import hbridge_ns
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@WeekendWarrior"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
HBridgeFan = hbridge_ns.class_("HBridgeFan", fan.FanState)
 | 
			
		||||
 | 
			
		||||
DecayMode = hbridge_ns.enum("DecayMode")
 | 
			
		||||
DECAY_MODE_OPTIONS = {
 | 
			
		||||
    "SLOW": DecayMode.DECAY_MODE_SLOW,
 | 
			
		||||
    "FAST": DecayMode.DECAY_MODE_FAST,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# 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),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "fan.hbridge.brake",
 | 
			
		||||
    BrakeAction,
 | 
			
		||||
    maybe_simple_id({cv.Required(CONF_ID): 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])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(
 | 
			
		||||
        config[CONF_ID],
 | 
			
		||||
        config[CONF_SPEED_COUNT],
 | 
			
		||||
        config[CONF_DECAY_MODE],
 | 
			
		||||
    )
 | 
			
		||||
    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])
 | 
			
		||||
    cg.add(var.set_pin_b(pin_b_))
 | 
			
		||||
 | 
			
		||||
    if CONF_ENABLE_PIN in config:
 | 
			
		||||
        enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN])
 | 
			
		||||
        cg.add(var.set_enable_pin(enable_pin))
 | 
			
		||||
							
								
								
									
										85
									
								
								esphome/components/hbridge/fan/hbridge_fan.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								esphome/components/hbridge/fan/hbridge_fan.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
#include "hbridge_fan.h"
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace hbridge {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "fan.hbridge";
 | 
			
		||||
 | 
			
		||||
void HBridgeFan::set_hbridge_levels_(float a_level, float b_level) {
 | 
			
		||||
  this->pin_a_->set_level(a_level);
 | 
			
		||||
  this->pin_b_->set_level(b_level);
 | 
			
		||||
  ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f", a_level, b_level);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// constant IN1/IN2, PWM on EN => power control, fast current decay
 | 
			
		||||
// constant IN1/EN, PWM on IN2 => power control, slow current decay
 | 
			
		||||
void HBridgeFan::set_hbridge_levels_(float a_level, float b_level, float enable) {
 | 
			
		||||
  this->pin_a_->set_level(a_level);
 | 
			
		||||
  this->pin_b_->set_level(b_level);
 | 
			
		||||
  this->enable_->set_level(enable);
 | 
			
		||||
  ESP_LOGD(TAG, "Setting speed: a: %.2f, b: %.2f, enable: %.2f", a_level, b_level, enable);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fan::FanStateCall HBridgeFan::brake() {
 | 
			
		||||
  ESP_LOGD(TAG, "Braking");
 | 
			
		||||
  (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f) : this->set_hbridge_levels_(1.0f, 1.0f, 1.0f);
 | 
			
		||||
  return this->make_call().set_state(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HBridgeFan::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Fan '%s':", this->get_name().c_str());
 | 
			
		||||
  if (this->get_traits().supports_oscillation()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Oscillation: YES");
 | 
			
		||||
  }
 | 
			
		||||
  if (this->get_traits().supports_direction()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Direction: YES");
 | 
			
		||||
  }
 | 
			
		||||
  if (this->decay_mode_ == DECAY_MODE_SLOW) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Decay Mode: Slow");
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Decay Mode: Fast");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void HBridgeFan::setup() {
 | 
			
		||||
  auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_);
 | 
			
		||||
  this->set_traits(traits);
 | 
			
		||||
  this->add_on_state_callback([this]() { this->next_update_ = true; });
 | 
			
		||||
}
 | 
			
		||||
void HBridgeFan::loop() {
 | 
			
		||||
  if (!this->next_update_) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->next_update_ = false;
 | 
			
		||||
 | 
			
		||||
  float speed = 0.0f;
 | 
			
		||||
  if (this->state) {
 | 
			
		||||
    speed = static_cast<float>(this->speed) / static_cast<float>(this->speed_count_);
 | 
			
		||||
  }
 | 
			
		||||
  if (speed == 0.0f) {  // off means idle
 | 
			
		||||
    (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, speed)
 | 
			
		||||
                               : this->set_hbridge_levels_(speed, speed, speed);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->direction == fan::FAN_DIRECTION_FORWARD) {
 | 
			
		||||
    if (this->decay_mode_ == DECAY_MODE_SLOW) {
 | 
			
		||||
      (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f - speed, 1.0f)
 | 
			
		||||
                                 : this->set_hbridge_levels_(1.0f - speed, 1.0f, 1.0f);
 | 
			
		||||
    } else {  // DECAY_MODE_FAST
 | 
			
		||||
      (this->enable_ == nullptr) ? this->set_hbridge_levels_(0.0f, speed)
 | 
			
		||||
                                 : this->set_hbridge_levels_(0.0f, 1.0f, speed);
 | 
			
		||||
    }
 | 
			
		||||
  } else {  // fan::FAN_DIRECTION_REVERSE
 | 
			
		||||
    if (this->decay_mode_ == DECAY_MODE_SLOW) {
 | 
			
		||||
      (this->enable_ == nullptr) ? this->set_hbridge_levels_(1.0f, 1.0f - speed)
 | 
			
		||||
                                 : this->set_hbridge_levels_(1.0f, 1.0f - speed, 1.0f);
 | 
			
		||||
    } else {  // DECAY_MODE_FAST
 | 
			
		||||
      (this->enable_ == nullptr) ? this->set_hbridge_levels_(speed, 0.0f)
 | 
			
		||||
                                 : this->set_hbridge_levels_(1.0f, 0.0f, speed);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace hbridge
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										58
									
								
								esphome/components/hbridge/fan/hbridge_fan.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								esphome/components/hbridge/fan/hbridge_fan.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/components/output/binary_output.h"
 | 
			
		||||
#include "esphome/components/output/float_output.h"
 | 
			
		||||
#include "esphome/components/fan/fan_state.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace hbridge {
 | 
			
		||||
 | 
			
		||||
enum DecayMode {
 | 
			
		||||
  DECAY_MODE_SLOW = 0,
 | 
			
		||||
  DECAY_MODE_FAST = 1,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class HBridgeFan : public fan::FanState {
 | 
			
		||||
 public:
 | 
			
		||||
  HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
 | 
			
		||||
 | 
			
		||||
  void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
 | 
			
		||||
  void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
 | 
			
		||||
  void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::HARDWARE; }
 | 
			
		||||
 | 
			
		||||
  fan::FanStateCall brake();
 | 
			
		||||
 | 
			
		||||
  int get_speed_count() { return this->speed_count_; }
 | 
			
		||||
  // update Hbridge without a triggered FanState change, eg. for acceleration/deceleration ramping
 | 
			
		||||
  void internal_update() { this->next_update_ = true; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  output::FloatOutput *pin_a_;
 | 
			
		||||
  output::FloatOutput *pin_b_;
 | 
			
		||||
  output::FloatOutput *enable_{nullptr};
 | 
			
		||||
  output::BinaryOutput *oscillating_{nullptr};
 | 
			
		||||
  bool next_update_{true};
 | 
			
		||||
  int speed_count_{};
 | 
			
		||||
  DecayMode decay_mode_{DECAY_MODE_SLOW};
 | 
			
		||||
 | 
			
		||||
  void set_hbridge_levels_(float a_level, float b_level);
 | 
			
		||||
  void set_hbridge_levels_(float a_level, float b_level, float enable);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BrakeAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->parent_->brake(); }
 | 
			
		||||
 | 
			
		||||
  HBridgeFan *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace hbridge
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -2,8 +2,10 @@ import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import light, output
 | 
			
		||||
from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B
 | 
			
		||||
from .. import hbridge_ns
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@DotNetDann"]
 | 
			
		||||
 | 
			
		||||
hbridge_ns = cg.esphome_ns.namespace("hbridge")
 | 
			
		||||
HBridgeLightOutput = hbridge_ns.class_(
 | 
			
		||||
    "HBridgeLightOutput", cg.PollingComponent, light.LightOutput
 | 
			
		||||
)
 | 
			
		||||
@@ -18,8 +18,8 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    LAST_RESET_TYPE_AUTO,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
@@ -78,8 +78,7 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            last_reset_type=LAST_RESET_TYPE_AUTO,
 | 
			
		||||
            state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
 | 
			
		||||
        cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,10 @@ from esphome.const import (
 | 
			
		||||
    CONF_PM_2_5,
 | 
			
		||||
    CONF_PM_10_0,
 | 
			
		||||
    CONF_PM_1_0,
 | 
			
		||||
    DEVICE_CLASS_AQI,
 | 
			
		||||
    DEVICE_CLASS_PM1,
 | 
			
		||||
    DEVICE_CLASS_PM10,
 | 
			
		||||
    DEVICE_CLASS_PM25,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
			
		||||
    ICON_CHEMICAL_WEAPON,
 | 
			
		||||
@@ -45,24 +49,28 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
			
		||||
                icon=ICON_CHEMICAL_WEAPON,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_PM1,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
			
		||||
                icon=ICON_CHEMICAL_WEAPON,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_PM25,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
			
		||||
                icon=ICON_CHEMICAL_WEAPON,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_PM10,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_AQI): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_INDEX,
 | 
			
		||||
                icon=ICON_CHEMICAL_WEAPON,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_AQI,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ).extend(
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("1s"))
 | 
			
		||||
    .extend(spi.spi_device_schema()),
 | 
			
		||||
    .extend(spi.spi_device_schema(False)),
 | 
			
		||||
    cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_DEFAULT_TRANSITION_LENGTH,
 | 
			
		||||
    CONF_DISABLED_BY_DEFAULT,
 | 
			
		||||
    CONF_EFFECTS,
 | 
			
		||||
    CONF_FLASH_TRANSITION_LENGTH,
 | 
			
		||||
    CONF_GAMMA_CORRECT,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_INTERNAL,
 | 
			
		||||
@@ -85,6 +86,9 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_DEFAULT_TRANSITION_LENGTH, default="1s"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_FLASH_TRANSITION_LENGTH, default="0s"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
@@ -132,6 +136,10 @@ async def setup_light_core_(light_var, output_var, config):
 | 
			
		||||
                config[CONF_DEFAULT_TRANSITION_LENGTH]
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    if CONF_FLASH_TRANSITION_LENGTH in config:
 | 
			
		||||
        cg.add(
 | 
			
		||||
            light_var.set_flash_transition_length(config[CONF_FLASH_TRANSITION_LENGTH])
 | 
			
		||||
        )
 | 
			
		||||
    if CONF_GAMMA_CORRECT in config:
 | 
			
		||||
        cg.add(light_var.set_gamma_correct(config[CONF_GAMMA_CORRECT]))
 | 
			
		||||
    effects = await cg.build_registry_list(
 | 
			
		||||
 
 | 
			
		||||
@@ -12,14 +12,13 @@ void AddressableLight::call_setup() {
 | 
			
		||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
 | 
			
		||||
  this->set_interval(5000, [this]() {
 | 
			
		||||
    const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str();
 | 
			
		||||
    ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_),
 | 
			
		||||
              YESNO(this->next_show_));
 | 
			
		||||
    ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s)", name, YESNO(this->effect_active_));
 | 
			
		||||
    for (int i = 0; i < this->size(); i++) {
 | 
			
		||||
      auto color = this->get(i);
 | 
			
		||||
      ESP_LOGVV(TAG, "  [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(),
 | 
			
		||||
                color.get_blue_raw(), color.get_white_raw());
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGVV(TAG, "");
 | 
			
		||||
    ESP_LOGVV(TAG, " ");
 | 
			
		||||
  });
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
@@ -36,7 +35,7 @@ Color esp_color_from_light_color_values(LightColorValues val) {
 | 
			
		||||
  return Color(r, g, b, w);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AddressableLight::write_state(LightState *state) {
 | 
			
		||||
void AddressableLight::update_state(LightState *state) {
 | 
			
		||||
  auto val = state->current_values;
 | 
			
		||||
  auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state());
 | 
			
		||||
  this->correction_.set_local_brightness(max_brightness);
 | 
			
		||||
 
 | 
			
		||||
@@ -51,9 +51,9 @@ class AddressableLight : public LightOutput, public Component {
 | 
			
		||||
      amnt = this->size();
 | 
			
		||||
    this->range(amnt, this->size()) = this->range(0, -amnt);
 | 
			
		||||
  }
 | 
			
		||||
  // Indicates whether an effect that directly updates the output buffer is active to prevent overwriting
 | 
			
		||||
  bool is_effect_active() const { return this->effect_active_; }
 | 
			
		||||
  void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; }
 | 
			
		||||
  void write_state(LightState *state) override;
 | 
			
		||||
  std::unique_ptr<LightTransformer> create_default_transition() override;
 | 
			
		||||
  void set_correction(float red, float green, float blue, float white = 1.0f) {
 | 
			
		||||
    this->correction_.set_max_brightness(
 | 
			
		||||
@@ -63,7 +63,8 @@ class AddressableLight : public LightOutput, public Component {
 | 
			
		||||
    this->correction_.calculate_gamma_table(state->get_gamma_correct());
 | 
			
		||||
    this->state_parent_ = state;
 | 
			
		||||
  }
 | 
			
		||||
  void schedule_show() { this->next_show_ = true; }
 | 
			
		||||
  void update_state(LightState *state) override;
 | 
			
		||||
  void schedule_show() { this->state_parent_->next_write_ = true; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_POWER_SUPPLY
 | 
			
		||||
  void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); }
 | 
			
		||||
@@ -74,9 +75,7 @@ class AddressableLight : public LightOutput, public Component {
 | 
			
		||||
 protected:
 | 
			
		||||
  friend class AddressableLightTransformer;
 | 
			
		||||
 | 
			
		||||
  bool should_show_() const { return this->effect_active_ || this->next_show_; }
 | 
			
		||||
  void mark_shown_() {
 | 
			
		||||
    this->next_show_ = false;
 | 
			
		||||
#ifdef USE_POWER_SUPPLY
 | 
			
		||||
    for (auto c : *this) {
 | 
			
		||||
      if (c.get().is_on()) {
 | 
			
		||||
@@ -90,7 +89,6 @@ class AddressableLight : public LightOutput, public Component {
 | 
			
		||||
  virtual ESPColorView get_view_internal(int32_t index) const = 0;
 | 
			
		||||
 | 
			
		||||
  bool effect_active_{false};
 | 
			
		||||
  bool next_show_{true};
 | 
			
		||||
  ESPColorCorrection correction_{};
 | 
			
		||||
#ifdef USE_POWER_SUPPLY
 | 
			
		||||
  power_supply::PowerSupplyRequester power_;
 | 
			
		||||
 
 | 
			
		||||
@@ -63,6 +63,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
 | 
			
		||||
      this->last_run_ = now;
 | 
			
		||||
      this->f_(it, current_color, this->initial_run_);
 | 
			
		||||
      this->initial_run_ = false;
 | 
			
		||||
      it.schedule_show();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -87,6 +88,7 @@ class AddressableRainbowLightEffect : public AddressableLightEffect {
 | 
			
		||||
      var = hsv;
 | 
			
		||||
      hue += add;
 | 
			
		||||
    }
 | 
			
		||||
    it.schedule_show();
 | 
			
		||||
  }
 | 
			
		||||
  void set_speed(uint32_t speed) { this->speed_ = speed; }
 | 
			
		||||
  void set_width(uint16_t width) { this->width_ = width; }
 | 
			
		||||
@@ -134,6 +136,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
 | 
			
		||||
        new_color.b = c.b;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    it.schedule_show();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -151,25 +154,27 @@ class AddressableScanEffect : public AddressableLightEffect {
 | 
			
		||||
  void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; }
 | 
			
		||||
  void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; }
 | 
			
		||||
  void apply(AddressableLight &it, const Color ¤t_color) override {
 | 
			
		||||
    it.all() = Color::BLACK;
 | 
			
		||||
    const uint32_t now = millis();
 | 
			
		||||
    if (now - this->last_move_ < this->move_interval_)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    if (direction_) {
 | 
			
		||||
      this->at_led_++;
 | 
			
		||||
      if (this->at_led_ == it.size() - this->scan_width_)
 | 
			
		||||
        this->direction_ = false;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->at_led_--;
 | 
			
		||||
      if (this->at_led_ == 0)
 | 
			
		||||
        this->direction_ = true;
 | 
			
		||||
    }
 | 
			
		||||
    this->last_move_ = now;
 | 
			
		||||
 | 
			
		||||
    it.all() = Color::BLACK;
 | 
			
		||||
    for (auto i = 0; i < this->scan_width_; i++) {
 | 
			
		||||
      it[this->at_led_ + i] = current_color;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const uint32_t now = millis();
 | 
			
		||||
    if (now - this->last_move_ > this->move_interval_) {
 | 
			
		||||
      if (direction_) {
 | 
			
		||||
        this->at_led_++;
 | 
			
		||||
        if (this->at_led_ == it.size() - this->scan_width_)
 | 
			
		||||
          this->direction_ = false;
 | 
			
		||||
      } else {
 | 
			
		||||
        this->at_led_--;
 | 
			
		||||
        if (this->at_led_ == 0)
 | 
			
		||||
          this->direction_ = true;
 | 
			
		||||
      }
 | 
			
		||||
      this->last_move_ = now;
 | 
			
		||||
    }
 | 
			
		||||
    it.schedule_show();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -210,6 +215,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect {
 | 
			
		||||
        continue;
 | 
			
		||||
      addressable[pos].set_effect_data(1);
 | 
			
		||||
    }
 | 
			
		||||
    addressable.schedule_show();
 | 
			
		||||
  }
 | 
			
		||||
  void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
 | 
			
		||||
  void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
 | 
			
		||||
@@ -257,6 +263,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect {
 | 
			
		||||
      const uint8_t color = random_uint32() & 0b111;
 | 
			
		||||
      it[pos].set_effect_data(0b1000 | color);
 | 
			
		||||
    }
 | 
			
		||||
    it.schedule_show();
 | 
			
		||||
  }
 | 
			
		||||
  void set_twinkle_probability(float twinkle_probability) { this->twinkle_probability_ = twinkle_probability; }
 | 
			
		||||
  void set_progress_interval(uint32_t progress_interval) { this->progress_interval_ = progress_interval; }
 | 
			
		||||
@@ -301,6 +308,7 @@ class AddressableFireworksEffect : public AddressableLightEffect {
 | 
			
		||||
        it[pos] = current_color;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    it.schedule_show();
 | 
			
		||||
  }
 | 
			
		||||
  void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
 | 
			
		||||
  void set_spark_probability(float spark_probability) { this->spark_probability_ = spark_probability; }
 | 
			
		||||
@@ -335,6 +343,7 @@ class AddressableFlickerEffect : public AddressableLightEffect {
 | 
			
		||||
      // slowly fade back to "real" value
 | 
			
		||||
      var = (var.get() * inv_intensity) + (current_color * intensity);
 | 
			
		||||
    }
 | 
			
		||||
    it.schedule_show();
 | 
			
		||||
  }
 | 
			
		||||
  void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
 | 
			
		||||
  void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); }
 | 
			
		||||
 
 | 
			
		||||
@@ -156,7 +156,7 @@ class StrobeLightEffect : public LightEffect {
 | 
			
		||||
 | 
			
		||||
    if (!color.is_on()) {
 | 
			
		||||
      // Don't turn the light off, otherwise the light effect will be stopped
 | 
			
		||||
      call.set_brightness_if_supported(0.0f);
 | 
			
		||||
      call.set_brightness(0.0f);
 | 
			
		||||
      call.set_state(true);
 | 
			
		||||
    }
 | 
			
		||||
    call.set_publish(false);
 | 
			
		||||
@@ -196,7 +196,6 @@ class FlickerLightEffect : public LightEffect {
 | 
			
		||||
    out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha +
 | 
			
		||||
                       (random_cubic_float() * this->intensity_));
 | 
			
		||||
 | 
			
		||||
    auto traits = this->state_->get_traits();
 | 
			
		||||
    auto call = this->state_->make_call();
 | 
			
		||||
    call.set_publish(false);
 | 
			
		||||
    call.set_save(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ enum class ColorMode : uint8_t {
 | 
			
		||||
  /// Only on/off control.
 | 
			
		||||
  ON_OFF = (uint8_t) ColorCapability::ON_OFF,
 | 
			
		||||
  /// Dimmable light.
 | 
			
		||||
  BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS,
 | 
			
		||||
  BRIGHTNESS = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS),
 | 
			
		||||
  /// White output only (use only if the light also has another color mode such as RGB).
 | 
			
		||||
  WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE),
 | 
			
		||||
  /// Controllable color temperature output.
 | 
			
		||||
 
 | 
			
		||||
@@ -8,26 +8,23 @@ namespace light {
 | 
			
		||||
static const char *const TAG = "light";
 | 
			
		||||
 | 
			
		||||
static const char *color_mode_to_human(ColorMode color_mode) {
 | 
			
		||||
  switch (color_mode) {
 | 
			
		||||
    case ColorMode::UNKNOWN:
 | 
			
		||||
      return "Unknown";
 | 
			
		||||
    case ColorMode::WHITE:
 | 
			
		||||
      return "White";
 | 
			
		||||
    case ColorMode::COLOR_TEMPERATURE:
 | 
			
		||||
      return "Color temperature";
 | 
			
		||||
    case ColorMode::COLD_WARM_WHITE:
 | 
			
		||||
      return "Cold/warm white";
 | 
			
		||||
    case ColorMode::RGB:
 | 
			
		||||
      return "RGB";
 | 
			
		||||
    case ColorMode::RGB_WHITE:
 | 
			
		||||
      return "RGBW";
 | 
			
		||||
    case ColorMode::RGB_COLD_WARM_WHITE:
 | 
			
		||||
      return "RGB + cold/warm white";
 | 
			
		||||
    case ColorMode::RGB_COLOR_TEMPERATURE:
 | 
			
		||||
      return "RGB + color temperature";
 | 
			
		||||
    default:
 | 
			
		||||
      return "";
 | 
			
		||||
  }
 | 
			
		||||
  if (color_mode == ColorMode::UNKNOWN)
 | 
			
		||||
    return "Unknown";
 | 
			
		||||
  if (color_mode == ColorMode::WHITE)
 | 
			
		||||
    return "White";
 | 
			
		||||
  if (color_mode == ColorMode::COLOR_TEMPERATURE)
 | 
			
		||||
    return "Color temperature";
 | 
			
		||||
  if (color_mode == ColorMode::COLD_WARM_WHITE)
 | 
			
		||||
    return "Cold/warm white";
 | 
			
		||||
  if (color_mode == ColorMode::RGB)
 | 
			
		||||
    return "RGB";
 | 
			
		||||
  if (color_mode == ColorMode::RGB_WHITE)
 | 
			
		||||
    return "RGBW";
 | 
			
		||||
  if (color_mode == ColorMode::RGB_COLD_WARM_WHITE)
 | 
			
		||||
    return "RGB + cold/warm white";
 | 
			
		||||
  if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE)
 | 
			
		||||
    return "RGB + color temperature";
 | 
			
		||||
  return "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LightCall::perform() {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,13 @@ class LightOutput {
 | 
			
		||||
 | 
			
		||||
  virtual void setup_state(LightState *state) {}
 | 
			
		||||
 | 
			
		||||
  /// Called on every update of the current values of the associated LightState,
 | 
			
		||||
  /// can optionally be used to do processing of this change.
 | 
			
		||||
  virtual void update_state(LightState *state) {}
 | 
			
		||||
 | 
			
		||||
  /// Called from loop() every time the light state has changed, and should
 | 
			
		||||
  /// should write the new state to hardware. Every call to write_state() is
 | 
			
		||||
  /// preceded by (at least) one call to update_state().
 | 
			
		||||
  virtual void write_state(LightState *state) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -114,9 +114,11 @@ void LightState::loop() {
 | 
			
		||||
  // Apply transformer (if any)
 | 
			
		||||
  if (this->transformer_ != nullptr) {
 | 
			
		||||
    auto values = this->transformer_->apply();
 | 
			
		||||
    this->next_write_ = values.has_value();  // don't write if transformer doesn't want us to
 | 
			
		||||
    if (values.has_value())
 | 
			
		||||
    if (values.has_value()) {
 | 
			
		||||
      this->current_values = *values;
 | 
			
		||||
      this->output_->update_state(this);
 | 
			
		||||
      this->next_write_ = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->transformer_->is_finished()) {
 | 
			
		||||
      this->transformer_->stop();
 | 
			
		||||
@@ -127,18 +129,15 @@ void LightState::loop() {
 | 
			
		||||
 | 
			
		||||
  // Write state to the light
 | 
			
		||||
  if (this->next_write_) {
 | 
			
		||||
    this->output_->write_state(this);
 | 
			
		||||
    this->next_write_ = false;
 | 
			
		||||
    this->output_->write_state(this);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
 | 
			
		||||
uint32_t LightState::hash_base() { return 1114400283; }
 | 
			
		||||
 | 
			
		||||
void LightState::publish_state() {
 | 
			
		||||
  this->remote_values_callback_.call();
 | 
			
		||||
  this->next_write_ = true;
 | 
			
		||||
}
 | 
			
		||||
void LightState::publish_state() { this->remote_values_callback_.call(); }
 | 
			
		||||
 | 
			
		||||
LightOutput *LightState::get_output() const { return this->output_; }
 | 
			
		||||
std::string LightState::get_effect_name() {
 | 
			
		||||
@@ -158,6 +157,11 @@ void LightState::add_new_target_state_reached_callback(std::function<void()> &&s
 | 
			
		||||
void LightState::set_default_transition_length(uint32_t default_transition_length) {
 | 
			
		||||
  this->default_transition_length_ = default_transition_length;
 | 
			
		||||
}
 | 
			
		||||
uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; }
 | 
			
		||||
void LightState::set_flash_transition_length(uint32_t flash_transition_length) {
 | 
			
		||||
  this->flash_transition_length_ = flash_transition_length;
 | 
			
		||||
}
 | 
			
		||||
uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; }
 | 
			
		||||
void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
 | 
			
		||||
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
 | 
			
		||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
 | 
			
		||||
@@ -235,7 +239,7 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) {
 | 
			
		||||
  // If starting a flash if one is already happening, set end values to end values of current flash
 | 
			
		||||
  // Hacky but works
 | 
			
		||||
  if (this->transformer_ != nullptr)
 | 
			
		||||
    end_colors = this->transformer_->get_target_values();
 | 
			
		||||
    end_colors = this->transformer_->get_start_values();
 | 
			
		||||
 | 
			
		||||
  this->transformer_ = make_unique<LightFlashTransformer>(*this);
 | 
			
		||||
  this->transformer_->setup(end_colors, target, length);
 | 
			
		||||
@@ -248,6 +252,7 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
 | 
			
		||||
  if (set_remote_values) {
 | 
			
		||||
    this->remote_values = target;
 | 
			
		||||
  }
 | 
			
		||||
  this->output_->update_state(this);
 | 
			
		||||
  this->next_write_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -99,6 +99,11 @@ class LightState : public Nameable, public Component {
 | 
			
		||||
 | 
			
		||||
  /// Set the default transition length, i.e. the transition length when no transition is provided.
 | 
			
		||||
  void set_default_transition_length(uint32_t default_transition_length);
 | 
			
		||||
  uint32_t get_default_transition_length() const;
 | 
			
		||||
 | 
			
		||||
  /// Set the flash transition length
 | 
			
		||||
  void set_flash_transition_length(uint32_t flash_transition_length);
 | 
			
		||||
  uint32_t get_flash_transition_length() const;
 | 
			
		||||
 | 
			
		||||
  /// Set the gamma correction factor
 | 
			
		||||
  void set_gamma_correct(float gamma_correct);
 | 
			
		||||
@@ -188,6 +193,8 @@ class LightState : public Nameable, public Component {
 | 
			
		||||
 | 
			
		||||
  /// Default transition length for all transitions in ms.
 | 
			
		||||
  uint32_t default_transition_length_{};
 | 
			
		||||
  /// Transition length to use for flash transitions.
 | 
			
		||||
  uint32_t flash_transition_length_{};
 | 
			
		||||
  /// Gamma correction factor for the light.
 | 
			
		||||
  float gamma_correct_{};
 | 
			
		||||
  /// Restore mode of the light.
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,43 @@ class LightFlashTransformer : public LightTransformer {
 | 
			
		||||
 public:
 | 
			
		||||
  LightFlashTransformer(LightState &state) : state_(state) {}
 | 
			
		||||
 | 
			
		||||
  optional<LightColorValues> apply() override { return this->get_target_values(); }
 | 
			
		||||
  void start() override {
 | 
			
		||||
    this->transition_length_ = this->state_.get_flash_transition_length();
 | 
			
		||||
    if (this->transition_length_ * 2 > this->length_)
 | 
			
		||||
      this->transition_length_ = this->length_ / 2;
 | 
			
		||||
 | 
			
		||||
    // do not create transition if length is 0
 | 
			
		||||
    if (this->transition_length_ == 0)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    // first transition to original target
 | 
			
		||||
    this->transformer_ = this->state_.get_output()->create_default_transition();
 | 
			
		||||
    this->transformer_->setup(this->state_.current_values, this->target_values_, this->transition_length_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  optional<LightColorValues> apply() override {
 | 
			
		||||
    // transition transformer does not handle 0 length as progress returns nan
 | 
			
		||||
    if (this->transition_length_ == 0)
 | 
			
		||||
      return this->target_values_;
 | 
			
		||||
 | 
			
		||||
    if (this->transformer_ != nullptr) {
 | 
			
		||||
      if (!this->transformer_->is_finished()) {
 | 
			
		||||
        return this->transformer_->apply();
 | 
			
		||||
      } else {
 | 
			
		||||
        this->transformer_->stop();
 | 
			
		||||
        this->transformer_ = nullptr;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (millis() > this->start_time_ + this->length_ - this->transition_length_) {
 | 
			
		||||
      // second transition back to start value
 | 
			
		||||
      this->transformer_ = this->state_.get_output()->create_default_transition();
 | 
			
		||||
      this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // once transition is complete, don't change states until next transition
 | 
			
		||||
    return optional<LightColorValues>();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Restore the original values after the flash.
 | 
			
		||||
  void stop() override {
 | 
			
		||||
@@ -69,6 +105,8 @@ class LightFlashTransformer : public LightTransformer {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  LightState &state_;
 | 
			
		||||
  uint32_t transition_length_;
 | 
			
		||||
  std::unique_ptr<LightTransformer> transformer_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace light
 | 
			
		||||
 
 | 
			
		||||
@@ -43,21 +43,24 @@ void Logger::write_header_(int level, const char *tag, int line) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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))
 | 
			
		||||
  if (level > this->level_for(tag) || recursion_guard_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  recursion_guard_ = true;
 | 
			
		||||
  this->reset_buffer_();
 | 
			
		||||
  this->write_header_(level, tag, line);
 | 
			
		||||
  this->vprintf_to_buffer_(format, args);
 | 
			
		||||
  this->write_footer_();
 | 
			
		||||
  this->log_message_(level, tag);
 | 
			
		||||
  recursion_guard_ = false;
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
 | 
			
		||||
void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
 | 
			
		||||
                          va_list args) {  // NOLINT
 | 
			
		||||
  if (level > this->level_for(tag))
 | 
			
		||||
  if (level > this->level_for(tag) || recursion_guard_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  recursion_guard_ = true;
 | 
			
		||||
  this->reset_buffer_();
 | 
			
		||||
  // copy format string
 | 
			
		||||
  const char *format_pgm_p = (PGM_P) format;
 | 
			
		||||
@@ -78,6 +81,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
 | 
			
		||||
  this->vprintf_to_buffer_(this->tx_buffer_, args);
 | 
			
		||||
  this->write_footer_();
 | 
			
		||||
  this->log_message_(level, tag, offset);
 | 
			
		||||
  recursion_guard_ = false;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -113,6 +113,8 @@ class Logger : public Component {
 | 
			
		||||
  };
 | 
			
		||||
  std::vector<LogLevelOverride> log_levels_;
 | 
			
		||||
  CallbackManager<void(int, const char *, const char *)> log_callback_{};
 | 
			
		||||
  /// Prevents recursive log calls, if true a log message is already being processed.
 | 
			
		||||
  bool recursion_guard_ = false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern Logger *global_logger;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ async def mcp23xxx_pin_to_code(config):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# BEGIN Removed pin schemas below to show error in configuration
 | 
			
		||||
# TODO remove in 1.19.0
 | 
			
		||||
# TODO remove in 2022.5.0
 | 
			
		||||
 | 
			
		||||
for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]:
 | 
			
		||||
    PIN_SCHEMA = cv.Schema(
 | 
			
		||||
@@ -110,6 +110,7 @@ for id in ["mcp23008", "mcp23s08", "mcp23017", "mcp23s17"]:
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=cell-var-from-loop
 | 
			
		||||
    @pins.PIN_SCHEMA_REGISTRY.register(id, (PIN_SCHEMA, PIN_SCHEMA))
 | 
			
		||||
    def pin_to_code(config):
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    ICON_MOLECULE_CO2,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PARTS_PER_MILLION,
 | 
			
		||||
@@ -34,6 +35,7 @@ CONFIG_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.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										173
									
								
								esphome/components/midea/adapter.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								esphome/components/midea/adapter.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,173 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "adapter.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea {
 | 
			
		||||
 | 
			
		||||
const char *const Constants::TAG = "midea";
 | 
			
		||||
const std::string Constants::FREEZE_PROTECTION = "freeze protection";
 | 
			
		||||
const std::string Constants::SILENT = "silent";
 | 
			
		||||
const std::string Constants::TURBO = "turbo";
 | 
			
		||||
 | 
			
		||||
ClimateMode Converters::to_climate_mode(MideaMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case MideaMode::MODE_AUTO:
 | 
			
		||||
      return ClimateMode::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    case MideaMode::MODE_COOL:
 | 
			
		||||
      return ClimateMode::CLIMATE_MODE_COOL;
 | 
			
		||||
    case MideaMode::MODE_DRY:
 | 
			
		||||
      return ClimateMode::CLIMATE_MODE_DRY;
 | 
			
		||||
    case MideaMode::MODE_FAN_ONLY:
 | 
			
		||||
      return ClimateMode::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
    case MideaMode::MODE_HEAT:
 | 
			
		||||
      return ClimateMode::CLIMATE_MODE_HEAT;
 | 
			
		||||
    default:
 | 
			
		||||
      return ClimateMode::CLIMATE_MODE_OFF;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MideaMode Converters::to_midea_mode(ClimateMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case ClimateMode::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      return MideaMode::MODE_AUTO;
 | 
			
		||||
    case ClimateMode::CLIMATE_MODE_COOL:
 | 
			
		||||
      return MideaMode::MODE_COOL;
 | 
			
		||||
    case ClimateMode::CLIMATE_MODE_DRY:
 | 
			
		||||
      return MideaMode::MODE_DRY;
 | 
			
		||||
    case ClimateMode::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
      return MideaMode::MODE_FAN_ONLY;
 | 
			
		||||
    case ClimateMode::CLIMATE_MODE_HEAT:
 | 
			
		||||
      return MideaMode::MODE_HEAT;
 | 
			
		||||
    default:
 | 
			
		||||
      return MideaMode::MODE_OFF;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case MideaSwingMode::SWING_VERTICAL:
 | 
			
		||||
      return ClimateSwingMode::CLIMATE_SWING_VERTICAL;
 | 
			
		||||
    case MideaSwingMode::SWING_HORIZONTAL:
 | 
			
		||||
      return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL;
 | 
			
		||||
    case MideaSwingMode::SWING_BOTH:
 | 
			
		||||
      return ClimateSwingMode::CLIMATE_SWING_BOTH;
 | 
			
		||||
    default:
 | 
			
		||||
      return ClimateSwingMode::CLIMATE_SWING_OFF;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case ClimateSwingMode::CLIMATE_SWING_VERTICAL:
 | 
			
		||||
      return MideaSwingMode::SWING_VERTICAL;
 | 
			
		||||
    case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL:
 | 
			
		||||
      return MideaSwingMode::SWING_HORIZONTAL;
 | 
			
		||||
    case ClimateSwingMode::CLIMATE_SWING_BOTH:
 | 
			
		||||
      return MideaSwingMode::SWING_BOTH;
 | 
			
		||||
    default:
 | 
			
		||||
      return MideaSwingMode::SWING_OFF;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case ClimateFanMode::CLIMATE_FAN_LOW:
 | 
			
		||||
      return MideaFanMode::FAN_LOW;
 | 
			
		||||
    case ClimateFanMode::CLIMATE_FAN_MEDIUM:
 | 
			
		||||
      return MideaFanMode::FAN_MEDIUM;
 | 
			
		||||
    case ClimateFanMode::CLIMATE_FAN_HIGH:
 | 
			
		||||
      return MideaFanMode::FAN_HIGH;
 | 
			
		||||
    default:
 | 
			
		||||
      return MideaFanMode::FAN_AUTO;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case MideaFanMode::FAN_LOW:
 | 
			
		||||
      return ClimateFanMode::CLIMATE_FAN_LOW;
 | 
			
		||||
    case MideaFanMode::FAN_MEDIUM:
 | 
			
		||||
      return ClimateFanMode::CLIMATE_FAN_MEDIUM;
 | 
			
		||||
    case MideaFanMode::FAN_HIGH:
 | 
			
		||||
      return ClimateFanMode::CLIMATE_FAN_HIGH;
 | 
			
		||||
    default:
 | 
			
		||||
      return ClimateFanMode::CLIMATE_FAN_AUTO;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case MideaFanMode::FAN_SILENT:
 | 
			
		||||
    case MideaFanMode::FAN_TURBO:
 | 
			
		||||
      return true;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case MideaFanMode::FAN_SILENT:
 | 
			
		||||
      return Constants::SILENT;
 | 
			
		||||
    default:
 | 
			
		||||
      return Constants::TURBO;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) {
 | 
			
		||||
  if (mode == Constants::SILENT)
 | 
			
		||||
    return MideaFanMode::FAN_SILENT;
 | 
			
		||||
  return MideaFanMode::FAN_TURBO;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
MideaPreset Converters::to_midea_preset(ClimatePreset preset) {
 | 
			
		||||
  switch (preset) {
 | 
			
		||||
    case ClimatePreset::CLIMATE_PRESET_SLEEP:
 | 
			
		||||
      return MideaPreset::PRESET_SLEEP;
 | 
			
		||||
    case ClimatePreset::CLIMATE_PRESET_ECO:
 | 
			
		||||
      return MideaPreset::PRESET_ECO;
 | 
			
		||||
    case ClimatePreset::CLIMATE_PRESET_BOOST:
 | 
			
		||||
      return MideaPreset::PRESET_TURBO;
 | 
			
		||||
    default:
 | 
			
		||||
      return MideaPreset::PRESET_NONE;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimatePreset Converters::to_climate_preset(MideaPreset preset) {
 | 
			
		||||
  switch (preset) {
 | 
			
		||||
    case MideaPreset::PRESET_SLEEP:
 | 
			
		||||
      return ClimatePreset::CLIMATE_PRESET_SLEEP;
 | 
			
		||||
    case MideaPreset::PRESET_ECO:
 | 
			
		||||
      return ClimatePreset::CLIMATE_PRESET_ECO;
 | 
			
		||||
    case MideaPreset::PRESET_TURBO:
 | 
			
		||||
      return ClimatePreset::CLIMATE_PRESET_BOOST;
 | 
			
		||||
    default:
 | 
			
		||||
      return ClimatePreset::CLIMATE_PRESET_NONE;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; }
 | 
			
		||||
 | 
			
		||||
const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; }
 | 
			
		||||
 | 
			
		||||
MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; }
 | 
			
		||||
 | 
			
		||||
void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) {
 | 
			
		||||
  if (capabilities.supportAutoMode())
 | 
			
		||||
    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL);
 | 
			
		||||
  if (capabilities.supportCoolMode())
 | 
			
		||||
    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL);
 | 
			
		||||
  if (capabilities.supportHeatMode())
 | 
			
		||||
    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT);
 | 
			
		||||
  if (capabilities.supportDryMode())
 | 
			
		||||
    traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY);
 | 
			
		||||
  if (capabilities.supportTurboPreset())
 | 
			
		||||
    traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST);
 | 
			
		||||
  if (capabilities.supportEcoPreset())
 | 
			
		||||
    traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
 | 
			
		||||
  if (capabilities.supportFrostProtectionPreset())
 | 
			
		||||
    traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace midea
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										42
									
								
								esphome/components/midea/adapter.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/midea/adapter.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <Appliance/AirConditioner/AirConditioner.h>
 | 
			
		||||
#include "esphome/components/climate/climate_traits.h"
 | 
			
		||||
#include "appliance_base.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea {
 | 
			
		||||
 | 
			
		||||
using MideaMode = dudanov::midea::ac::Mode;
 | 
			
		||||
using MideaSwingMode = dudanov::midea::ac::SwingMode;
 | 
			
		||||
using MideaFanMode = dudanov::midea::ac::FanMode;
 | 
			
		||||
using MideaPreset = dudanov::midea::ac::Preset;
 | 
			
		||||
 | 
			
		||||
class Constants {
 | 
			
		||||
 public:
 | 
			
		||||
  static const char *const TAG;
 | 
			
		||||
  static const std::string FREEZE_PROTECTION;
 | 
			
		||||
  static const std::string SILENT;
 | 
			
		||||
  static const std::string TURBO;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Converters {
 | 
			
		||||
 public:
 | 
			
		||||
  static MideaMode to_midea_mode(ClimateMode mode);
 | 
			
		||||
  static ClimateMode to_climate_mode(MideaMode mode);
 | 
			
		||||
  static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode);
 | 
			
		||||
  static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode);
 | 
			
		||||
  static MideaPreset to_midea_preset(ClimatePreset preset);
 | 
			
		||||
  static MideaPreset to_midea_preset(const std::string &preset);
 | 
			
		||||
  static bool is_custom_midea_preset(MideaPreset preset);
 | 
			
		||||
  static ClimatePreset to_climate_preset(MideaPreset preset);
 | 
			
		||||
  static const std::string &to_custom_climate_preset(MideaPreset preset);
 | 
			
		||||
  static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode);
 | 
			
		||||
  static MideaFanMode to_midea_fan_mode(const std::string &fan_mode);
 | 
			
		||||
  static bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
 | 
			
		||||
  static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode);
 | 
			
		||||
  static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode);
 | 
			
		||||
  static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace midea
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										152
									
								
								esphome/components/midea/air_conditioner.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								esphome/components/midea/air_conditioner.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,152 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "air_conditioner.h"
 | 
			
		||||
#include "adapter.h"
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
#include "midea_ir.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea {
 | 
			
		||||
 | 
			
		||||
static void set_sensor(Sensor *sensor, float value) {
 | 
			
		||||
  if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
 | 
			
		||||
    sensor->publish_state(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T> void update_property(T &property, const T &value, bool &flag) {
 | 
			
		||||
  if (property != value) {
 | 
			
		||||
    property = value;
 | 
			
		||||
    flag = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirConditioner::on_status_change() {
 | 
			
		||||
  bool need_publish = false;
 | 
			
		||||
  update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
 | 
			
		||||
  update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
 | 
			
		||||
  auto mode = Converters::to_climate_mode(this->base_.getMode());
 | 
			
		||||
  update_property(this->mode, mode, need_publish);
 | 
			
		||||
  auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode());
 | 
			
		||||
  update_property(this->swing_mode, swing_mode, need_publish);
 | 
			
		||||
  // Preset
 | 
			
		||||
  auto preset = this->base_.getPreset();
 | 
			
		||||
  if (Converters::is_custom_midea_preset(preset)) {
 | 
			
		||||
    if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset)))
 | 
			
		||||
      need_publish = true;
 | 
			
		||||
  } else if (this->set_preset_(Converters::to_climate_preset(preset))) {
 | 
			
		||||
    need_publish = true;
 | 
			
		||||
  }
 | 
			
		||||
  // Fan mode
 | 
			
		||||
  auto fan_mode = this->base_.getFanMode();
 | 
			
		||||
  if (Converters::is_custom_midea_fan_mode(fan_mode)) {
 | 
			
		||||
    if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode)))
 | 
			
		||||
      need_publish = true;
 | 
			
		||||
  } else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) {
 | 
			
		||||
    need_publish = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (need_publish)
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
  set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp());
 | 
			
		||||
  set_sensor(this->power_sensor_, this->base_.getPowerUsage());
 | 
			
		||||
  set_sensor(this->humidity_sensor_, this->base_.getIndoorHum());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirConditioner::control(const ClimateCall &call) {
 | 
			
		||||
  dudanov::midea::ac::Control ctrl{};
 | 
			
		||||
  if (call.get_target_temperature().has_value())
 | 
			
		||||
    ctrl.targetTemp = call.get_target_temperature().value();
 | 
			
		||||
  if (call.get_swing_mode().has_value())
 | 
			
		||||
    ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value());
 | 
			
		||||
  if (call.get_mode().has_value())
 | 
			
		||||
    ctrl.mode = Converters::to_midea_mode(call.get_mode().value());
 | 
			
		||||
  if (call.get_preset().has_value())
 | 
			
		||||
    ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
 | 
			
		||||
  else if (call.get_custom_preset().has_value())
 | 
			
		||||
    ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value());
 | 
			
		||||
  if (call.get_fan_mode().has_value())
 | 
			
		||||
    ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
 | 
			
		||||
  else if (call.get_custom_fan_mode().has_value())
 | 
			
		||||
    ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value());
 | 
			
		||||
  this->base_.control(ctrl);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateTraits AirConditioner::traits() {
 | 
			
		||||
  auto traits = ClimateTraits();
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_visual_min_temperature(17);
 | 
			
		||||
  traits.set_visual_max_temperature(30);
 | 
			
		||||
  traits.set_visual_temperature_step(0.5);
 | 
			
		||||
  traits.set_supported_modes(this->supported_modes_);
 | 
			
		||||
  traits.set_supported_swing_modes(this->supported_swing_modes_);
 | 
			
		||||
  traits.set_supported_presets(this->supported_presets_);
 | 
			
		||||
  traits.set_supported_custom_presets(this->supported_custom_presets_);
 | 
			
		||||
  traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
 | 
			
		||||
  /* + MINIMAL SET OF CAPABILITIES */
 | 
			
		||||
  traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
 | 
			
		||||
  traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY);
 | 
			
		||||
  traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
 | 
			
		||||
  traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
 | 
			
		||||
  traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
 | 
			
		||||
  traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
 | 
			
		||||
  traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
 | 
			
		||||
  traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL);
 | 
			
		||||
  traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
 | 
			
		||||
  traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
 | 
			
		||||
  if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
 | 
			
		||||
    Converters::to_climate_traits(traits, this->base_.getCapabilities());
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirConditioner::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(Constants::TAG, "MideaDongle:");
 | 
			
		||||
  ESP_LOGCONFIG(Constants::TAG, "  [x] Period: %dms", this->base_.getPeriod());
 | 
			
		||||
  ESP_LOGCONFIG(Constants::TAG, "  [x] Response timeout: %dms", this->base_.getTimeout());
 | 
			
		||||
  ESP_LOGCONFIG(Constants::TAG, "  [x] Request attempts: %d", this->base_.getNumAttempts());
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
  ESP_LOGCONFIG(Constants::TAG, "  [x] Using RemoteTransmitter");
 | 
			
		||||
#endif
 | 
			
		||||
  if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
 | 
			
		||||
    this->base_.getCapabilities().dump();
 | 
			
		||||
  } else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) {
 | 
			
		||||
    ESP_LOGW(Constants::TAG,
 | 
			
		||||
             "Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your "
 | 
			
		||||
             "appliance options.");
 | 
			
		||||
  }
 | 
			
		||||
  this->dump_traits_(Constants::TAG);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ACTIONS */
 | 
			
		||||
 | 
			
		||||
void AirConditioner::do_follow_me(float temperature, bool beeper) {
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
  IrFollowMeData data(static_cast<uint8_t>(lroundf(temperature)), beeper);
 | 
			
		||||
  this->transmit_ir(data);
 | 
			
		||||
#else
 | 
			
		||||
  ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirConditioner::do_swing_step() {
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
  IrSpecialData data(0x01);
 | 
			
		||||
  this->transmit_ir(data);
 | 
			
		||||
#else
 | 
			
		||||
  ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirConditioner::do_display_toggle() {
 | 
			
		||||
  if (this->base_.getCapabilities().supportLightControl()) {
 | 
			
		||||
    this->base_.displayToggle();
 | 
			
		||||
  } else {
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
    IrSpecialData data(0x08);
 | 
			
		||||
    this->transmit_ir(data);
 | 
			
		||||
#else
 | 
			
		||||
    ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace midea
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										41
									
								
								esphome/components/midea/air_conditioner.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/midea/air_conditioner.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <Appliance/AirConditioner/AirConditioner.h>
 | 
			
		||||
#include "appliance_base.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea {
 | 
			
		||||
 | 
			
		||||
using sensor::Sensor;
 | 
			
		||||
using climate::ClimateCall;
 | 
			
		||||
 | 
			
		||||
class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner> {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
 | 
			
		||||
  void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
 | 
			
		||||
  void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
 | 
			
		||||
  void on_status_change() override;
 | 
			
		||||
 | 
			
		||||
  /* ############### */
 | 
			
		||||
  /* ### ACTIONS ### */
 | 
			
		||||
  /* ############### */
 | 
			
		||||
 | 
			
		||||
  void do_follow_me(float temperature, bool beeper = false);
 | 
			
		||||
  void do_display_toggle();
 | 
			
		||||
  void do_swing_step();
 | 
			
		||||
  void do_beeper_on() { this->set_beeper_feedback(true); }
 | 
			
		||||
  void do_beeper_off() { this->set_beeper_feedback(false); }
 | 
			
		||||
  void do_power_on() { this->base_.setPowerState(true); }
 | 
			
		||||
  void do_power_off() { this->base_.setPowerState(false); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const ClimateCall &call) override;
 | 
			
		||||
  ClimateTraits traits() override;
 | 
			
		||||
  Sensor *outdoor_sensor_{nullptr};
 | 
			
		||||
  Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  Sensor *power_sensor_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace midea
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										76
									
								
								esphome/components/midea/appliance_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/midea/appliance_base.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
#include "esphome/components/remote_base/midea_protocol.h"
 | 
			
		||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include <Appliance/ApplianceBase.h>
 | 
			
		||||
#include <Helpers/Logger.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea {
 | 
			
		||||
 | 
			
		||||
using climate::ClimatePreset;
 | 
			
		||||
using climate::ClimateTraits;
 | 
			
		||||
using climate::ClimateMode;
 | 
			
		||||
using climate::ClimateSwingMode;
 | 
			
		||||
using climate::ClimateFanMode;
 | 
			
		||||
 | 
			
		||||
template<typename T> class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate {
 | 
			
		||||
  static_assert(std::is_base_of<dudanov::midea::ApplianceBase, T>::value,
 | 
			
		||||
                "T must derive from dudanov::midea::ApplianceBase class");
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  ApplianceBase() {
 | 
			
		||||
    this->base_.setStream(this);
 | 
			
		||||
    this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this));
 | 
			
		||||
    dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) {
 | 
			
		||||
      esp_log_vprintf_(level, tag, line, format.c_str(), args);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  bool can_proceed() override {
 | 
			
		||||
    return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS;
 | 
			
		||||
  }
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; }
 | 
			
		||||
  void setup() override { this->base_.setup(); }
 | 
			
		||||
  void loop() override { this->base_.loop(); }
 | 
			
		||||
  void set_period(uint32_t ms) { this->base_.setPeriod(ms); }
 | 
			
		||||
  void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); }
 | 
			
		||||
  void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); }
 | 
			
		||||
  void set_beeper_feedback(bool state) { this->base_.setBeeper(state); }
 | 
			
		||||
  void set_autoconf(bool value) { this->base_.setAutoconf(value); }
 | 
			
		||||
  void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
 | 
			
		||||
  void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
 | 
			
		||||
  void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
 | 
			
		||||
  void set_custom_presets(std::set<std::string> presets) { this->supported_custom_presets_ = std::move(presets); }
 | 
			
		||||
  void set_custom_fan_modes(std::set<std::string> modes) { this->supported_custom_fan_modes_ = std::move(modes); }
 | 
			
		||||
  virtual void on_status_change() = 0;
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
  void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
 | 
			
		||||
    this->transmitter_ = transmitter;
 | 
			
		||||
  }
 | 
			
		||||
  void transmit_ir(remote_base::MideaData &data) {
 | 
			
		||||
    data.finalize();
 | 
			
		||||
    auto transmit = this->transmitter_->transmit();
 | 
			
		||||
    remote_base::MideaProtocol().encode(transmit.get_data(), data);
 | 
			
		||||
    transmit.perform();
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  T base_;
 | 
			
		||||
  std::set<ClimateMode> supported_modes_{};
 | 
			
		||||
  std::set<ClimateSwingMode> supported_swing_modes_{};
 | 
			
		||||
  std::set<ClimatePreset> supported_presets_{};
 | 
			
		||||
  std::set<std::string> supported_custom_presets_{};
 | 
			
		||||
  std::set<std::string> supported_custom_fan_modes_{};
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
  remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace midea
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										56
									
								
								esphome/components/midea/automations.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/midea/automations.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "air_conditioner.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea {
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class MideaActionBase : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_parent(AirConditioner *parent) { this->parent_ = parent; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  AirConditioner *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
 | 
			
		||||
  TEMPLATABLE_VALUE(float, temperature)
 | 
			
		||||
  TEMPLATABLE_VALUE(bool, beeper)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...));
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class SwingStepAction : public MideaActionBase<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->do_swing_step(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayToggleAction : public MideaActionBase<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->do_display_toggle(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BeeperOnAction : public MideaActionBase<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->do_beeper_on(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BeeperOffAction : public MideaActionBase<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->do_beeper_off(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class PowerOnAction : public MideaActionBase<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->do_power_on(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->do_power_off(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace midea
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										284
									
								
								esphome/components/midea/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								esphome/components/midea/climate.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,284 @@
 | 
			
		||||
from esphome.core import coroutine
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.components import climate, sensor, uart, remote_transmitter
 | 
			
		||||
from esphome.components.remote_base import CONF_TRANSMITTER_ID
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_AUTOCONF,
 | 
			
		||||
    CONF_BEEPER,
 | 
			
		||||
    CONF_CUSTOM_FAN_MODES,
 | 
			
		||||
    CONF_CUSTOM_PRESETS,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_NUM_ATTEMPTS,
 | 
			
		||||
    CONF_PERIOD,
 | 
			
		||||
    CONF_SUPPORTED_MODES,
 | 
			
		||||
    CONF_SUPPORTED_PRESETS,
 | 
			
		||||
    CONF_SUPPORTED_SWING_MODES,
 | 
			
		||||
    CONF_TIMEOUT,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    ICON_POWER,
 | 
			
		||||
    ICON_THERMOMETER,
 | 
			
		||||
    ICON_WATER_PERCENT,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
)
 | 
			
		||||
from esphome.components.climate import (
 | 
			
		||||
    ClimateMode,
 | 
			
		||||
    ClimatePreset,
 | 
			
		||||
    ClimateSwingMode,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@dudanov"]
 | 
			
		||||
DEPENDENCIES = ["climate", "uart", "wifi"]
 | 
			
		||||
AUTO_LOAD = ["sensor"]
 | 
			
		||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
 | 
			
		||||
CONF_POWER_USAGE = "power_usage"
 | 
			
		||||
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
 | 
			
		||||
midea_ns = cg.esphome_ns.namespace("midea")
 | 
			
		||||
AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component)
 | 
			
		||||
Capabilities = midea_ns.namespace("Constants")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def templatize(value):
 | 
			
		||||
    if isinstance(value, cv.Schema):
 | 
			
		||||
        value = value.schema
 | 
			
		||||
    ret = {}
 | 
			
		||||
    for key, val in value.items():
 | 
			
		||||
        ret[key] = cv.templatable(val)
 | 
			
		||||
    return cv.Schema(ret)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_action(name, type_, schema):
 | 
			
		||||
    validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA)
 | 
			
		||||
    registerer = automation.register_action(f"midea_ac.{name}", type_, validator)
 | 
			
		||||
 | 
			
		||||
    def decorator(func):
 | 
			
		||||
        async def new_func(config, action_id, template_arg, args):
 | 
			
		||||
            ac_ = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
            var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
            cg.add(var.set_parent(ac_))
 | 
			
		||||
            await coroutine(func)(var, config, args)
 | 
			
		||||
            return var
 | 
			
		||||
 | 
			
		||||
        return registerer(new_func)
 | 
			
		||||
 | 
			
		||||
    return decorator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ALLOWED_CLIMATE_MODES = {
 | 
			
		||||
    "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL,
 | 
			
		||||
    "COOL": ClimateMode.CLIMATE_MODE_COOL,
 | 
			
		||||
    "HEAT": ClimateMode.CLIMATE_MODE_HEAT,
 | 
			
		||||
    "DRY": ClimateMode.CLIMATE_MODE_DRY,
 | 
			
		||||
    "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ALLOWED_CLIMATE_PRESETS = {
 | 
			
		||||
    "ECO": ClimatePreset.CLIMATE_PRESET_ECO,
 | 
			
		||||
    "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
 | 
			
		||||
    "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ALLOWED_CLIMATE_SWING_MODES = {
 | 
			
		||||
    "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
 | 
			
		||||
    "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
 | 
			
		||||
    "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CUSTOM_FAN_MODES = {
 | 
			
		||||
    "SILENT": Capabilities.SILENT,
 | 
			
		||||
    "TURBO": Capabilities.TURBO,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CUSTOM_PRESETS = {
 | 
			
		||||
    "FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
 | 
			
		||||
validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
 | 
			
		||||
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
 | 
			
		||||
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(
 | 
			
		||||
        {
 | 
			
		||||
            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),
 | 
			
		||||
            cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
 | 
			
		||||
                remote_transmitter.RemoteTransmitterComponent
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_BEEPER, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_AUTOCONF, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
 | 
			
		||||
            cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
 | 
			
		||||
                validate_swing_modes
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets),
 | 
			
		||||
            cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets),
 | 
			
		||||
            cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
 | 
			
		||||
                validate_custom_fan_modes
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                icon=ICON_THERMOMETER,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                icon=ICON_POWER,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                icon=ICON_WATER_PERCENT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# Actions
 | 
			
		||||
FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action)
 | 
			
		||||
DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action)
 | 
			
		||||
SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action)
 | 
			
		||||
BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action)
 | 
			
		||||
BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action)
 | 
			
		||||
PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action)
 | 
			
		||||
PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action)
 | 
			
		||||
 | 
			
		||||
MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_ID): cv.use_id(AirConditioner),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# FollowMe action
 | 
			
		||||
MIDEA_FOLLOW_ME_MIN = 0
 | 
			
		||||
MIDEA_FOLLOW_ME_MAX = 37
 | 
			
		||||
MIDEA_FOLLOW_ME_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature),
 | 
			
		||||
        cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA)
 | 
			
		||||
async def follow_me_to_code(var, config, args):
 | 
			
		||||
    template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_)
 | 
			
		||||
    cg.add(var.set_beeper(template_))
 | 
			
		||||
    template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_)
 | 
			
		||||
    cg.add(var.set_temperature(template_))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Toggle Display action
 | 
			
		||||
@register_action(
 | 
			
		||||
    "display_toggle",
 | 
			
		||||
    DisplayToggleAction,
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
)
 | 
			
		||||
async def display_toggle_to_code(var, config, args):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Swing Step action
 | 
			
		||||
@register_action(
 | 
			
		||||
    "swing_step",
 | 
			
		||||
    SwingStepAction,
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
)
 | 
			
		||||
async def swing_step_to_code(var, config, args):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Beeper On action
 | 
			
		||||
@register_action(
 | 
			
		||||
    "beeper_on",
 | 
			
		||||
    BeeperOnAction,
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
)
 | 
			
		||||
async def beeper_on_to_code(var, config, args):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Beeper Off action
 | 
			
		||||
@register_action(
 | 
			
		||||
    "beeper_off",
 | 
			
		||||
    BeeperOffAction,
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
)
 | 
			
		||||
async def beeper_off_to_code(var, config, args):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Power On action
 | 
			
		||||
@register_action(
 | 
			
		||||
    "power_on",
 | 
			
		||||
    PowerOnAction,
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
)
 | 
			
		||||
async def power_on_to_code(var, config, args):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Power Off action
 | 
			
		||||
@register_action(
 | 
			
		||||
    "power_off",
 | 
			
		||||
    PowerOffAction,
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
)
 | 
			
		||||
async def power_off_to_code(var, config, args):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await uart.register_uart_device(var, config)
 | 
			
		||||
    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]))
 | 
			
		||||
    if CONF_TRANSMITTER_ID in config:
 | 
			
		||||
        cg.add_define("USE_REMOTE_TRANSMITTER")
 | 
			
		||||
        transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
 | 
			
		||||
        cg.add(var.set_transmitter(transmitter_))
 | 
			
		||||
    cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
 | 
			
		||||
    cg.add(var.set_autoconf(config[CONF_AUTOCONF]))
 | 
			
		||||
    if CONF_SUPPORTED_MODES in config:
 | 
			
		||||
        cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
 | 
			
		||||
    if CONF_SUPPORTED_SWING_MODES in config:
 | 
			
		||||
        cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
 | 
			
		||||
    if CONF_SUPPORTED_PRESETS in config:
 | 
			
		||||
        cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
 | 
			
		||||
    if CONF_CUSTOM_PRESETS in config:
 | 
			
		||||
        cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
 | 
			
		||||
    if CONF_CUSTOM_FAN_MODES in config:
 | 
			
		||||
        cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
 | 
			
		||||
    if CONF_OUTDOOR_TEMPERATURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
 | 
			
		||||
        cg.add(var.set_outdoor_temperature_sensor(sens))
 | 
			
		||||
    if CONF_POWER_USAGE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
 | 
			
		||||
        cg.add(var.set_power_sensor(sens))
 | 
			
		||||
    if CONF_HUMIDITY_SETPOINT in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
 | 
			
		||||
        cg.add(var.set_humidity_setpoint_sensor(sens))
 | 
			
		||||
    cg.add_library("dudanov/MideaUART", "1.1.5")
 | 
			
		||||
							
								
								
									
										42
									
								
								esphome/components/midea/midea_ir.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/midea/midea_ir.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#ifdef USE_REMOTE_TRANSMITTER
 | 
			
		||||
#include "esphome/components/remote_base/midea_protocol.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea {
 | 
			
		||||
 | 
			
		||||
using IrData = remote_base::MideaData;
 | 
			
		||||
 | 
			
		||||
class IrFollowMeData : public IrData {
 | 
			
		||||
 public:
 | 
			
		||||
  // Default constructor (temp: 30C, beeper: off)
 | 
			
		||||
  IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {}
 | 
			
		||||
  // Copy from Base
 | 
			
		||||
  IrFollowMeData(const IrData &data) : IrData(data) {}
 | 
			
		||||
  // Direct from temperature and beeper values
 | 
			
		||||
  IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() {
 | 
			
		||||
    this->set_temp(temp);
 | 
			
		||||
    this->set_beeper(beeper);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* TEMPERATURE */
 | 
			
		||||
  uint8_t temp() const { return this->data_[4] - 1; }
 | 
			
		||||
  void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; }
 | 
			
		||||
 | 
			
		||||
  /* BEEPER */
 | 
			
		||||
  bool beeper() const { return this->data_[3] & 128; }
 | 
			
		||||
  void set_beeper(bool val) { this->set_value_(3, 1, 7, val); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  static const uint8_t MAX_TEMP = 37;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class IrSpecialData : public IrData {
 | 
			
		||||
 public:
 | 
			
		||||
  IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace midea
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,115 +1,3 @@
 | 
			
		||||
from esphome.components import climate, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CUSTOM_FAN_MODES,
 | 
			
		||||
    CONF_CUSTOM_PRESETS,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_PRESET_BOOST,
 | 
			
		||||
    CONF_PRESET_ECO,
 | 
			
		||||
    CONF_PRESET_SLEEP,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    ICON_THERMOMETER,
 | 
			
		||||
    ICON_POWER,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    ICON_WATER_PERCENT,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
)
 | 
			
		||||
from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["climate", "sensor", "midea_dongle"]
 | 
			
		||||
CODEOWNERS = ["@dudanov"]
 | 
			
		||||
CONF_BEEPER = "beeper"
 | 
			
		||||
CONF_SWING_HORIZONTAL = "swing_horizontal"
 | 
			
		||||
CONF_SWING_BOTH = "swing_both"
 | 
			
		||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
 | 
			
		||||
CONF_POWER_USAGE = "power_usage"
 | 
			
		||||
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
 | 
			
		||||
midea_ac_ns = cg.esphome_ns.namespace("midea_ac")
 | 
			
		||||
MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component)
 | 
			
		||||
 | 
			
		||||
CLIMATE_CUSTOM_FAN_MODES = {
 | 
			
		||||
    "SILENT": "silent",
 | 
			
		||||
    "TURBO": "turbo",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True)
 | 
			
		||||
 | 
			
		||||
CLIMATE_CUSTOM_PRESETS = {
 | 
			
		||||
    "FREEZE_PROTECTION": "freeze protection",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(MideaAC),
 | 
			
		||||
            cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle),
 | 
			
		||||
            cv.Optional(CONF_BEEPER, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
 | 
			
		||||
                validate_climate_custom_fan_mode
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(
 | 
			
		||||
                validate_climate_custom_preset
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                icon=ICON_THERMOMETER,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                icon=ICON_POWER,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                icon=ICON_WATER_PERCENT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID])
 | 
			
		||||
    cg.add(var.set_midea_dongle_parent(paren))
 | 
			
		||||
    cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
 | 
			
		||||
    if CONF_CUSTOM_FAN_MODES in config:
 | 
			
		||||
        cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
 | 
			
		||||
    if CONF_CUSTOM_PRESETS in config:
 | 
			
		||||
        cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
 | 
			
		||||
    cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL]))
 | 
			
		||||
    cg.add(var.set_swing_both(config[CONF_SWING_BOTH]))
 | 
			
		||||
    cg.add(var.set_preset_eco(config[CONF_PRESET_ECO]))
 | 
			
		||||
    cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP]))
 | 
			
		||||
    cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST]))
 | 
			
		||||
    if CONF_OUTDOOR_TEMPERATURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
 | 
			
		||||
        cg.add(var.set_outdoor_temperature_sensor(sens))
 | 
			
		||||
    if CONF_POWER_USAGE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
 | 
			
		||||
        cg.add(var.set_power_sensor(sens))
 | 
			
		||||
    if CONF_HUMIDITY_SETPOINT in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
 | 
			
		||||
        cg.add(var.set_humidity_setpoint_sensor(sens))
 | 
			
		||||
CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,208 +0,0 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "midea_climate.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea_ac {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "midea_ac";
 | 
			
		||||
 | 
			
		||||
static void set_sensor(sensor::Sensor *sensor, float value) {
 | 
			
		||||
  if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
 | 
			
		||||
    sensor->publish_state(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T> void set_property(T &property, T value, bool &flag) {
 | 
			
		||||
  if (property != value) {
 | 
			
		||||
    property = value;
 | 
			
		||||
    flag = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MideaAC::on_frame(const midea_dongle::Frame &frame) {
 | 
			
		||||
  const auto p = frame.as<PropertiesFrame>();
 | 
			
		||||
  if (p.has_power_info()) {
 | 
			
		||||
    set_sensor(this->power_sensor_, p.get_power_usage());
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (!p.has_properties()) {
 | 
			
		||||
    ESP_LOGW(TAG, "RX: frame has unknown type");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) {
 | 
			
		||||
    ESP_LOGD(TAG, "RX: control frame");
 | 
			
		||||
    this->ctrl_request_ = false;
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGD(TAG, "RX: query frame");
 | 
			
		||||
  }
 | 
			
		||||
  if (this->ctrl_request_)
 | 
			
		||||
    return;
 | 
			
		||||
  this->cmd_frame_.set_properties(p);  // copy properties from response
 | 
			
		||||
  bool need_publish = false;
 | 
			
		||||
  set_property(this->mode, p.get_mode(), need_publish);
 | 
			
		||||
  set_property(this->target_temperature, p.get_target_temp(), need_publish);
 | 
			
		||||
  set_property(this->current_temperature, p.get_indoor_temp(), need_publish);
 | 
			
		||||
  if (p.is_custom_fan_mode()) {
 | 
			
		||||
    this->fan_mode.reset();
 | 
			
		||||
    optional<std::string> mode = p.get_custom_fan_mode();
 | 
			
		||||
    set_property(this->custom_fan_mode, mode, need_publish);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->custom_fan_mode.reset();
 | 
			
		||||
    optional<climate::ClimateFanMode> mode = p.get_fan_mode();
 | 
			
		||||
    set_property(this->fan_mode, mode, need_publish);
 | 
			
		||||
  }
 | 
			
		||||
  set_property(this->swing_mode, p.get_swing_mode(), need_publish);
 | 
			
		||||
  if (p.is_custom_preset()) {
 | 
			
		||||
    this->preset.reset();
 | 
			
		||||
    optional<std::string> preset = p.get_custom_preset();
 | 
			
		||||
    set_property(this->custom_preset, preset, need_publish);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->custom_preset.reset();
 | 
			
		||||
    set_property(this->preset, p.get_preset(), need_publish);
 | 
			
		||||
  }
 | 
			
		||||
  if (need_publish)
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
  set_sensor(this->outdoor_sensor_, p.get_outdoor_temp());
 | 
			
		||||
  set_sensor(this->humidity_sensor_, p.get_humidity_setpoint());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MideaAC::on_update() {
 | 
			
		||||
  if (this->ctrl_request_) {
 | 
			
		||||
    ESP_LOGD(TAG, "TX: control");
 | 
			
		||||
    this->parent_->write_frame(this->cmd_frame_);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGD(TAG, "TX: query");
 | 
			
		||||
    if (this->power_sensor_ == nullptr || this->request_num_++ % 32)
 | 
			
		||||
      this->parent_->write_frame(this->query_frame_);
 | 
			
		||||
    else
 | 
			
		||||
      this->parent_->write_frame(this->power_frame_);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool MideaAC::allow_preset(climate::ClimatePreset preset) const {
 | 
			
		||||
  switch (preset) {
 | 
			
		||||
    case climate::CLIMATE_PRESET_ECO:
 | 
			
		||||
      if (this->mode == climate::CLIMATE_MODE_COOL) {
 | 
			
		||||
        return true;
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "ECO preset is only available in COOL mode");
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_PRESET_SLEEP:
 | 
			
		||||
      if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) {
 | 
			
		||||
        ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode");
 | 
			
		||||
      } else {
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_PRESET_BOOST:
 | 
			
		||||
      if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) {
 | 
			
		||||
        return true;
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode");
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_PRESET_NONE:
 | 
			
		||||
      return true;
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool MideaAC::allow_custom_preset(const std::string &custom_preset) const {
 | 
			
		||||
  if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) {
 | 
			
		||||
    if (this->mode == climate::CLIMATE_MODE_HEAT) {
 | 
			
		||||
      return true;
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MideaAC::control(const climate::ClimateCall &call) {
 | 
			
		||||
  if (call.get_mode().has_value() && call.get_mode().value() != this->mode) {
 | 
			
		||||
    this->cmd_frame_.set_mode(call.get_mode().value());
 | 
			
		||||
    this->ctrl_request_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) {
 | 
			
		||||
    this->cmd_frame_.set_target_temp(call.get_target_temperature().value());
 | 
			
		||||
    this->ctrl_request_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_fan_mode().has_value() &&
 | 
			
		||||
      (!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) {
 | 
			
		||||
    this->custom_fan_mode.reset();
 | 
			
		||||
    this->cmd_frame_.set_fan_mode(call.get_fan_mode().value());
 | 
			
		||||
    this->ctrl_request_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_custom_fan_mode().has_value() &&
 | 
			
		||||
      (!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) {
 | 
			
		||||
    this->fan_mode.reset();
 | 
			
		||||
    this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value());
 | 
			
		||||
    this->ctrl_request_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) {
 | 
			
		||||
    this->cmd_frame_.set_swing_mode(call.get_swing_mode().value());
 | 
			
		||||
    this->ctrl_request_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) &&
 | 
			
		||||
      (!this->preset.has_value() || this->preset.value() != call.get_preset().value())) {
 | 
			
		||||
    this->custom_preset.reset();
 | 
			
		||||
    this->cmd_frame_.set_preset(call.get_preset().value());
 | 
			
		||||
    this->ctrl_request_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) &&
 | 
			
		||||
      (!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) {
 | 
			
		||||
    this->preset.reset();
 | 
			
		||||
    this->cmd_frame_.set_custom_preset(call.get_custom_preset().value());
 | 
			
		||||
    this->ctrl_request_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->ctrl_request_) {
 | 
			
		||||
    this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_);
 | 
			
		||||
    this->cmd_frame_.finalize();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits MideaAC::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.set_visual_min_temperature(17);
 | 
			
		||||
  traits.set_visual_max_temperature(30);
 | 
			
		||||
  traits.set_visual_temperature_step(0.5);
 | 
			
		||||
  traits.set_supported_modes({
 | 
			
		||||
      climate::CLIMATE_MODE_OFF,
 | 
			
		||||
      climate::CLIMATE_MODE_HEAT_COOL,
 | 
			
		||||
      climate::CLIMATE_MODE_COOL,
 | 
			
		||||
      climate::CLIMATE_MODE_DRY,
 | 
			
		||||
      climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
      climate::CLIMATE_MODE_FAN_ONLY,
 | 
			
		||||
  });
 | 
			
		||||
  traits.set_supported_fan_modes({
 | 
			
		||||
      climate::CLIMATE_FAN_AUTO,
 | 
			
		||||
      climate::CLIMATE_FAN_LOW,
 | 
			
		||||
      climate::CLIMATE_FAN_MEDIUM,
 | 
			
		||||
      climate::CLIMATE_FAN_HIGH,
 | 
			
		||||
  });
 | 
			
		||||
  traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_);
 | 
			
		||||
  traits.set_supported_swing_modes({
 | 
			
		||||
      climate::CLIMATE_SWING_OFF,
 | 
			
		||||
      climate::CLIMATE_SWING_VERTICAL,
 | 
			
		||||
  });
 | 
			
		||||
  if (traits_swing_horizontal_)
 | 
			
		||||
    traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL);
 | 
			
		||||
  if (traits_swing_both_)
 | 
			
		||||
    traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH);
 | 
			
		||||
  traits.set_supported_presets({
 | 
			
		||||
      climate::CLIMATE_PRESET_NONE,
 | 
			
		||||
  });
 | 
			
		||||
  if (traits_preset_eco_)
 | 
			
		||||
    traits.add_supported_preset(climate::CLIMATE_PRESET_ECO);
 | 
			
		||||
  if (traits_preset_sleep_)
 | 
			
		||||
    traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP);
 | 
			
		||||
  if (traits_preset_boost_)
 | 
			
		||||
    traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST);
 | 
			
		||||
  traits.set_supported_custom_presets(this->traits_custom_presets_);
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace midea_ac
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,68 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/midea_dongle/midea_dongle.h"
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/components/midea_dongle/midea_dongle.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "midea_frame.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea_ac {
 | 
			
		||||
 | 
			
		||||
class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::LATE; }
 | 
			
		||||
  void on_frame(const midea_dongle::Frame &frame) override;
 | 
			
		||||
  void on_update() override;
 | 
			
		||||
  void setup() override { this->parent_->set_appliance(this); }
 | 
			
		||||
  void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; }
 | 
			
		||||
  void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
 | 
			
		||||
  void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
 | 
			
		||||
  void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
 | 
			
		||||
  void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; }
 | 
			
		||||
  void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; }
 | 
			
		||||
  void set_swing_both(bool state) { this->traits_swing_both_ = state; }
 | 
			
		||||
  void set_preset_eco(bool state) { this->traits_preset_eco_ = state; }
 | 
			
		||||
  void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; }
 | 
			
		||||
  void set_preset_boost(bool state) { this->traits_preset_boost_ = state; }
 | 
			
		||||
  bool allow_preset(climate::ClimatePreset preset) const;
 | 
			
		||||
  void set_custom_fan_modes(std::set<std::string> custom_fan_modes) {
 | 
			
		||||
    this->traits_custom_fan_modes_ = std::move(custom_fan_modes);
 | 
			
		||||
  }
 | 
			
		||||
  void set_custom_presets(std::set<std::string> custom_presets) {
 | 
			
		||||
    this->traits_custom_presets_ = std::move(custom_presets);
 | 
			
		||||
  }
 | 
			
		||||
  bool allow_custom_preset(const std::string &custom_preset) const;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Override control to change settings of the climate device.
 | 
			
		||||
  void control(const climate::ClimateCall &call) override;
 | 
			
		||||
  /// Return the traits of this controller.
 | 
			
		||||
  climate::ClimateTraits traits() override;
 | 
			
		||||
 | 
			
		||||
  const QueryFrame query_frame_;
 | 
			
		||||
  const PowerQueryFrame power_frame_;
 | 
			
		||||
  CommandFrame cmd_frame_;
 | 
			
		||||
  midea_dongle::MideaDongle *parent_{nullptr};
 | 
			
		||||
  sensor::Sensor *outdoor_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *power_sensor_{nullptr};
 | 
			
		||||
  uint8_t request_num_{0};
 | 
			
		||||
  bool ctrl_request_{false};
 | 
			
		||||
  bool beeper_feedback_{false};
 | 
			
		||||
  bool traits_swing_horizontal_{false};
 | 
			
		||||
  bool traits_swing_both_{false};
 | 
			
		||||
  bool traits_preset_eco_{false};
 | 
			
		||||
  bool traits_preset_sleep_{false};
 | 
			
		||||
  bool traits_preset_boost_{false};
 | 
			
		||||
  std::set<std::string> traits_custom_fan_modes_{{}};
 | 
			
		||||
  std::set<std::string> traits_custom_presets_{{}};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace midea_ac
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,238 +0,0 @@
 | 
			
		||||
#include "midea_frame.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea_ac {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "midea_ac";
 | 
			
		||||
const std::string MIDEA_SILENT_FAN_MODE = "silent";
 | 
			
		||||
const std::string MIDEA_TURBO_FAN_MODE = "turbo";
 | 
			
		||||
const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection";
 | 
			
		||||
 | 
			
		||||
const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81,
 | 
			
		||||
                                    0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
                                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31};
 | 
			
		||||
 | 
			
		||||
const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21,
 | 
			
		||||
                                         0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
                                         0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A};
 | 
			
		||||
 | 
			
		||||
const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00,
 | 
			
		||||
                                      0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
                                      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
 | 
			
		||||
 | 
			
		||||
float PropertiesFrame::get_target_temp() const {
 | 
			
		||||
  float temp = static_cast<float>((this->pbuf_[12] & 0x0F) + 16);
 | 
			
		||||
  if (this->pbuf_[12] & 0x10)
 | 
			
		||||
    temp += 0.5;
 | 
			
		||||
  return temp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PropertiesFrame::set_target_temp(float temp) {
 | 
			
		||||
  uint8_t tmp = static_cast<uint8_t>(temp * 16.0) + 4;
 | 
			
		||||
  tmp = ((tmp & 8) << 1) | (tmp >> 4);
 | 
			
		||||
  this->pbuf_[12] &= ~0x1F;
 | 
			
		||||
  this->pbuf_[12] |= tmp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static float i16tof(int16_t in) { return static_cast<float>(in - 50) / 2.0; }
 | 
			
		||||
float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); }
 | 
			
		||||
float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); }
 | 
			
		||||
float PropertiesFrame::get_humidity_setpoint() const { return static_cast<float>(this->pbuf_[29] & 0x7F); }
 | 
			
		||||
 | 
			
		||||
climate::ClimateMode PropertiesFrame::get_mode() const {
 | 
			
		||||
  if (!this->get_power_())
 | 
			
		||||
    return climate::CLIMATE_MODE_OFF;
 | 
			
		||||
  switch (this->pbuf_[12] >> 5) {
 | 
			
		||||
    case MIDEA_MODE_AUTO:
 | 
			
		||||
      return climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    case MIDEA_MODE_COOL:
 | 
			
		||||
      return climate::CLIMATE_MODE_COOL;
 | 
			
		||||
    case MIDEA_MODE_DRY:
 | 
			
		||||
      return climate::CLIMATE_MODE_DRY;
 | 
			
		||||
    case MIDEA_MODE_HEAT:
 | 
			
		||||
      return climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
    case MIDEA_MODE_FAN_ONLY:
 | 
			
		||||
      return climate::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
    default:
 | 
			
		||||
      return climate::CLIMATE_MODE_OFF;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PropertiesFrame::set_mode(climate::ClimateMode mode) {
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      m = MIDEA_MODE_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_COOL:
 | 
			
		||||
      m = MIDEA_MODE_COOL;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_DRY:
 | 
			
		||||
      m = MIDEA_MODE_DRY;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
      m = MIDEA_MODE_HEAT;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
      m = MIDEA_MODE_FAN_ONLY;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      this->set_power_(false);
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
  this->set_power_(true);
 | 
			
		||||
  this->pbuf_[12] &= ~0xE0;
 | 
			
		||||
  this->pbuf_[12] |= m << 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<climate::ClimatePreset> PropertiesFrame::get_preset() const {
 | 
			
		||||
  if (this->get_eco_mode())
 | 
			
		||||
    return climate::CLIMATE_PRESET_ECO;
 | 
			
		||||
  if (this->get_sleep_mode())
 | 
			
		||||
    return climate::CLIMATE_PRESET_SLEEP;
 | 
			
		||||
  if (this->get_turbo_mode())
 | 
			
		||||
    return climate::CLIMATE_PRESET_BOOST;
 | 
			
		||||
  return climate::CLIMATE_PRESET_NONE;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PropertiesFrame::set_preset(climate::ClimatePreset preset) {
 | 
			
		||||
  this->clear_presets();
 | 
			
		||||
  switch (preset) {
 | 
			
		||||
    case climate::CLIMATE_PRESET_ECO:
 | 
			
		||||
      this->set_eco_mode(true);
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_PRESET_SLEEP:
 | 
			
		||||
      this->set_sleep_mode(true);
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_PRESET_BOOST:
 | 
			
		||||
      this->set_turbo_mode(true);
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PropertiesFrame::clear_presets() {
 | 
			
		||||
  this->set_eco_mode(false);
 | 
			
		||||
  this->set_sleep_mode(false);
 | 
			
		||||
  this->set_turbo_mode(false);
 | 
			
		||||
  this->set_freeze_protection_mode(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); }
 | 
			
		||||
 | 
			
		||||
const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; };
 | 
			
		||||
 | 
			
		||||
void PropertiesFrame::set_custom_preset(const std::string &preset) {
 | 
			
		||||
  this->clear_presets();
 | 
			
		||||
  if (preset == MIDEA_FREEZE_PROTECTION_PRESET)
 | 
			
		||||
    this->set_freeze_protection_mode(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool PropertiesFrame::is_custom_fan_mode() const {
 | 
			
		||||
  switch (this->pbuf_[13]) {
 | 
			
		||||
    case MIDEA_FAN_SILENT:
 | 
			
		||||
    case MIDEA_FAN_TURBO:
 | 
			
		||||
      return true;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
climate::ClimateFanMode PropertiesFrame::get_fan_mode() const {
 | 
			
		||||
  switch (this->pbuf_[13]) {
 | 
			
		||||
    case MIDEA_FAN_LOW:
 | 
			
		||||
      return climate::CLIMATE_FAN_LOW;
 | 
			
		||||
    case MIDEA_FAN_MEDIUM:
 | 
			
		||||
      return climate::CLIMATE_FAN_MEDIUM;
 | 
			
		||||
    case MIDEA_FAN_HIGH:
 | 
			
		||||
      return climate::CLIMATE_FAN_HIGH;
 | 
			
		||||
    default:
 | 
			
		||||
      return climate::CLIMATE_FAN_AUTO;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) {
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case climate::CLIMATE_FAN_LOW:
 | 
			
		||||
      m = MIDEA_FAN_LOW;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_FAN_MEDIUM:
 | 
			
		||||
      m = MIDEA_FAN_MEDIUM;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_FAN_HIGH:
 | 
			
		||||
      m = MIDEA_FAN_HIGH;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      m = MIDEA_FAN_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  this->pbuf_[13] = m;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const std::string &PropertiesFrame::get_custom_fan_mode() const {
 | 
			
		||||
  switch (this->pbuf_[13]) {
 | 
			
		||||
    case MIDEA_FAN_SILENT:
 | 
			
		||||
      return MIDEA_SILENT_FAN_MODE;
 | 
			
		||||
    default:
 | 
			
		||||
      return MIDEA_TURBO_FAN_MODE;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PropertiesFrame::set_custom_fan_mode(const std::string &mode) {
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  if (mode == MIDEA_SILENT_FAN_MODE) {
 | 
			
		||||
    m = MIDEA_FAN_SILENT;
 | 
			
		||||
  } else {
 | 
			
		||||
    m = MIDEA_FAN_TURBO;
 | 
			
		||||
  }
 | 
			
		||||
  this->pbuf_[13] = m;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const {
 | 
			
		||||
  switch (this->pbuf_[17] & 0x0F) {
 | 
			
		||||
    case MIDEA_SWING_VERTICAL:
 | 
			
		||||
      return climate::CLIMATE_SWING_VERTICAL;
 | 
			
		||||
    case MIDEA_SWING_HORIZONTAL:
 | 
			
		||||
      return climate::CLIMATE_SWING_HORIZONTAL;
 | 
			
		||||
    case MIDEA_SWING_BOTH:
 | 
			
		||||
      return climate::CLIMATE_SWING_BOTH;
 | 
			
		||||
    default:
 | 
			
		||||
      return climate::CLIMATE_SWING_OFF;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) {
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case climate::CLIMATE_SWING_VERTICAL:
 | 
			
		||||
      m = MIDEA_SWING_VERTICAL;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_SWING_HORIZONTAL:
 | 
			
		||||
      m = MIDEA_SWING_HORIZONTAL;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_SWING_BOTH:
 | 
			
		||||
      m = MIDEA_SWING_BOTH;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      m = MIDEA_SWING_OFF;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  this->pbuf_[17] = 0x30 | m;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float PropertiesFrame::get_power_usage() const {
 | 
			
		||||
  uint32_t power = 0;
 | 
			
		||||
  const uint8_t *ptr = this->pbuf_ + 28;
 | 
			
		||||
  for (uint32_t weight = 1;; weight *= 10, ptr--) {
 | 
			
		||||
    power += (*ptr % 16) * weight;
 | 
			
		||||
    weight *= 10;
 | 
			
		||||
    power += (*ptr / 16) * weight;
 | 
			
		||||
    if (weight == 100000)
 | 
			
		||||
      return static_cast<float>(power) * 0.1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace midea_ac
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,165 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/components/midea_dongle/midea_frame.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace midea_ac {
 | 
			
		||||
 | 
			
		||||
extern const std::string MIDEA_SILENT_FAN_MODE;
 | 
			
		||||
extern const std::string MIDEA_TURBO_FAN_MODE;
 | 
			
		||||
extern const std::string MIDEA_FREEZE_PROTECTION_PRESET;
 | 
			
		||||
 | 
			
		||||
/// Enum for all modes a Midea device can be in.
 | 
			
		||||
enum MideaMode : uint8_t {
 | 
			
		||||
  /// The Midea device is set to automatically change the heating/cooling cycle
 | 
			
		||||
  MIDEA_MODE_AUTO = 1,
 | 
			
		||||
  /// The Midea device is manually set to cool mode (not in auto mode!)
 | 
			
		||||
  MIDEA_MODE_COOL = 2,
 | 
			
		||||
  /// The Midea device is manually set to dry mode
 | 
			
		||||
  MIDEA_MODE_DRY = 3,
 | 
			
		||||
  /// The Midea device is manually set to heat mode (not in auto mode!)
 | 
			
		||||
  MIDEA_MODE_HEAT = 4,
 | 
			
		||||
  /// The Midea device is manually set to fan only mode
 | 
			
		||||
  MIDEA_MODE_FAN_ONLY = 5,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Enum for all modes a Midea fan can be in
 | 
			
		||||
enum MideaFanMode : uint8_t {
 | 
			
		||||
  /// The fan mode is set to Auto
 | 
			
		||||
  MIDEA_FAN_AUTO = 102,
 | 
			
		||||
  /// The fan mode is set to Silent
 | 
			
		||||
  MIDEA_FAN_SILENT = 20,
 | 
			
		||||
  /// The fan mode is set to Low
 | 
			
		||||
  MIDEA_FAN_LOW = 40,
 | 
			
		||||
  /// The fan mode is set to Medium
 | 
			
		||||
  MIDEA_FAN_MEDIUM = 60,
 | 
			
		||||
  /// The fan mode is set to High
 | 
			
		||||
  MIDEA_FAN_HIGH = 80,
 | 
			
		||||
  /// The fan mode is set to Turbo
 | 
			
		||||
  MIDEA_FAN_TURBO = 100,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Enum for all modes a Midea swing can be in
 | 
			
		||||
enum MideaSwingMode : uint8_t {
 | 
			
		||||
  /// The sing mode is set to Off
 | 
			
		||||
  MIDEA_SWING_OFF = 0b0000,
 | 
			
		||||
  /// The fan mode is set to Both
 | 
			
		||||
  MIDEA_SWING_BOTH = 0b1111,
 | 
			
		||||
  /// The fan mode is set to Vertical
 | 
			
		||||
  MIDEA_SWING_VERTICAL = 0b1100,
 | 
			
		||||
  /// The fan mode is set to Horizontal
 | 
			
		||||
  MIDEA_SWING_HORIZONTAL = 0b0011,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class PropertiesFrame : public midea_dongle::BaseFrame {
 | 
			
		||||
 public:
 | 
			
		||||
  PropertiesFrame() = delete;
 | 
			
		||||
  PropertiesFrame(uint8_t *data) : BaseFrame(data) {}
 | 
			
		||||
  PropertiesFrame(const Frame &frame) : BaseFrame(frame) {}
 | 
			
		||||
 | 
			
		||||
  bool has_properties() const {
 | 
			
		||||
    return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool has_power_info() const { return this->has_response_type(0xC1); }
 | 
			
		||||
 | 
			
		||||
  /* TARGET TEMPERATURE */
 | 
			
		||||
 | 
			
		||||
  float get_target_temp() const;
 | 
			
		||||
  void set_target_temp(float temp);
 | 
			
		||||
 | 
			
		||||
  /* MODE */
 | 
			
		||||
  climate::ClimateMode get_mode() const;
 | 
			
		||||
  void set_mode(climate::ClimateMode mode);
 | 
			
		||||
 | 
			
		||||
  /* FAN SPEED */
 | 
			
		||||
  bool is_custom_fan_mode() const;
 | 
			
		||||
  climate::ClimateFanMode get_fan_mode() const;
 | 
			
		||||
  void set_fan_mode(climate::ClimateFanMode mode);
 | 
			
		||||
 | 
			
		||||
  const std::string &get_custom_fan_mode() const;
 | 
			
		||||
  void set_custom_fan_mode(const std::string &mode);
 | 
			
		||||
 | 
			
		||||
  /* SWING MODE */
 | 
			
		||||
  climate::ClimateSwingMode get_swing_mode() const;
 | 
			
		||||
  void set_swing_mode(climate::ClimateSwingMode mode);
 | 
			
		||||
 | 
			
		||||
  /* INDOOR TEMPERATURE */
 | 
			
		||||
  float get_indoor_temp() const;
 | 
			
		||||
 | 
			
		||||
  /* OUTDOOR TEMPERATURE */
 | 
			
		||||
  float get_outdoor_temp() const;
 | 
			
		||||
 | 
			
		||||
  /* HUMIDITY SETPOINT */
 | 
			
		||||
  float get_humidity_setpoint() const;
 | 
			
		||||
 | 
			
		||||
  /* ECO MODE */
 | 
			
		||||
  bool get_eco_mode() const { return this->pbuf_[19] & 0x10; }
 | 
			
		||||
  void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); }
 | 
			
		||||
 | 
			
		||||
  /* SLEEP MODE */
 | 
			
		||||
  bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; }
 | 
			
		||||
  void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); }
 | 
			
		||||
 | 
			
		||||
  /* TURBO MODE */
 | 
			
		||||
  bool get_turbo_mode() const { return this->pbuf_[18] & 0x20 || this->pbuf_[20] & 0x02; }
 | 
			
		||||
  void set_turbo_mode(bool state) {
 | 
			
		||||
    this->set_bytemask_(18, 0x20, state);
 | 
			
		||||
    this->set_bytemask_(20, 0x02, state);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* FREEZE PROTECTION */
 | 
			
		||||
  bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; }
 | 
			
		||||
  void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); }
 | 
			
		||||
 | 
			
		||||
  /* PRESET */
 | 
			
		||||
  optional<climate::ClimatePreset> get_preset() const;
 | 
			
		||||
  void set_preset(climate::ClimatePreset preset);
 | 
			
		||||
  void clear_presets();
 | 
			
		||||
 | 
			
		||||
  bool is_custom_preset() const;
 | 
			
		||||
  const std::string &get_custom_preset() const;
 | 
			
		||||
  void set_custom_preset(const std::string &preset);
 | 
			
		||||
 | 
			
		||||
  /* POWER USAGE */
 | 
			
		||||
  float get_power_usage() const;
 | 
			
		||||
 | 
			
		||||
  /// Set properties from another frame
 | 
			
		||||
  void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /* POWER */
 | 
			
		||||
  bool get_power_() const { return this->pbuf_[11] & 0x01; }
 | 
			
		||||
  void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Query state frame (read-only)
 | 
			
		||||
class QueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
 | 
			
		||||
 public:
 | 
			
		||||
  QueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  static const uint8_t PROGMEM INIT[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Power query state frame (read-only)
 | 
			
		||||
class PowerQueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
 | 
			
		||||
 public:
 | 
			
		||||
  PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  static const uint8_t PROGMEM INIT[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Command frame
 | 
			
		||||
class CommandFrame : public midea_dongle::StaticFrame<PropertiesFrame> {
 | 
			
		||||
 public:
 | 
			
		||||
  CommandFrame() : StaticFrame(FPSTR(this->INIT)) {}
 | 
			
		||||
  void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  static const uint8_t PROGMEM INIT[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace midea_ac
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import uart
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["wifi", "uart"]
 | 
			
		||||
CODEOWNERS = ["@dudanov"]
 | 
			
		||||
 | 
			
		||||
midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle")
 | 
			
		||||
MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice)
 | 
			
		||||
 | 
			
		||||
CONF_MIDEA_DONGLE_ID = "midea_dongle_id"
 | 
			
		||||
CONF_STRENGTH_ICON = "strength_icon"
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(MideaDongle),
 | 
			
		||||
            cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await uart.register_uart_device(var, config)
 | 
			
		||||
    cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON]))
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user