mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			155 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f329c74a15 | ||
| 
						 | 
					7c86f3fa9e | ||
| 
						 | 
					203b8b01bf | ||
| 
						 | 
					8a1034a92f | ||
| 
						 | 
					aa0c2dedd9 | ||
| 
						 | 
					8c9948bb56 | ||
| 
						 | 
					2d1abaa68e | ||
| 
						 | 
					664a3df0b4 | ||
| 
						 | 
					9ff893881c | ||
| 
						 | 
					94f6c6861a | ||
| 
						 | 
					b1d614e6c4 | ||
| 
						 | 
					7fceb070e5 | ||
| 
						 | 
					5c7c0834c0 | ||
| 
						 | 
					f3a25de11d | ||
| 
						 | 
					041bef8bcd | ||
| 
						 | 
					6e83790308 | ||
| 
						 | 
					d2d4eb4eae | ||
| 
						 | 
					5942a3898c | ||
| 
						 | 
					93421f0fa7 | ||
| 
						 | 
					6cb5cd48c2 | ||
| 
						 | 
					746fd1122f | ||
| 
						 | 
					9663760ec5 | ||
| 
						 | 
					a3d73d1e23 | ||
| 
						 | 
					d63e14a4b6 | ||
| 
						 | 
					03944e6cd8 | ||
| 
						 | 
					0d1028be2e | ||
| 
						 | 
					6a85259e4d | ||
| 
						 | 
					ebca936b7e | ||
| 
						 | 
					31c4551890 | ||
| 
						 | 
					dd470d4197 | ||
| 
						 | 
					612822490b | ||
| 
						 | 
					f8969605e8 | ||
| 
						 | 
					dd24ffa24e | ||
| 
						 | 
					d0dda48932 | ||
| 
						 | 
					6349b5f654 | ||
| 
						 | 
					a6ff02a3cf | ||
| 
						 | 
					4f57bf786b | ||
| 
						 | 
					6221f6d47d | ||
| 
						 | 
					a922efeafa | ||
| 
						 | 
					5aa42e5e66 | ||
| 
						 | 
					708672ec7e | ||
| 
						 | 
					d2cefbf224 | ||
| 
						 | 
					adb7aa6950 | ||
| 
						 | 
					77f322166e | ||
| 
						 | 
					f3f6e54818 | ||
| 
						 | 
					fb0fec1f25 | ||
| 
						 | 
					b66af9fb4d | ||
| 
						 | 
					6617d576a7 | ||
| 
						 | 
					cd35ead890 | ||
| 
						 | 
					9dc804ee27 | ||
| 
						 | 
					a8ceeaa7b0 | ||
| 
						 | 
					7092f7663e | ||
| 
						 | 
					d9d2edeb08 | ||
| 
						 | 
					dda1ddcb26 | ||
| 
						 | 
					f0c890f160 | ||
| 
						 | 
					4f52d43347 | ||
| 
						 | 
					0ed7db979b | ||
| 
						 | 
					9c78049359 | ||
| 
						 | 
					7882105661 | ||
| 
						 | 
					c000e1d6dd | ||
| 
						 | 
					420dacb22d | ||
| 
						 | 
					ae2f6ad4d1 | ||
| 
						 | 
					2c28d79bf8 | ||
| 
						 | 
					c5069edc78 | ||
| 
						 | 
					282d9e138c | ||
| 
						 | 
					72fcf2cbe1 | ||
| 
						 | 
					6f49f5465b | ||
| 
						 | 
					17b8bd8316 | ||
| 
						 | 
					9b6b9c1fa2 | ||
| 
						 | 
					609a2ca592 | ||
| 
						 | 
					6dabf24bf3 | ||
| 
						 | 
					7e88938932 | ||
| 
						 | 
					c707e64685 | ||
| 
						 | 
					a639690716 | ||
| 
						 | 
					01222dbab7 | ||
| 
						 | 
					93e2506279 | ||
| 
						 | 
					f62d5d3b9d | ||
| 
						 | 
					0665acd190 | ||
| 
						 | 
					fea05e9d33 | ||
| 
						 | 
					7a03c7d56f | ||
| 
						 | 
					2dc2aec954 | ||
| 
						 | 
					39c6c2417a | ||
| 
						 | 
					ff72d6a146 | ||
| 
						 | 
					603d0d0c7c | ||
| 
						 | 
					28883f711b | ||
| 
						 | 
					e914828add | ||
| 
						 | 
					c1480029fb | ||
| 
						 | 
					40f622949e | ||
| 
						 | 
					63096ac2bc | ||
| 
						 | 
					03d5a0ec1d | ||
| 
						 | 
					1c873e0034 | ||
| 
						 | 
					bcb47c306c | ||
| 
						 | 
					01c4d3c225 | ||
| 
						 | 
					c2aaae4818 | ||
| 
						 | 
					3f678e218d | ||
| 
						 | 
					c2a59cb476 | ||
| 
						 | 
					f8a1bd4e79 | ||
| 
						 | 
					d6e039a1d1 | ||
| 
						 | 
					0f1a7c2b69 | ||
| 
						 | 
					40ad9f4911 | ||
| 
						 | 
					4116caff6a | ||
| 
						 | 
					0b69f72315 | ||
| 
						 | 
					c569f5ddcf | ||
| 
						 | 
					62f9e181e0 | ||
| 
						 | 
					235a97ea10 | ||
| 
						 | 
					e541ae400c | ||
| 
						 | 
					4822abde86 | ||
| 
						 | 
					b7e52812f8 | ||
| 
						 | 
					69118120d9 | ||
| 
						 | 
					7cba0c6fb0 | ||
| 
						 | 
					5fac67ce15 | ||
| 
						 | 
					98c733108e | ||
| 
						 | 
					782186e13d | ||
| 
						 | 
					4e1f6518e8 | ||
| 
						 | 
					53e0fe8e51 | ||
| 
						 | 
					0e547390da | ||
| 
						 | 
					86b52df839 | ||
| 
						 | 
					d685fdf54a | ||
| 
						 | 
					d9caab4108 | ||
| 
						 | 
					44b68f140e | ||
| 
						 | 
					3a3d97dfa7 | ||
| 
						 | 
					47898b527c | ||
| 
						 | 
					a35f36ad39 | ||
| 
						 | 
					d13a397f8e | ||
| 
						 | 
					df999723f8 | ||
| 
						 | 
					8236e840a7 | ||
| 
						 | 
					e5b3625f73 | ||
| 
						 | 
					2e4645310b | ||
| 
						 | 
					50a32b387e | ||
| 
						 | 
					2059283707 | ||
| 
						 | 
					8e3af515c9 | ||
| 
						 | 
					6f88f0ea3f | ||
| 
						 | 
					d2f37cf3f9 | ||
| 
						 | 
					7c30d6254e | ||
| 
						 | 
					64fb39a653 | ||
| 
						 | 
					91895aa70c | ||
| 
						 | 
					68dfaf238b | ||
| 
						 | 
					ebf13a0ba0 | ||
| 
						 | 
					2bff9937b7 | ||
| 
						 | 
					256395c28d | ||
| 
						 | 
					3346bc8bba | ||
| 
						 | 
					6fe22a7e62 | ||
| 
						 | 
					757b98748b | ||
| 
						 | 
					7a778f3f33 | ||
| 
						 | 
					9576d246ee | ||
| 
						 | 
					988d3ea8ba | ||
| 
						 | 
					0767b92b62 | ||
| 
						 | 
					2064abe16d | ||
| 
						 | 
					b605982f94 | ||
| 
						 | 
					93b628d9a8 | ||
| 
						 | 
					6bac551d9f | ||
| 
						 | 
					70a35656e4 | ||
| 
						 | 
					047c18eac0 | ||
| 
						 | 
					b4a86ce6cf | ||
| 
						 | 
					b778eed419 | 
							
								
								
									
										14
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -28,8 +28,10 @@ esphome/components/atc_mithermometer/* @ahpohl
 | 
			
		||||
esphome/components/b_parasite/* @rbaron
 | 
			
		||||
esphome/components/ballu/* @bazuchan
 | 
			
		||||
esphome/components/bang_bang/* @OttoWinter
 | 
			
		||||
esphome/components/bedjet/* @jhansche
 | 
			
		||||
esphome/components/bh1750/* @OttoWinter
 | 
			
		||||
esphome/components/binary_sensor/* @esphome/core
 | 
			
		||||
esphome/components/bl0939/* @ziceva
 | 
			
		||||
esphome/components/bl0940/* @tobias-
 | 
			
		||||
esphome/components/ble_client/* @buxtronix
 | 
			
		||||
esphome/components/bme680_bsec/* @trvrnrth
 | 
			
		||||
@@ -53,11 +55,13 @@ esphome/components/current_based/* @djwmarcx
 | 
			
		||||
esphome/components/daly_bms/* @s1lvi0
 | 
			
		||||
esphome/components/dashboard_import/* @esphome/core
 | 
			
		||||
esphome/components/debug/* @OttoWinter
 | 
			
		||||
esphome/components/delonghi/* @grob6000
 | 
			
		||||
esphome/components/dfplayer/* @glmnet
 | 
			
		||||
esphome/components/dht/* @OttoWinter
 | 
			
		||||
esphome/components/ds1307/* @badbadc0ffee
 | 
			
		||||
esphome/components/dsmr/* @glmnet @zuidwijk
 | 
			
		||||
esphome/components/ektf2232/* @jesserockz
 | 
			
		||||
esphome/components/ens210/* @itn3rd77
 | 
			
		||||
esphome/components/esp32/* @esphome/core
 | 
			
		||||
esphome/components/esp32_ble/* @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_server/* @jesserockz
 | 
			
		||||
@@ -84,6 +88,7 @@ esphome/components/honeywellabp/* @RubyBailey
 | 
			
		||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
 | 
			
		||||
esphome/components/hydreon_rgxx/* @functionpointer
 | 
			
		||||
esphome/components/i2c/* @esphome/core
 | 
			
		||||
esphome/components/i2s_audio/* @jesserockz
 | 
			
		||||
esphome/components/improv_serial/* @esphome/core
 | 
			
		||||
esphome/components/ina260/* @MrEditor97
 | 
			
		||||
esphome/components/inkbird_ibsth1_mini/* @fkirill
 | 
			
		||||
@@ -98,6 +103,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
 | 
			
		||||
esphome/components/lock/* @esphome/core
 | 
			
		||||
esphome/components/logger/* @esphome/core
 | 
			
		||||
esphome/components/ltr390/* @sjtrny
 | 
			
		||||
esphome/components/max31865/* @DAVe3283
 | 
			
		||||
esphome/components/max44009/* @berfenger
 | 
			
		||||
esphome/components/max7219digit/* @rspaargaren
 | 
			
		||||
esphome/components/max9611/* @mckaymatthew
 | 
			
		||||
@@ -115,6 +121,7 @@ esphome/components/mcp47a1/* @jesserockz
 | 
			
		||||
esphome/components/mcp9808/* @k7hpn
 | 
			
		||||
esphome/components/md5/* @esphome/core
 | 
			
		||||
esphome/components/mdns/* @esphome/core
 | 
			
		||||
esphome/components/media_player/* @jesserockz
 | 
			
		||||
esphome/components/midea/* @dudanov
 | 
			
		||||
esphome/components/midea_ir/* @dudanov
 | 
			
		||||
esphome/components/mitsubishi/* @RubyBailey
 | 
			
		||||
@@ -164,23 +171,27 @@ esphome/components/rf_bridge/* @jesserockz
 | 
			
		||||
esphome/components/rgbct/* @jesserockz
 | 
			
		||||
esphome/components/rtttl/* @glmnet
 | 
			
		||||
esphome/components/safe_mode/* @jsuanet @paulmonigatti
 | 
			
		||||
esphome/components/scd4x/* @sjtrny
 | 
			
		||||
esphome/components/scd4x/* @martgras @sjtrny
 | 
			
		||||
esphome/components/script/* @esphome/core
 | 
			
		||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
 | 
			
		||||
esphome/components/sdp3x/* @Azimath
 | 
			
		||||
esphome/components/selec_meter/* @sourabhjaiswal
 | 
			
		||||
esphome/components/select/* @esphome/core
 | 
			
		||||
esphome/components/sen5x/* @martgras
 | 
			
		||||
esphome/components/sensirion_common/* @martgras
 | 
			
		||||
esphome/components/sensor/* @esphome/core
 | 
			
		||||
esphome/components/sgp40/* @SenexCrenshaw
 | 
			
		||||
esphome/components/sgp4x/* @SenexCrenshaw @martgras
 | 
			
		||||
esphome/components/shelly_dimmer/* @edge90 @rnauber
 | 
			
		||||
esphome/components/sht4x/* @sjtrny
 | 
			
		||||
esphome/components/shutdown/* @esphome/core @jsuanet
 | 
			
		||||
esphome/components/sim800l/* @glmnet
 | 
			
		||||
esphome/components/sm2135/* @BoukeHaarsma23
 | 
			
		||||
esphome/components/sml/* @alengwenus
 | 
			
		||||
esphome/components/socket/* @esphome/core
 | 
			
		||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
 | 
			
		||||
esphome/components/spi/* @esphome/core
 | 
			
		||||
esphome/components/sps30/* @martgras
 | 
			
		||||
esphome/components/ssd1322_base/* @kbx81
 | 
			
		||||
esphome/components/ssd1322_spi/* @kbx81
 | 
			
		||||
esphome/components/ssd1325_base/* @kbx81
 | 
			
		||||
@@ -215,6 +226,7 @@ esphome/components/tsl2591/* @wjcarpenter
 | 
			
		||||
esphome/components/tuya/binary_sensor/* @jesserockz
 | 
			
		||||
esphome/components/tuya/climate/* @jesserockz
 | 
			
		||||
esphome/components/tuya/number/* @frankiboy1
 | 
			
		||||
esphome/components/tuya/select/* @bearpawmaxim
 | 
			
		||||
esphome/components/tuya/sensor/* @jesserockz
 | 
			
		||||
esphome/components/tuya/switch/* @jesserockz
 | 
			
		||||
esphome/components/tuya/text_sensor/* @dentra
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ RUN \
 | 
			
		||||
        iputils-ping=3:20210202-1 \
 | 
			
		||||
        git=1:2.30.2-1 \
 | 
			
		||||
        curl=7.74.0-1.3+deb11u1 \
 | 
			
		||||
        openssh-client=1:8.4p1-5 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import argparse
 | 
			
		||||
import functools
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
@@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.config import iter_components, read_config, strip_default_ids
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    ALLOWED_NAME_CHARS,
 | 
			
		||||
    CONF_BAUD_RATE,
 | 
			
		||||
    CONF_BROKER,
 | 
			
		||||
    CONF_DEASSERT_RTS_DTR,
 | 
			
		||||
    CONF_LOGGER,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_OTA,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
    CONF_PORT,
 | 
			
		||||
    CONF_ESPHOME,
 | 
			
		||||
    CONF_PLATFORMIO_OPTIONS,
 | 
			
		||||
    CONF_SUBSTITUTIONS,
 | 
			
		||||
    SECRETS_FILES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
			
		||||
@@ -481,6 +485,98 @@ def command_idedata(args, config):
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_rename(args, config):
 | 
			
		||||
    for c in args.name:
 | 
			
		||||
        if c not in ALLOWED_NAME_CHARS:
 | 
			
		||||
            print(
 | 
			
		||||
                color(
 | 
			
		||||
                    Fore.BOLD_RED,
 | 
			
		||||
                    f"'{c}' is an invalid character for names. Valid characters are: "
 | 
			
		||||
                    f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            return 1
 | 
			
		||||
    # Load existing yaml file
 | 
			
		||||
    with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file:
 | 
			
		||||
        raw_contents = raw_file.read()
 | 
			
		||||
 | 
			
		||||
    yaml = yaml_util.load_yaml(CORE.config_path)
 | 
			
		||||
    if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
 | 
			
		||||
        print(
 | 
			
		||||
            color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
 | 
			
		||||
        )
 | 
			
		||||
        return 1
 | 
			
		||||
    old_name = yaml[CONF_ESPHOME][CONF_NAME]
 | 
			
		||||
    match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name)
 | 
			
		||||
    if match is None:
 | 
			
		||||
        new_raw = re.sub(
 | 
			
		||||
            rf"name:\s+[\"']?{old_name}[\"']?",
 | 
			
		||||
            f'name: "{args.name}"',
 | 
			
		||||
            raw_contents,
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)]
 | 
			
		||||
        if (
 | 
			
		||||
            len(
 | 
			
		||||
                re.findall(
 | 
			
		||||
                    rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?",
 | 
			
		||||
                    raw_contents,
 | 
			
		||||
                    flags=re.MULTILINE,
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            > 1
 | 
			
		||||
        ):
 | 
			
		||||
            print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
 | 
			
		||||
            return 1
 | 
			
		||||
 | 
			
		||||
        new_raw = re.sub(
 | 
			
		||||
            rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?",
 | 
			
		||||
            f'\\1: "{args.name}"',
 | 
			
		||||
            raw_contents,
 | 
			
		||||
            flags=re.MULTILINE,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
 | 
			
		||||
    print(
 | 
			
		||||
        f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
 | 
			
		||||
    )
 | 
			
		||||
    print()
 | 
			
		||||
 | 
			
		||||
    with open(new_path, mode="w", encoding="utf-8") as new_file:
 | 
			
		||||
        new_file.write(new_raw)
 | 
			
		||||
 | 
			
		||||
    rc = run_external_process("esphome", "config", new_path)
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
 | 
			
		||||
        os.remove(new_path)
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    cli_args = [
 | 
			
		||||
        "run",
 | 
			
		||||
        new_path,
 | 
			
		||||
        "--no-logs",
 | 
			
		||||
        "--device",
 | 
			
		||||
        CORE.address,
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if args.dashboard:
 | 
			
		||||
        cli_args.insert(0, "--dashboard")
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        rc = run_external_process("esphome", *cli_args)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        rc = 1
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        os.remove(new_path)
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    os.remove(CORE.config_path)
 | 
			
		||||
 | 
			
		||||
    print(color(Fore.BOLD_GREEN, "SUCCESS"))
 | 
			
		||||
    print()
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
PRE_CONFIG_ACTIONS = {
 | 
			
		||||
    "wizard": command_wizard,
 | 
			
		||||
    "version": command_version,
 | 
			
		||||
@@ -499,6 +595,7 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "mqtt-fingerprint": command_mqtt_fingerprint,
 | 
			
		||||
    "clean": command_clean,
 | 
			
		||||
    "idedata": command_idedata,
 | 
			
		||||
    "rename": command_rename,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -681,6 +778,15 @@ def parse_args(argv):
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs=1
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_rename = subparsers.add_parser(
 | 
			
		||||
        "rename",
 | 
			
		||||
        help="Rename a device in YAML, compile the binary and upload it.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_rename.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file.", nargs=1
 | 
			
		||||
    )
 | 
			
		||||
    parser_rename.add_argument("name", help="The new name for the device.", type=str)
 | 
			
		||||
 | 
			
		||||
    # Keep backward compatibility with the old command line format of
 | 
			
		||||
    # esphome <config> <command>.
 | 
			
		||||
    #
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,7 @@ from esphome.cpp_types import (  # noqa
 | 
			
		||||
    uint64,
 | 
			
		||||
    int32,
 | 
			
		||||
    int64,
 | 
			
		||||
    size_t,
 | 
			
		||||
    const_char_ptr,
 | 
			
		||||
    NAN,
 | 
			
		||||
    esphome_ns,
 | 
			
		||||
 
 | 
			
		||||
@@ -121,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
      // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
 | 
			
		||||
      // also take into account min_power
 | 
			
		||||
      auto min_us = this->cycle_time_us * this->min_power / 1000;
 | 
			
		||||
      this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
 | 
			
		||||
      // calculate required value to provide a true RMS voltage output
 | 
			
		||||
      this->enable_time_us =
 | 
			
		||||
          std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) *
 | 
			
		||||
                                            (this->cycle_time_us - min_us)) /
 | 
			
		||||
                                     65535);
 | 
			
		||||
      if (this->method == DIM_METHOD_LEADING_PULSE) {
 | 
			
		||||
        // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone
 | 
			
		||||
        // this is for brightness near 99%
 | 
			
		||||
 
 | 
			
		||||
@@ -15,10 +15,21 @@ namespace esphome {
 | 
			
		||||
namespace adc {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "adc";
 | 
			
		||||
// 13 bits for S3 / 12 bit for all other esp32 variants
 | 
			
		||||
// create a const to avoid the repated cast to enum
 | 
			
		||||
 | 
			
		||||
// 13bit for S2, and 12bit for all other esp32 variants
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
 | 
			
		||||
 | 
			
		||||
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
 | 
			
		||||
#if USE_ESP32_VARIANT_ESP32S2
 | 
			
		||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 13;
 | 
			
		||||
#else
 | 
			
		||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 12;
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;    // 4095 (12 bit) or 8191 (13 bit)
 | 
			
		||||
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;  // 2048 (12 bit) or 4096 (13 bit)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void ADCSensor::setup() {
 | 
			
		||||
@@ -51,8 +62,8 @@ void ADCSensor::setup() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
  // adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2)
 | 
			
		||||
  adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_);
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
@@ -75,16 +86,16 @@ void ADCSensor::dump_config() {
 | 
			
		||||
  } else {
 | 
			
		||||
    switch (this->attenuation_) {
 | 
			
		||||
      case ADC_ATTEN_DB_0:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)");
 | 
			
		||||
        ESP_LOGCONFIG(TAG, " Attenuation: 0db");
 | 
			
		||||
        break;
 | 
			
		||||
      case ADC_ATTEN_DB_2_5:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)");
 | 
			
		||||
        ESP_LOGCONFIG(TAG, " Attenuation: 2.5db");
 | 
			
		||||
        break;
 | 
			
		||||
      case ADC_ATTEN_DB_6:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)");
 | 
			
		||||
        ESP_LOGCONFIG(TAG, " Attenuation: 6db");
 | 
			
		||||
        break;
 | 
			
		||||
      case ADC_ATTEN_DB_11:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)");
 | 
			
		||||
        ESP_LOGCONFIG(TAG, " Attenuation: 11db");
 | 
			
		||||
        break;
 | 
			
		||||
      default:  // This is to satisfy the unused ADC_ATTEN_MAX
 | 
			
		||||
        break;
 | 
			
		||||
@@ -129,16 +140,16 @@ float ADCSensor::sample() {
 | 
			
		||||
    return mv / 1000.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095;
 | 
			
		||||
  int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
 | 
			
		||||
  adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11);
 | 
			
		||||
  raw11 = adc1_get_raw(channel_);
 | 
			
		||||
  if (raw11 < 4095) {
 | 
			
		||||
  if (raw11 < ADC_MAX) {
 | 
			
		||||
    adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6);
 | 
			
		||||
    raw6 = adc1_get_raw(channel_);
 | 
			
		||||
    if (raw6 < 4095) {
 | 
			
		||||
    if (raw6 < ADC_MAX) {
 | 
			
		||||
      adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5);
 | 
			
		||||
      raw2 = adc1_get_raw(channel_);
 | 
			
		||||
      if (raw2 < 4095) {
 | 
			
		||||
      if (raw2 < ADC_MAX) {
 | 
			
		||||
        adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0);
 | 
			
		||||
        raw0 = adc1_get_raw(channel_);
 | 
			
		||||
      }
 | 
			
		||||
@@ -154,15 +165,15 @@ float ADCSensor::sample() {
 | 
			
		||||
  uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]);
 | 
			
		||||
  uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]);
 | 
			
		||||
 | 
			
		||||
  // Contribution of each value, in range 0-2048
 | 
			
		||||
  uint32_t c11 = std::min(raw11, 2048);
 | 
			
		||||
  uint32_t c6 = 2048 - std::abs(raw6 - 2048);
 | 
			
		||||
  uint32_t c2 = 2048 - std::abs(raw2 - 2048);
 | 
			
		||||
  uint32_t c0 = std::min(4095 - raw0, 2048);
 | 
			
		||||
  // max theoretical csum value is 2048*4 = 8192
 | 
			
		||||
  // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
 | 
			
		||||
  uint32_t c11 = std::min(raw11, ADC_HALF);
 | 
			
		||||
  uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
 | 
			
		||||
  uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
 | 
			
		||||
  uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
 | 
			
		||||
  // max theoretical csum value is 4096*4 = 16384
 | 
			
		||||
  uint32_t csum = c11 + c6 + c2 + c0;
 | 
			
		||||
 | 
			
		||||
  // each mv is max 3900; so max value is 3900*2048*4, fits in unsigned
 | 
			
		||||
  // each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32
 | 
			
		||||
  uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
 | 
			
		||||
  return mv_scaled / (float) (csum * 1000U);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,8 @@ class AddressableLightDisplay : public display::DisplayBuffer, public PollingCom
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void display();
 | 
			
		||||
 | 
			
		||||
  display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int get_width_internal() override;
 | 
			
		||||
  int get_height_internal() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -94,6 +94,29 @@ async def to_code(config):
 | 
			
		||||
                data[pos] = pix[2]
 | 
			
		||||
                pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] == "RGB565":
 | 
			
		||||
        data = [0 for _ in range(height * width * 2 * frames)]
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for frameIndex in range(frames):
 | 
			
		||||
            image.seek(frameIndex)
 | 
			
		||||
            frame = image.convert("RGB")
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                frame = frame.resize([width, height])
 | 
			
		||||
            pixels = list(frame.getdata())
 | 
			
		||||
            if len(pixels) != height * width:
 | 
			
		||||
                raise core.EsphomeError(
 | 
			
		||||
                    f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
 | 
			
		||||
                )
 | 
			
		||||
            for pix in pixels:
 | 
			
		||||
                R = pix[0] >> 3
 | 
			
		||||
                G = pix[1] >> 2
 | 
			
		||||
                B = pix[2] >> 3
 | 
			
		||||
                rgb = (R << 11) | (G << 5) | B
 | 
			
		||||
                data[pos] = rgb >> 8
 | 
			
		||||
                pos += 1
 | 
			
		||||
                data[pos] = rgb & 255
 | 
			
		||||
                pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] == "BINARY":
 | 
			
		||||
        width8 = ((width + 7) // 8) * 8
 | 
			
		||||
        data = [0 for _ in range((height * width8 // 8) * frames)]
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@ service APIConnection {
 | 
			
		||||
  rpc select_command (SelectCommandRequest) returns (void) {}
 | 
			
		||||
  rpc button_command (ButtonCommandRequest) returns (void) {}
 | 
			
		||||
  rpc lock_command (LockCommandRequest) returns (void) {}
 | 
			
		||||
  rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -991,7 +992,7 @@ message ListEntitiesLockResponse {
 | 
			
		||||
  bool supports_open = 9;
 | 
			
		||||
  bool requires_code = 10;
 | 
			
		||||
 | 
			
		||||
  # Not yet implemented:
 | 
			
		||||
  // Not yet implemented:
 | 
			
		||||
  string code_format = 11;
 | 
			
		||||
}
 | 
			
		||||
message LockStateResponse {
 | 
			
		||||
@@ -1010,7 +1011,7 @@ message LockCommandRequest {
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  LockCommand command = 2;
 | 
			
		||||
 | 
			
		||||
  # Not yet implemented:
 | 
			
		||||
  // Not yet implemented:
 | 
			
		||||
  bool has_code = 3;
 | 
			
		||||
  string code = 4;
 | 
			
		||||
}
 | 
			
		||||
@@ -1040,3 +1041,60 @@ message ButtonCommandRequest {
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== MEDIA PLAYER ====================
 | 
			
		||||
enum MediaPlayerState {
 | 
			
		||||
  MEDIA_PLAYER_STATE_NONE = 0;
 | 
			
		||||
  MEDIA_PLAYER_STATE_IDLE = 1;
 | 
			
		||||
  MEDIA_PLAYER_STATE_PLAYING = 2;
 | 
			
		||||
  MEDIA_PLAYER_STATE_PAUSED = 3;
 | 
			
		||||
}
 | 
			
		||||
enum MediaPlayerCommand {
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_PLAY = 0;
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_PAUSE = 1;
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_STOP = 2;
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_MUTE = 3;
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_UNMUTE = 4;
 | 
			
		||||
}
 | 
			
		||||
message ListEntitiesMediaPlayerResponse {
 | 
			
		||||
  option (id) = 63;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_MEDIA_PLAYER";
 | 
			
		||||
 | 
			
		||||
  string object_id = 1;
 | 
			
		||||
  fixed32 key = 2;
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  string unique_id = 4;
 | 
			
		||||
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  bool disabled_by_default = 6;
 | 
			
		||||
  EntityCategory entity_category = 7;
 | 
			
		||||
 | 
			
		||||
  bool supports_pause = 8;
 | 
			
		||||
}
 | 
			
		||||
message MediaPlayerStateResponse {
 | 
			
		||||
  option (id) = 64;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_MEDIA_PLAYER";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  MediaPlayerState state = 2;
 | 
			
		||||
  float volume = 3;
 | 
			
		||||
  bool muted = 4;
 | 
			
		||||
}
 | 
			
		||||
message MediaPlayerCommandRequest {
 | 
			
		||||
  option (id) = 65;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (ifdef) = "USE_MEDIA_PLAYER";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
 | 
			
		||||
  bool has_command = 2;
 | 
			
		||||
  MediaPlayerCommand command = 3;
 | 
			
		||||
 | 
			
		||||
  bool has_volume = 4;
 | 
			
		||||
  float volume = 5;
 | 
			
		||||
 | 
			
		||||
  bool has_media_url = 6;
 | 
			
		||||
  string media_url = 7;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,6 @@
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
#include "esphome/components/homeassistant/time/homeassistant_time.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
@@ -253,9 +250,6 @@ 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::Fan *fan) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
    return false;
 | 
			
		||||
@@ -268,7 +262,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
 | 
			
		||||
    resp.oscillating = fan->oscillating;
 | 
			
		||||
  if (traits.supports_speed()) {
 | 
			
		||||
    resp.speed_level = fan->speed;
 | 
			
		||||
    resp.speed = static_cast<enums::FanSpeed>(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count()));
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_direction())
 | 
			
		||||
    resp.direction = static_cast<enums::FanDirection>(fan->direction);
 | 
			
		||||
@@ -295,8 +288,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
  if (fan == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto traits = fan->get_traits();
 | 
			
		||||
 | 
			
		||||
  auto call = fan->make_call();
 | 
			
		||||
  if (msg.has_state)
 | 
			
		||||
    call.set_state(msg.state);
 | 
			
		||||
@@ -305,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
  if (msg.has_speed_level) {
 | 
			
		||||
    // Prefer level
 | 
			
		||||
    call.set_speed(msg.speed_level);
 | 
			
		||||
  } else if (msg.has_speed) {
 | 
			
		||||
    call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count()));
 | 
			
		||||
  }
 | 
			
		||||
  if (msg.has_direction)
 | 
			
		||||
    call.set_direction(static_cast<fan::FanDirection>(msg.direction));
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
@@ -745,6 +733,52 @@ void APIConnection::lock_command(const LockCommandRequest &msg) {
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  MediaPlayerStateResponse resp{};
 | 
			
		||||
  resp.key = media_player->get_object_id_hash();
 | 
			
		||||
  resp.state = static_cast<enums::MediaPlayerState>(media_player->state);
 | 
			
		||||
  resp.volume = media_player->volume;
 | 
			
		||||
  resp.muted = media_player->is_muted();
 | 
			
		||||
  return this->send_media_player_state_response(resp);
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) {
 | 
			
		||||
  ListEntitiesMediaPlayerResponse msg;
 | 
			
		||||
  msg.key = media_player->get_object_id_hash();
 | 
			
		||||
  msg.object_id = media_player->get_object_id();
 | 
			
		||||
  msg.name = media_player->get_name();
 | 
			
		||||
  msg.unique_id = get_default_unique_id("media_player", media_player);
 | 
			
		||||
  msg.icon = media_player->get_icon();
 | 
			
		||||
  msg.disabled_by_default = media_player->is_disabled_by_default();
 | 
			
		||||
  msg.entity_category = static_cast<enums::EntityCategory>(media_player->get_entity_category());
 | 
			
		||||
 | 
			
		||||
  auto traits = media_player->get_traits();
 | 
			
		||||
  msg.supports_pause = traits.get_supports_pause();
 | 
			
		||||
 | 
			
		||||
  return this->send_list_entities_media_player_response(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
 | 
			
		||||
  media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key);
 | 
			
		||||
  if (media_player == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto call = media_player->make_call();
 | 
			
		||||
  if (msg.has_command) {
 | 
			
		||||
    call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
 | 
			
		||||
  }
 | 
			
		||||
  if (msg.has_volume) {
 | 
			
		||||
    call.set_volume(msg.volume);
 | 
			
		||||
  }
 | 
			
		||||
  if (msg.has_media_url) {
 | 
			
		||||
    call.set_media_url(msg.media_url);
 | 
			
		||||
  }
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
 | 
			
		||||
  if (!this->state_subscription_)
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
 | 
			
		||||
  bool send_lock_info(lock::Lock *a_lock);
 | 
			
		||||
  void lock_command(const LockCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  bool send_media_player_state(media_player::MediaPlayer *media_player);
 | 
			
		||||
  bool send_media_player_info(media_player::MediaPlayer *media_player);
 | 
			
		||||
  void media_player_command(const MediaPlayerCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
  bool send_log_message(int level, const char *tag, const char *line);
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
 
 | 
			
		||||
@@ -308,6 +308,36 @@ template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockComma
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::MediaPlayerState value) {
 | 
			
		||||
  switch (value) {
 | 
			
		||||
    case enums::MEDIA_PLAYER_STATE_NONE:
 | 
			
		||||
      return "MEDIA_PLAYER_STATE_NONE";
 | 
			
		||||
    case enums::MEDIA_PLAYER_STATE_IDLE:
 | 
			
		||||
      return "MEDIA_PLAYER_STATE_IDLE";
 | 
			
		||||
    case enums::MEDIA_PLAYER_STATE_PLAYING:
 | 
			
		||||
      return "MEDIA_PLAYER_STATE_PLAYING";
 | 
			
		||||
    case enums::MEDIA_PLAYER_STATE_PAUSED:
 | 
			
		||||
      return "MEDIA_PLAYER_STATE_PAUSED";
 | 
			
		||||
    default:
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::MediaPlayerCommand value) {
 | 
			
		||||
  switch (value) {
 | 
			
		||||
    case enums::MEDIA_PLAYER_COMMAND_PLAY:
 | 
			
		||||
      return "MEDIA_PLAYER_COMMAND_PLAY";
 | 
			
		||||
    case enums::MEDIA_PLAYER_COMMAND_PAUSE:
 | 
			
		||||
      return "MEDIA_PLAYER_COMMAND_PAUSE";
 | 
			
		||||
    case enums::MEDIA_PLAYER_COMMAND_STOP:
 | 
			
		||||
      return "MEDIA_PLAYER_COMMAND_STOP";
 | 
			
		||||
    case enums::MEDIA_PLAYER_COMMAND_MUTE:
 | 
			
		||||
      return "MEDIA_PLAYER_COMMAND_MUTE";
 | 
			
		||||
    case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
 | 
			
		||||
      return "MEDIA_PLAYER_COMMAND_UNMUTE";
 | 
			
		||||
    default:
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1: {
 | 
			
		||||
@@ -4574,6 +4604,254 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 6: {
 | 
			
		||||
      this->disabled_by_default = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 7: {
 | 
			
		||||
      this->entity_category = value.as_enum<enums::EntityCategory>();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 8: {
 | 
			
		||||
      this->supports_pause = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1: {
 | 
			
		||||
      this->object_id = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 3: {
 | 
			
		||||
      this->name = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 4: {
 | 
			
		||||
      this->unique_id = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 5: {
 | 
			
		||||
      this->icon = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 2: {
 | 
			
		||||
      this->key = value.as_fixed32();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_string(1, this->object_id);
 | 
			
		||||
  buffer.encode_fixed32(2, this->key);
 | 
			
		||||
  buffer.encode_string(3, this->name);
 | 
			
		||||
  buffer.encode_string(4, this->unique_id);
 | 
			
		||||
  buffer.encode_string(5, this->icon);
 | 
			
		||||
  buffer.encode_bool(6, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
 | 
			
		||||
  buffer.encode_bool(8, this->supports_pause);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
 | 
			
		||||
  __attribute__((unused)) char buffer[64];
 | 
			
		||||
  out.append("ListEntitiesMediaPlayerResponse {\n");
 | 
			
		||||
  out.append("  object_id: ");
 | 
			
		||||
  out.append("'").append(this->object_id).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  key: ");
 | 
			
		||||
  sprintf(buffer, "%u", this->key);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  name: ");
 | 
			
		||||
  out.append("'").append(this->name).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  unique_id: ");
 | 
			
		||||
  out.append("'").append(this->unique_id).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  icon: ");
 | 
			
		||||
  out.append("'").append(this->icon).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  disabled_by_default: ");
 | 
			
		||||
  out.append(YESNO(this->disabled_by_default));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  entity_category: ");
 | 
			
		||||
  out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  supports_pause: ");
 | 
			
		||||
  out.append(YESNO(this->supports_pause));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 2: {
 | 
			
		||||
      this->state = value.as_enum<enums::MediaPlayerState>();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 4: {
 | 
			
		||||
      this->muted = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1: {
 | 
			
		||||
      this->key = value.as_fixed32();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 3: {
 | 
			
		||||
      this->volume = value.as_float();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_fixed32(1, this->key);
 | 
			
		||||
  buffer.encode_enum<enums::MediaPlayerState>(2, this->state);
 | 
			
		||||
  buffer.encode_float(3, this->volume);
 | 
			
		||||
  buffer.encode_bool(4, this->muted);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void MediaPlayerStateResponse::dump_to(std::string &out) const {
 | 
			
		||||
  __attribute__((unused)) char buffer[64];
 | 
			
		||||
  out.append("MediaPlayerStateResponse {\n");
 | 
			
		||||
  out.append("  key: ");
 | 
			
		||||
  sprintf(buffer, "%u", this->key);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  state: ");
 | 
			
		||||
  out.append(proto_enum_to_string<enums::MediaPlayerState>(this->state));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  volume: ");
 | 
			
		||||
  sprintf(buffer, "%g", this->volume);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  muted: ");
 | 
			
		||||
  out.append(YESNO(this->muted));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 2: {
 | 
			
		||||
      this->has_command = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 3: {
 | 
			
		||||
      this->command = value.as_enum<enums::MediaPlayerCommand>();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 4: {
 | 
			
		||||
      this->has_volume = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 6: {
 | 
			
		||||
      this->has_media_url = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 7: {
 | 
			
		||||
      this->media_url = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1: {
 | 
			
		||||
      this->key = value.as_fixed32();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 5: {
 | 
			
		||||
      this->volume = value.as_float();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_fixed32(1, this->key);
 | 
			
		||||
  buffer.encode_bool(2, this->has_command);
 | 
			
		||||
  buffer.encode_enum<enums::MediaPlayerCommand>(3, this->command);
 | 
			
		||||
  buffer.encode_bool(4, this->has_volume);
 | 
			
		||||
  buffer.encode_float(5, this->volume);
 | 
			
		||||
  buffer.encode_bool(6, this->has_media_url);
 | 
			
		||||
  buffer.encode_string(7, this->media_url);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void MediaPlayerCommandRequest::dump_to(std::string &out) const {
 | 
			
		||||
  __attribute__((unused)) char buffer[64];
 | 
			
		||||
  out.append("MediaPlayerCommandRequest {\n");
 | 
			
		||||
  out.append("  key: ");
 | 
			
		||||
  sprintf(buffer, "%u", this->key);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  has_command: ");
 | 
			
		||||
  out.append(YESNO(this->has_command));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  command: ");
 | 
			
		||||
  out.append(proto_enum_to_string<enums::MediaPlayerCommand>(this->command));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  has_volume: ");
 | 
			
		||||
  out.append(YESNO(this->has_volume));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  volume: ");
 | 
			
		||||
  sprintf(buffer, "%g", this->volume);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  has_media_url: ");
 | 
			
		||||
  out.append(YESNO(this->has_media_url));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  media_url: ");
 | 
			
		||||
  out.append("'").append(this->media_url).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -141,6 +141,19 @@ enum LockCommand : uint32_t {
 | 
			
		||||
  LOCK_LOCK = 1,
 | 
			
		||||
  LOCK_OPEN = 2,
 | 
			
		||||
};
 | 
			
		||||
enum MediaPlayerState : uint32_t {
 | 
			
		||||
  MEDIA_PLAYER_STATE_NONE = 0,
 | 
			
		||||
  MEDIA_PLAYER_STATE_IDLE = 1,
 | 
			
		||||
  MEDIA_PLAYER_STATE_PLAYING = 2,
 | 
			
		||||
  MEDIA_PLAYER_STATE_PAUSED = 3,
 | 
			
		||||
};
 | 
			
		||||
enum MediaPlayerCommand : uint32_t {
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_PLAY = 0,
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_PAUSE = 1,
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_STOP = 2,
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_MUTE = 3,
 | 
			
		||||
  MEDIA_PLAYER_COMMAND_UNMUTE = 4,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace enums
 | 
			
		||||
 | 
			
		||||
@@ -1146,6 +1159,60 @@ class ButtonCommandRequest : public ProtoMessage {
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
 | 
			
		||||
};
 | 
			
		||||
class ListEntitiesMediaPlayerResponse : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  std::string object_id{};
 | 
			
		||||
  uint32_t key{0};
 | 
			
		||||
  std::string name{};
 | 
			
		||||
  std::string unique_id{};
 | 
			
		||||
  std::string icon{};
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  enums::EntityCategory entity_category{};
 | 
			
		||||
  bool supports_pause{false};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
class MediaPlayerStateResponse : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  uint32_t key{0};
 | 
			
		||||
  enums::MediaPlayerState state{};
 | 
			
		||||
  float volume{0.0f};
 | 
			
		||||
  bool muted{false};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
class MediaPlayerCommandRequest : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  uint32_t key{0};
 | 
			
		||||
  bool has_command{false};
 | 
			
		||||
  enums::MediaPlayerCommand command{};
 | 
			
		||||
  bool has_volume{false};
 | 
			
		||||
  float volume{0.0f};
 | 
			
		||||
  bool has_media_url{false};
 | 
			
		||||
  std::string media_url{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -310,6 +310,24 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesMediaPlayerResponse>(msg, 63);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayerStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_media_player_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<MediaPlayerStateResponse>(msg, 64);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
#endif
 | 
			
		||||
bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
 | 
			
		||||
  switch (msg_type) {
 | 
			
		||||
    case 1: {
 | 
			
		||||
@@ -563,6 +581,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
      ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_button_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 65: {
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
      MediaPlayerCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_media_player_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -813,6 +842,19 @@ void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg)
 | 
			
		||||
  this->lock_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->media_player_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -144,6 +144,15 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  virtual void on_button_command_request(const ButtonCommandRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  bool send_media_player_state_response(const MediaPlayerStateResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
 | 
			
		||||
@@ -192,6 +201,9 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  virtual void lock_command(const LockCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  void on_hello_request(const HelloRequest &msg) override;
 | 
			
		||||
@@ -236,6 +248,9 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  void on_lock_command_request(const LockCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
@@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServer::on_select_update(select::Select *obj, const std::string &state) {
 | 
			
		||||
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
@@ -272,6 +272,15 @@ void APIServer::on_lock_update(lock::Lock *obj) {
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_media_player_state(obj);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
 | 
			
		||||
APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 
 | 
			
		||||
@@ -64,10 +64,13 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  void on_number_update(number::Number *obj, float state) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  void on_select_update(select::Select *obj, const std::string &state) override;
 | 
			
		||||
  void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  void on_lock_update(lock::Lock *obj) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  void on_media_player_update(media_player::MediaPlayer *obj) override;
 | 
			
		||||
#endif
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
 | 
			
		||||
  void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
 | 
			
		||||
 
 | 
			
		||||
@@ -64,5 +64,11 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie
 | 
			
		||||
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) {
 | 
			
		||||
  return this->client_->send_media_player_info(media_player);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,9 @@ class ListEntitiesIterator : public ComponentIterator {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  bool on_lock(lock::Lock *a_lock) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  bool on_media_player(media_player::MediaPlayer *media_player) override;
 | 
			
		||||
#endif
 | 
			
		||||
  bool on_end() override;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,11 @@ bool InitialStateIterator::on_select(select::Select *select) {
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) {
 | 
			
		||||
  return this->client_->send_media_player_state(media_player);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,9 @@ class InitialStateIterator : public ComponentIterator {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  bool on_lock(lock::Lock *a_lock) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  bool on_media_player(media_player::MediaPlayer *media_player) override;
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  APIConnection *client_;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@jhansche"]
 | 
			
		||||
							
								
								
									
										675
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										675
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,675 @@
 | 
			
		||||
#include "bedjet.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bedjet {
 | 
			
		||||
 | 
			
		||||
using namespace esphome::climate;
 | 
			
		||||
 | 
			
		||||
/// Converts a BedJet temp step into degrees Celsius.
 | 
			
		||||
float bedjet_temp_to_c(const uint8_t temp) {
 | 
			
		||||
  // BedJet temp is "C*2"; to get C, divide by 2.
 | 
			
		||||
  return temp / 2.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
 | 
			
		||||
uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
 | 
			
		||||
  //  0 =  5%
 | 
			
		||||
  // 19 = 100%
 | 
			
		||||
  return 5 * fan + 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
 | 
			
		||||
  if (fan_step >= 0 && fan_step <= 19)
 | 
			
		||||
    return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
 | 
			
		||||
  return nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
 | 
			
		||||
  for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) {
 | 
			
		||||
    if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
 | 
			
		||||
      return i;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static BedjetButton heat_button(BedjetHeatMode mode) {
 | 
			
		||||
  BedjetButton btn = BTN_HEAT;
 | 
			
		||||
  if (mode == HEAT_MODE_EXTENDED) {
 | 
			
		||||
    btn = BTN_EXTHT;
 | 
			
		||||
  }
 | 
			
		||||
  return btn;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Bedjet::upgrade_firmware() {
 | 
			
		||||
  auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE);
 | 
			
		||||
  auto status = this->write_bedjet_packet_(pkt);
 | 
			
		||||
 | 
			
		||||
  if (status) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Bedjet::dump_config() {
 | 
			
		||||
  LOG_CLIMATE("", "BedJet Climate", this);
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Supported modes:");
 | 
			
		||||
  for (auto mode : traits.get_supported_modes()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_mode_to_string(mode)));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Supported fan modes:");
 | 
			
		||||
  for (const auto &mode : traits.get_supported_fan_modes()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
 | 
			
		||||
  }
 | 
			
		||||
  for (const auto &mode : traits.get_supported_custom_fan_modes()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "   - %s (c)", mode.c_str());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Supported presets:");
 | 
			
		||||
  for (auto preset : traits.get_supported_presets()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
 | 
			
		||||
  }
 | 
			
		||||
  for (const auto &preset : traits.get_supported_custom_presets()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "   - %s (c)", preset.c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Bedjet::setup() {
 | 
			
		||||
  this->codec_ = make_unique<BedjetCodec>();
 | 
			
		||||
 | 
			
		||||
  // restore set points
 | 
			
		||||
  auto restore = this->restore_state_();
 | 
			
		||||
  if (restore.has_value()) {
 | 
			
		||||
    ESP_LOGI(TAG, "Restored previous saved state.");
 | 
			
		||||
    restore->apply(this);
 | 
			
		||||
  } else {
 | 
			
		||||
    // Initial status is unknown until we connect
 | 
			
		||||
    this->reset_state_();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
  this->setup_time_();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Resets states to defaults. */
 | 
			
		||||
void Bedjet::reset_state_() {
 | 
			
		||||
  this->mode = climate::CLIMATE_MODE_OFF;
 | 
			
		||||
  this->action = climate::CLIMATE_ACTION_IDLE;
 | 
			
		||||
  this->target_temperature = NAN;
 | 
			
		||||
  this->current_temperature = NAN;
 | 
			
		||||
  this->preset.reset();
 | 
			
		||||
  this->custom_preset.reset();
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Bedjet::loop() {}
 | 
			
		||||
 | 
			
		||||
void Bedjet::control(const ClimateCall &call) {
 | 
			
		||||
  ESP_LOGD(TAG, "Received Bedjet::control");
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGW(TAG, "Not connected, cannot handle control call yet.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (call.get_mode().has_value()) {
 | 
			
		||||
    ClimateMode mode = *call.get_mode();
 | 
			
		||||
    BedjetPacket *pkt;
 | 
			
		||||
    switch (mode) {
 | 
			
		||||
      case climate::CLIMATE_MODE_OFF:
 | 
			
		||||
        pkt = this->codec_->get_button_request(BTN_OFF);
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
        pkt = this->codec_->get_button_request(heat_button(this->heating_mode_));
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
        pkt = this->codec_->get_button_request(BTN_COOL);
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_DRY:
 | 
			
		||||
        pkt = this->codec_->get_button_request(BTN_DRY);
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        ESP_LOGW(TAG, "Unsupported mode: %d", mode);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto status = this->write_bedjet_packet_(pkt);
 | 
			
		||||
 | 
			
		||||
    if (status) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->force_refresh_ = true;
 | 
			
		||||
      this->mode = mode;
 | 
			
		||||
      // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
 | 
			
		||||
      this->custom_preset.reset();
 | 
			
		||||
      this->preset.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (call.get_target_temperature().has_value()) {
 | 
			
		||||
    auto target_temp = *call.get_target_temperature();
 | 
			
		||||
    auto *pkt = this->codec_->get_set_target_temp_request(target_temp);
 | 
			
		||||
    auto status = this->write_bedjet_packet_(pkt);
 | 
			
		||||
 | 
			
		||||
    if (status) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->target_temperature = target_temp;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (call.get_preset().has_value()) {
 | 
			
		||||
    ClimatePreset preset = *call.get_preset();
 | 
			
		||||
    BedjetPacket *pkt;
 | 
			
		||||
 | 
			
		||||
    if (preset == climate::CLIMATE_PRESET_BOOST) {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_TURBO);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Unsupported preset: %d", preset);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto status = this->write_bedjet_packet_(pkt);
 | 
			
		||||
    if (status) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
    } else {
 | 
			
		||||
      // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode.
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
      this->preset = preset;
 | 
			
		||||
      this->custom_preset.reset();
 | 
			
		||||
      this->force_refresh_ = true;
 | 
			
		||||
    }
 | 
			
		||||
  } else if (call.get_custom_preset().has_value()) {
 | 
			
		||||
    std::string preset = *call.get_custom_preset();
 | 
			
		||||
    BedjetPacket *pkt;
 | 
			
		||||
 | 
			
		||||
    if (preset == "M1") {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_M1);
 | 
			
		||||
    } else if (preset == "M2") {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_M2);
 | 
			
		||||
    } else if (preset == "M3") {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_M3);
 | 
			
		||||
    } else if (preset == "LTD HT") {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_HEAT);
 | 
			
		||||
    } else if (preset == "EXT HT") {
 | 
			
		||||
      pkt = this->codec_->get_button_request(BTN_EXTHT);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto status = this->write_bedjet_packet_(pkt);
 | 
			
		||||
    if (status) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->force_refresh_ = true;
 | 
			
		||||
      this->custom_preset = preset;
 | 
			
		||||
      this->preset.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (call.get_fan_mode().has_value()) {
 | 
			
		||||
    // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments.
 | 
			
		||||
    // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here.
 | 
			
		||||
    auto fan_mode = *call.get_fan_mode();
 | 
			
		||||
    BedjetPacket *pkt;
 | 
			
		||||
    if (fan_mode == climate::CLIMATE_FAN_LOW) {
 | 
			
		||||
      pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */);
 | 
			
		||||
    } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) {
 | 
			
		||||
      pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */);
 | 
			
		||||
    } else if (fan_mode == climate::CLIMATE_FAN_HIGH) {
 | 
			
		||||
      pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(),
 | 
			
		||||
               LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto status = this->write_bedjet_packet_(pkt);
 | 
			
		||||
    if (status) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->force_refresh_ = true;
 | 
			
		||||
    }
 | 
			
		||||
  } else if (call.get_custom_fan_mode().has_value()) {
 | 
			
		||||
    auto fan_mode = *call.get_custom_fan_mode();
 | 
			
		||||
    auto fan_step = bedjet_fan_speed_to_step(fan_mode);
 | 
			
		||||
    if (fan_step >= 0 && fan_step <= 19) {
 | 
			
		||||
      ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
 | 
			
		||||
               fan_step);
 | 
			
		||||
      // The index should represent the fan_step index.
 | 
			
		||||
      BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step);
 | 
			
		||||
      auto status = this->write_bedjet_packet_(pkt);
 | 
			
		||||
      if (status) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
      } else {
 | 
			
		||||
        this->force_refresh_ = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Bedjet::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_DISCONNECT_EVT: {
 | 
			
		||||
      ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason);
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->char_handle_cmd_ = chr->handle;
 | 
			
		||||
 | 
			
		||||
      chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this->char_handle_status_ = chr->handle;
 | 
			
		||||
      // We also need to obtain the config descriptor for this handle.
 | 
			
		||||
      // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be
 | 
			
		||||
      // able to look it up.
 | 
			
		||||
      auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_);
 | 
			
		||||
      if (descr == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications",
 | 
			
		||||
                 this->char_handle_status_);
 | 
			
		||||
      } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
 | 
			
		||||
                 descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
 | 
			
		||||
        ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_,
 | 
			
		||||
                 descr->uuid.to_string().c_str());
 | 
			
		||||
      } else {
 | 
			
		||||
        this->config_descr_status_ = descr->handle;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID);
 | 
			
		||||
      if (chr != nullptr) {
 | 
			
		||||
        this->char_handle_name_ = chr->handle;
 | 
			
		||||
        auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_,
 | 
			
		||||
                                              ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
        if (status) {
 | 
			
		||||
          ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ESP_LOGD(TAG, "Services complete: obtained char handles.");
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
 | 
			
		||||
      this->set_notify_(true);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
      if (this->time_id_.has_value()) {
 | 
			
		||||
        this->send_local_time();
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_WRITE_DESCR_EVT: {
 | 
			
		||||
      if (param->write.status != ESP_GATT_OK) {
 | 
			
		||||
        // ESP_GATT_INVALID_ATTR_LEN
 | 
			
		||||
        ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      // [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0
 | 
			
		||||
      // This might be the enable-notify descriptor? (or disable-notify)
 | 
			
		||||
      ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle,
 | 
			
		||||
               param->write.status);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_WRITE_CHAR_EVT: {
 | 
			
		||||
      if (param->write.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      if (param->write.handle == this->char_handle_cmd_) {
 | 
			
		||||
        if (this->force_refresh_) {
 | 
			
		||||
          // Command write was successful. Publish the pending state, hoping that notify will kick in.
 | 
			
		||||
          this->publish_state();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      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->char_handle_status_) {
 | 
			
		||||
        // This is the additional packet that doesn't fit in the notify packet.
 | 
			
		||||
        this->codec_->decode_extra(param->read.value, param->read.value_len);
 | 
			
		||||
      } else if (param->read.handle == this->char_handle_name_) {
 | 
			
		||||
        // The data should represent the name.
 | 
			
		||||
        if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) {
 | 
			
		||||
          std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len);
 | 
			
		||||
          // this->set_name(bedjet_name);
 | 
			
		||||
          ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str());
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      // This event means that ESP received the request to enable notifications on the client side. But we also have to
 | 
			
		||||
      // tell the server that we want it to send notifications. Normally BLEClient parent would handle this
 | 
			
		||||
      // automatically, but as soon as we set our status to Established, the parent is going to purge all the
 | 
			
		||||
      // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable
 | 
			
		||||
      // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write
 | 
			
		||||
      // doesn't break anything.
 | 
			
		||||
 | 
			
		||||
      if (param->reg_for_notify.handle != this->char_handle_status_) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x",
 | 
			
		||||
                 this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this->write_notify_config_descriptor_(true);
 | 
			
		||||
      this->last_notify_ = 0;
 | 
			
		||||
      this->force_refresh_ = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      // This event is not handled by the parent BLEClient, so we need to do this either way.
 | 
			
		||||
      if (param->unreg_for_notify.handle != this->char_handle_status_) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x",
 | 
			
		||||
                 this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this->write_notify_config_descriptor_(false);
 | 
			
		||||
      this->last_notify_ = 0;
 | 
			
		||||
      // Now we wait until the next update() poll to re-register notify...
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.handle != this->char_handle_status_) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(),
 | 
			
		||||
                 this->char_handle_status_, param->notify.handle);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we
 | 
			
		||||
      //  throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds).
 | 
			
		||||
      //  Another idea would be to keep notify off by default, and use update() as an opportunity to turn on
 | 
			
		||||
      //  notify to get enough data to update status, then turn off notify again.
 | 
			
		||||
 | 
			
		||||
      uint32_t now = millis();
 | 
			
		||||
      auto delta = now - this->last_notify_;
 | 
			
		||||
 | 
			
		||||
      if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) {
 | 
			
		||||
        bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len);
 | 
			
		||||
        this->last_notify_ = now;
 | 
			
		||||
 | 
			
		||||
        if (needs_extra) {
 | 
			
		||||
          // this means the packet was partial, so read the status characteristic to get the second part.
 | 
			
		||||
          auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id,
 | 
			
		||||
                                                this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
          if (status) {
 | 
			
		||||
            ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str());
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this->force_refresh_) {
 | 
			
		||||
          // If we requested an immediate update, do that now.
 | 
			
		||||
          this->update();
 | 
			
		||||
          this->force_refresh_ = false;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT.
 | 
			
		||||
 *
 | 
			
		||||
 * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order
 | 
			
		||||
 * to undo the same on unregister. It also allows us to maintain the config descriptor separately,
 | 
			
		||||
 * since the parent BLEClient is going to purge all descriptors once we set our connection status
 | 
			
		||||
 * to `Established`.
 | 
			
		||||
 */
 | 
			
		||||
uint8_t Bedjet::write_notify_config_descriptor_(bool enable) {
 | 
			
		||||
  auto handle = this->config_descr_status_;
 | 
			
		||||
  if (handle == 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_);
 | 
			
		||||
    return -1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits.
 | 
			
		||||
  uint8_t notify_en[] = {0, 0};
 | 
			
		||||
  notify_en[0] = enable;
 | 
			
		||||
  auto status =
 | 
			
		||||
      esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en),
 | 
			
		||||
                                     ¬ify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (status) {
 | 
			
		||||
    ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status);
 | 
			
		||||
    return status;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false",
 | 
			
		||||
           handle);
 | 
			
		||||
  return ESP_GATT_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
/** Attempts to sync the local time (via `time_id`) to the BedJet device. */
 | 
			
		||||
void Bedjet::send_local_time() {
 | 
			
		||||
  if (this->time_id_.has_value()) {
 | 
			
		||||
    auto *time_id = *this->time_id_;
 | 
			
		||||
    time::ESPTime now = time_id->now();
 | 
			
		||||
    if (now.is_valid()) {
 | 
			
		||||
      this->set_clock(now.hour, now.minute);
 | 
			
		||||
      ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Initializes time sync callbacks to support syncing current time to the BedJet. */
 | 
			
		||||
void Bedjet::setup_time_() {
 | 
			
		||||
  if (this->time_id_.has_value()) {
 | 
			
		||||
    this->send_local_time();
 | 
			
		||||
    auto *time_id = *this->time_id_;
 | 
			
		||||
    time_id->add_on_time_sync_callback([this] { this->send_local_time(); });
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
/** Attempt to set the BedJet device's clock to the specified time. */
 | 
			
		||||
void Bedjet::set_clock(uint8_t hour, uint8_t minute) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute);
 | 
			
		||||
  auto status = this->write_bedjet_packet_(pkt);
 | 
			
		||||
  if (status) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */
 | 
			
		||||
uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    if (!this->parent_->enabled) {
 | 
			
		||||
      ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str());
 | 
			
		||||
    }
 | 
			
		||||
    return -1;
 | 
			
		||||
  }
 | 
			
		||||
  auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_,
 | 
			
		||||
                                         pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP,
 | 
			
		||||
                                         ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  return status;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */
 | 
			
		||||
uint8_t Bedjet::set_notify_(const bool enable) {
 | 
			
		||||
  uint8_t status;
 | 
			
		||||
  if (enable) {
 | 
			
		||||
    status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
 | 
			
		||||
                                               this->char_handle_status_);
 | 
			
		||||
    if (status) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda,
 | 
			
		||||
                                                 this->char_handle_status_);
 | 
			
		||||
    if (status) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status);
 | 
			
		||||
  return status;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Attempts to update the climate device from the last received BedjetStatusPacket.
 | 
			
		||||
 *
 | 
			
		||||
 * @return `true` if the status has been applied; `false` if there is nothing to apply.
 | 
			
		||||
 */
 | 
			
		||||
bool Bedjet::update_status_() {
 | 
			
		||||
  if (!this->codec_->has_status())
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  BedjetStatusPacket status = *this->codec_->get_status_packet();
 | 
			
		||||
 | 
			
		||||
  auto converted_temp = bedjet_temp_to_c(status.target_temp_step);
 | 
			
		||||
  if (converted_temp > 0)
 | 
			
		||||
    this->target_temperature = converted_temp;
 | 
			
		||||
  converted_temp = bedjet_temp_to_c(status.ambient_temp_step);
 | 
			
		||||
  if (converted_temp > 0)
 | 
			
		||||
    this->current_temperature = converted_temp;
 | 
			
		||||
 | 
			
		||||
  const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step);
 | 
			
		||||
  if (fan_mode_name != nullptr) {
 | 
			
		||||
    this->custom_fan_mode = *fan_mode_name;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
 | 
			
		||||
  switch (status.mode) {
 | 
			
		||||
    case MODE_WAIT:  // Biorhythm "wait" step: device is idle
 | 
			
		||||
    case MODE_STANDBY:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_OFF;
 | 
			
		||||
      this->action = climate::CLIMATE_ACTION_IDLE;
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_OFF;
 | 
			
		||||
      this->custom_preset.reset();
 | 
			
		||||
      this->preset.reset();
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case MODE_HEAT:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
      this->action = climate::CLIMATE_ACTION_HEATING;
 | 
			
		||||
      this->preset.reset();
 | 
			
		||||
      if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
 | 
			
		||||
        this->set_custom_preset_("LTD HT");
 | 
			
		||||
      } else {
 | 
			
		||||
        this->custom_preset.reset();
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case MODE_EXTHT:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
      this->action = climate::CLIMATE_ACTION_HEATING;
 | 
			
		||||
      this->preset.reset();
 | 
			
		||||
      if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
 | 
			
		||||
        this->custom_preset.reset();
 | 
			
		||||
      } else {
 | 
			
		||||
        this->set_custom_preset_("EXT HT");
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case MODE_COOL:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
      this->action = climate::CLIMATE_ACTION_COOLING;
 | 
			
		||||
      this->custom_preset.reset();
 | 
			
		||||
      this->preset.reset();
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case MODE_DRY:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_DRY;
 | 
			
		||||
      this->action = climate::CLIMATE_ACTION_DRYING;
 | 
			
		||||
      this->custom_preset.reset();
 | 
			
		||||
      this->preset.reset();
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case MODE_TURBO:
 | 
			
		||||
      this->preset = climate::CLIMATE_PRESET_BOOST;
 | 
			
		||||
      this->custom_preset.reset();
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
      this->action = climate::CLIMATE_ACTION_HEATING;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->is_valid_()) {
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
    this->codec_->clear_status();
 | 
			
		||||
    this->status_clear_warning();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Bedjet::update() {
 | 
			
		||||
  ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str());
 | 
			
		||||
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    if (!this->parent()->enabled) {
 | 
			
		||||
      ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      // Possibly still trying to connect.
 | 
			
		||||
      ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto result = this->update_status_();
 | 
			
		||||
  if (!result) {
 | 
			
		||||
    uint32_t now = millis();
 | 
			
		||||
    uint32_t diff = now - this->last_notify_;
 | 
			
		||||
 | 
			
		||||
    if (this->last_notify_ == 0) {
 | 
			
		||||
      // This means we're connected and haven't received a notification, so it likely means that the BedJet is off.
 | 
			
		||||
      // However, it could also mean that it's running, but failing to send notifications.
 | 
			
		||||
      // We can try to unregister for notifications now, and then re-register, hoping to clear it up...
 | 
			
		||||
      // But how do we know for sure which state we're in, and how do we actually clear out the buggy state?
 | 
			
		||||
 | 
			
		||||
      ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str());
 | 
			
		||||
      this->set_notify_(false);
 | 
			
		||||
    } else if (diff > NOTIFY_WARN_THRESHOLD) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_);
 | 
			
		||||
      this->parent()->set_enabled(false);
 | 
			
		||||
      this->parent()->set_enabled(true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										133
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "bedjet_base.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
#include "esphome/components/time/real_time_clock.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bedjet {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574");
 | 
			
		||||
static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574");
 | 
			
		||||
static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574");
 | 
			
		||||
static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574");
 | 
			
		||||
 | 
			
		||||
class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void update() 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 dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
  void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
 | 
			
		||||
  void send_local_time();
 | 
			
		||||
#endif
 | 
			
		||||
  void set_clock(uint8_t hour, uint8_t minute);
 | 
			
		||||
  void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
 | 
			
		||||
  /** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */
 | 
			
		||||
  void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; }
 | 
			
		||||
 | 
			
		||||
  /** Attempts to check for and apply firmware updates. */
 | 
			
		||||
  void upgrade_firmware();
 | 
			
		||||
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.set_supports_action(true);
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.set_supported_modes({
 | 
			
		||||
        climate::CLIMATE_MODE_OFF,
 | 
			
		||||
        climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
        // climate::CLIMATE_MODE_TURBO // Not supported by Climate: see presets instead
 | 
			
		||||
        climate::CLIMATE_MODE_FAN_ONLY,
 | 
			
		||||
        climate::CLIMATE_MODE_DRY,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // It would be better if we had a slider for the fan modes.
 | 
			
		||||
    traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
 | 
			
		||||
    traits.set_supported_presets({
 | 
			
		||||
        // If we support NONE, then have to decide what happens if the user switches to it (turn off?)
 | 
			
		||||
        // climate::CLIMATE_PRESET_NONE,
 | 
			
		||||
        // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
 | 
			
		||||
        climate::CLIMATE_PRESET_BOOST,
 | 
			
		||||
    });
 | 
			
		||||
    traits.set_supported_custom_presets({
 | 
			
		||||
        // We could fetch biodata from bedjet and set these names that way.
 | 
			
		||||
        // But then we have to invert the lookup in order to send the right preset.
 | 
			
		||||
        // For now, we can leave them as M1-3 to match the remote buttons.
 | 
			
		||||
        // EXT HT added to match remote button.
 | 
			
		||||
        "EXT HT",
 | 
			
		||||
        "M1",
 | 
			
		||||
        "M2",
 | 
			
		||||
        "M3",
 | 
			
		||||
    });
 | 
			
		||||
    if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
 | 
			
		||||
      traits.add_supported_custom_preset("LTD HT");
 | 
			
		||||
    } else {
 | 
			
		||||
      traits.add_supported_custom_preset("EXT HT");
 | 
			
		||||
    }
 | 
			
		||||
    traits.set_visual_min_temperature(19.0);
 | 
			
		||||
    traits.set_visual_max_temperature(43.0);
 | 
			
		||||
    traits.set_visual_temperature_step(1.0);
 | 
			
		||||
    return traits;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const climate::ClimateCall &call) override;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
  void setup_time_();
 | 
			
		||||
  optional<time::RealTimeClock *> time_id_{};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
 | 
			
		||||
  BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT;
 | 
			
		||||
 | 
			
		||||
  static const uint32_t MIN_NOTIFY_THROTTLE = 5000;
 | 
			
		||||
  static const uint32_t NOTIFY_WARN_THRESHOLD = 300000;
 | 
			
		||||
  static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000;
 | 
			
		||||
 | 
			
		||||
  uint8_t set_notify_(bool enable);
 | 
			
		||||
  uint8_t write_bedjet_packet_(BedjetPacket *pkt);
 | 
			
		||||
  void reset_state_();
 | 
			
		||||
  bool update_status_();
 | 
			
		||||
 | 
			
		||||
  bool is_valid_() {
 | 
			
		||||
    // FIXME: find a better way to check this?
 | 
			
		||||
    return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) &&
 | 
			
		||||
           this->current_temperature > 1 && this->target_temperature > 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t last_notify_ = 0;
 | 
			
		||||
  bool force_refresh_ = false;
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<BedjetCodec> codec_;
 | 
			
		||||
  uint16_t char_handle_cmd_;
 | 
			
		||||
  uint16_t char_handle_name_;
 | 
			
		||||
  uint16_t char_handle_status_;
 | 
			
		||||
  uint16_t config_descr_status_;
 | 
			
		||||
 | 
			
		||||
  uint8_t write_notify_config_descriptor_(bool enable);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
#include "bedjet_base.h"
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bedjet {
 | 
			
		||||
 | 
			
		||||
/// Converts a BedJet temp step into degrees Fahrenheit.
 | 
			
		||||
float bedjet_temp_to_f(const uint8_t temp) {
 | 
			
		||||
  // BedJet temp is "C*2"; to get F, multiply by 0.9 (half 1.8) and add 32.
 | 
			
		||||
  return 0.9f * temp + 32.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Cleans up the packet before sending. */
 | 
			
		||||
BedjetPacket *BedjetCodec::clean_packet_() {
 | 
			
		||||
  // So far no commands require more than 2 bytes of data.
 | 
			
		||||
  assert(this->packet_.data_length <= 2);
 | 
			
		||||
  for (int i = this->packet_.data_length; i < 2; i++) {
 | 
			
		||||
    this->packet_.data[i] = '\0';
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Created packet: %02X, %02X %02X", this->packet_.command, this->packet_.data[0], this->packet_.data[1]);
 | 
			
		||||
  return &this->packet_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Returns a BedjetPacket that will initiate a BedjetButton press. */
 | 
			
		||||
BedjetPacket *BedjetCodec::get_button_request(BedjetButton button) {
 | 
			
		||||
  this->packet_.command = CMD_BUTTON;
 | 
			
		||||
  this->packet_.data_length = 1;
 | 
			
		||||
  this->packet_.data[0] = button;
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Returns a BedjetPacket that will set the device's target `temperature`. */
 | 
			
		||||
BedjetPacket *BedjetCodec::get_set_target_temp_request(float temperature) {
 | 
			
		||||
  this->packet_.command = CMD_SET_TEMP;
 | 
			
		||||
  this->packet_.data_length = 1;
 | 
			
		||||
  this->packet_.data[0] = temperature * 2;
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Returns a BedjetPacket that will set the device's target fan speed. */
 | 
			
		||||
BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) {
 | 
			
		||||
  this->packet_.command = CMD_SET_FAN;
 | 
			
		||||
  this->packet_.data_length = 1;
 | 
			
		||||
  this->packet_.data[0] = fan_step;
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Returns a BedjetPacket that will set the device's current time. */
 | 
			
		||||
BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) {
 | 
			
		||||
  this->packet_.command = CMD_SET_TIME;
 | 
			
		||||
  this->packet_.data_length = 2;
 | 
			
		||||
  this->packet_.data[0] = hour;
 | 
			
		||||
  this->packet_.data[1] = minute;
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Decodes the extra bytes that were received after being notified with a partial packet. */
 | 
			
		||||
void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
 | 
			
		||||
  ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
 | 
			
		||||
  uint8_t offset = this->last_buffer_size_;
 | 
			
		||||
  if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) {
 | 
			
		||||
    memcpy(((uint8_t *) (&this->buf_)) + offset, data, length);
 | 
			
		||||
    ESP_LOGV(TAG,
 | 
			
		||||
             "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, "
 | 
			
		||||
             "flags=BedjetFlags <conn=%c, leds=%c, units=%c, mute=%c, others=%02x>",
 | 
			
		||||
             this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase,
 | 
			
		||||
             this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0',
 | 
			
		||||
             this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0',
 | 
			
		||||
             this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01));
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset,
 | 
			
		||||
             sizeof(BedjetStatusPacket), length + offset);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Decodes the incoming status packet received on the BEDJET_STATUS_UUID.
 | 
			
		||||
 *
 | 
			
		||||
 * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise.
 | 
			
		||||
 */
 | 
			
		||||
bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
 | 
			
		||||
  ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
 | 
			
		||||
 | 
			
		||||
  if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) {
 | 
			
		||||
    this->status_packet_.reset();
 | 
			
		||||
 | 
			
		||||
    // Clear old buffer
 | 
			
		||||
    memset(&this->buf_, 0, sizeof(BedjetStatusPacket));
 | 
			
		||||
    // Copy new data into buffer
 | 
			
		||||
    memcpy(&this->buf_, data, length);
 | 
			
		||||
    this->last_buffer_size_ = length;
 | 
			
		||||
 | 
			
		||||
    // TODO: validate the packet checksum?
 | 
			
		||||
    if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 &&
 | 
			
		||||
        this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 &&
 | 
			
		||||
        this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) {
 | 
			
		||||
      // and save it for the update() loop
 | 
			
		||||
      this->status_packet_ = this->buf_;
 | 
			
		||||
      return this->buf_.is_partial == 1;
 | 
			
		||||
    } else {
 | 
			
		||||
      // TODO: log a warning if we detect that we connected to a non-V3 device.
 | 
			
		||||
      ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) {
 | 
			
		||||
    // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself.
 | 
			
		||||
    ESP_LOGV(TAG,
 | 
			
		||||
             "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF;  [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
 | 
			
		||||
             "[12]=%d, [-1]=%d",
 | 
			
		||||
             bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9],
 | 
			
		||||
             data[10], data[11], data[12], data[length - 1]);
 | 
			
		||||
 | 
			
		||||
    if (this->has_status()) {
 | 
			
		||||
      this->status_packet_->ambient_temp_step = data[6];
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // TODO: log a warning if we detect that we connected to a non-V3 device.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include "bedjet_const.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bedjet {
 | 
			
		||||
 | 
			
		||||
struct BedjetPacket {
 | 
			
		||||
  uint8_t data_length;
 | 
			
		||||
  BedjetCommand command;
 | 
			
		||||
  uint8_t data[2];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct BedjetFlags {
 | 
			
		||||
  /* uint8_t */
 | 
			
		||||
  int a_ : 1;                // 0x80
 | 
			
		||||
  int b_ : 1;                // 0x40
 | 
			
		||||
  int conn_test_passed : 1;  ///< (0x20) Bit is set `1` if the last connection test passed.
 | 
			
		||||
  int leds_enabled : 1;      ///< (0x10) Bit is set `1` if the LEDs on the device are enabled.
 | 
			
		||||
  int c_ : 1;                // 0x08
 | 
			
		||||
  int units_setup : 1;       ///< (0x04) Bit is set `1` if the device's units have been configured.
 | 
			
		||||
  int d_ : 1;                // 0x02
 | 
			
		||||
  int beeps_muted : 1;       ///< (0x01) Bit is set `1` if the device's sound output is muted.
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
enum BedjetPacketFormat : uint8_t {
 | 
			
		||||
  PACKET_FORMAT_DEBUG = 0x05,    //  5
 | 
			
		||||
  PACKET_FORMAT_V3_HOME = 0x56,  // 86
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum BedjetPacketType : uint8_t {
 | 
			
		||||
  PACKET_TYPE_STATUS = 0x1,
 | 
			
		||||
  PACKET_TYPE_DEBUG = 0x2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** The format of a BedJet V3 status packet. */
 | 
			
		||||
struct BedjetStatusPacket {
 | 
			
		||||
  // [0]
 | 
			
		||||
  uint8_t is_partial : 8;  ///< `1` indicates that this is a partial packet, and more data can be read directly from the
 | 
			
		||||
                           ///< characteristic.
 | 
			
		||||
  BedjetPacketFormat packet_format : 8;  ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet
 | 
			
		||||
                                         ///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets.
 | 
			
		||||
  uint8_t
 | 
			
		||||
      expecting_length : 8;  ///< The expected total length of the status packet after merging the additional packet.
 | 
			
		||||
  BedjetPacketType packet_type : 8;  ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet.
 | 
			
		||||
 | 
			
		||||
  // [4]
 | 
			
		||||
  uint8_t time_remaining_hrs : 8;   ///< Hours remaining in program runtime
 | 
			
		||||
  uint8_t time_remaining_mins : 8;  ///< Minutes remaining in program runtime
 | 
			
		||||
  uint8_t time_remaining_secs : 8;  ///< Seconds remaining in program runtime
 | 
			
		||||
 | 
			
		||||
  // [7]
 | 
			
		||||
  uint8_t actual_temp_step : 8;  ///< Actual temp of the air blown by the BedJet fan; value represents `2 *
 | 
			
		||||
                                 ///< degrees_celsius`. See #bedjet_temp_to_c and #bedjet_temp_to_f
 | 
			
		||||
  uint8_t target_temp_step : 8;  ///< Target temp that the BedJet will try to heat to. See #actual_temp_step.
 | 
			
		||||
 | 
			
		||||
  // [9]
 | 
			
		||||
  BedjetMode mode : 8;  ///< BedJet operating mode.
 | 
			
		||||
 | 
			
		||||
  // [10]
 | 
			
		||||
  uint8_t fan_step : 8;  ///< BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5
 | 
			
		||||
                         ///< * fan_step`
 | 
			
		||||
  uint8_t max_hrs : 8;   ///< Max hours of mode runtime
 | 
			
		||||
  uint8_t max_mins : 8;  ///< Max minutes of mode runtime
 | 
			
		||||
  uint8_t min_temp_step : 8;  ///< Min temp allowed in mode. See #actual_temp_step.
 | 
			
		||||
  uint8_t max_temp_step : 8;  ///< Max temp allowed in mode. See #actual_temp_step.
 | 
			
		||||
 | 
			
		||||
  // [15-16]
 | 
			
		||||
  uint16_t turbo_time : 16;  ///< Time remaining in BedjetMode::MODE_TURBO.
 | 
			
		||||
 | 
			
		||||
  // [17]
 | 
			
		||||
  uint8_t ambient_temp_step : 8;  ///< Current ambient air temp. This is the coldest air the BedJet can blow. See
 | 
			
		||||
                                  ///< #actual_temp_step.
 | 
			
		||||
  uint8_t shutdown_reason : 8;    ///< The reason for the last device shutdown.
 | 
			
		||||
 | 
			
		||||
  // [19-25]; the initial partial packet cuts off here after [19]
 | 
			
		||||
  // Skip 7 bytes?
 | 
			
		||||
  uint32_t _skip_1_ : 32;  // Unknown 19-22 = 0x01810112
 | 
			
		||||
 | 
			
		||||
  uint16_t _skip_2_ : 16;  // Unknown 23-24 = 0x1310
 | 
			
		||||
  uint8_t _skip_3_ : 8;    // Unknown 25 = 0x00
 | 
			
		||||
 | 
			
		||||
  // [26]
 | 
			
		||||
  //   0x18(24) = "Connection test has completed OK"
 | 
			
		||||
  //   0x1a(26) = "Firmware update is not needed"
 | 
			
		||||
  uint8_t update_phase : 8;  ///< The current status/phase of a firmware update.
 | 
			
		||||
 | 
			
		||||
  // [27]
 | 
			
		||||
  // FIXME: cannot nest packed struct of matching length here?
 | 
			
		||||
  /* BedjetFlags */ uint8_t flags : 8;  /// See BedjetFlags for the packed byte flags.
 | 
			
		||||
  // [28-31]; 20+11 bytes
 | 
			
		||||
  uint32_t _skip_4_ : 32;  // Unknown
 | 
			
		||||
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
/** This class is responsible for encoding command packets and decoding status packets.
 | 
			
		||||
 *
 | 
			
		||||
 * Status Packets
 | 
			
		||||
 * ==============
 | 
			
		||||
 * The BedJet protocol depends on registering for notifications on the esphome::BedJet::BEDJET_SERVICE_UUID
 | 
			
		||||
 * characteristic. If the BedJet is on, it will send rapid updates as notifications. If it is off,
 | 
			
		||||
 * it generally will not notify of any status.
 | 
			
		||||
 *
 | 
			
		||||
 * As the BedJet V3's BedjetStatusPacket exceeds the buffer size allowed for BLE notification packets,
 | 
			
		||||
 * the notification packet will contain `BedjetStatusPacket::is_partial == 1`. When that happens, an additional
 | 
			
		||||
 * read of the esphome::BedJet::BEDJET_SERVICE_UUID characteristic will contain the second portion of the
 | 
			
		||||
 * full status packet.
 | 
			
		||||
 *
 | 
			
		||||
 * Command Packets
 | 
			
		||||
 * ===============
 | 
			
		||||
 * This class supports encoding a number of BedjetPacket commands:
 | 
			
		||||
 * - Button press
 | 
			
		||||
 *   This simulates a press of one of the BedjetButton values.
 | 
			
		||||
 *   - BedjetPacket#command = BedjetCommand::CMD_BUTTON
 | 
			
		||||
 *   - BedjetPacket#data [0] contains the BedjetButton value
 | 
			
		||||
 * - Set target temp
 | 
			
		||||
 *   This sets the BedJet's target temp to a concrete temperature value.
 | 
			
		||||
 *   - BedjetPacket#command = BedjetCommand::CMD_SET_TEMP
 | 
			
		||||
 *   - BedjetPacket#data [0] contains the BedJet temp value; see BedjetStatusPacket#actual_temp_step
 | 
			
		||||
 * - Set fan speed
 | 
			
		||||
 *   This sets the BedJet fan speed.
 | 
			
		||||
 *   - BedjetPacket#command = BedjetCommand::CMD_SET_FAN
 | 
			
		||||
 *   - BedjetPacket#data [0] contains the BedJet fan step in the range 0-19.
 | 
			
		||||
 * - Set current time
 | 
			
		||||
 *   The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might
 | 
			
		||||
 *   contain time-of-day based step rules.
 | 
			
		||||
 *   - BedjetPacket#command = BedjetCommand::CMD_SET_TIME
 | 
			
		||||
 *   - BedjetPacket#data [0] is hours, [1] is minutes
 | 
			
		||||
 */
 | 
			
		||||
class BedjetCodec {
 | 
			
		||||
 public:
 | 
			
		||||
  BedjetPacket *get_button_request(BedjetButton button);
 | 
			
		||||
  BedjetPacket *get_set_target_temp_request(float temperature);
 | 
			
		||||
  BedjetPacket *get_set_fan_speed_request(uint8_t fan_step);
 | 
			
		||||
  BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute);
 | 
			
		||||
 | 
			
		||||
  bool decode_notify(const uint8_t *data, uint16_t length);
 | 
			
		||||
  void decode_extra(const uint8_t *data, uint16_t length);
 | 
			
		||||
 | 
			
		||||
  inline bool has_status() { return this->status_packet_.has_value(); }
 | 
			
		||||
  const optional<BedjetStatusPacket> &get_status_packet() const { return this->status_packet_; }
 | 
			
		||||
  void clear_status() { this->status_packet_.reset(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  BedjetPacket *clean_packet_();
 | 
			
		||||
 | 
			
		||||
  uint8_t last_buffer_size_ = 0;
 | 
			
		||||
 | 
			
		||||
  BedjetPacket packet_;
 | 
			
		||||
 | 
			
		||||
  optional<BedjetStatusPacket> status_packet_;
 | 
			
		||||
  BedjetStatusPacket buf_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										86
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <set>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bedjet {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bedjet";
 | 
			
		||||
 | 
			
		||||
enum BedjetMode : uint8_t {
 | 
			
		||||
  /// BedJet is Off
 | 
			
		||||
  MODE_STANDBY = 0,
 | 
			
		||||
  /// BedJet is in Heat mode (limited to 4 hours)
 | 
			
		||||
  MODE_HEAT = 1,
 | 
			
		||||
  /// BedJet is in Turbo mode (high heat, limited time)
 | 
			
		||||
  MODE_TURBO = 2,
 | 
			
		||||
  /// BedJet is in Extended Heat mode (limited to 10 hours)
 | 
			
		||||
  MODE_EXTHT = 3,
 | 
			
		||||
  /// BedJet is in Cool mode (actually "Fan only" mode)
 | 
			
		||||
  MODE_COOL = 4,
 | 
			
		||||
  /// BedJet is in Dry mode (high speed, no heat)
 | 
			
		||||
  MODE_DRY = 5,
 | 
			
		||||
  /// BedJet is in "wait" mode, a step during a biorhythm program
 | 
			
		||||
  MODE_WAIT = 6,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Optional heating strategies to use for climate::CLIMATE_MODE_HEAT. */
 | 
			
		||||
enum BedjetHeatMode {
 | 
			
		||||
  /// HVACMode.HEAT is handled using BTN_HEAT (default)
 | 
			
		||||
  HEAT_MODE_HEAT,
 | 
			
		||||
  /// HVACMode.HEAT is handled using BTN_EXTHT
 | 
			
		||||
  HEAT_MODE_EXTENDED,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum BedjetButton : uint8_t {
 | 
			
		||||
  /// Turn BedJet off
 | 
			
		||||
  BTN_OFF = 0x1,
 | 
			
		||||
  /// Enter Cool mode (fan only)
 | 
			
		||||
  BTN_COOL = 0x2,
 | 
			
		||||
  /// Enter Heat mode (limited to 4 hours)
 | 
			
		||||
  BTN_HEAT = 0x3,
 | 
			
		||||
  /// Enter Turbo mode (high heat, limited to 10 minutes)
 | 
			
		||||
  BTN_TURBO = 0x4,
 | 
			
		||||
  /// Enter Dry mode (high speed, no heat)
 | 
			
		||||
  BTN_DRY = 0x5,
 | 
			
		||||
  /// Enter Extended Heat mode (limited to 10 hours)
 | 
			
		||||
  BTN_EXTHT = 0x6,
 | 
			
		||||
 | 
			
		||||
  /// Start the M1 biorhythm/preset program
 | 
			
		||||
  BTN_M1 = 0x20,
 | 
			
		||||
  /// Start the M2 biorhythm/preset program
 | 
			
		||||
  BTN_M2 = 0x21,
 | 
			
		||||
  /// Start the M3 biorhythm/preset program
 | 
			
		||||
  BTN_M3 = 0x22,
 | 
			
		||||
 | 
			
		||||
  /* These are "MAGIC" buttons */
 | 
			
		||||
 | 
			
		||||
  /// Turn debug mode on/off
 | 
			
		||||
  MAGIC_DEBUG_ON = 0x40,
 | 
			
		||||
  MAGIC_DEBUG_OFF = 0x41,
 | 
			
		||||
  /// Perform a connection test.
 | 
			
		||||
  MAGIC_CONNTEST = 0x42,
 | 
			
		||||
  /// Request a firmware update. This will also restart the Bedjet.
 | 
			
		||||
  MAGIC_UPDATE = 0x43,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum BedjetCommand : uint8_t {
 | 
			
		||||
  CMD_BUTTON = 0x1,
 | 
			
		||||
  CMD_SET_TEMP = 0x3,
 | 
			
		||||
  CMD_STATUS = 0x6,
 | 
			
		||||
  CMD_SET_FAN = 0x7,
 | 
			
		||||
  CMD_SET_TIME = 0x8,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#define BEDJET_FAN_STEP_NAMES_ \
 | 
			
		||||
  { \
 | 
			
		||||
    "5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \
 | 
			
		||||
        "85%", "90%", "95%", "100%" \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										52
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import climate, ble_client, time
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HEAT_MODE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_RECEIVE_TIMEOUT,
 | 
			
		||||
    CONF_TIME_ID,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@jhansche"]
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
 | 
			
		||||
bedjet_ns = cg.esphome_ns.namespace("bedjet")
 | 
			
		||||
Bedjet = bedjet_ns.class_(
 | 
			
		||||
    "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
 | 
			
		||||
)
 | 
			
		||||
BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode")
 | 
			
		||||
BEDJET_HEAT_MODES = {
 | 
			
		||||
    "heat": BedjetHeatMode.HEAT_MODE_HEAT,
 | 
			
		||||
    "extended": BedjetHeatMode.HEAT_MODE_EXTENDED,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Bedjet),
 | 
			
		||||
            cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
 | 
			
		||||
                BEDJET_HEAT_MODES, lower=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_RECEIVE_TIMEOUT, default="0s"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
    .extend(cv.polling_component_schema("30s"))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
    cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))
 | 
			
		||||
    if CONF_TIME_ID in config:
 | 
			
		||||
        time_ = await cg.get_variable(config[CONF_TIME_ID])
 | 
			
		||||
        cg.add(var.set_time_id(time_))
 | 
			
		||||
    if CONF_RECEIVE_TIMEOUT in config:
 | 
			
		||||
        cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT]))
 | 
			
		||||
@@ -69,7 +69,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool BinarySensor::has_state() const { return this->has_state_; }
 | 
			
		||||
uint32_t BinarySensor::hash_base() { return 1210250844UL; }
 | 
			
		||||
bool BinarySensor::is_status_binary_sensor() const { return false; }
 | 
			
		||||
 | 
			
		||||
}  // namespace binary_sensor
 | 
			
		||||
 
 | 
			
		||||
@@ -76,8 +76,6 @@ class BinarySensor : public EntityBase {
 | 
			
		||||
  virtual std::string device_class();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void(bool)> state_callback_{};
 | 
			
		||||
  optional<std::string> device_class_{};  ///< Stores the override of the device class
 | 
			
		||||
  Filter *filter_list_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/bl0939/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bl0939/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@ziceva"]
 | 
			
		||||
							
								
								
									
										144
									
								
								esphome/components/bl0939/bl0939.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								esphome/components/bl0939/bl0939.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
#include "bl0939.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bl0939 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bl0939";
 | 
			
		||||
 | 
			
		||||
// https://www.belling.com.cn/media/file_object/bel_product/BL0939/datasheet/BL0939_V1.2_cn.pdf
 | 
			
		||||
// (unfortunatelly chinese, but the protocol can be understood with some translation tool)
 | 
			
		||||
static const uint8_t BL0939_READ_COMMAND = 0x55;  // 0x5{A4,A3,A2,A1}
 | 
			
		||||
static const uint8_t BL0939_FULL_PACKET = 0xAA;
 | 
			
		||||
static const uint8_t BL0939_PACKET_HEADER = 0x55;
 | 
			
		||||
 | 
			
		||||
static const uint8_t BL0939_WRITE_COMMAND = 0xA5;  // 0xA{A4,A3,A2,A1}
 | 
			
		||||
static const uint8_t BL0939_REG_IA_FAST_RMS_CTRL = 0x10;
 | 
			
		||||
static const uint8_t BL0939_REG_IB_FAST_RMS_CTRL = 0x1E;
 | 
			
		||||
static const uint8_t BL0939_REG_MODE = 0x18;
 | 
			
		||||
static const uint8_t BL0939_REG_SOFT_RESET = 0x19;
 | 
			
		||||
static const uint8_t BL0939_REG_USR_WRPROT = 0x1A;
 | 
			
		||||
static const uint8_t BL0939_REG_TPS_CTRL = 0x1B;
 | 
			
		||||
 | 
			
		||||
const uint8_t BL0939_INIT[6][6] = {
 | 
			
		||||
    // Reset to default
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33},
 | 
			
		||||
    // Enable User Operation Write
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xEB},
 | 
			
		||||
    // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_MODE, 0x00, 0x10, 0x00, 0x32},
 | 
			
		||||
    // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xF9},
 | 
			
		||||
    // 0x181C = Half cycle, Fast RMS threshold 6172
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_IA_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x16},
 | 
			
		||||
    // 0x181C = Half cycle, Fast RMS threshold 6172
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_IB_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x08}};
 | 
			
		||||
 | 
			
		||||
void BL0939::loop() {
 | 
			
		||||
  DataPacket buffer;
 | 
			
		||||
  if (!this->available()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (read_array((uint8_t *) &buffer, sizeof(buffer))) {
 | 
			
		||||
    if (validate_checksum(&buffer)) {
 | 
			
		||||
      received_package_(&buffer);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
 | 
			
		||||
    while (read() >= 0)
 | 
			
		||||
      ;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BL0939::validate_checksum(const DataPacket *data) {
 | 
			
		||||
  uint8_t checksum = BL0939_READ_COMMAND;
 | 
			
		||||
  // Whole package but checksum
 | 
			
		||||
  for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) {
 | 
			
		||||
    checksum += data->raw[i];
 | 
			
		||||
  }
 | 
			
		||||
  checksum ^= 0xFF;
 | 
			
		||||
  if (checksum != data->checksum) {
 | 
			
		||||
    ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
 | 
			
		||||
  }
 | 
			
		||||
  return checksum == data->checksum;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0939::update() {
 | 
			
		||||
  this->flush();
 | 
			
		||||
  this->write_byte(BL0939_READ_COMMAND);
 | 
			
		||||
  this->write_byte(BL0939_FULL_PACKET);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0939::setup() {
 | 
			
		||||
  for (auto *i : BL0939_INIT) {
 | 
			
		||||
    this->write_array(i, 6);
 | 
			
		||||
    delay(1);
 | 
			
		||||
  }
 | 
			
		||||
  this->flush();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0939::received_package_(const DataPacket *data) const {
 | 
			
		||||
  // Bad header
 | 
			
		||||
  if (data->frame_header != BL0939_PACKET_HEADER) {
 | 
			
		||||
    ESP_LOGI("bl0939", "Invalid data. Header mismatch: %d", data->frame_header);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_;
 | 
			
		||||
  float ia_rms = (float) to_uint32_t(data->ia_rms) / current_reference_;
 | 
			
		||||
  float ib_rms = (float) to_uint32_t(data->ib_rms) / current_reference_;
 | 
			
		||||
  float a_watt = (float) to_int32_t(data->a_watt) / power_reference_;
 | 
			
		||||
  float b_watt = (float) to_int32_t(data->b_watt) / power_reference_;
 | 
			
		||||
  int32_t cfa_cnt = to_int32_t(data->cfa_cnt);
 | 
			
		||||
  int32_t cfb_cnt = to_int32_t(data->cfb_cnt);
 | 
			
		||||
  float a_energy_consumption = (float) cfa_cnt / energy_reference_;
 | 
			
		||||
  float b_energy_consumption = (float) cfb_cnt / energy_reference_;
 | 
			
		||||
  float total_energy_consumption = a_energy_consumption + b_energy_consumption;
 | 
			
		||||
 | 
			
		||||
  if (voltage_sensor_ != nullptr) {
 | 
			
		||||
    voltage_sensor_->publish_state(v_rms);
 | 
			
		||||
  }
 | 
			
		||||
  if (current_sensor_1_ != nullptr) {
 | 
			
		||||
    current_sensor_1_->publish_state(ia_rms);
 | 
			
		||||
  }
 | 
			
		||||
  if (current_sensor_2_ != nullptr) {
 | 
			
		||||
    current_sensor_2_->publish_state(ib_rms);
 | 
			
		||||
  }
 | 
			
		||||
  if (power_sensor_1_ != nullptr) {
 | 
			
		||||
    power_sensor_1_->publish_state(a_watt);
 | 
			
		||||
  }
 | 
			
		||||
  if (power_sensor_2_ != nullptr) {
 | 
			
		||||
    power_sensor_2_->publish_state(b_watt);
 | 
			
		||||
  }
 | 
			
		||||
  if (energy_sensor_1_ != nullptr) {
 | 
			
		||||
    energy_sensor_1_->publish_state(a_energy_consumption);
 | 
			
		||||
  }
 | 
			
		||||
  if (energy_sensor_2_ != nullptr) {
 | 
			
		||||
    energy_sensor_2_->publish_state(b_energy_consumption);
 | 
			
		||||
  }
 | 
			
		||||
  if (energy_sensor_sum_ != nullptr) {
 | 
			
		||||
    energy_sensor_sum_->publish_state(total_energy_consumption);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV("bl0939", "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %d, CntB %d, ∫P1 %fkWh, ∫P2 %fkWh", v_rms,
 | 
			
		||||
           ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0939::dump_config() {  // NOLINT(readability-function-cognitive-complexity)
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "BL0939:");
 | 
			
		||||
  LOG_SENSOR("", "Voltage", this->voltage_sensor_);
 | 
			
		||||
  LOG_SENSOR("", "Current 1", this->current_sensor_1_);
 | 
			
		||||
  LOG_SENSOR("", "Current 2", this->current_sensor_2_);
 | 
			
		||||
  LOG_SENSOR("", "Power 1", this->power_sensor_1_);
 | 
			
		||||
  LOG_SENSOR("", "Power 2", this->power_sensor_2_);
 | 
			
		||||
  LOG_SENSOR("", "Energy 1", this->energy_sensor_1_);
 | 
			
		||||
  LOG_SENSOR("", "Energy 2", this->energy_sensor_2_);
 | 
			
		||||
  LOG_SENSOR("", "Energy sum", this->energy_sensor_sum_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; }
 | 
			
		||||
 | 
			
		||||
int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; }
 | 
			
		||||
 | 
			
		||||
}  // namespace bl0939
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										107
									
								
								esphome/components/bl0939/bl0939.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								esphome/components/bl0939/bl0939.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bl0939 {
 | 
			
		||||
 | 
			
		||||
// https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf
 | 
			
		||||
// (unfortunatelly chinese, but the formulas can be easily understood)
 | 
			
		||||
// Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm)
 | 
			
		||||
// and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm)
 | 
			
		||||
// as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V)
 | 
			
		||||
static const float BL0939_IREF = 324004 * 1 / 1.218;
 | 
			
		||||
static const float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51));
 | 
			
		||||
static const float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51));
 | 
			
		||||
static const float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51));
 | 
			
		||||
 | 
			
		||||
struct ube24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align)
 | 
			
		||||
  uint8_t l;
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  uint8_t h;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
struct ube16_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align)
 | 
			
		||||
  uint8_t l;
 | 
			
		||||
  uint8_t h;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
struct sbe24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align)
 | 
			
		||||
  uint8_t l;
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  int8_t h;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
// Caveat: All these values are big endian (low - middle - high)
 | 
			
		||||
 | 
			
		||||
union DataPacket {  // NOLINT(altera-struct-pack-align)
 | 
			
		||||
  uint8_t raw[35];
 | 
			
		||||
  struct {
 | 
			
		||||
    uint8_t frame_header;  // 0x55 according to docs
 | 
			
		||||
    ube24_t ia_fast_rms;
 | 
			
		||||
    ube24_t ia_rms;
 | 
			
		||||
    ube24_t ib_rms;
 | 
			
		||||
    ube24_t v_rms;
 | 
			
		||||
    ube24_t ib_fast_rms;
 | 
			
		||||
    sbe24_t a_watt;
 | 
			
		||||
    sbe24_t b_watt;
 | 
			
		||||
    sbe24_t cfa_cnt;
 | 
			
		||||
    sbe24_t cfb_cnt;
 | 
			
		||||
    ube16_t tps1;
 | 
			
		||||
    uint8_t RESERVED1;  // value of 0x00
 | 
			
		||||
    ube16_t tps2;
 | 
			
		||||
    uint8_t RESERVED2;  // value of 0x00
 | 
			
		||||
    uint8_t checksum;   // checksum
 | 
			
		||||
  };
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
class BL0939 : public PollingComponent, public uart::UARTDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
 | 
			
		||||
  void set_current_sensor_1(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; }
 | 
			
		||||
  void set_current_sensor_2(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; }
 | 
			
		||||
  void set_power_sensor_1(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; }
 | 
			
		||||
  void set_power_sensor_2(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; }
 | 
			
		||||
  void set_energy_sensor_1(sensor::Sensor *energy_sensor_1) { energy_sensor_1_ = energy_sensor_1; }
 | 
			
		||||
  void set_energy_sensor_2(sensor::Sensor *energy_sensor_2) { energy_sensor_2_ = energy_sensor_2; }
 | 
			
		||||
  void set_energy_sensor_sum(sensor::Sensor *energy_sensor_sum) { energy_sensor_sum_ = energy_sensor_sum; }
 | 
			
		||||
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *voltage_sensor_;
 | 
			
		||||
  sensor::Sensor *current_sensor_1_;
 | 
			
		||||
  sensor::Sensor *current_sensor_2_;
 | 
			
		||||
  // NB This may be negative as the circuits is seemingly able to measure
 | 
			
		||||
  // power in both directions
 | 
			
		||||
  sensor::Sensor *power_sensor_1_;
 | 
			
		||||
  sensor::Sensor *power_sensor_2_;
 | 
			
		||||
  sensor::Sensor *energy_sensor_1_;
 | 
			
		||||
  sensor::Sensor *energy_sensor_2_;
 | 
			
		||||
  sensor::Sensor *energy_sensor_sum_;
 | 
			
		||||
 | 
			
		||||
  // Divide by this to turn into Watt
 | 
			
		||||
  float power_reference_ = BL0939_PREF;
 | 
			
		||||
  // Divide by this to turn into Volt
 | 
			
		||||
  float voltage_reference_ = BL0939_UREF;
 | 
			
		||||
  // Divide by this to turn into Ampere
 | 
			
		||||
  float current_reference_ = BL0939_IREF;
 | 
			
		||||
  // Divide by this to turn into kWh
 | 
			
		||||
  float energy_reference_ = BL0939_EREF;
 | 
			
		||||
 | 
			
		||||
  static uint32_t to_uint32_t(ube24_t input);
 | 
			
		||||
 | 
			
		||||
  static int32_t to_int32_t(sbe24_t input);
 | 
			
		||||
 | 
			
		||||
  static bool validate_checksum(const DataPacket *data);
 | 
			
		||||
 | 
			
		||||
  void received_package_(const DataPacket *data) const;
 | 
			
		||||
};
 | 
			
		||||
}  // namespace bl0939
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										123
									
								
								esphome/components/bl0939/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								esphome/components/bl0939/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, uart
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_CURRENT,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_KILOWATT_HOURS,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
 | 
			
		||||
CONF_CURRENT_1 = "current_1"
 | 
			
		||||
CONF_CURRENT_2 = "current_2"
 | 
			
		||||
CONF_ACTIVE_POWER_1 = "active_power_1"
 | 
			
		||||
CONF_ACTIVE_POWER_2 = "active_power_2"
 | 
			
		||||
CONF_ENERGY_1 = "energy_1"
 | 
			
		||||
CONF_ENERGY_2 = "energy_2"
 | 
			
		||||
CONF_ENERGY_TOTAL = "energy_total"
 | 
			
		||||
 | 
			
		||||
bl0939_ns = cg.esphome_ns.namespace("bl0939")
 | 
			
		||||
BL0939 = bl0939_ns.class_("BL0939", cg.PollingComponent, uart.UARTDevice)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BL0939),
 | 
			
		||||
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT_1): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT_2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENERGY_1): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENERGY_2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENERGY_TOTAL): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .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)
 | 
			
		||||
 | 
			
		||||
    if CONF_VOLTAGE in config:
 | 
			
		||||
        conf = config[CONF_VOLTAGE]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_voltage_sensor(sens))
 | 
			
		||||
    if CONF_CURRENT_1 in config:
 | 
			
		||||
        conf = config[CONF_CURRENT_1]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_current_sensor_1(sens))
 | 
			
		||||
    if CONF_CURRENT_2 in config:
 | 
			
		||||
        conf = config[CONF_CURRENT_2]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_current_sensor_2(sens))
 | 
			
		||||
    if CONF_ACTIVE_POWER_1 in config:
 | 
			
		||||
        conf = config[CONF_ACTIVE_POWER_1]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_power_sensor_1(sens))
 | 
			
		||||
    if CONF_ACTIVE_POWER_2 in config:
 | 
			
		||||
        conf = config[CONF_ACTIVE_POWER_2]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_power_sensor_2(sens))
 | 
			
		||||
    if CONF_ENERGY_1 in config:
 | 
			
		||||
        conf = config[CONF_ENERGY_1]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_energy_sensor_1(sens))
 | 
			
		||||
    if CONF_ENERGY_2 in config:
 | 
			
		||||
        conf = config[CONF_ENERGY_2]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_energy_sensor_2(sens))
 | 
			
		||||
    if CONF_ENERGY_TOTAL in config:
 | 
			
		||||
        conf = config[CONF_ENERGY_TOTAL]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_energy_sensor_sum(sens))
 | 
			
		||||
@@ -11,8 +11,6 @@ namespace ble_client {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "ble_sensor";
 | 
			
		||||
 | 
			
		||||
uint32_t BLESensor::hash_base() { return 343459825UL; }
 | 
			
		||||
 | 
			
		||||
void BLESensor::loop() {}
 | 
			
		||||
 | 
			
		||||
void BLESensor::dump_config() {
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
 | 
			
		||||
  uint16_t handle;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
  float parse_data_(uint8_t *value, uint16_t value_len);
 | 
			
		||||
  optional<data_to_value_t> data_to_value_func_{};
 | 
			
		||||
  bool notify_;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,6 @@ static const char *const TAG = "ble_text_sensor";
 | 
			
		||||
 | 
			
		||||
static const std::string EMPTY = "";
 | 
			
		||||
 | 
			
		||||
uint32_t BLETextSensor::hash_base() { return 193967603UL; }
 | 
			
		||||
 | 
			
		||||
void BLETextSensor::loop() {}
 | 
			
		||||
 | 
			
		||||
void BLETextSensor::dump_config() {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
 | 
			
		||||
  uint16_t handle;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
  bool notify_;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,11 @@ static const char *iir_filter_to_str(BME280IIRFilter filter) {
 | 
			
		||||
void BME280Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up BME280...");
 | 
			
		||||
  uint8_t chip_id = 0;
 | 
			
		||||
 | 
			
		||||
  // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
 | 
			
		||||
  // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component.
 | 
			
		||||
  this->component_state_ &= ~COMPONENT_STATE_FAILED;
 | 
			
		||||
 | 
			
		||||
  if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
 
 | 
			
		||||
@@ -169,6 +169,14 @@ void BME680BSECComponent::loop() {
 | 
			
		||||
  } else {
 | 
			
		||||
    this->status_clear_warning();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Process a single action from the queue. These are primarily sensor state publishes
 | 
			
		||||
  // that in totality take too long to send in a single call.
 | 
			
		||||
  if (this->queue_.size()) {
 | 
			
		||||
    auto action = std::move(this->queue_.front());
 | 
			
		||||
    this->queue_.pop();
 | 
			
		||||
    action();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::run_() {
 | 
			
		||||
@@ -306,37 +314,39 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) {
 | 
			
		||||
  ESP_LOGV(TAG, "Publishing sensor states");
 | 
			
		||||
  ESP_LOGV(TAG, "Queuing sensor state publish actions");
 | 
			
		||||
  for (uint8_t i = 0; i < num_outputs; i++) {
 | 
			
		||||
    float signal = outputs[i].signal;
 | 
			
		||||
    switch (outputs[i].sensor_id) {
 | 
			
		||||
      case BSEC_OUTPUT_IAQ:
 | 
			
		||||
      case BSEC_OUTPUT_STATIC_IAQ:
 | 
			
		||||
        uint8_t accuracy;
 | 
			
		||||
        accuracy = outputs[i].accuracy;
 | 
			
		||||
        this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal);
 | 
			
		||||
        this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
 | 
			
		||||
        this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true);
 | 
			
		||||
      case BSEC_OUTPUT_STATIC_IAQ: {
 | 
			
		||||
        uint8_t accuracy = outputs[i].accuracy;
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
 | 
			
		||||
        this->queue_push_([this, accuracy]() {
 | 
			
		||||
          this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
 | 
			
		||||
        });
 | 
			
		||||
        this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
 | 
			
		||||
 | 
			
		||||
        // Queue up an opportunity to save state
 | 
			
		||||
        this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); });
 | 
			
		||||
        break;
 | 
			
		||||
        this->queue_push_([this, accuracy]() { this->save_state_(accuracy); });
 | 
			
		||||
      } break;
 | 
			
		||||
      case BSEC_OUTPUT_CO2_EQUIVALENT:
 | 
			
		||||
        this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
 | 
			
		||||
        this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_RAW_PRESSURE:
 | 
			
		||||
        this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_RAW_GAS:
 | 
			
		||||
        this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
 | 
			
		||||
        this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
 | 
			
		||||
        this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() {
 | 
			
		||||
  return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) {
 | 
			
		||||
void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) {
 | 
			
		||||
  if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  sensor->publish_state(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
 | 
			
		||||
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
 | 
			
		||||
  if (!sensor || (sensor->has_state() && sensor->state == value)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
  void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
 | 
			
		||||
  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, const std::string &value);
 | 
			
		||||
  void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
 | 
			
		||||
  void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
 | 
			
		||||
 | 
			
		||||
  void load_state_();
 | 
			
		||||
  void save_state_(uint8_t accuracy);
 | 
			
		||||
 | 
			
		||||
  void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
 | 
			
		||||
 | 
			
		||||
  struct bme680_dev bme680_;
 | 
			
		||||
  bsec_library_return_t bsec_status_{BSEC_OK};
 | 
			
		||||
  int8_t bme680_status_{BME680_OK};
 | 
			
		||||
@@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
  uint32_t millis_overflow_counter_{0};
 | 
			
		||||
  int64_t next_call_ns_{0};
 | 
			
		||||
 | 
			
		||||
  std::queue<std::function<void()>> queue_;
 | 
			
		||||
 | 
			
		||||
  ESPPreferenceObject bsec_state_;
 | 
			
		||||
  uint32_t state_save_interval_ms_{21600000};  // 6 hours - 4 times a day
 | 
			
		||||
  uint32_t last_state_save_ms_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ OVERSAMPLING_OPTIONS = {
 | 
			
		||||
    "4X": Oversampling.OVERSAMPLING_X4,
 | 
			
		||||
    "8X": Oversampling.OVERSAMPLING_X8,
 | 
			
		||||
    "16X": Oversampling.OVERSAMPLING_X16,
 | 
			
		||||
    "32x": Oversampling.OVERSAMPLING_X32,
 | 
			
		||||
    "32X": Oversampling.OVERSAMPLING_X32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
IIRFilter = bmp3xx_ns.enum("IIRFilter")
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@ void Button::press() {
 | 
			
		||||
  this->press_callback_.call();
 | 
			
		||||
}
 | 
			
		||||
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
 | 
			
		||||
uint32_t Button::hash_base() { return 1495763804UL; }
 | 
			
		||||
 | 
			
		||||
void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; }
 | 
			
		||||
std::string Button::get_device_class() { return this->device_class_; }
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@ class Button : public EntityBase {
 | 
			
		||||
   */
 | 
			
		||||
  virtual void press_action() = 0;
 | 
			
		||||
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void()> press_callback_{};
 | 
			
		||||
  std::string device_class_{};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,7 @@ CANBUS_SCHEMA = cv.Schema(
 | 
			
		||||
                    min=0, max=0x1FFFFFFF
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
 | 
			
		||||
                cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
 | 
			
		||||
            },
 | 
			
		||||
            validate_id,
 | 
			
		||||
        ),
 | 
			
		||||
@@ -100,10 +101,20 @@ async def setup_canbus_core_(var, config):
 | 
			
		||||
        trigger = cg.new_Pvariable(
 | 
			
		||||
            conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
 | 
			
		||||
        )
 | 
			
		||||
        if CONF_REMOTE_TRANSMISSION_REQUEST in conf:
 | 
			
		||||
            cg.add(
 | 
			
		||||
                trigger.set_remote_transmission_request(
 | 
			
		||||
                    conf[CONF_REMOTE_TRANSMISSION_REQUEST]
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        await cg.register_component(trigger, conf)
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
            trigger,
 | 
			
		||||
            [(cg.std_vector.template(cg.uint8), "x"), (cg.uint32, "can_id")],
 | 
			
		||||
            [
 | 
			
		||||
                (cg.std_vector.template(cg.uint8), "x"),
 | 
			
		||||
                (cg.uint32, "can_id"),
 | 
			
		||||
                (cg.bool_, "remote_transmission_request"),
 | 
			
		||||
            ],
 | 
			
		||||
            conf,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -81,8 +81,10 @@ void Canbus::loop() {
 | 
			
		||||
    // fire all triggers
 | 
			
		||||
    for (auto *trigger : this->triggers_) {
 | 
			
		||||
      if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
 | 
			
		||||
          (trigger->use_extended_id_ == can_message.use_extended_id)) {
 | 
			
		||||
        trigger->trigger(data, can_message.can_id);
 | 
			
		||||
          (trigger->use_extended_id_ == can_message.use_extended_id) &&
 | 
			
		||||
          (!trigger->remote_transmission_request_.has_value() ||
 | 
			
		||||
           trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) {
 | 
			
		||||
        trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -126,13 +126,18 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
 | 
			
		||||
  std::vector<uint8_t> data_static_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Component {
 | 
			
		||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
 | 
			
		||||
  friend class Canbus;
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
 | 
			
		||||
                         const bool use_extended_id)
 | 
			
		||||
      : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){};
 | 
			
		||||
 | 
			
		||||
  void set_remote_transmission_request(bool remote_transmission_request) {
 | 
			
		||||
    this->remote_transmission_request_ = remote_transmission_request;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setup() override { this->parent_->add_trigger(this); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -140,6 +145,7 @@ class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Com
 | 
			
		||||
  uint32_t can_id_;
 | 
			
		||||
  uint32_t can_id_mask_;
 | 
			
		||||
  bool use_extended_id_;
 | 
			
		||||
  optional<bool> remote_transmission_request_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace canbus
 | 
			
		||||
 
 | 
			
		||||
@@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
			
		||||
    if (request->method() == HTTP_GET) {
 | 
			
		||||
      if (request->url() == "/")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/stylesheet.css")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-1.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-2.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-3.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-4.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/lock.svg")
 | 
			
		||||
      if (request->url() == "/config.json")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifisave")
 | 
			
		||||
        return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
 | 
			
		||||
            validate_climate_fan_mode
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict,
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable(
 | 
			
		||||
            cv.string_strict
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset),
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict,
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict),
 | 
			
		||||
        cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
@@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
 | 
			
		||||
        cg.add(var.set_fan_mode(template_))
 | 
			
		||||
    if CONF_CUSTOM_FAN_MODE in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str)
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
            config[CONF_CUSTOM_FAN_MODE], args, cg.std_string
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_custom_fan_mode(template_))
 | 
			
		||||
    if CONF_PRESET in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
 | 
			
		||||
        cg.add(var.set_preset(template_))
 | 
			
		||||
    if CONF_CUSTOM_PRESET in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str)
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
            config[CONF_CUSTOM_PRESET], args, cg.std_string
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_custom_preset(template_))
 | 
			
		||||
    if CONF_SWING_MODE in config:
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
 
 | 
			
		||||
@@ -419,7 +419,6 @@ void Climate::publish_state() {
 | 
			
		||||
  // Save state
 | 
			
		||||
  this->save_state_();
 | 
			
		||||
}
 | 
			
		||||
uint32_t Climate::hash_base() { return 3104134496UL; }
 | 
			
		||||
 | 
			
		||||
ClimateTraits Climate::get_traits() {
 | 
			
		||||
  auto traits = this->traits();
 | 
			
		||||
 
 | 
			
		||||
@@ -282,7 +282,6 @@ class Climate : public EntityBase {
 | 
			
		||||
   */
 | 
			
		||||
  void save_state_();
 | 
			
		||||
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
  void dump_traits_(const char *tag);
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void()> state_callback_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace copy {
 | 
			
		||||
static const char *const TAG = "copy.select";
 | 
			
		||||
 | 
			
		||||
void CopySelect::setup() {
 | 
			
		||||
  source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
 | 
			
		||||
  source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
 | 
			
		||||
 | 
			
		||||
  traits.set_options(source_->traits.get_options());
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,8 +33,6 @@ const char *cover_operation_to_str(CoverOperation op) {
 | 
			
		||||
 | 
			
		||||
Cover::Cover(const std::string &name) : EntityBase(name), position{COVER_OPEN} {}
 | 
			
		||||
 | 
			
		||||
uint32_t Cover::hash_base() { return 1727367479UL; }
 | 
			
		||||
 | 
			
		||||
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
 | 
			
		||||
CoverCall &CoverCall::set_command(const char *command) {
 | 
			
		||||
  if (strcasecmp(command, "OPEN") == 0) {
 | 
			
		||||
 
 | 
			
		||||
@@ -177,7 +177,6 @@ class Cover : public EntityBase {
 | 
			
		||||
  virtual std::string device_class();
 | 
			
		||||
 | 
			
		||||
  optional<CoverRestoreState> restore_state_();
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void()> state_callback_{};
 | 
			
		||||
  optional<std::string> device_class_override_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,10 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N
 | 
			
		||||
        config = {
 | 
			
		||||
            "substitutions": {"name": name},
 | 
			
		||||
            "packages": {project_name: import_url},
 | 
			
		||||
            "esphome": {"name_add_mac_suffix": False},
 | 
			
		||||
            "esphome": {
 | 
			
		||||
                "name": "${name}",
 | 
			
		||||
                "name_add_mac_suffix": False,
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
        p.write_text(
 | 
			
		||||
            dump(config) + WIFI_CONFIG,
 | 
			
		||||
 
 | 
			
		||||
@@ -93,7 +93,14 @@ deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
 | 
			
		||||
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
 | 
			
		||||
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
 | 
			
		||||
PreventDeepSleepAction = deep_sleep_ns.class_(
 | 
			
		||||
    "PreventDeepSleepAction", automation.Action
 | 
			
		||||
    "PreventDeepSleepAction",
 | 
			
		||||
    automation.Action,
 | 
			
		||||
    cg.Parented.template(DeepSleepComponent),
 | 
			
		||||
)
 | 
			
		||||
AllowDeepSleepAction = deep_sleep_ns.class_(
 | 
			
		||||
    "AllowDeepSleepAction",
 | 
			
		||||
    automation.Action,
 | 
			
		||||
    cg.Parented.template(DeepSleepComponent),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode")
 | 
			
		||||
@@ -208,28 +215,32 @@ async def to_code(config):
 | 
			
		||||
    cg.add_define("USE_DEEP_SLEEP")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
 | 
			
		||||
    automation.maybe_simple_id(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.use_id(DeepSleepComponent),
 | 
			
		||||
            cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
 | 
			
		||||
                cv.positive_time_period_milliseconds
 | 
			
		||||
            ),
 | 
			
		||||
            # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
 | 
			
		||||
            cv.Exclusive(CONF_UNTIL, "time"): cv.All(cv.only_on_esp32, cv.time_of_day),
 | 
			
		||||
            cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id(
 | 
			
		||||
DEEP_SLEEP_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(DeepSleepComponent),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
 | 
			
		||||
    automation.maybe_simple_id(
 | 
			
		||||
        DEEP_SLEEP_ACTION_SCHEMA.extend(
 | 
			
		||||
            cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
 | 
			
		||||
                        cv.positive_time_period_milliseconds
 | 
			
		||||
                    ),
 | 
			
		||||
                    # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
 | 
			
		||||
                    cv.Exclusive(CONF_UNTIL, "time"): cv.All(
 | 
			
		||||
                        cv.only_on_esp32, cv.time_of_day
 | 
			
		||||
                    ),
 | 
			
		||||
                    cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    ),
 | 
			
		||||
    cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA
 | 
			
		||||
@@ -252,8 +263,16 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA
 | 
			
		||||
    "deep_sleep.prevent",
 | 
			
		||||
    PreventDeepSleepAction,
 | 
			
		||||
    automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
 | 
			
		||||
)
 | 
			
		||||
async def deep_sleep_prevent_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)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "deep_sleep.allow",
 | 
			
		||||
    AllowDeepSleepAction,
 | 
			
		||||
    automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
 | 
			
		||||
)
 | 
			
		||||
async def deep_sleep_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
    return var
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
 | 
			
		||||
    switch (wakeup_cause) {
 | 
			
		||||
      case ESP_SLEEP_WAKEUP_EXT0:
 | 
			
		||||
      case ESP_SLEEP_WAKEUP_EXT1:
 | 
			
		||||
      case ESP_SLEEP_WAKEUP_GPIO:
 | 
			
		||||
        return this->wakeup_cause_to_run_duration_->gpio_cause;
 | 
			
		||||
      case ESP_SLEEP_WAKEUP_TOUCHPAD:
 | 
			
		||||
        return this->wakeup_cause_to_run_duration_->touch_cause;
 | 
			
		||||
@@ -72,16 +73,27 @@ float DeepSleepComponent::get_loop_priority() const {
 | 
			
		||||
  return -100.0f;  // run after everything else is ready
 | 
			
		||||
}
 | 
			
		||||
void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#if defined(USE_ESP32)
 | 
			
		||||
void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
 | 
			
		||||
  this->wakeup_pin_mode_ = wakeup_pin_mode;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32)
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
 | 
			
		||||
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
 | 
			
		||||
 | 
			
		||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
 | 
			
		||||
  wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
 | 
			
		||||
void DeepSleepComponent::begin_sleep(bool manual) {
 | 
			
		||||
  if (this->prevent_ && !manual) {
 | 
			
		||||
@@ -107,7 +119,8 @@ void DeepSleepComponent::begin_sleep(bool manual) {
 | 
			
		||||
 | 
			
		||||
  App.run_safe_shutdown_hooks();
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
#if defined(USE_ESP32)
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
  if (this->sleep_duration_.has_value())
 | 
			
		||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
			
		||||
  if (this->wakeup_pin_ != nullptr) {
 | 
			
		||||
@@ -125,10 +138,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
 | 
			
		||||
    esp_sleep_enable_touchpad_wakeup();
 | 
			
		||||
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  esp_deep_sleep_start();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_VARIANT_ESP32C3
 | 
			
		||||
  if (this->sleep_duration_.has_value())
 | 
			
		||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
			
		||||
@@ -137,9 +147,12 @@ void DeepSleepComponent::begin_sleep(bool manual) {
 | 
			
		||||
    if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
 | 
			
		||||
      level = !level;
 | 
			
		||||
    }
 | 
			
		||||
    esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level);
 | 
			
		||||
    esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()),
 | 
			
		||||
                                      static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  esp_deep_sleep_start();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
  ESP.deepSleep(*this->sleep_duration_);  // NOLINT(readability-static-accessed-through-instance)
 | 
			
		||||
@@ -147,6 +160,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
 | 
			
		||||
}
 | 
			
		||||
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
 | 
			
		||||
void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
 | 
			
		||||
void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; }
 | 
			
		||||
 | 
			
		||||
}  // namespace deep_sleep
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -70,17 +70,19 @@ class DeepSleepComponent : public Component {
 | 
			
		||||
  void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
#if defined(USE_ESP32)
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
 | 
			
		||||
  void set_ext1_wakeup(Ext1Wakeup ext1_wakeup);
 | 
			
		||||
 | 
			
		||||
  void set_touch_wakeup(bool touch_wakeup);
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
  // Set the duration in ms for how long the code should run before entering
 | 
			
		||||
  // deep sleep mode, according to the cause the ESP32 has woken.
 | 
			
		||||
  void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration);
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  /// Set a duration in ms for how long the code should run before entering deep sleep mode.
 | 
			
		||||
  void set_run_duration(uint32_t time_ms);
 | 
			
		||||
 | 
			
		||||
@@ -94,6 +96,7 @@ class DeepSleepComponent : public Component {
 | 
			
		||||
  void begin_sleep(bool manual = false);
 | 
			
		||||
 | 
			
		||||
  void prevent_deep_sleep();
 | 
			
		||||
  void allow_deep_sleep();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Returns nullopt if no run duration is set. Otherwise, returns the run
 | 
			
		||||
@@ -187,14 +190,14 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...> {
 | 
			
		||||
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
 | 
			
		||||
 public:
 | 
			
		||||
  PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {}
 | 
			
		||||
  void play(Ts... x) override { this->parent_->prevent_deep_sleep(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DeepSleepComponent *deep_sleep_;
 | 
			
		||||
template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->allow_deep_sleep(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace deep_sleep
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/delonghi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/delonghi/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@grob6000"]
 | 
			
		||||
							
								
								
									
										20
									
								
								esphome/components/delonghi/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/delonghi/climate.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import climate_ir
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["climate_ir"]
 | 
			
		||||
 | 
			
		||||
delonghi_ns = cg.esphome_ns.namespace("delonghi")
 | 
			
		||||
DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(DelonghiClimate),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await climate_ir.register_climate_ir(var, config)
 | 
			
		||||
							
								
								
									
										186
									
								
								esphome/components/delonghi/delonghi.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								esphome/components/delonghi/delonghi.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
			
		||||
#include "delonghi.h"
 | 
			
		||||
#include "esphome/components/remote_base/remote_base.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace delonghi {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "delonghi.climate";
 | 
			
		||||
 | 
			
		||||
void DelonghiClimate::transmit_state() {
 | 
			
		||||
  uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0};
 | 
			
		||||
  remote_state[0] = DELONGHI_ADDRESS;
 | 
			
		||||
  remote_state[1] = this->temperature_();
 | 
			
		||||
  remote_state[1] |= (this->fan_speed_()) << 5;
 | 
			
		||||
  remote_state[2] = this->operation_mode_();
 | 
			
		||||
  // Calculate checksum
 | 
			
		||||
  for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) {
 | 
			
		||||
    remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto transmit = this->transmitter_->transmit();
 | 
			
		||||
  auto *data = transmit.get_data();
 | 
			
		||||
  data->set_carrier_frequency(DELONGHI_IR_FREQUENCY);
 | 
			
		||||
 | 
			
		||||
  data->mark(DELONGHI_HEADER_MARK);
 | 
			
		||||
  data->space(DELONGHI_HEADER_SPACE);
 | 
			
		||||
  for (unsigned char b : remote_state) {
 | 
			
		||||
    for (uint8_t mask = 1; mask > 0; mask <<= 1) {  // iterate through bit mask
 | 
			
		||||
      data->mark(DELONGHI_BIT_MARK);
 | 
			
		||||
      bool bit = b & mask;
 | 
			
		||||
      data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  data->mark(DELONGHI_BIT_MARK);
 | 
			
		||||
  data->space(0);
 | 
			
		||||
 | 
			
		||||
  transmit.perform();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t DelonghiClimate::operation_mode_() {
 | 
			
		||||
  uint8_t operating_mode = DELONGHI_MODE_ON;
 | 
			
		||||
  switch (this->mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_COOL:
 | 
			
		||||
      operating_mode |= DELONGHI_MODE_COOL;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_DRY:
 | 
			
		||||
      operating_mode |= DELONGHI_MODE_DRY;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
      operating_mode |= DELONGHI_MODE_HEAT;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      operating_mode |= DELONGHI_MODE_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
      operating_mode |= DELONGHI_MODE_FAN;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_OFF:
 | 
			
		||||
    default:
 | 
			
		||||
      operating_mode = DELONGHI_MODE_OFF;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  return operating_mode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t DelonghiClimate::fan_speed_() {
 | 
			
		||||
  uint16_t fan_speed;
 | 
			
		||||
  switch (this->fan_mode.value()) {
 | 
			
		||||
    case climate::CLIMATE_FAN_LOW:
 | 
			
		||||
      fan_speed = DELONGHI_FAN_LOW;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_FAN_MEDIUM:
 | 
			
		||||
      fan_speed = DELONGHI_FAN_MEDIUM;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_FAN_HIGH:
 | 
			
		||||
      fan_speed = DELONGHI_FAN_HIGH;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_FAN_AUTO:
 | 
			
		||||
    default:
 | 
			
		||||
      fan_speed = DELONGHI_FAN_AUTO;
 | 
			
		||||
  }
 | 
			
		||||
  return fan_speed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t DelonghiClimate::temperature_() {
 | 
			
		||||
  // Force special temperatures depending on the mode
 | 
			
		||||
  uint8_t temperature = 0b0001;
 | 
			
		||||
  switch (this->mode) {
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
      temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT;
 | 
			
		||||
      break;
 | 
			
		||||
    case climate::CLIMATE_MODE_COOL:
 | 
			
		||||
    case climate::CLIMATE_MODE_DRY:
 | 
			
		||||
    case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
    case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
    case climate::CLIMATE_MODE_OFF:
 | 
			
		||||
    default:
 | 
			
		||||
      temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL;
 | 
			
		||||
  }
 | 
			
		||||
  if (temperature > 0x0F) {
 | 
			
		||||
    temperature = 0x0F;  // clamp maximum
 | 
			
		||||
  }
 | 
			
		||||
  return temperature;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
 | 
			
		||||
  uint8_t checksum = 0;
 | 
			
		||||
  for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) {
 | 
			
		||||
    checksum += frame[i];
 | 
			
		||||
  }
 | 
			
		||||
  if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t mode = frame[2] & 0x0F;
 | 
			
		||||
  if (mode & DELONGHI_MODE_ON) {
 | 
			
		||||
    switch (mode & 0x0E) {
 | 
			
		||||
      case DELONGHI_MODE_COOL:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_COOL;
 | 
			
		||||
        break;
 | 
			
		||||
      case DELONGHI_MODE_DRY:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_DRY;
 | 
			
		||||
        break;
 | 
			
		||||
      case DELONGHI_MODE_HEAT:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
        break;
 | 
			
		||||
      case DELONGHI_MODE_AUTO:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
        break;
 | 
			
		||||
      case DELONGHI_MODE_FAN:
 | 
			
		||||
        this->mode = climate::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_OFF;
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t temperature = frame[1] & 0x0F;
 | 
			
		||||
  if (this->mode == climate::CLIMATE_MODE_HEAT) {
 | 
			
		||||
    this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL;
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t fan_mode = frame[1] >> 5;
 | 
			
		||||
  switch (fan_mode) {
 | 
			
		||||
    case DELONGHI_FAN_LOW:
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_LOW;
 | 
			
		||||
      break;
 | 
			
		||||
    case DELONGHI_FAN_MEDIUM:
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
 | 
			
		||||
      break;
 | 
			
		||||
    case DELONGHI_FAN_HIGH:
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_HIGH;
 | 
			
		||||
      break;
 | 
			
		||||
    case DELONGHI_FAN_AUTO:
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
  uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {};
 | 
			
		||||
  if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) {
 | 
			
		||||
    uint8_t byte = 0;
 | 
			
		||||
    for (int8_t bit = 0; bit < 8; bit++) {
 | 
			
		||||
      if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) {
 | 
			
		||||
        byte |= 1 << bit;
 | 
			
		||||
      } else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    state_frame[pos] = byte;
 | 
			
		||||
    if (pos == 0) {
 | 
			
		||||
      // frame header
 | 
			
		||||
      if (byte != DELONGHI_ADDRESS) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return this->parse_state_frame_(state_frame);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace delonghi
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										64
									
								
								esphome/components/delonghi/delonghi.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/delonghi/delonghi.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/climate_ir/climate_ir.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace delonghi {
 | 
			
		||||
 | 
			
		||||
// Values for DELONGHI ARC43XXX IR Controllers
 | 
			
		||||
const uint8_t DELONGHI_ADDRESS = 83;
 | 
			
		||||
 | 
			
		||||
// Temperature
 | 
			
		||||
const uint8_t DELONGHI_TEMP_MIN = 13;          // Celsius
 | 
			
		||||
const uint8_t DELONGHI_TEMP_MAX = 32;          // Celsius
 | 
			
		||||
const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17;  // Celsius
 | 
			
		||||
const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12;  // Celsius
 | 
			
		||||
 | 
			
		||||
// Modes
 | 
			
		||||
const uint8_t DELONGHI_MODE_AUTO = 0b1000;
 | 
			
		||||
const uint8_t DELONGHI_MODE_COOL = 0b0000;
 | 
			
		||||
const uint8_t DELONGHI_MODE_HEAT = 0b0110;
 | 
			
		||||
const uint8_t DELONGHI_MODE_DRY = 0b0010;
 | 
			
		||||
const uint8_t DELONGHI_MODE_FAN = 0b0100;
 | 
			
		||||
const uint8_t DELONGHI_MODE_OFF = 0b0000;
 | 
			
		||||
const uint8_t DELONGHI_MODE_ON = 0b0001;
 | 
			
		||||
 | 
			
		||||
// Fan Speed
 | 
			
		||||
const uint8_t DELONGHI_FAN_AUTO = 0b00;
 | 
			
		||||
const uint8_t DELONGHI_FAN_HIGH = 0b01;
 | 
			
		||||
const uint8_t DELONGHI_FAN_MEDIUM = 0b10;
 | 
			
		||||
const uint8_t DELONGHI_FAN_LOW = 0b11;
 | 
			
		||||
 | 
			
		||||
// IR Transmission - similar to NEC1
 | 
			
		||||
const uint32_t DELONGHI_IR_FREQUENCY = 38000;
 | 
			
		||||
const uint32_t DELONGHI_HEADER_MARK = 9000;
 | 
			
		||||
const uint32_t DELONGHI_HEADER_SPACE = 4500;
 | 
			
		||||
const uint32_t DELONGHI_BIT_MARK = 465;
 | 
			
		||||
const uint32_t DELONGHI_ONE_SPACE = 1750;
 | 
			
		||||
const uint32_t DELONGHI_ZERO_SPACE = 670;
 | 
			
		||||
 | 
			
		||||
// State Frame size
 | 
			
		||||
const uint8_t DELONGHI_STATE_FRAME_SIZE = 8;
 | 
			
		||||
 | 
			
		||||
class DelonghiClimate : public climate_ir::ClimateIR {
 | 
			
		||||
 public:
 | 
			
		||||
  DelonghiClimate()
 | 
			
		||||
      : climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_TEMP_MAX, 1.0f, true, true,
 | 
			
		||||
                              {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
 | 
			
		||||
                               climate::CLIMATE_FAN_HIGH},
 | 
			
		||||
                              {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
 | 
			
		||||
                               climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Transmit via IR the state of this climate controller.
 | 
			
		||||
  void transmit_state() override;
 | 
			
		||||
  uint8_t operation_mode_();
 | 
			
		||||
  uint16_t fan_speed_();
 | 
			
		||||
  uint8_t temperature_();
 | 
			
		||||
  // Handle received IR Buffer
 | 
			
		||||
  bool on_receive(remote_base::RemoteReceiveData data) override;
 | 
			
		||||
  bool parse_state_frame_(const uint8_t frame[]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace delonghi
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -242,6 +242,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case IMAGE_TYPE_RGB565:
 | 
			
		||||
      for (int img_x = 0; img_x < image->get_width(); img_x++) {
 | 
			
		||||
        for (int img_y = 0; img_y < image->get_height(); img_y++) {
 | 
			
		||||
          this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y));
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -497,6 +504,17 @@ Color Image::get_color_pixel(int x, int y) const {
 | 
			
		||||
                           (progmem_read_byte(this->data_start_ + pos + 0) << 16);
 | 
			
		||||
  return Color(color32);
 | 
			
		||||
}
 | 
			
		||||
Color Image::get_rgb565_pixel(int x, int y) const {
 | 
			
		||||
  if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
 | 
			
		||||
    return Color::BLACK;
 | 
			
		||||
  const uint32_t pos = (x + y * this->width_) * 2;
 | 
			
		||||
  uint16_t rgb565 =
 | 
			
		||||
      progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
 | 
			
		||||
  auto r = (rgb565 & 0xF800) >> 11;
 | 
			
		||||
  auto g = (rgb565 & 0x07E0) >> 5;
 | 
			
		||||
  auto b = rgb565 & 0x001F;
 | 
			
		||||
  return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
 | 
			
		||||
}
 | 
			
		||||
Color Image::get_grayscale_pixel(int x, int y) const {
 | 
			
		||||
  if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
 | 
			
		||||
    return Color::BLACK;
 | 
			
		||||
@@ -532,6 +550,20 @@ Color Animation::get_color_pixel(int x, int y) const {
 | 
			
		||||
                           (progmem_read_byte(this->data_start_ + pos + 0) << 16);
 | 
			
		||||
  return Color(color32);
 | 
			
		||||
}
 | 
			
		||||
Color Animation::get_rgb565_pixel(int x, int y) const {
 | 
			
		||||
  if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
 | 
			
		||||
    return Color::BLACK;
 | 
			
		||||
  const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_;
 | 
			
		||||
  if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_))
 | 
			
		||||
    return Color::BLACK;
 | 
			
		||||
  const uint32_t pos = (x + y * this->width_ + frame_index) * 2;
 | 
			
		||||
  uint16_t rgb565 =
 | 
			
		||||
      progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
 | 
			
		||||
  auto r = (rgb565 & 0xF800) >> 11;
 | 
			
		||||
  auto g = (rgb565 & 0x07E0) >> 5;
 | 
			
		||||
  auto b = rgb565 & 0x001F;
 | 
			
		||||
  return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
 | 
			
		||||
}
 | 
			
		||||
Color Animation::get_grayscale_pixel(int x, int y) const {
 | 
			
		||||
  if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
 | 
			
		||||
    return Color::BLACK;
 | 
			
		||||
@@ -552,6 +584,12 @@ void Animation::next_frame() {
 | 
			
		||||
    this->current_frame_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Animation::prev_frame() {
 | 
			
		||||
  this->current_frame_--;
 | 
			
		||||
  if (this->current_frame_ < 0) {
 | 
			
		||||
    this->current_frame_ = this->animation_frame_count_ - 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
 | 
			
		||||
void DisplayPage::show() { this->parent_->show_page(this); }
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,13 @@ enum ImageType {
 | 
			
		||||
  IMAGE_TYPE_GRAYSCALE = 1,
 | 
			
		||||
  IMAGE_TYPE_RGB24 = 2,
 | 
			
		||||
  IMAGE_TYPE_TRANSPARENT_BINARY = 3,
 | 
			
		||||
  IMAGE_TYPE_RGB565 = 4,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum DisplayType {
 | 
			
		||||
  DISPLAY_TYPE_BINARY = 1,
 | 
			
		||||
  DISPLAY_TYPE_GRAYSCALE = 2,
 | 
			
		||||
  DISPLAY_TYPE_COLOR = 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum DisplayRotation {
 | 
			
		||||
@@ -360,6 +367,11 @@ class DisplayBuffer {
 | 
			
		||||
  virtual int get_width_internal() = 0;
 | 
			
		||||
  DisplayRotation get_rotation() const { return this->rotation_; }
 | 
			
		||||
 | 
			
		||||
  /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
 | 
			
		||||
   * returns the type the display is currently configured to.
 | 
			
		||||
   */
 | 
			
		||||
  virtual DisplayType get_display_type() = 0;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
 | 
			
		||||
 | 
			
		||||
@@ -453,6 +465,7 @@ class Image {
 | 
			
		||||
  Image(const uint8_t *data_start, int width, int height, ImageType type);
 | 
			
		||||
  virtual bool get_pixel(int x, int y) const;
 | 
			
		||||
  virtual Color get_color_pixel(int x, int y) const;
 | 
			
		||||
  virtual Color get_rgb565_pixel(int x, int y) const;
 | 
			
		||||
  virtual Color get_grayscale_pixel(int x, int y) const;
 | 
			
		||||
  int get_width() const;
 | 
			
		||||
  int get_height() const;
 | 
			
		||||
@@ -470,11 +483,13 @@ class Animation : public Image {
 | 
			
		||||
  Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
 | 
			
		||||
  bool get_pixel(int x, int y) const override;
 | 
			
		||||
  Color get_color_pixel(int x, int y) const override;
 | 
			
		||||
  Color get_rgb565_pixel(int x, int y) const override;
 | 
			
		||||
  Color get_grayscale_pixel(int x, int y) const override;
 | 
			
		||||
 | 
			
		||||
  int get_animation_frame_count() const;
 | 
			
		||||
  int get_current_frame() const;
 | 
			
		||||
  void next_frame();
 | 
			
		||||
  void prev_frame();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int current_frame_;
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,9 @@ class ColorUtil {
 | 
			
		||||
    }
 | 
			
		||||
    return color_return;
 | 
			
		||||
  }
 | 
			
		||||
  static inline Color rgb332_to_color(uint8_t rgb332_color) {
 | 
			
		||||
    return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332);
 | 
			
		||||
  }
 | 
			
		||||
  static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) {
 | 
			
		||||
    uint16_t red_color, green_color, blue_color;
 | 
			
		||||
 | 
			
		||||
@@ -100,11 +103,57 @@ class ColorUtil {
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static uint32_t color_to_grayscale4(Color color) {
 | 
			
		||||
    uint32_t gs4 = esp_scale8(color.white, 15);
 | 
			
		||||
    return gs4;
 | 
			
		||||
  }
 | 
			
		||||
  /***
 | 
			
		||||
   * Converts a Color value to an 8bit index using a 24bit 888 palette.
 | 
			
		||||
   * Uses euclidiean distance to calculate the linear distance between
 | 
			
		||||
   * two points in an RGB cube, then iterates through the full palette
 | 
			
		||||
   * returning the closest match.
 | 
			
		||||
   * @param[in] color The target color.
 | 
			
		||||
   * @param[in] palette The 256*3 byte RGB palette.
 | 
			
		||||
   * @return The 8 bit index of the closest color (e.g. for display buffer).
 | 
			
		||||
   */
 | 
			
		||||
  // static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) {
 | 
			
		||||
  static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) {
 | 
			
		||||
    uint8_t closest_index = 0;
 | 
			
		||||
    uint32_t minimum_dist2 = UINT32_MAX;  // Smallest distance^2 to the target
 | 
			
		||||
                                          // so far
 | 
			
		||||
    // int8_t(*plt)[][3] = palette;
 | 
			
		||||
    int16_t tgt_r = color.r;
 | 
			
		||||
    int16_t tgt_g = color.g;
 | 
			
		||||
    int16_t tgt_b = color.b;
 | 
			
		||||
    uint16_t x, y, z;
 | 
			
		||||
    // Loop through each row of the palette
 | 
			
		||||
    for (uint16_t i = 0; i < 256; i++) {
 | 
			
		||||
      // Get the pallet rgb color
 | 
			
		||||
      int16_t plt_r = (int16_t) palette[i * 3 + 0];
 | 
			
		||||
      int16_t plt_g = (int16_t) palette[i * 3 + 1];
 | 
			
		||||
      int16_t plt_b = (int16_t) palette[i * 3 + 2];
 | 
			
		||||
      // Calculate euclidian distance (linear distance in rgb cube).
 | 
			
		||||
      x = (uint32_t) std::abs(tgt_r - plt_r);
 | 
			
		||||
      y = (uint32_t) std::abs(tgt_g - plt_g);
 | 
			
		||||
      z = (uint32_t) std::abs(tgt_b - plt_b);
 | 
			
		||||
      uint32_t dist2 = x * x + y * y + z * z;
 | 
			
		||||
      if (dist2 < minimum_dist2) {
 | 
			
		||||
        minimum_dist2 = dist2;
 | 
			
		||||
        closest_index = (uint8_t) i;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return closest_index;
 | 
			
		||||
  }
 | 
			
		||||
  /***
 | 
			
		||||
   * Converts an 8bit palette index (e.g. from a display buffer) to a color.
 | 
			
		||||
   * @param[in] index The index to look up.
 | 
			
		||||
   * @param[in] palette The 256*3 byte RGB palette.
 | 
			
		||||
   * @return The RGBW Color object looked up by the palette.
 | 
			
		||||
   */
 | 
			
		||||
  static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) {
 | 
			
		||||
    Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0);
 | 
			
		||||
    return color;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								esphome/components/ens210/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/ens210/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										230
									
								
								esphome/components/ens210/ens210.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								esphome/components/ens210/ens210.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,230 @@
 | 
			
		||||
// ENS210 relative humidity and temperature sensor with I2C interface from ScioSense
 | 
			
		||||
//
 | 
			
		||||
// Datasheet: https://www.sciosense.com/wp-content/uploads/2021/01/ENS210.pdf
 | 
			
		||||
//
 | 
			
		||||
// Implementation based on:
 | 
			
		||||
//   https://github.com/maarten-pennings/ENS210
 | 
			
		||||
//   https://github.com/sciosense/ENS210_driver
 | 
			
		||||
 | 
			
		||||
#include "ens210.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ens210 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "ens210";
 | 
			
		||||
 | 
			
		||||
// ENS210 chip constants
 | 
			
		||||
static const uint8_t ENS210_BOOTING_MS = 2;  // Booting time in ms (also after reset, or going to high power)
 | 
			
		||||
static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS =
 | 
			
		||||
    130;                                        // Conversion time in ms for single shot T/H measurement
 | 
			
		||||
static const uint16_t ENS210_PART_ID = 0x0210;  // The expected part id of the ENS210
 | 
			
		||||
 | 
			
		||||
// Addresses of the ENS210 registers
 | 
			
		||||
static const uint8_t ENS210_REGISTER_PART_ID = 0x00;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_UID = 0x04;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_SYS_CTRL = 0x10;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_SYS_STAT = 0x11;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_SENS_RUN = 0x21;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_SENS_START = 0x22;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_SENS_STOP = 0x23;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_SENS_STAT = 0x24;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_T_VAL = 0x30;
 | 
			
		||||
static const uint8_t ENS210_REGISTER_H_VAL = 0x33;
 | 
			
		||||
 | 
			
		||||
// CRC-7 constants
 | 
			
		||||
static const uint8_t CRC7_WIDTH = 7;    // A 7 bits CRC has polynomial of 7th order, which has 8 terms
 | 
			
		||||
static const uint8_t CRC7_POLY = 0x89;  // The 8 coefficients of the polynomial
 | 
			
		||||
static const uint8_t CRC7_IVEC = 0x7F;  // Initial vector has all 7 bits high
 | 
			
		||||
 | 
			
		||||
// Payload data constants
 | 
			
		||||
static const uint8_t DATA7_WIDTH = 17;
 | 
			
		||||
static const uint32_t DATA7_MASK = ((1UL << DATA7_WIDTH) - 1);  // 0b 0 1111 1111 1111 1111
 | 
			
		||||
static const uint32_t DATA7_MSB = (1UL << (DATA7_WIDTH - 1));   // 0b 1 0000 0000 0000 0000
 | 
			
		||||
 | 
			
		||||
// Converts a status to a human readable string
 | 
			
		||||
static const LogString *ens210_status_to_human(int status) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case ENS210Component::ENS210_STATUS_I2C_ERROR:
 | 
			
		||||
      return LOG_STR("I2C error - communication with ENS210 failed!");
 | 
			
		||||
    case ENS210Component::ENS210_STATUS_CRC_ERROR:
 | 
			
		||||
      return LOG_STR("CRC error");
 | 
			
		||||
    case ENS210Component::ENS210_STATUS_INVALID:
 | 
			
		||||
      return LOG_STR("Invalid data");
 | 
			
		||||
    case ENS210Component::ENS210_STATUS_OK:
 | 
			
		||||
      return LOG_STR("Status OK");
 | 
			
		||||
    case ENS210Component::ENS210_WRONG_CHIP_ID:
 | 
			
		||||
      return LOG_STR("ENS210 has wrong chip ID! Is it a ENS210?");
 | 
			
		||||
    default:
 | 
			
		||||
      return LOG_STR("Unknown");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compute the CRC-7 of 'value' (should only have 17 bits)
 | 
			
		||||
// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation
 | 
			
		||||
static uint32_t crc7(uint32_t value) {
 | 
			
		||||
  // Setup polynomial
 | 
			
		||||
  uint32_t polynomial = CRC7_POLY;
 | 
			
		||||
  // Align polynomial with data
 | 
			
		||||
  polynomial = polynomial << (DATA7_WIDTH - CRC7_WIDTH - 1);
 | 
			
		||||
  // Loop variable (indicates which bit to test, start with highest)
 | 
			
		||||
  uint32_t bit = DATA7_MSB;
 | 
			
		||||
  // Make room for CRC value
 | 
			
		||||
  value = value << CRC7_WIDTH;
 | 
			
		||||
  bit = bit << CRC7_WIDTH;
 | 
			
		||||
  polynomial = polynomial << CRC7_WIDTH;
 | 
			
		||||
  // Insert initial vector
 | 
			
		||||
  value |= CRC7_IVEC;
 | 
			
		||||
  // Apply division until all bits done
 | 
			
		||||
  while (bit & (DATA7_MASK << CRC7_WIDTH)) {
 | 
			
		||||
    if (bit & value)
 | 
			
		||||
      value ^= polynomial;
 | 
			
		||||
    bit >>= 1;
 | 
			
		||||
    polynomial >>= 1;
 | 
			
		||||
  }
 | 
			
		||||
  return value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ENS210Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ENS210...");
 | 
			
		||||
  uint8_t data[2];
 | 
			
		||||
  uint16_t part_id = 0;
 | 
			
		||||
  // Reset
 | 
			
		||||
  if (!this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80)) {
 | 
			
		||||
    this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80);
 | 
			
		||||
    this->error_code_ = ENS210_STATUS_I2C_ERROR;
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Wait to boot after reset
 | 
			
		||||
  delay(ENS210_BOOTING_MS);
 | 
			
		||||
  // Must disable low power to read PART_ID
 | 
			
		||||
  if (!set_low_power_(false)) {
 | 
			
		||||
    // Try to go back to default mode (low power enabled)
 | 
			
		||||
    set_low_power_(true);
 | 
			
		||||
    this->error_code_ = ENS210_STATUS_I2C_ERROR;
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Read the PART_ID
 | 
			
		||||
  if (!this->read_bytes(ENS210_REGISTER_PART_ID, data, 2)) {
 | 
			
		||||
    // Try to go back to default mode (low power enabled)
 | 
			
		||||
    set_low_power_(true);
 | 
			
		||||
    this->error_code_ = ENS210_STATUS_I2C_ERROR;
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Pack bytes into partid
 | 
			
		||||
  part_id = data[1] * 256U + data[0] * 1U;
 | 
			
		||||
  // Check expected part id of the ENS210
 | 
			
		||||
  if (part_id != ENS210_PART_ID) {
 | 
			
		||||
    this->error_code_ = ENS210_WRONG_CHIP_ID;
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
  }
 | 
			
		||||
  // Set default power mode (low power enabled)
 | 
			
		||||
  set_low_power_(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ENS210Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ENS210:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, "%s", LOG_STR_ARG(ens210_status_to_human(this->error_code_)));
 | 
			
		||||
  }
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ENS210Component::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
void ENS210Component::update() {
 | 
			
		||||
  // Execute a single measurement
 | 
			
		||||
  if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) {
 | 
			
		||||
    ESP_LOGE(TAG, "Starting single measurement failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Trigger measurement
 | 
			
		||||
  if (!this->write_byte(ENS210_REGISTER_SENS_START, 0x03)) {
 | 
			
		||||
    ESP_LOGE(TAG, "Trigger of measurement failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Wait for measurement to complete
 | 
			
		||||
  this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() {
 | 
			
		||||
    int temperature_data, temperature_status, humidity_data, humidity_status;
 | 
			
		||||
    uint8_t data[6];
 | 
			
		||||
    uint32_t h_val_data, t_val_data;
 | 
			
		||||
    // Set default status for early bail out
 | 
			
		||||
    temperature_status = ENS210_STATUS_I2C_ERROR;
 | 
			
		||||
    humidity_status = ENS210_STATUS_I2C_ERROR;
 | 
			
		||||
 | 
			
		||||
    // Read T_VAL and H_VAL
 | 
			
		||||
    if (!this->read_bytes(ENS210_REGISTER_T_VAL, data, 6)) {
 | 
			
		||||
      ESP_LOGE(TAG, "Communication with ENS210 failed!");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // Pack bytes for humidity
 | 
			
		||||
    h_val_data = (uint32_t)((uint32_t) data[5] << 16 | (uint32_t) data[4] << 8 | (uint32_t) data[3]);
 | 
			
		||||
    // Extract humidity data and update the status
 | 
			
		||||
    extract_measurement_(h_val_data, &humidity_data, &humidity_status);
 | 
			
		||||
 | 
			
		||||
    if (humidity_status == ENS210_STATUS_OK) {
 | 
			
		||||
      if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
        float humidity = (humidity_data & 0xFFFF) / 512.0;
 | 
			
		||||
        this->humidity_sensor_->publish_state(humidity);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Humidity status failure: %s", LOG_STR_ARG(ens210_status_to_human(humidity_status)));
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // Pack bytes for temperature
 | 
			
		||||
    t_val_data = (uint32_t)((uint32_t) data[2] << 16 | (uint32_t) data[1] << 8 | (uint32_t) data[0]);
 | 
			
		||||
    // Extract temperature data and update the status
 | 
			
		||||
    extract_measurement_(t_val_data, &temperature_data, &temperature_status);
 | 
			
		||||
 | 
			
		||||
    if (temperature_status == ENS210_STATUS_OK) {
 | 
			
		||||
      if (this->temperature_sensor_ != nullptr) {
 | 
			
		||||
        // Temperature in Celsius
 | 
			
		||||
        float temperature = (temperature_data & 0xFFFF) / 64.0 - 27315L / 100.0;
 | 
			
		||||
        this->temperature_sensor_->publish_state(temperature);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Temperature status failure: %s", LOG_STR_ARG(ens210_status_to_human(temperature_status)));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment.
 | 
			
		||||
void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) {
 | 
			
		||||
  *data = (val >> 0) & 0xffff;
 | 
			
		||||
  int valid = (val >> 16) & 0x1;
 | 
			
		||||
  uint32_t crc = (val >> 17) & 0x7f;
 | 
			
		||||
  uint32_t payload = (val >> 0) & 0x1ffff;
 | 
			
		||||
  // Check CRC
 | 
			
		||||
  uint8_t crc_ok = crc7(payload) == crc;
 | 
			
		||||
 | 
			
		||||
  if (!crc_ok) {
 | 
			
		||||
    *status = ENS210_STATUS_CRC_ERROR;
 | 
			
		||||
  } else if (!valid) {
 | 
			
		||||
    *status = ENS210_STATUS_INVALID;
 | 
			
		||||
  } else {
 | 
			
		||||
    *status = ENS210_STATUS_OK;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
 | 
			
		||||
bool ENS210Component::set_low_power_(bool enable) {
 | 
			
		||||
  uint8_t low_power_cmd = enable ? 0x01 : 0x00;
 | 
			
		||||
  ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
 | 
			
		||||
  bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
 | 
			
		||||
  delay(ENS210_BOOTING_MS);
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ens210
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										39
									
								
								esphome/components/ens210/ens210.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								esphome/components/ens210/ens210.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ens210 {
 | 
			
		||||
 | 
			
		||||
/// This class implements support for the ENS210 relative humidity and temperature i2c sensor.
 | 
			
		||||
class ENS210Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
 | 
			
		||||
  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
 | 
			
		||||
 | 
			
		||||
  enum ErrorCode {
 | 
			
		||||
    ENS210_STATUS_OK = 0,     // The value was read, the CRC matches, and data is valid
 | 
			
		||||
    ENS210_STATUS_INVALID,    // The value was read, the CRC matches, but the data is invalid (e.g. the measurement was
 | 
			
		||||
                              // not yet finished)
 | 
			
		||||
    ENS210_STATUS_CRC_ERROR,  // The value was read, but the CRC over the payload (valid and data) does not match
 | 
			
		||||
    ENS210_STATUS_I2C_ERROR,  // There was an I2C communication error
 | 
			
		||||
    ENS210_WRONG_CHIP_ID      // The read PART_ID is not the expected part id of the ENS210
 | 
			
		||||
  } error_code_{ENS210_STATUS_OK};
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool set_low_power_(bool enable);
 | 
			
		||||
  void extract_measurement_(uint32_t val, int *data, int *status);
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *temperature_sensor_;
 | 
			
		||||
  sensor::Sensor *humidity_sensor_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ens210
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										58
									
								
								esphome/components/ens210/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								esphome/components/ens210/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@itn3rd77"]
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
ens210_ns = cg.esphome_ns.namespace("ens210")
 | 
			
		||||
 | 
			
		||||
ENS210Component = ens210_ns.class_(
 | 
			
		||||
    "ENS210Component", cg.PollingComponent, i2c.I2CDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ENS210Component),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x43))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_TEMPERATURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_HUMIDITY in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
@@ -107,7 +107,7 @@ def validate_gpio_pin(value):
 | 
			
		||||
    value = _translate_pin(value)
 | 
			
		||||
    variant = CORE.data[KEY_ESP32][KEY_VARIANT]
 | 
			
		||||
    if variant not in _esp32_validations:
 | 
			
		||||
        raise cv.Invalid("Unsupported ESP32 variant {variant}")
 | 
			
		||||
        raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
 | 
			
		||||
 | 
			
		||||
    return _esp32_validations[variant].pin_validation(value)
 | 
			
		||||
 | 
			
		||||
@@ -121,7 +121,7 @@ def validate_supports(value):
 | 
			
		||||
    is_pulldown = mode[CONF_PULLDOWN]
 | 
			
		||||
    variant = CORE.data[KEY_ESP32][KEY_VARIANT]
 | 
			
		||||
    if variant not in _esp32_validations:
 | 
			
		||||
        raise cv.Invalid("Unsupported ESP32 variant {variant}")
 | 
			
		||||
        raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
 | 
			
		||||
 | 
			
		||||
    if is_open_drain and not is_output:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
 
 | 
			
		||||
@@ -118,12 +118,17 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
    // go through vector from back to front (makes erase easier/more efficient)
 | 
			
		||||
    for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
 | 
			
		||||
      const auto &save = s_pending_save[i];
 | 
			
		||||
      esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
 | 
			
		||||
      if (err != 0) {
 | 
			
		||||
        ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
 | 
			
		||||
                 esp_err_to_name(err));
 | 
			
		||||
        any_failed = true;
 | 
			
		||||
        continue;
 | 
			
		||||
      ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str());
 | 
			
		||||
      if (is_changed(nvs_handle, save)) {
 | 
			
		||||
        esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
 | 
			
		||||
        if (err != 0) {
 | 
			
		||||
          ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
 | 
			
		||||
                   esp_err_to_name(err));
 | 
			
		||||
          any_failed = true;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "NVS data not changed skipping %s  len=%u", save.key.c_str(), save.data.size());
 | 
			
		||||
      }
 | 
			
		||||
      s_pending_save.erase(s_pending_save.begin() + i);
 | 
			
		||||
    }
 | 
			
		||||
@@ -137,6 +142,22 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
 | 
			
		||||
    return !any_failed;
 | 
			
		||||
  }
 | 
			
		||||
  bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
 | 
			
		||||
    NVSData stored_data{};
 | 
			
		||||
    size_t actual_len;
 | 
			
		||||
    esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len);
 | 
			
		||||
    if (err != 0) {
 | 
			
		||||
      ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err));
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    stored_data.data.reserve(actual_len);
 | 
			
		||||
    err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len);
 | 
			
		||||
    if (err != 0) {
 | 
			
		||||
      ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err));
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    return to_save.data != stored_data.data;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void setup_preferences() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_FREQUENCY,
 | 
			
		||||
@@ -12,6 +13,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_RESOLUTION,
 | 
			
		||||
    CONF_BRIGHTNESS,
 | 
			
		||||
    CONF_CONTRAST,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
@@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"]
 | 
			
		||||
 | 
			
		||||
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
 | 
			
		||||
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
 | 
			
		||||
 | 
			
		||||
ESP32CameraStreamStartTrigger = esp32_camera_ns.class_(
 | 
			
		||||
    "ESP32CameraStreamStartTrigger",
 | 
			
		||||
    automation.Trigger.template(),
 | 
			
		||||
)
 | 
			
		||||
ESP32CameraStreamStopTrigger = esp32_camera_ns.class_(
 | 
			
		||||
    "ESP32CameraStreamStopTrigger",
 | 
			
		||||
    automation.Trigger.template(),
 | 
			
		||||
)
 | 
			
		||||
ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize")
 | 
			
		||||
FRAME_SIZES = {
 | 
			
		||||
    "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
 | 
			
		||||
@@ -111,6 +120,10 @@ CONF_TEST_PATTERN = "test_pattern"
 | 
			
		||||
CONF_MAX_FRAMERATE = "max_framerate"
 | 
			
		||||
CONF_IDLE_FRAMERATE = "idle_framerate"
 | 
			
		||||
 | 
			
		||||
# stream trigger
 | 
			
		||||
CONF_ON_STREAM_START = "on_stream_start"
 | 
			
		||||
CONF_ON_STREAM_STOP = "on_stream_stop"
 | 
			
		||||
 | 
			
		||||
camera_range_param = cv.int_range(min=-2, max=2)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
@@ -178,6 +191,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
        cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
 | 
			
		||||
            cv.framerate, cv.Range(min=0, max=1)
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32CameraStreamStartTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32CameraStreamStopTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
@@ -238,3 +265,11 @@ async def to_code(config):
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        cg.add_library("espressif/esp32-camera", "1.0.0")
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True)
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_STREAM_START, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_STREAM_STOP, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 
 | 
			
		||||
@@ -282,8 +282,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
 | 
			
		||||
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) {
 | 
			
		||||
  this->new_image_callback_.add(std::move(f));
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); }
 | 
			
		||||
void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); }
 | 
			
		||||
void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) {
 | 
			
		||||
  this->stream_start_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::add_stream_stop_callback(std::function<void()> &&callback) {
 | 
			
		||||
  this->stream_stop_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::start_stream(CameraRequester requester) {
 | 
			
		||||
  this->stream_start_callback_.call();
 | 
			
		||||
  this->stream_requesters_ |= (1U << requester);
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::stop_stream(CameraRequester requester) {
 | 
			
		||||
  this->stream_stop_callback_.call();
 | 
			
		||||
  this->stream_requesters_ &= ~(1U << requester);
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); }
 | 
			
		||||
void ESP32Camera::update_camera_parameters() {
 | 
			
		||||
  sensor_t *s = esp_camera_sensor_get();
 | 
			
		||||
@@ -310,7 +322,6 @@ void ESP32Camera::update_camera_parameters() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ---------------- Internal methods ---------------- */
 | 
			
		||||
uint32_t ESP32Camera::hash_base() { return 3010542557UL; }
 | 
			
		||||
bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; }
 | 
			
		||||
bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; }
 | 
			
		||||
void ESP32Camera::framebuffer_task(void *pv) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
@@ -145,9 +146,11 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
  void request_image(CameraRequester requester);
 | 
			
		||||
  void update_camera_parameters();
 | 
			
		||||
 | 
			
		||||
  void add_stream_start_callback(std::function<void()> &&callback);
 | 
			
		||||
  void add_stream_stop_callback(std::function<void()> &&callback);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /* internal methods */
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
  bool has_requested_image_() const;
 | 
			
		||||
  bool can_return_image_() const;
 | 
			
		||||
 | 
			
		||||
@@ -187,6 +190,8 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
  QueueHandle_t framebuffer_get_queue_;
 | 
			
		||||
  QueueHandle_t framebuffer_return_queue_;
 | 
			
		||||
  CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_;
 | 
			
		||||
  CallbackManager<void()> stream_start_callback_{};
 | 
			
		||||
  CallbackManager<void()> stream_stop_callback_{};
 | 
			
		||||
 | 
			
		||||
  uint32_t last_idle_request_{0};
 | 
			
		||||
  uint32_t last_update_{0};
 | 
			
		||||
@@ -195,6 +200,23 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
extern ESP32Camera *global_esp32_camera;
 | 
			
		||||
 | 
			
		||||
class ESP32CameraStreamStartTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) {
 | 
			
		||||
    parent->add_stream_start_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
class ESP32CameraStreamStopTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) {
 | 
			
		||||
    parent->add_stream_stop_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_camera
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ from esphome.helpers import copy_file_if_changed
 | 
			
		||||
 | 
			
		||||
from .const import (
 | 
			
		||||
    CONF_RESTORE_FROM_FLASH,
 | 
			
		||||
    CONF_EARLY_PIN_INIT,
 | 
			
		||||
    KEY_BOARD,
 | 
			
		||||
    KEY_ESP8266,
 | 
			
		||||
    KEY_PIN_INITIAL_STATES,
 | 
			
		||||
@@ -148,6 +149,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Required(CONF_BOARD): cv.string_strict,
 | 
			
		||||
            cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
 | 
			
		||||
            cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of(
 | 
			
		||||
                *BUILD_FLASH_MODES, lower=True
 | 
			
		||||
            ),
 | 
			
		||||
@@ -197,6 +199,9 @@ async def to_code(config):
 | 
			
		||||
    if config[CONF_RESTORE_FROM_FLASH]:
 | 
			
		||||
        cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
 | 
			
		||||
 | 
			
		||||
    if config[CONF_EARLY_PIN_INIT]:
 | 
			
		||||
        cg.add_define("USE_ESP8266_EARLY_PIN_INIT")
 | 
			
		||||
 | 
			
		||||
    # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
 | 
			
		||||
    # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
 | 
			
		||||
    # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ KEY_ESP8266 = "esp8266"
 | 
			
		||||
KEY_BOARD = "board"
 | 
			
		||||
KEY_PIN_INITIAL_STATES = "pin_initial_states"
 | 
			
		||||
CONF_RESTORE_FROM_FLASH = "restore_from_flash"
 | 
			
		||||
CONF_EARLY_PIN_INIT = "early_pin_init"
 | 
			
		||||
 | 
			
		||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
 | 
			
		||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
 | 
			
		||||
#include "core.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "preferences.h"
 | 
			
		||||
@@ -55,6 +56,7 @@ extern "C" void resetPins() {  // NOLINT
 | 
			
		||||
  // ourselves and this causes pins to toggle during reboot.
 | 
			
		||||
  force_link_symbols();
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266_EARLY_PIN_INIT
 | 
			
		||||
  for (int i = 0; i < 16; i++) {
 | 
			
		||||
    uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i];
 | 
			
		||||
    uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i];
 | 
			
		||||
@@ -63,6 +65,7 @@ extern "C" void resetPins() {  // NOLINT
 | 
			
		||||
    if (level != 255)
 | 
			
		||||
      digitalWrite(i, level);  // NOLINT
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#include "fan.h"
 | 
			
		||||
#include "fan_helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -61,22 +60,6 @@ void FanCall::validate_() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
FanCall &FanCall::set_speed(const char *legacy_speed) {
 | 
			
		||||
  const auto supported_speed_count = this->parent_.get_traits().supported_speed_count();
 | 
			
		||||
  if (strcasecmp(legacy_speed, "low") == 0) {
 | 
			
		||||
    this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count));
 | 
			
		||||
  } else if (strcasecmp(legacy_speed, "medium") == 0) {
 | 
			
		||||
    this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count));
 | 
			
		||||
  } else if (strcasecmp(legacy_speed, "high") == 0) {
 | 
			
		||||
    this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count));
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
 | 
			
		||||
FanCall FanRestoreState::to_call(Fan &fan) {
 | 
			
		||||
  auto call = fan.make_call();
 | 
			
		||||
  call.set_state(this->state);
 | 
			
		||||
@@ -169,7 +152,6 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
 | 
			
		||||
  if (this->get_traits().supports_direction())
 | 
			
		||||
    ESP_LOGCONFIG(tag, "%s  Direction: YES", prefix);
 | 
			
		||||
}
 | 
			
		||||
uint32_t Fan::hash_base() { return 418001110UL; }
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -16,13 +16,6 @@ namespace fan {
 | 
			
		||||
    (obj)->dump_traits_(TAG, prefix); \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
 | 
			
		||||
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.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Simple enum to represent the direction of a fan.
 | 
			
		||||
enum class FanDirection { FORWARD = 0, REVERSE = 1 };
 | 
			
		||||
 | 
			
		||||
@@ -143,7 +136,6 @@ class Fan : public EntityBase {
 | 
			
		||||
  void save_state_();
 | 
			
		||||
 | 
			
		||||
  void dump_traits_(const char *tag, const char *prefix);
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void()> state_callback_{};
 | 
			
		||||
  ESPPreferenceObject rtc_;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
#include <cassert>
 | 
			
		||||
#include "fan_helpers.h"
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
  return static_cast<FanSpeed>(legacy_level - 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) {
 | 
			
		||||
  const auto enum_level = static_cast<int>(speed) + 1;
 | 
			
		||||
  const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels);
 | 
			
		||||
  return static_cast<int>(speed_level);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "fan.h"
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#include "hbridge_fan.h"
 | 
			
		||||
#include "esphome/components/fan/fan_helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 
 | 
			
		||||
@@ -195,7 +195,7 @@ void HydreonRGxxComponent::process_line_() {
 | 
			
		||||
      if (n == std::string::npos) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10);
 | 
			
		||||
      float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr);
 | 
			
		||||
      this->sensors_[i]->publish_state(data);
 | 
			
		||||
      ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state());
 | 
			
		||||
      this->sensors_received_ |= (1 << i);
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ SUPPORTED_SENSORS = {
 | 
			
		||||
PROTOCOL_NAMES = {
 | 
			
		||||
    CONF_MOISTURE: "R",
 | 
			
		||||
    CONF_ACC: "Acc",
 | 
			
		||||
    CONF_R_INT: "Rint",
 | 
			
		||||
    CONF_R_INT: "RInt",
 | 
			
		||||
    CONF_EVENT_ACC: "EventAcc",
 | 
			
		||||
    CONF_TOTAL_ACC: "TotalAcc",
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								esphome/components/i2s_audio/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/i2s_audio/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										160
									
								
								esphome/components/i2s_audio/i2s_audio_media_player.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								esphome/components/i2s_audio/i2s_audio_media_player.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
			
		||||
#include "i2s_audio_media_player.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace i2s_audio {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "audio";
 | 
			
		||||
 | 
			
		||||
void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
 | 
			
		||||
  if (call.get_media_url().has_value()) {
 | 
			
		||||
    if (this->audio_->isRunning())
 | 
			
		||||
      this->audio_->stopSong();
 | 
			
		||||
    this->high_freq_.start();
 | 
			
		||||
    this->audio_->connecttohost(call.get_media_url().value().c_str());
 | 
			
		||||
    this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_volume().has_value()) {
 | 
			
		||||
    this->volume = call.get_volume().value();
 | 
			
		||||
    this->set_volume_(volume);
 | 
			
		||||
    this->unmute_();
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_command().has_value()) {
 | 
			
		||||
    switch (call.get_command().value()) {
 | 
			
		||||
      case media_player::MEDIA_PLAYER_COMMAND_PLAY:
 | 
			
		||||
        if (!this->audio_->isRunning())
 | 
			
		||||
          this->audio_->pauseResume();
 | 
			
		||||
        this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
 | 
			
		||||
        break;
 | 
			
		||||
      case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
 | 
			
		||||
        if (this->audio_->isRunning())
 | 
			
		||||
          this->audio_->pauseResume();
 | 
			
		||||
        this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
 | 
			
		||||
        break;
 | 
			
		||||
      case media_player::MEDIA_PLAYER_COMMAND_STOP:
 | 
			
		||||
        this->stop_();
 | 
			
		||||
        break;
 | 
			
		||||
      case media_player::MEDIA_PLAYER_COMMAND_MUTE:
 | 
			
		||||
        this->mute_();
 | 
			
		||||
        break;
 | 
			
		||||
      case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
 | 
			
		||||
        this->unmute_();
 | 
			
		||||
        break;
 | 
			
		||||
      case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
 | 
			
		||||
        this->audio_->pauseResume();
 | 
			
		||||
        if (this->audio_->isRunning()) {
 | 
			
		||||
          this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
 | 
			
		||||
        } else {
 | 
			
		||||
          this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: {
 | 
			
		||||
        float new_volume = this->volume + 0.1f;
 | 
			
		||||
        if (new_volume > 1.0f)
 | 
			
		||||
          new_volume = 1.0f;
 | 
			
		||||
        this->set_volume_(new_volume);
 | 
			
		||||
        this->unmute_();
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: {
 | 
			
		||||
        float new_volume = this->volume - 0.1f;
 | 
			
		||||
        if (new_volume < 0.0f)
 | 
			
		||||
          new_volume = 0.0f;
 | 
			
		||||
        this->set_volume_(new_volume);
 | 
			
		||||
        this->unmute_();
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void I2SAudioMediaPlayer::mute_() {
 | 
			
		||||
  if (this->mute_pin_ != nullptr) {
 | 
			
		||||
    this->mute_pin_->digital_write(true);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_volume_(0.0f, false);
 | 
			
		||||
  }
 | 
			
		||||
  this->muted_ = true;
 | 
			
		||||
}
 | 
			
		||||
void I2SAudioMediaPlayer::unmute_() {
 | 
			
		||||
  if (this->mute_pin_ != nullptr) {
 | 
			
		||||
    this->mute_pin_->digital_write(false);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_volume_(this->volume, false);
 | 
			
		||||
  }
 | 
			
		||||
  this->muted_ = false;
 | 
			
		||||
}
 | 
			
		||||
void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) {
 | 
			
		||||
  this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21));
 | 
			
		||||
  if (publish)
 | 
			
		||||
    this->volume = volume;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void I2SAudioMediaPlayer::stop_() {
 | 
			
		||||
  if (this->audio_->isRunning())
 | 
			
		||||
    this->audio_->stopSong();
 | 
			
		||||
  this->high_freq_.stop();
 | 
			
		||||
  this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void I2SAudioMediaPlayer::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up Audio...");
 | 
			
		||||
  if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
 | 
			
		||||
    this->audio_ = make_unique<Audio>(true, this->internal_dac_mode_);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->audio_ = make_unique<Audio>(false);
 | 
			
		||||
    this->audio_->setPinout(this->bclk_pin_, this->lrclk_pin_, this->dout_pin_);
 | 
			
		||||
    this->audio_->forceMono(this->external_dac_channels_ == 1);
 | 
			
		||||
    if (this->mute_pin_ != nullptr) {
 | 
			
		||||
      this->mute_pin_->setup();
 | 
			
		||||
      this->mute_pin_->digital_write(false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void I2SAudioMediaPlayer::loop() {
 | 
			
		||||
  this->audio_->loop();
 | 
			
		||||
  if (this->state == media_player::MEDIA_PLAYER_STATE_PLAYING && !this->audio_->isRunning()) {
 | 
			
		||||
    this->stop_();
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() {
 | 
			
		||||
  auto traits = media_player::MediaPlayerTraits();
 | 
			
		||||
  traits.set_supports_pause(true);
 | 
			
		||||
  return traits;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void I2SAudioMediaPlayer::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Audio:");
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "Audio failed to initialize!");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
 | 
			
		||||
    switch (this->internal_dac_mode_) {
 | 
			
		||||
      case I2S_DAC_CHANNEL_LEFT_EN:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, "  Internal DAC mode: Left");
 | 
			
		||||
        break;
 | 
			
		||||
      case I2S_DAC_CHANNEL_RIGHT_EN:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, "  Internal DAC mode: Right");
 | 
			
		||||
        break;
 | 
			
		||||
      case I2S_DAC_CHANNEL_BOTH_EN:
 | 
			
		||||
        ESP_LOGCONFIG(TAG, "  Internal DAC mode: Left & Right");
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace i2s_audio
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_FRAMEWORK_ARDUINO
 | 
			
		||||
							
								
								
									
										63
									
								
								esphome/components/i2s_audio/i2s_audio_media_player.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								esphome/components/i2s_audio/i2s_audio_media_player.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/media_player/media_player.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/gpio.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include <Audio.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace i2s_audio {
 | 
			
		||||
 | 
			
		||||
class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return esphome::setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
 | 
			
		||||
  void set_bclk_pin(uint8_t pin) { this->bclk_pin_ = pin; }
 | 
			
		||||
  void set_lrclk_pin(uint8_t pin) { this->lrclk_pin_ = pin; }
 | 
			
		||||
  void set_mute_pin(GPIOPin *mute_pin) { this->mute_pin_ = mute_pin; }
 | 
			
		||||
  void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
 | 
			
		||||
  void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; }
 | 
			
		||||
 | 
			
		||||
  media_player::MediaPlayerTraits get_traits() override;
 | 
			
		||||
 | 
			
		||||
  bool is_muted() const override { return this->muted_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const media_player::MediaPlayerCall &call) override;
 | 
			
		||||
 | 
			
		||||
  void mute_();
 | 
			
		||||
  void unmute_();
 | 
			
		||||
  void set_volume_(float volume, bool publish = true);
 | 
			
		||||
  void stop_();
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<Audio> audio_;
 | 
			
		||||
 | 
			
		||||
  uint8_t dout_pin_{0};
 | 
			
		||||
  uint8_t din_pin_{0};
 | 
			
		||||
  uint8_t bclk_pin_;
 | 
			
		||||
  uint8_t lrclk_pin_;
 | 
			
		||||
 | 
			
		||||
  GPIOPin *mute_pin_{nullptr};
 | 
			
		||||
  bool muted_{false};
 | 
			
		||||
  float unmuted_volume_{0};
 | 
			
		||||
 | 
			
		||||
  i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
 | 
			
		||||
  uint8_t external_dac_channels_;
 | 
			
		||||
 | 
			
		||||
  HighFrequencyLoopRequester high_freq_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace i2s_audio
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_FRAMEWORK_ARDUINO
 | 
			
		||||
							
								
								
									
										94
									
								
								esphome/components/i2s_audio/media_player.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								esphome/components/i2s_audio/media_player.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import media_player
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
 | 
			
		||||
from esphome import pins
 | 
			
		||||
 | 
			
		||||
from esphome.const import CONF_ID, CONF_MODE
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
 | 
			
		||||
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
 | 
			
		||||
 | 
			
		||||
I2SAudioMediaPlayer = i2s_audio_ns.class_(
 | 
			
		||||
    "I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
 | 
			
		||||
 | 
			
		||||
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
 | 
			
		||||
CONF_I2S_BCLK_PIN = "i2s_bclk_pin"
 | 
			
		||||
CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin"
 | 
			
		||||
CONF_MUTE_PIN = "mute_pin"
 | 
			
		||||
CONF_AUDIO_ID = "audio_id"
 | 
			
		||||
CONF_DAC_TYPE = "dac_type"
 | 
			
		||||
 | 
			
		||||
INTERNAL_DAC_OPTIONS = {
 | 
			
		||||
    "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
 | 
			
		||||
    "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
 | 
			
		||||
    "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.typed_schema(
 | 
			
		||||
        {
 | 
			
		||||
            "internal": cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
 | 
			
		||||
                    cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            .extend(media_player.MEDIA_PLAYER_SCHEMA)
 | 
			
		||||
            .extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
            "external": cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(): cv.declare_id(I2SAudioMediaPlayer),
 | 
			
		||||
                    cv.Required(
 | 
			
		||||
                        CONF_I2S_DOUT_PIN
 | 
			
		||||
                    ): pins.internal_gpio_output_pin_number,
 | 
			
		||||
                    cv.Required(
 | 
			
		||||
                        CONF_I2S_BCLK_PIN
 | 
			
		||||
                    ): pins.internal_gpio_output_pin_number,
 | 
			
		||||
                    cv.Required(
 | 
			
		||||
                        CONF_I2S_LRCLK_PIN
 | 
			
		||||
                    ): pins.internal_gpio_output_pin_number,
 | 
			
		||||
                    cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
                    cv.Optional(CONF_MODE, default="mono"): cv.one_of(
 | 
			
		||||
                        *EXTERNAL_DAC_OPTIONS, lower=True
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            .extend(media_player.MEDIA_PLAYER_SCHEMA)
 | 
			
		||||
            .extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
        },
 | 
			
		||||
        key=CONF_DAC_TYPE,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.only_with_arduino,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await media_player.register_media_player(var, config)
 | 
			
		||||
 | 
			
		||||
    if config[CONF_DAC_TYPE] == "internal":
 | 
			
		||||
        cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
 | 
			
		||||
        cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
 | 
			
		||||
        cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
 | 
			
		||||
        if CONF_MUTE_PIN in config:
 | 
			
		||||
            pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN])
 | 
			
		||||
            cg.add(var.set_mute_pin(pin))
 | 
			
		||||
        cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
 | 
			
		||||
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        cg.add_library("WiFiClientSecure", None)
 | 
			
		||||
        cg.add_library("HTTPClient", None)
 | 
			
		||||
        cg.add_library("esphome/ESP32-audioI2S", "2.1.0")
 | 
			
		||||
        cg.add_build_flag("-DAUDIO_NO_SD_FS")
 | 
			
		||||
@@ -3,13 +3,16 @@ import esphome.config_validation as cv
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.components import display, spi
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_COLOR_PALETTE,
 | 
			
		||||
    CONF_DC_PIN,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    CONF_MODEL,
 | 
			
		||||
    CONF_PAGES,
 | 
			
		||||
    CONF_RAW_DATA_ID,
 | 
			
		||||
    CONF_RESET_PIN,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import HexInt
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["spi"]
 | 
			
		||||
 | 
			
		||||
@@ -21,16 +24,21 @@ ili9341 = ili9341_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341)
 | 
			
		||||
ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341)
 | 
			
		||||
ILI9341TFT24R = ili9341_ns.class_("ILI9341TFT24R", ili9341)
 | 
			
		||||
 | 
			
		||||
ILI9341Model = ili9341_ns.enum("ILI9341Model")
 | 
			
		||||
ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode")
 | 
			
		||||
 | 
			
		||||
MODELS = {
 | 
			
		||||
    "M5STACK": ILI9341Model.M5STACK,
 | 
			
		||||
    "TFT_2.4": ILI9341Model.TFT_24,
 | 
			
		||||
    "TFT_2.4R": ILI9341Model.TFT_24R,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_")
 | 
			
		||||
 | 
			
		||||
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE")
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    display.FULL_DISPLAY_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
@@ -39,6 +47,8 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE,
 | 
			
		||||
            cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("1s"))
 | 
			
		||||
@@ -52,6 +62,8 @@ async def to_code(config):
 | 
			
		||||
        lcd_type = ILI9341M5Stack
 | 
			
		||||
    if config[CONF_MODEL] == "TFT_2.4":
 | 
			
		||||
        lcd_type = ILI9341TFT24
 | 
			
		||||
    if config[CONF_MODEL] == "TFT_2.4R":
 | 
			
		||||
        lcd_type = ILI9341TFT24R
 | 
			
		||||
    rhs = lcd_type.new()
 | 
			
		||||
    var = cg.Pvariable(config[CONF_ID], rhs)
 | 
			
		||||
 | 
			
		||||
@@ -73,3 +85,13 @@ async def to_code(config):
 | 
			
		||||
    if CONF_LED_PIN in config:
 | 
			
		||||
        led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN])
 | 
			
		||||
        cg.add(var.set_led_pin(led_pin))
 | 
			
		||||
 | 
			
		||||
    if config[CONF_COLOR_PALETTE] == "GRAYSCALE":
 | 
			
		||||
        cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED))
 | 
			
		||||
        rhs = []
 | 
			
		||||
        for x in range(256):
 | 
			
		||||
            rhs.extend([HexInt(x), HexInt(x), HexInt(x)])
 | 
			
		||||
        prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
 | 
			
		||||
        cg.add(var.set_palette(prog_arr))
 | 
			
		||||
    else:
 | 
			
		||||
        pass
 | 
			
		||||
 
 | 
			
		||||
@@ -112,29 +112,9 @@ void ILI9341Display::display_() {
 | 
			
		||||
  this->y_high_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) {
 | 
			
		||||
  int r = color_8bit >> 5;
 | 
			
		||||
  int g = (color_8bit >> 2) & 0x07;
 | 
			
		||||
  int b = color_8bit & 0x03;
 | 
			
		||||
  uint16_t color = (r * 0x04) << 11;
 | 
			
		||||
  color |= (g * 0x09) << 5;
 | 
			
		||||
  color |= (b * 0x0A);
 | 
			
		||||
 | 
			
		||||
  return color;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) {
 | 
			
		||||
  // convert 16bit color to 8 bit buffer
 | 
			
		||||
  uint8_t r = color_16bit >> 11;
 | 
			
		||||
  uint8_t g = (color_16bit >> 5) & 0x3F;
 | 
			
		||||
  uint8_t b = color_16bit & 0x1F;
 | 
			
		||||
 | 
			
		||||
  return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ILI9341Display::fill(Color color) {
 | 
			
		||||
  auto color565 = display::ColorUtil::color_to_565(color);
 | 
			
		||||
  memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_());
 | 
			
		||||
  uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
 | 
			
		||||
  memset(this->buffer_, color332, this->get_buffer_length_());
 | 
			
		||||
  this->x_low_ = 0;
 | 
			
		||||
  this->y_low_ = 0;
 | 
			
		||||
  this->x_high_ = this->get_width_internal() - 1;
 | 
			
		||||
@@ -181,8 +161,13 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color)
 | 
			
		||||
  this->y_high_ = (y > this->y_high_) ? y : this->y_high_;
 | 
			
		||||
 | 
			
		||||
  uint32_t pos = (y * width_) + x;
 | 
			
		||||
  auto color565 = display::ColorUtil::color_to_565(color);
 | 
			
		||||
  buffer_[pos] = convert_to_8bit_color_(color565);
 | 
			
		||||
  if (this->buffer_color_mode_ == BITS_8) {
 | 
			
		||||
    uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
 | 
			
		||||
    buffer_[pos] = color332;
 | 
			
		||||
  } else {  // if (this->buffer_color_mode_ == BITS_8_INDEXED) {
 | 
			
		||||
    uint8_t index = display::ColorUtil::color_to_index8_palette888(color, this->palette_);
 | 
			
		||||
    buffer_[pos] = index;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color
 | 
			
		||||
@@ -247,7 +232,13 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint32_t i = 0; i < sz; ++i) {
 | 
			
		||||
    uint16_t color = convert_to_16bit_color_(*src++);
 | 
			
		||||
    uint16_t color;
 | 
			
		||||
    if (this->buffer_color_mode_ == BITS_8) {
 | 
			
		||||
      color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++));
 | 
			
		||||
    } else {  //  if (this->buffer_color_mode == BITS_8_INDEXED) {
 | 
			
		||||
      Color col = display::ColorUtil::index8_to_color_palette888(*src++, this->palette_);
 | 
			
		||||
      color = display::ColorUtil::color_to_565(col);
 | 
			
		||||
    }
 | 
			
		||||
    *dst++ = (uint8_t)(color >> 8);
 | 
			
		||||
    *dst++ = (uint8_t) color;
 | 
			
		||||
  }
 | 
			
		||||
@@ -272,5 +263,13 @@ void ILI9341TFT24::initialize() {
 | 
			
		||||
  this->fill_internal_(Color::BLACK);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//   24_TFT rotated display
 | 
			
		||||
void ILI9341TFT24R::initialize() {
 | 
			
		||||
  this->init_lcd_(INITCMD_TFT);
 | 
			
		||||
  this->width_ = 320;
 | 
			
		||||
  this->height_ = 240;
 | 
			
		||||
  this->fill_internal_(Color::BLACK);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ili9341
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,12 @@ namespace ili9341 {
 | 
			
		||||
enum ILI9341Model {
 | 
			
		||||
  M5STACK = 0,
 | 
			
		||||
  TFT_24,
 | 
			
		||||
  TFT_24R,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum ILI9341ColorMode {
 | 
			
		||||
  BITS_8,
 | 
			
		||||
  BITS_8_INDEXED,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ILI9341Display : public PollingComponent,
 | 
			
		||||
@@ -24,6 +30,8 @@ class ILI9341Display : public PollingComponent,
 | 
			
		||||
  void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
 | 
			
		||||
  void set_led_pin(GPIOPin *led) { this->led_pin_ = led; }
 | 
			
		||||
  void set_model(ILI9341Model model) { this->model_ = model; }
 | 
			
		||||
  void set_palette(const uint8_t *palette) { this->palette_ = palette; }
 | 
			
		||||
  void set_buffer_color_mode(ILI9341ColorMode color_mode) { this->buffer_color_mode_ = color_mode; }
 | 
			
		||||
 | 
			
		||||
  void command(uint8_t value);
 | 
			
		||||
  void data(uint8_t value);
 | 
			
		||||
@@ -41,6 +49,8 @@ class ILI9341Display : public PollingComponent,
 | 
			
		||||
    this->initialize();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void draw_absolute_pixel_internal(int x, int y, Color color) override;
 | 
			
		||||
  void setup_pins_();
 | 
			
		||||
@@ -51,8 +61,6 @@ class ILI9341Display : public PollingComponent,
 | 
			
		||||
  void reset_();
 | 
			
		||||
  void fill_internal_(Color color);
 | 
			
		||||
  void display_();
 | 
			
		||||
  uint16_t convert_to_16bit_color_(uint8_t color_8bit);
 | 
			
		||||
  uint8_t convert_to_8bit_color_(uint16_t color_16bit);
 | 
			
		||||
 | 
			
		||||
  ILI9341Model model_;
 | 
			
		||||
  int16_t width_{320};   ///< Display width as modified by current rotation
 | 
			
		||||
@@ -61,6 +69,9 @@ class ILI9341Display : public PollingComponent,
 | 
			
		||||
  uint16_t y_low_{0};
 | 
			
		||||
  uint16_t x_high_{0};
 | 
			
		||||
  uint16_t y_high_{0};
 | 
			
		||||
  const uint8_t *palette_;
 | 
			
		||||
 | 
			
		||||
  ILI9341ColorMode buffer_color_mode_{BITS_8};
 | 
			
		||||
 | 
			
		||||
  uint32_t get_buffer_length_();
 | 
			
		||||
  int get_width_internal() override;
 | 
			
		||||
@@ -92,5 +103,12 @@ class ILI9341TFT24 : public ILI9341Display {
 | 
			
		||||
 public:
 | 
			
		||||
  void initialize() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
//-----------   ILI9341_24_TFT rotated display --------------
 | 
			
		||||
class ILI9341TFT24R : public ILI9341Display {
 | 
			
		||||
 public:
 | 
			
		||||
  void initialize() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ili9341
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ IMAGE_TYPE = {
 | 
			
		||||
    "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
 | 
			
		||||
    "RGB24": ImageType.IMAGE_TYPE_RGB24,
 | 
			
		||||
    "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
 | 
			
		||||
    "RGB565": ImageType.IMAGE_TYPE_RGB565,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Image_ = display.display_ns.class_("Image")
 | 
			
		||||
@@ -89,6 +90,21 @@ async def to_code(config):
 | 
			
		||||
            data[pos] = pix[2]
 | 
			
		||||
            pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] == "RGB565":
 | 
			
		||||
        image = image.convert("RGB")
 | 
			
		||||
        pixels = list(image.getdata())
 | 
			
		||||
        data = [0 for _ in range(height * width * 3)]
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for pix in pixels:
 | 
			
		||||
            R = pix[0] >> 3
 | 
			
		||||
            G = pix[1] >> 2
 | 
			
		||||
            B = pix[2] >> 3
 | 
			
		||||
            rgb = (R << 11) | (G << 5) | B
 | 
			
		||||
            data[pos] = rgb >> 8
 | 
			
		||||
            pos += 1
 | 
			
		||||
            data[pos] = rgb & 255
 | 
			
		||||
            pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] == "BINARY":
 | 
			
		||||
        image = image.convert("1", dither=dither)
 | 
			
		||||
        width8 = ((width + 7) // 8) * 8
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER
 | 
			
		||||
from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG
 | 
			
		||||
from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
@@ -17,14 +19,19 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_logger_baud_rate(config):
 | 
			
		||||
def validate_logger(config):
 | 
			
		||||
    logger_conf = fv.full_config.get()[CONF_LOGGER]
 | 
			
		||||
    if logger_conf[CONF_BAUD_RATE] == 0:
 | 
			
		||||
        raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0")
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "improv_serial does not support the selected logger hardware_uart"
 | 
			
		||||
            )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = validate_logger
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ class ImprovSerialComponent : public Component {
 | 
			
		||||
  void write_data_(std::vector<uint8_t> &data);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
  HardwareSerial *hw_serial_{nullptr};
 | 
			
		||||
  Stream *hw_serial_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
  uart_port_t uart_num_;
 | 
			
		||||
 
 | 
			
		||||
@@ -86,6 +86,10 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public
 | 
			
		||||
 | 
			
		||||
  void block_partial() { this->block_partial_ = true; }
 | 
			
		||||
 | 
			
		||||
  display::DisplayType get_display_type() override {
 | 
			
		||||
    return get_greyscale() ? display::DisplayType::DISPLAY_TYPE_GRAYSCALE : display::DisplayType::DISPLAY_TYPE_BINARY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void draw_absolute_pixel_internal(int x, int y, Color color) override;
 | 
			
		||||
  void display1b_();
 | 
			
		||||
 
 | 
			
		||||
@@ -26,21 +26,33 @@ std::string build_json(const json_build_t &f) {
 | 
			
		||||
  const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  const size_t request_size = std::min(free_heap, (size_t) 512);
 | 
			
		||||
 | 
			
		||||
  DynamicJsonDocument json_document(request_size);
 | 
			
		||||
  if (json_document.capacity() == 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
 | 
			
		||||
             request_size, free_heap);
 | 
			
		||||
    return "{}";
 | 
			
		||||
  size_t request_size = std::min(free_heap, (size_t) 512);
 | 
			
		||||
  while (true) {
 | 
			
		||||
    ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size);
 | 
			
		||||
    DynamicJsonDocument json_document(request_size);
 | 
			
		||||
    if (json_document.capacity() == 0) {
 | 
			
		||||
      ESP_LOGE(TAG,
 | 
			
		||||
               "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
 | 
			
		||||
               request_size, free_heap);
 | 
			
		||||
      return "{}";
 | 
			
		||||
    }
 | 
			
		||||
    JsonObject root = json_document.to<JsonObject>();
 | 
			
		||||
    f(root);
 | 
			
		||||
    if (json_document.overflowed()) {
 | 
			
		||||
      if (request_size == free_heap) {
 | 
			
		||||
        ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes",
 | 
			
		||||
                 free_heap);
 | 
			
		||||
        return "{}";
 | 
			
		||||
      }
 | 
			
		||||
      request_size = std::min(request_size * 2, free_heap);
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    json_document.shrinkToFit();
 | 
			
		||||
    ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
 | 
			
		||||
    std::string output;
 | 
			
		||||
    serializeJson(json_document, output);
 | 
			
		||||
    return output;
 | 
			
		||||
  }
 | 
			
		||||
  JsonObject root = json_document.to<JsonObject>();
 | 
			
		||||
  f(root);
 | 
			
		||||
  json_document.shrinkToFit();
 | 
			
		||||
  ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
 | 
			
		||||
  std::string output;
 | 
			
		||||
  serializeJson(json_document, output);
 | 
			
		||||
  return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void parse_json(const std::string &data, const json_parse_t &f) {
 | 
			
		||||
 
 | 
			
		||||
@@ -145,7 +145,6 @@ void LightState::loop() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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(); }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -150,8 +150,6 @@ class LightState : public EntityBase, public Component {
 | 
			
		||||
  friend LightCall;
 | 
			
		||||
  friend class AddressableLight;
 | 
			
		||||
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
 | 
			
		||||
  /// Internal method to start an effect with the given index
 | 
			
		||||
  void start_effect_(uint32_t effect_index);
 | 
			
		||||
  /// Internal method to get the currently active effect
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,6 @@ void Lock::publish_state(LockState state) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Lock::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
 | 
			
		||||
uint32_t Lock::hash_base() { return 856245656UL; }
 | 
			
		||||
 | 
			
		||||
void LockCall::perform() {
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
 | 
			
		||||
 
 | 
			
		||||
@@ -167,8 +167,6 @@ class Lock : public EntityBase {
 | 
			
		||||
   */
 | 
			
		||||
  virtual void control(const LockCall &call) = 0;
 | 
			
		||||
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void()> state_callback_{};
 | 
			
		||||
  Deduplicator<LockState> publish_dedup_;
 | 
			
		||||
  ESPPreferenceObject rtc_;
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user