mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			143 Commits
		
	
	
		
			2022.4.0b4
			...
			2022.5.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					77f322166e | ||
| 
						 | 
					f3f6e54818 | ||
| 
						 | 
					fb0fec1f25 | ||
| 
						 | 
					b66af9fb4d | ||
| 
						 | 
					6617d576a7 | ||
| 
						 | 
					420dacb22d | ||
| 
						 | 
					ae2f6ad4d1 | ||
| 
						 | 
					2c28d79bf8 | ||
| 
						 | 
					c5069edc78 | ||
| 
						 | 
					282d9e138c | ||
| 
						 | 
					72fcf2cbe1 | ||
| 
						 | 
					6f49f5465b | ||
| 
						 | 
					17b8bd8316 | ||
| 
						 | 
					7e88938932 | ||
| 
						 | 
					c707e64685 | ||
| 
						 | 
					a639690716 | ||
| 
						 | 
					01222dbab7 | ||
| 
						 | 
					ff72d6a146 | ||
| 
						 | 
					603d0d0c7c | ||
| 
						 | 
					28883f711b | ||
| 
						 | 
					e914828add | ||
| 
						 | 
					c1480029fb | ||
| 
						 | 
					40f622949e | ||
| 
						 | 
					63096ac2bc | ||
| 
						 | 
					c2a59cb476 | ||
| 
						 | 
					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 | ||
| 
						 | 
					993044c870 | ||
| 
						 | 
					a8c1b63edb | ||
| 
						 | 
					db7d946e1b | ||
| 
						 | 
					9576d246ee | ||
| 
						 | 
					988d3ea8ba | ||
| 
						 | 
					0767b92b62 | ||
| 
						 | 
					2064abe16d | ||
| 
						 | 
					b605982f94 | ||
| 
						 | 
					93b628d9a8 | ||
| 
						 | 
					6bac551d9f | ||
| 
						 | 
					70a35656e4 | ||
| 
						 | 
					047c18eac0 | ||
| 
						 | 
					b4a86ce6cf | ||
| 
						 | 
					b778eed419 | ||
| 
						 | 
					fc7348d46d | ||
| 
						 | 
					8be2456c7e | ||
| 
						 | 
					bb5f7249a6 | ||
| 
						 | 
					fc94a5d0ee | ||
| 
						 | 
					24029cc918 | ||
| 
						 | 
					9a9d5964ee | ||
| 
						 | 
					4e4a512107 | ||
| 
						 | 
					0729ed538e | ||
| 
						 | 
					24b75b7ed6 | ||
| 
						 | 
					ec3618ecb8 | ||
| 
						 | 
					792a24f38d | ||
| 
						 | 
					652e8a015b | ||
| 
						 | 
					1ef6fd8fb0 | ||
| 
						 | 
					942b0de7fd | ||
| 
						 | 
					859cca49d1 | ||
| 
						 | 
					8f7ff25624 | ||
| 
						 | 
					97aca8e54c | ||
| 
						 | 
					95acf19067 | ||
| 
						 | 
					3d0899aa58 | ||
| 
						 | 
					138d6e505b | ||
| 
						 | 
					2748e6ba29 | ||
| 
						 | 
					dbd4e927d8 | ||
| 
						 | 
					e73d47918f | ||
| 
						 | 
					b881bc071e | ||
| 
						 | 
					1d0395d1c7 | ||
| 
						 | 
					616c787e37 | ||
| 
						 | 
					0c4de2bc97 | ||
| 
						 | 
					c2f5ac9eba | ||
| 
						 | 
					5764c988af | ||
| 
						 | 
					ccc2fbfd67 | ||
| 
						 | 
					10b4adb8e6 | ||
| 
						 | 
					83b7181bcb | ||
| 
						 | 
					8886b7e141 | ||
| 
						 | 
					7dcc4d030b | ||
| 
						 | 
					b9398897c1 | ||
| 
						 | 
					657b1c60ae | ||
| 
						 | 
					dc54b17778 | ||
| 
						 | 
					1fb214165b | ||
| 
						 | 
					81b2fd78f5 | ||
| 
						 | 
					69002fb1e6 | ||
| 
						 | 
					75332a752d | ||
| 
						 | 
					09ed1aed93 | ||
| 
						 | 
					53d3718028 | ||
| 
						 | 
					2b5dce5232 | ||
| 
						 | 
					9ad84150aa | ||
| 
						 | 
					c0523590b4 | ||
| 
						 | 
					c7f091ab10 | ||
| 
						 | 
					7479e0aada | ||
| 
						 | 
					5bbee1a1fe | ||
| 
						 | 
					bdb9546ca3 | ||
| 
						 | 
					46af4cad6e | ||
| 
						 | 
					76a238912b | ||
| 
						 | 
					909a526967 | ||
| 
						 | 
					cd6f4fb93f | ||
| 
						 | 
					c19458696e | ||
| 
						 | 
					318b930e9f | ||
| 
						 | 
					9296a078a7 | 
@@ -28,8 +28,10 @@ esphome/components/atc_mithermometer/* @ahpohl
 | 
				
			|||||||
esphome/components/b_parasite/* @rbaron
 | 
					esphome/components/b_parasite/* @rbaron
 | 
				
			||||||
esphome/components/ballu/* @bazuchan
 | 
					esphome/components/ballu/* @bazuchan
 | 
				
			||||||
esphome/components/bang_bang/* @OttoWinter
 | 
					esphome/components/bang_bang/* @OttoWinter
 | 
				
			||||||
 | 
					esphome/components/bedjet/* @jhansche
 | 
				
			||||||
esphome/components/bh1750/* @OttoWinter
 | 
					esphome/components/bh1750/* @OttoWinter
 | 
				
			||||||
esphome/components/binary_sensor/* @esphome/core
 | 
					esphome/components/binary_sensor/* @esphome/core
 | 
				
			||||||
 | 
					esphome/components/bl0939/* @ziceva
 | 
				
			||||||
esphome/components/bl0940/* @tobias-
 | 
					esphome/components/bl0940/* @tobias-
 | 
				
			||||||
esphome/components/ble_client/* @buxtronix
 | 
					esphome/components/ble_client/* @buxtronix
 | 
				
			||||||
esphome/components/bme680_bsec/* @trvrnrth
 | 
					esphome/components/bme680_bsec/* @trvrnrth
 | 
				
			||||||
@@ -53,11 +55,13 @@ esphome/components/current_based/* @djwmarcx
 | 
				
			|||||||
esphome/components/daly_bms/* @s1lvi0
 | 
					esphome/components/daly_bms/* @s1lvi0
 | 
				
			||||||
esphome/components/dashboard_import/* @esphome/core
 | 
					esphome/components/dashboard_import/* @esphome/core
 | 
				
			||||||
esphome/components/debug/* @OttoWinter
 | 
					esphome/components/debug/* @OttoWinter
 | 
				
			||||||
 | 
					esphome/components/delonghi/* @grob6000
 | 
				
			||||||
esphome/components/dfplayer/* @glmnet
 | 
					esphome/components/dfplayer/* @glmnet
 | 
				
			||||||
esphome/components/dht/* @OttoWinter
 | 
					esphome/components/dht/* @OttoWinter
 | 
				
			||||||
esphome/components/ds1307/* @badbadc0ffee
 | 
					esphome/components/ds1307/* @badbadc0ffee
 | 
				
			||||||
esphome/components/dsmr/* @glmnet @zuidwijk
 | 
					esphome/components/dsmr/* @glmnet @zuidwijk
 | 
				
			||||||
esphome/components/ektf2232/* @jesserockz
 | 
					esphome/components/ektf2232/* @jesserockz
 | 
				
			||||||
 | 
					esphome/components/ens210/* @itn3rd77
 | 
				
			||||||
esphome/components/esp32/* @esphome/core
 | 
					esphome/components/esp32/* @esphome/core
 | 
				
			||||||
esphome/components/esp32_ble/* @jesserockz
 | 
					esphome/components/esp32_ble/* @jesserockz
 | 
				
			||||||
esphome/components/esp32_ble_server/* @jesserockz
 | 
					esphome/components/esp32_ble_server/* @jesserockz
 | 
				
			||||||
@@ -164,12 +168,13 @@ esphome/components/rf_bridge/* @jesserockz
 | 
				
			|||||||
esphome/components/rgbct/* @jesserockz
 | 
					esphome/components/rgbct/* @jesserockz
 | 
				
			||||||
esphome/components/rtttl/* @glmnet
 | 
					esphome/components/rtttl/* @glmnet
 | 
				
			||||||
esphome/components/safe_mode/* @jsuanet @paulmonigatti
 | 
					esphome/components/safe_mode/* @jsuanet @paulmonigatti
 | 
				
			||||||
esphome/components/scd4x/* @sjtrny
 | 
					esphome/components/scd4x/* @martgras @sjtrny
 | 
				
			||||||
esphome/components/script/* @esphome/core
 | 
					esphome/components/script/* @esphome/core
 | 
				
			||||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
 | 
					esphome/components/sdm_meter/* @jesserockz @polyfaces
 | 
				
			||||||
esphome/components/sdp3x/* @Azimath
 | 
					esphome/components/sdp3x/* @Azimath
 | 
				
			||||||
esphome/components/selec_meter/* @sourabhjaiswal
 | 
					esphome/components/selec_meter/* @sourabhjaiswal
 | 
				
			||||||
esphome/components/select/* @esphome/core
 | 
					esphome/components/select/* @esphome/core
 | 
				
			||||||
 | 
					esphome/components/sen5x/* @martgras
 | 
				
			||||||
esphome/components/sensirion_common/* @martgras
 | 
					esphome/components/sensirion_common/* @martgras
 | 
				
			||||||
esphome/components/sensor/* @esphome/core
 | 
					esphome/components/sensor/* @esphome/core
 | 
				
			||||||
esphome/components/sgp40/* @SenexCrenshaw
 | 
					esphome/components/sgp40/* @SenexCrenshaw
 | 
				
			||||||
@@ -178,9 +183,11 @@ esphome/components/sht4x/* @sjtrny
 | 
				
			|||||||
esphome/components/shutdown/* @esphome/core @jsuanet
 | 
					esphome/components/shutdown/* @esphome/core @jsuanet
 | 
				
			||||||
esphome/components/sim800l/* @glmnet
 | 
					esphome/components/sim800l/* @glmnet
 | 
				
			||||||
esphome/components/sm2135/* @BoukeHaarsma23
 | 
					esphome/components/sm2135/* @BoukeHaarsma23
 | 
				
			||||||
 | 
					esphome/components/sml/* @alengwenus
 | 
				
			||||||
esphome/components/socket/* @esphome/core
 | 
					esphome/components/socket/* @esphome/core
 | 
				
			||||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
 | 
					esphome/components/sonoff_d1/* @anatoly-savchenkov
 | 
				
			||||||
esphome/components/spi/* @esphome/core
 | 
					esphome/components/spi/* @esphome/core
 | 
				
			||||||
 | 
					esphome/components/sps30/* @martgras
 | 
				
			||||||
esphome/components/ssd1322_base/* @kbx81
 | 
					esphome/components/ssd1322_base/* @kbx81
 | 
				
			||||||
esphome/components/ssd1322_spi/* @kbx81
 | 
					esphome/components/ssd1322_spi/* @kbx81
 | 
				
			||||||
esphome/components/ssd1325_base/* @kbx81
 | 
					esphome/components/ssd1325_base/* @kbx81
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,7 @@ RUN \
 | 
				
			|||||||
        iputils-ping=3:20210202-1 \
 | 
					        iputils-ping=3:20210202-1 \
 | 
				
			||||||
        git=1:2.30.2-1 \
 | 
					        git=1:2.30.2-1 \
 | 
				
			||||||
        curl=7.74.0-1.3+deb11u1 \
 | 
					        curl=7.74.0-1.3+deb11u1 \
 | 
				
			||||||
 | 
					        openssh-client=1:8.4p1-5 \
 | 
				
			||||||
    && rm -rf \
 | 
					    && rm -rf \
 | 
				
			||||||
        /tmp/* \
 | 
					        /tmp/* \
 | 
				
			||||||
        /var/{cache,log}/* \
 | 
					        /var/{cache,log}/* \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import argparse
 | 
				
			|||||||
import functools
 | 
					import functools
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util
 | 
				
			|||||||
import esphome.codegen as cg
 | 
					import esphome.codegen as cg
 | 
				
			||||||
from esphome.config import iter_components, read_config, strip_default_ids
 | 
					from esphome.config import iter_components, read_config, strip_default_ids
 | 
				
			||||||
from esphome.const import (
 | 
					from esphome.const import (
 | 
				
			||||||
 | 
					    ALLOWED_NAME_CHARS,
 | 
				
			||||||
    CONF_BAUD_RATE,
 | 
					    CONF_BAUD_RATE,
 | 
				
			||||||
    CONF_BROKER,
 | 
					    CONF_BROKER,
 | 
				
			||||||
    CONF_DEASSERT_RTS_DTR,
 | 
					    CONF_DEASSERT_RTS_DTR,
 | 
				
			||||||
    CONF_LOGGER,
 | 
					    CONF_LOGGER,
 | 
				
			||||||
 | 
					    CONF_NAME,
 | 
				
			||||||
    CONF_OTA,
 | 
					    CONF_OTA,
 | 
				
			||||||
    CONF_PASSWORD,
 | 
					    CONF_PASSWORD,
 | 
				
			||||||
    CONF_PORT,
 | 
					    CONF_PORT,
 | 
				
			||||||
    CONF_ESPHOME,
 | 
					    CONF_ESPHOME,
 | 
				
			||||||
    CONF_PLATFORMIO_OPTIONS,
 | 
					    CONF_PLATFORMIO_OPTIONS,
 | 
				
			||||||
 | 
					    CONF_SUBSTITUTIONS,
 | 
				
			||||||
    SECRETS_FILES,
 | 
					    SECRETS_FILES,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
					from esphome.core import CORE, EsphomeError, coroutine
 | 
				
			||||||
@@ -481,6 +485,98 @@ def command_idedata(args, config):
 | 
				
			|||||||
    return 0
 | 
					    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 = {
 | 
					PRE_CONFIG_ACTIONS = {
 | 
				
			||||||
    "wizard": command_wizard,
 | 
					    "wizard": command_wizard,
 | 
				
			||||||
    "version": command_version,
 | 
					    "version": command_version,
 | 
				
			||||||
@@ -499,6 +595,7 @@ POST_CONFIG_ACTIONS = {
 | 
				
			|||||||
    "mqtt-fingerprint": command_mqtt_fingerprint,
 | 
					    "mqtt-fingerprint": command_mqtt_fingerprint,
 | 
				
			||||||
    "clean": command_clean,
 | 
					    "clean": command_clean,
 | 
				
			||||||
    "idedata": command_idedata,
 | 
					    "idedata": command_idedata,
 | 
				
			||||||
 | 
					    "rename": command_rename,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -681,6 +778,15 @@ def parse_args(argv):
 | 
				
			|||||||
        "configuration", help="Your YAML configuration file(s).", nargs=1
 | 
					        "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
 | 
					    # Keep backward compatibility with the old command line format of
 | 
				
			||||||
    # esphome <config> <command>.
 | 
					    # esphome <config> <command>.
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,6 +64,7 @@ from esphome.cpp_types import (  # noqa
 | 
				
			|||||||
    uint64,
 | 
					    uint64,
 | 
				
			||||||
    int32,
 | 
					    int32,
 | 
				
			||||||
    int64,
 | 
					    int64,
 | 
				
			||||||
 | 
					    size_t,
 | 
				
			||||||
    const_char_ptr,
 | 
					    const_char_ptr,
 | 
				
			||||||
    NAN,
 | 
					    NAN,
 | 
				
			||||||
    esphome_ns,
 | 
					    esphome_ns,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,8 +51,8 @@ void ADCSensor::setup() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2
 | 
					  // 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)
 | 
					#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_);
 | 
					  adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_);
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#endif  // USE_ESP32
 | 
					#endif  // USE_ESP32
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -94,6 +94,29 @@ async def to_code(config):
 | 
				
			|||||||
                data[pos] = pix[2]
 | 
					                data[pos] = pix[2]
 | 
				
			||||||
                pos += 1
 | 
					                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":
 | 
					    elif config[CONF_TYPE] == "BINARY":
 | 
				
			||||||
        width8 = ((width + 7) // 8) * 8
 | 
					        width8 = ((width + 7) // 8) * 8
 | 
				
			||||||
        data = [0 for _ in range((height * width8 // 8) * frames)]
 | 
					        data = [0 for _ in range((height * width8 // 8) * frames)]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) {
 | 
				
			|||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_SELECT
 | 
					#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())
 | 
					  if (obj->is_internal())
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  for (auto &c : this->clients_)
 | 
					  for (auto &c : this->clients_)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,7 @@ class APIServer : public Component, public Controller {
 | 
				
			|||||||
  void on_number_update(number::Number *obj, float state) override;
 | 
					  void on_number_update(number::Number *obj, float state) override;
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#ifdef USE_SELECT
 | 
					#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
 | 
					#endif
 | 
				
			||||||
#ifdef USE_LOCK
 | 
					#ifdef USE_LOCK
 | 
				
			||||||
  void on_lock_update(lock::Lock *obj) override;
 | 
					  void on_lock_update(lock::Lock *obj) override;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					CODEOWNERS = ["@jhansche"]
 | 
				
			||||||
							
								
								
									
										644
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										644
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,644 @@
 | 
				
			|||||||
 | 
					#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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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(BTN_HEAT);
 | 
				
			||||||
 | 
					        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 == "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->node_state != espbt::ClientState::ESTABLISHED) {
 | 
				
			||||||
 | 
					    ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str());
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  auto *time_id = *this->time_id_;
 | 
				
			||||||
 | 
					  time::ESPTime now = time_id->now();
 | 
				
			||||||
 | 
					  if (now.is_valid()) {
 | 
				
			||||||
 | 
					    uint8_t hour = now.hour;
 | 
				
			||||||
 | 
					    uint8_t minute = now.minute;
 | 
				
			||||||
 | 
					    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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** 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_(); });
 | 
				
			||||||
 | 
					    time::ESPTime now = time_id->now();
 | 
				
			||||||
 | 
					    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.");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** 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:
 | 
				
			||||||
 | 
					    case MODE_EXTHT:
 | 
				
			||||||
 | 
					      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
				
			||||||
 | 
					      this->action = climate::CLIMATE_ACTION_HEATING;
 | 
				
			||||||
 | 
					      this->custom_preset.reset();
 | 
				
			||||||
 | 
					      this->preset.reset();
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
							
								
								
									
										124
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
				
			|||||||
 | 
					#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; }
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					  void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /** 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",
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    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_();
 | 
				
			||||||
 | 
					  void send_local_time_();
 | 
				
			||||||
 | 
					  optional<time::RealTimeClock *> time_id_{};
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
							
								
								
									
										78
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					#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,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
							
								
								
									
										42
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					import esphome.codegen as cg
 | 
				
			||||||
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
 | 
					from esphome.components import climate, ble_client, time
 | 
				
			||||||
 | 
					from esphome.const import (
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONFIG_SCHEMA = (
 | 
				
			||||||
 | 
					    climate.CLIMATE_SCHEMA.extend(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            cv.GenerateID(): cv.declare_id(Bedjet),
 | 
				
			||||||
 | 
					            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)
 | 
				
			||||||
 | 
					    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]))
 | 
				
			||||||
							
								
								
									
										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))
 | 
				
			||||||
@@ -81,6 +81,11 @@ static const char *iir_filter_to_str(BME280IIRFilter filter) {
 | 
				
			|||||||
void BME280Component::setup() {
 | 
					void BME280Component::setup() {
 | 
				
			||||||
  ESP_LOGCONFIG(TAG, "Setting up BME280...");
 | 
					  ESP_LOGCONFIG(TAG, "Setting up BME280...");
 | 
				
			||||||
  uint8_t chip_id = 0;
 | 
					  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)) {
 | 
					  if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
 | 
				
			||||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
					    this->error_code_ = COMMUNICATION_FAILED;
 | 
				
			||||||
    this->mark_failed();
 | 
					    this->mark_failed();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -169,6 +169,14 @@ void BME680BSECComponent::loop() {
 | 
				
			|||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    this->status_clear_warning();
 | 
					    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_() {
 | 
					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) {
 | 
					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++) {
 | 
					  for (uint8_t i = 0; i < num_outputs; i++) {
 | 
				
			||||||
 | 
					    float signal = outputs[i].signal;
 | 
				
			||||||
    switch (outputs[i].sensor_id) {
 | 
					    switch (outputs[i].sensor_id) {
 | 
				
			||||||
      case BSEC_OUTPUT_IAQ:
 | 
					      case BSEC_OUTPUT_IAQ:
 | 
				
			||||||
      case BSEC_OUTPUT_STATIC_IAQ:
 | 
					      case BSEC_OUTPUT_STATIC_IAQ: {
 | 
				
			||||||
        uint8_t accuracy;
 | 
					        uint8_t accuracy = outputs[i].accuracy;
 | 
				
			||||||
        accuracy = outputs[i].accuracy;
 | 
					        this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
 | 
				
			||||||
        this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal);
 | 
					        this->queue_push_([this, accuracy]() {
 | 
				
			||||||
        this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
 | 
					          this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
 | 
				
			||||||
        this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true);
 | 
					        });
 | 
				
			||||||
 | 
					        this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Queue up an opportunity to save state
 | 
					        // Queue up an opportunity to save state
 | 
				
			||||||
        this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); });
 | 
					        this->queue_push_([this, accuracy]() { this->save_state_(accuracy); });
 | 
				
			||||||
        break;
 | 
					      } break;
 | 
				
			||||||
      case BSEC_OUTPUT_CO2_EQUIVALENT:
 | 
					      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;
 | 
					        break;
 | 
				
			||||||
      case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
 | 
					      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;
 | 
					        break;
 | 
				
			||||||
      case BSEC_OUTPUT_RAW_PRESSURE:
 | 
					      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;
 | 
					        break;
 | 
				
			||||||
      case BSEC_OUTPUT_RAW_GAS:
 | 
					      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;
 | 
					        break;
 | 
				
			||||||
      case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
 | 
					      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;
 | 
					        break;
 | 
				
			||||||
      case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
 | 
					      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;
 | 
					        break;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() {
 | 
				
			|||||||
  return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000);
 | 
					  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)) {
 | 
					  if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  sensor->publish_state(value);
 | 
					  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)) {
 | 
					  if (!sensor || (sensor->has_state() && sensor->state == value)) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 | 
				
			|||||||
  void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
 | 
					  void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
 | 
				
			||||||
  int64_t get_time_ns_();
 | 
					  int64_t get_time_ns_();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
 | 
					  void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
 | 
				
			||||||
  void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
 | 
					  void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void load_state_();
 | 
					  void load_state_();
 | 
				
			||||||
  void save_state_(uint8_t accuracy);
 | 
					  void save_state_(uint8_t accuracy);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  struct bme680_dev bme680_;
 | 
					  struct bme680_dev bme680_;
 | 
				
			||||||
  bsec_library_return_t bsec_status_{BSEC_OK};
 | 
					  bsec_library_return_t bsec_status_{BSEC_OK};
 | 
				
			||||||
  int8_t bme680_status_{BME680_OK};
 | 
					  int8_t bme680_status_{BME680_OK};
 | 
				
			||||||
@@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 | 
				
			|||||||
  uint32_t millis_overflow_counter_{0};
 | 
					  uint32_t millis_overflow_counter_{0};
 | 
				
			||||||
  int64_t next_call_ns_{0};
 | 
					  int64_t next_call_ns_{0};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  std::queue<std::function<void()>> queue_;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ESPPreferenceObject bsec_state_;
 | 
					  ESPPreferenceObject bsec_state_;
 | 
				
			||||||
  uint32_t state_save_interval_ms_{21600000};  // 6 hours - 4 times a day
 | 
					  uint32_t state_save_interval_ms_{21600000};  // 6 hours - 4 times a day
 | 
				
			||||||
  uint32_t last_state_save_ms_ = 0;
 | 
					  uint32_t last_state_save_ms_ = 0;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -78,6 +78,7 @@ CANBUS_SCHEMA = cv.Schema(
 | 
				
			|||||||
                    min=0, max=0x1FFFFFFF
 | 
					                    min=0, max=0x1FFFFFFF
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
 | 
					                cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
 | 
				
			||||||
 | 
					                cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            validate_id,
 | 
					            validate_id,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
@@ -100,10 +101,20 @@ async def setup_canbus_core_(var, config):
 | 
				
			|||||||
        trigger = cg.new_Pvariable(
 | 
					        trigger = cg.new_Pvariable(
 | 
				
			||||||
            conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
 | 
					            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 cg.register_component(trigger, conf)
 | 
				
			||||||
        await automation.build_automation(
 | 
					        await automation.build_automation(
 | 
				
			||||||
            trigger,
 | 
					            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,
 | 
					            conf,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -81,8 +81,10 @@ void Canbus::loop() {
 | 
				
			|||||||
    // fire all triggers
 | 
					    // fire all triggers
 | 
				
			||||||
    for (auto *trigger : this->triggers_) {
 | 
					    for (auto *trigger : this->triggers_) {
 | 
				
			||||||
      if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
 | 
					      if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
 | 
				
			||||||
          (trigger->use_extended_id_ == can_message.use_extended_id)) {
 | 
					          (trigger->use_extended_id_ == can_message.use_extended_id) &&
 | 
				
			||||||
        trigger->trigger(data, can_message.can_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_{};
 | 
					  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;
 | 
					  friend class Canbus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
 | 
					  explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
 | 
				
			||||||
                         const bool use_extended_id)
 | 
					                         const bool use_extended_id)
 | 
				
			||||||
      : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(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); }
 | 
					  void setup() override { this->parent_->add_trigger(this); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 protected:
 | 
					 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_;
 | 
				
			||||||
  uint32_t can_id_mask_;
 | 
					  uint32_t can_id_mask_;
 | 
				
			||||||
  bool use_extended_id_;
 | 
					  bool use_extended_id_;
 | 
				
			||||||
 | 
					  optional<bool> remote_transmission_request_{};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace canbus
 | 
					}  // namespace canbus
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
				
			|||||||
    if (request->method() == HTTP_GET) {
 | 
					    if (request->method() == HTTP_GET) {
 | 
				
			||||||
      if (request->url() == "/")
 | 
					      if (request->url() == "/")
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      if (request->url() == "/stylesheet.css")
 | 
					      if (request->url() == "/config.json")
 | 
				
			||||||
        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")
 | 
					 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
      if (request->url() == "/wifisave")
 | 
					      if (request->url() == "/wifisave")
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
 | 
				
			|||||||
        cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
 | 
					        cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
 | 
				
			||||||
            validate_climate_fan_mode
 | 
					            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_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),
 | 
					        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)
 | 
					        template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
 | 
				
			||||||
        cg.add(var.set_fan_mode(template_))
 | 
					        cg.add(var.set_fan_mode(template_))
 | 
				
			||||||
    if CONF_CUSTOM_FAN_MODE in config:
 | 
					    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_))
 | 
					        cg.add(var.set_custom_fan_mode(template_))
 | 
				
			||||||
    if CONF_PRESET in config:
 | 
					    if CONF_PRESET in config:
 | 
				
			||||||
        template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
 | 
					        template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
 | 
				
			||||||
        cg.add(var.set_preset(template_))
 | 
					        cg.add(var.set_preset(template_))
 | 
				
			||||||
    if CONF_CUSTOM_PRESET in config:
 | 
					    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_))
 | 
					        cg.add(var.set_custom_preset(template_))
 | 
				
			||||||
    if CONF_SWING_MODE in config:
 | 
					    if CONF_SWING_MODE in config:
 | 
				
			||||||
        template_ = await cg.templatable(
 | 
					        template_ = await cg.templatable(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace copy {
 | 
				
			|||||||
static const char *const TAG = "copy.select";
 | 
					static const char *const TAG = "copy.select";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void CopySelect::setup() {
 | 
					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());
 | 
					  traits.set_options(source_->traits.get_options());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,7 +64,10 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N
 | 
				
			|||||||
        config = {
 | 
					        config = {
 | 
				
			||||||
            "substitutions": {"name": name},
 | 
					            "substitutions": {"name": name},
 | 
				
			||||||
            "packages": {project_name: import_url},
 | 
					            "packages": {project_name: import_url},
 | 
				
			||||||
            "esphome": {"name_add_mac_suffix": False},
 | 
					            "esphome": {
 | 
				
			||||||
 | 
					                "name": "${name}",
 | 
				
			||||||
 | 
					                "name_add_mac_suffix": False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        p.write_text(
 | 
					        p.write_text(
 | 
				
			||||||
            dump(config) + WIFI_CONFIG,
 | 
					            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)
 | 
					DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
 | 
				
			||||||
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
 | 
					EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
 | 
				
			||||||
PreventDeepSleepAction = deep_sleep_ns.class_(
 | 
					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")
 | 
					WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode")
 | 
				
			||||||
@@ -208,28 +215,32 @@ async def to_code(config):
 | 
				
			|||||||
    cg.add_define("USE_DEEP_SLEEP")
 | 
					    cg.add_define("USE_DEEP_SLEEP")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
 | 
					DEEP_SLEEP_ACTION_SCHEMA = cv.Schema(
 | 
				
			||||||
    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(
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        cv.GenerateID(): cv.use_id(DeepSleepComponent),
 | 
					        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(
 | 
					@automation.register_action(
 | 
				
			||||||
    "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA
 | 
					    "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(
 | 
					@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):
 | 
					@automation.register_action(
 | 
				
			||||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
					    "deep_sleep.allow",
 | 
				
			||||||
    return cg.new_Pvariable(action_id, template_arg, paren)
 | 
					    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) {
 | 
					    switch (wakeup_cause) {
 | 
				
			||||||
      case ESP_SLEEP_WAKEUP_EXT0:
 | 
					      case ESP_SLEEP_WAKEUP_EXT0:
 | 
				
			||||||
      case ESP_SLEEP_WAKEUP_EXT1:
 | 
					      case ESP_SLEEP_WAKEUP_EXT1:
 | 
				
			||||||
 | 
					      case ESP_SLEEP_WAKEUP_GPIO:
 | 
				
			||||||
        return this->wakeup_cause_to_run_duration_->gpio_cause;
 | 
					        return this->wakeup_cause_to_run_duration_->gpio_cause;
 | 
				
			||||||
      case ESP_SLEEP_WAKEUP_TOUCHPAD:
 | 
					      case ESP_SLEEP_WAKEUP_TOUCHPAD:
 | 
				
			||||||
        return this->wakeup_cause_to_run_duration_->touch_cause;
 | 
					        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
 | 
					  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; }
 | 
					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) {
 | 
					void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
 | 
				
			||||||
  this->wakeup_pin_mode_ = 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_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_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) {
 | 
					void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
 | 
				
			||||||
  wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
 | 
					  wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
 | 
					void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
 | 
				
			||||||
void DeepSleepComponent::begin_sleep(bool manual) {
 | 
					void DeepSleepComponent::begin_sleep(bool manual) {
 | 
				
			||||||
  if (this->prevent_ && !manual) {
 | 
					  if (this->prevent_ && !manual) {
 | 
				
			||||||
@@ -107,7 +119,8 @@ void DeepSleepComponent::begin_sleep(bool manual) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  App.run_safe_shutdown_hooks();
 | 
					  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())
 | 
					  if (this->sleep_duration_.has_value())
 | 
				
			||||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
					    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
				
			||||||
  if (this->wakeup_pin_ != nullptr) {
 | 
					  if (this->wakeup_pin_ != nullptr) {
 | 
				
			||||||
@@ -125,10 +138,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
 | 
				
			|||||||
    esp_sleep_enable_touchpad_wakeup();
 | 
					    esp_sleep_enable_touchpad_wakeup();
 | 
				
			||||||
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
 | 
					    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  esp_deep_sleep_start();
 | 
					 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					 | 
				
			||||||
#ifdef USE_ESP32_VARIANT_ESP32C3
 | 
					#ifdef USE_ESP32_VARIANT_ESP32C3
 | 
				
			||||||
  if (this->sleep_duration_.has_value())
 | 
					  if (this->sleep_duration_.has_value())
 | 
				
			||||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
					    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()) {
 | 
					    if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
 | 
				
			||||||
      level = !level;
 | 
					      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
 | 
					#endif
 | 
				
			||||||
 | 
					  esp_deep_sleep_start();
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_ESP8266
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
  ESP.deepSleep(*this->sleep_duration_);  // NOLINT(readability-static-accessed-through-instance)
 | 
					  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; }
 | 
					float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
 | 
				
			||||||
void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
 | 
					void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
 | 
				
			||||||
 | 
					void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace deep_sleep
 | 
					}  // namespace deep_sleep
 | 
				
			||||||
}  // namespace esphome
 | 
					}  // namespace esphome
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,17 +70,19 @@ class DeepSleepComponent : public Component {
 | 
				
			|||||||
  void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
 | 
					  void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
 | 
				
			||||||
#endif
 | 
					#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_ext1_wakeup(Ext1Wakeup ext1_wakeup);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void set_touch_wakeup(bool touch_wakeup);
 | 
					  void set_touch_wakeup(bool touch_wakeup);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
  // Set the duration in ms for how long the code should run before entering
 | 
					  // 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.
 | 
					  // deep sleep mode, according to the cause the ESP32 has woken.
 | 
				
			||||||
  void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration);
 | 
					  void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration);
 | 
				
			||||||
 | 
					 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Set a duration in ms for how long the code should run before entering deep sleep mode.
 | 
					  /// 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);
 | 
					  void set_run_duration(uint32_t time_ms);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -94,6 +96,7 @@ class DeepSleepComponent : public Component {
 | 
				
			|||||||
  void begin_sleep(bool manual = false);
 | 
					  void begin_sleep(bool manual = false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void prevent_deep_sleep();
 | 
					  void prevent_deep_sleep();
 | 
				
			||||||
 | 
					  void allow_deep_sleep();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 protected:
 | 
					 protected:
 | 
				
			||||||
  // Returns nullopt if no run duration is set. Otherwise, returns the run
 | 
					  // 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
 | 
					#endif
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...> {
 | 
					template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
 | 
				
			||||||
 public:
 | 
					 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(); }
 | 
					template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 protected:
 | 
					  void play(Ts... x) override { this->parent_->allow_deep_sleep(); }
 | 
				
			||||||
  DeepSleepComponent *deep_sleep_;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace 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;
 | 
					      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);
 | 
					                           (progmem_read_byte(this->data_start_ + pos + 0) << 16);
 | 
				
			||||||
  return Color(color32);
 | 
					  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 {
 | 
					Color Image::get_grayscale_pixel(int x, int y) const {
 | 
				
			||||||
  if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
 | 
					  if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
 | 
				
			||||||
    return Color::BLACK;
 | 
					    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);
 | 
					                           (progmem_read_byte(this->data_start_ + pos + 0) << 16);
 | 
				
			||||||
  return Color(color32);
 | 
					  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 {
 | 
					Color Animation::get_grayscale_pixel(int x, int y) const {
 | 
				
			||||||
  if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
 | 
					  if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
 | 
				
			||||||
    return Color::BLACK;
 | 
					    return Color::BLACK;
 | 
				
			||||||
@@ -552,6 +584,12 @@ void Animation::next_frame() {
 | 
				
			|||||||
    this->current_frame_ = 0;
 | 
					    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)) {}
 | 
					DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
 | 
				
			||||||
void DisplayPage::show() { this->parent_->show_page(this); }
 | 
					void DisplayPage::show() { this->parent_->show_page(this); }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -82,6 +82,7 @@ enum ImageType {
 | 
				
			|||||||
  IMAGE_TYPE_GRAYSCALE = 1,
 | 
					  IMAGE_TYPE_GRAYSCALE = 1,
 | 
				
			||||||
  IMAGE_TYPE_RGB24 = 2,
 | 
					  IMAGE_TYPE_RGB24 = 2,
 | 
				
			||||||
  IMAGE_TYPE_TRANSPARENT_BINARY = 3,
 | 
					  IMAGE_TYPE_TRANSPARENT_BINARY = 3,
 | 
				
			||||||
 | 
					  IMAGE_TYPE_RGB565 = 4,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum DisplayRotation {
 | 
					enum DisplayRotation {
 | 
				
			||||||
@@ -453,6 +454,7 @@ class Image {
 | 
				
			|||||||
  Image(const uint8_t *data_start, int width, int height, ImageType type);
 | 
					  Image(const uint8_t *data_start, int width, int height, ImageType type);
 | 
				
			||||||
  virtual bool get_pixel(int x, int y) const;
 | 
					  virtual bool get_pixel(int x, int y) const;
 | 
				
			||||||
  virtual Color get_color_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;
 | 
					  virtual Color get_grayscale_pixel(int x, int y) const;
 | 
				
			||||||
  int get_width() const;
 | 
					  int get_width() const;
 | 
				
			||||||
  int get_height() const;
 | 
					  int get_height() const;
 | 
				
			||||||
@@ -470,11 +472,13 @@ class Animation : public Image {
 | 
				
			|||||||
  Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
 | 
					  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;
 | 
					  bool get_pixel(int x, int y) const override;
 | 
				
			||||||
  Color get_color_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;
 | 
					  Color get_grayscale_pixel(int x, int y) const override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int get_animation_frame_count() const;
 | 
					  int get_animation_frame_count() const;
 | 
				
			||||||
  int get_current_frame() const;
 | 
					  int get_current_frame() const;
 | 
				
			||||||
  void next_frame();
 | 
					  void next_frame();
 | 
				
			||||||
 | 
					  void prev_frame();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 protected:
 | 
					 protected:
 | 
				
			||||||
  int current_frame_;
 | 
					  int current_frame_;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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)
 | 
					    value = _translate_pin(value)
 | 
				
			||||||
    variant = CORE.data[KEY_ESP32][KEY_VARIANT]
 | 
					    variant = CORE.data[KEY_ESP32][KEY_VARIANT]
 | 
				
			||||||
    if variant not in _esp32_validations:
 | 
					    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)
 | 
					    return _esp32_validations[variant].pin_validation(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -121,7 +121,7 @@ def validate_supports(value):
 | 
				
			|||||||
    is_pulldown = mode[CONF_PULLDOWN]
 | 
					    is_pulldown = mode[CONF_PULLDOWN]
 | 
				
			||||||
    variant = CORE.data[KEY_ESP32][KEY_VARIANT]
 | 
					    variant = CORE.data[KEY_ESP32][KEY_VARIANT]
 | 
				
			||||||
    if variant not in _esp32_validations:
 | 
					    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:
 | 
					    if is_open_drain and not is_output:
 | 
				
			||||||
        raise cv.Invalid(
 | 
					        raise cv.Invalid(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,12 +118,17 @@ class ESP32Preferences : public ESPPreferences {
 | 
				
			|||||||
    // go through vector from back to front (makes erase easier/more efficient)
 | 
					    // go through vector from back to front (makes erase easier/more efficient)
 | 
				
			||||||
    for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
 | 
					    for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
 | 
				
			||||||
      const auto &save = s_pending_save[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());
 | 
					      ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str());
 | 
				
			||||||
      if (err != 0) {
 | 
					      if (is_changed(nvs_handle, save)) {
 | 
				
			||||||
        ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
 | 
					        esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
 | 
				
			||||||
                 esp_err_to_name(err));
 | 
					        if (err != 0) {
 | 
				
			||||||
        any_failed = true;
 | 
					          ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
 | 
				
			||||||
        continue;
 | 
					                   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);
 | 
					      s_pending_save.erase(s_pending_save.begin() + i);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -137,6 +142,22 @@ class ESP32Preferences : public ESPPreferences {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return !any_failed;
 | 
					    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() {
 | 
					void setup_preferences() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ from esphome.helpers import copy_file_if_changed
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from .const import (
 | 
					from .const import (
 | 
				
			||||||
    CONF_RESTORE_FROM_FLASH,
 | 
					    CONF_RESTORE_FROM_FLASH,
 | 
				
			||||||
 | 
					    CONF_EARLY_PIN_INIT,
 | 
				
			||||||
    KEY_BOARD,
 | 
					    KEY_BOARD,
 | 
				
			||||||
    KEY_ESP8266,
 | 
					    KEY_ESP8266,
 | 
				
			||||||
    KEY_PIN_INITIAL_STATES,
 | 
					    KEY_PIN_INITIAL_STATES,
 | 
				
			||||||
@@ -148,6 +149,7 @@ CONFIG_SCHEMA = cv.All(
 | 
				
			|||||||
            cv.Required(CONF_BOARD): cv.string_strict,
 | 
					            cv.Required(CONF_BOARD): cv.string_strict,
 | 
				
			||||||
            cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
 | 
					            cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
 | 
				
			||||||
            cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean,
 | 
					            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(
 | 
					            cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of(
 | 
				
			||||||
                *BUILD_FLASH_MODES, lower=True
 | 
					                *BUILD_FLASH_MODES, lower=True
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -197,6 +199,9 @@ async def to_code(config):
 | 
				
			|||||||
    if config[CONF_RESTORE_FROM_FLASH]:
 | 
					    if config[CONF_RESTORE_FROM_FLASH]:
 | 
				
			||||||
        cg.add_define("USE_ESP8266_PREFERENCES_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
 | 
					    # 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
 | 
					    # 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
 | 
					    # 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_BOARD = "board"
 | 
				
			||||||
KEY_PIN_INITIAL_STATES = "pin_initial_states"
 | 
					KEY_PIN_INITIAL_STATES = "pin_initial_states"
 | 
				
			||||||
CONF_RESTORE_FROM_FLASH = "restore_from_flash"
 | 
					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 namespace is already defined by arduino, manually prefix esphome
 | 
				
			||||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
 | 
					esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
#ifdef USE_ESP8266
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "core.h"
 | 
					#include "core.h"
 | 
				
			||||||
 | 
					#include "esphome/core/defines.h"
 | 
				
			||||||
#include "esphome/core/hal.h"
 | 
					#include "esphome/core/hal.h"
 | 
				
			||||||
#include "esphome/core/helpers.h"
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
#include "preferences.h"
 | 
					#include "preferences.h"
 | 
				
			||||||
@@ -55,6 +56,7 @@ extern "C" void resetPins() {  // NOLINT
 | 
				
			|||||||
  // ourselves and this causes pins to toggle during reboot.
 | 
					  // ourselves and this causes pins to toggle during reboot.
 | 
				
			||||||
  force_link_symbols();
 | 
					  force_link_symbols();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef USE_ESP8266_EARLY_PIN_INIT
 | 
				
			||||||
  for (int i = 0; i < 16; i++) {
 | 
					  for (int i = 0; i < 16; i++) {
 | 
				
			||||||
    uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i];
 | 
					    uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i];
 | 
				
			||||||
    uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i];
 | 
					    uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i];
 | 
				
			||||||
@@ -63,6 +65,7 @@ extern "C" void resetPins() {  // NOLINT
 | 
				
			|||||||
    if (level != 255)
 | 
					    if (level != 255)
 | 
				
			||||||
      digitalWrite(i, level);  // NOLINT
 | 
					      digitalWrite(i, level);  // NOLINT
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace esphome
 | 
					}  // namespace esphome
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,7 @@ IMAGE_TYPE = {
 | 
				
			|||||||
    "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
 | 
					    "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
 | 
				
			||||||
    "RGB24": ImageType.IMAGE_TYPE_RGB24,
 | 
					    "RGB24": ImageType.IMAGE_TYPE_RGB24,
 | 
				
			||||||
    "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
 | 
					    "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
 | 
				
			||||||
 | 
					    "RGB565": ImageType.IMAGE_TYPE_RGB565,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Image_ = display.display_ns.class_("Image")
 | 
					Image_ = display.display_ns.class_("Image")
 | 
				
			||||||
@@ -89,6 +90,21 @@ async def to_code(config):
 | 
				
			|||||||
            data[pos] = pix[2]
 | 
					            data[pos] = pix[2]
 | 
				
			||||||
            pos += 1
 | 
					            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":
 | 
					    elif config[CONF_TYPE] == "BINARY":
 | 
				
			||||||
        image = image.convert("1", dither=dither)
 | 
					        image = image.convert("1", dither=dither)
 | 
				
			||||||
        width8 = ((width + 7) // 8) * 8
 | 
					        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.codegen as cg
 | 
				
			||||||
import esphome.config_validation as cv
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
 | 
					from esphome.core import CORE
 | 
				
			||||||
import esphome.final_validate as fv
 | 
					import esphome.final_validate as fv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CODEOWNERS = ["@esphome/core"]
 | 
					CODEOWNERS = ["@esphome/core"]
 | 
				
			||||||
@@ -17,14 +19,19 @@ CONFIG_SCHEMA = cv.Schema(
 | 
				
			|||||||
).extend(cv.COMPONENT_SCHEMA)
 | 
					).extend(cv.COMPONENT_SCHEMA)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def validate_logger_baud_rate(config):
 | 
					def validate_logger(config):
 | 
				
			||||||
    logger_conf = fv.full_config.get()[CONF_LOGGER]
 | 
					    logger_conf = fv.full_config.get()[CONF_LOGGER]
 | 
				
			||||||
    if logger_conf[CONF_BAUD_RATE] == 0:
 | 
					    if logger_conf[CONF_BAUD_RATE] == 0:
 | 
				
			||||||
        raise cv.Invalid("improv_serial requires the logger baud_rate to be not 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
 | 
					    return config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate
 | 
					FINAL_VALIDATE_SCHEMA = validate_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def to_code(config):
 | 
					async def to_code(config):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,7 +51,7 @@ class ImprovSerialComponent : public Component {
 | 
				
			|||||||
  void write_data_(std::vector<uint8_t> &data);
 | 
					  void write_data_(std::vector<uint8_t> &data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#ifdef USE_ARDUINO
 | 
					#ifdef USE_ARDUINO
 | 
				
			||||||
  HardwareSerial *hw_serial_{nullptr};
 | 
					  Stream *hw_serial_{nullptr};
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#ifdef USE_ESP_IDF
 | 
					#ifdef USE_ESP_IDF
 | 
				
			||||||
  uart_port_t uart_num_;
 | 
					  uart_port_t uart_num_;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
					  const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const size_t request_size = std::min(free_heap, (size_t) 512);
 | 
					  size_t request_size = std::min(free_heap, (size_t) 512);
 | 
				
			||||||
 | 
					  while (true) {
 | 
				
			||||||
  DynamicJsonDocument json_document(request_size);
 | 
					    ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size);
 | 
				
			||||||
  if (json_document.capacity() == 0) {
 | 
					    DynamicJsonDocument json_document(request_size);
 | 
				
			||||||
    ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
 | 
					    if (json_document.capacity() == 0) {
 | 
				
			||||||
             request_size, free_heap);
 | 
					      ESP_LOGE(TAG,
 | 
				
			||||||
    return "{}";
 | 
					               "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) {
 | 
					void parse_json(const std::string &data, const json_parse_t &f) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,8 +19,13 @@ from esphome.const import (
 | 
				
			|||||||
    CONF_TX_BUFFER_SIZE,
 | 
					    CONF_TX_BUFFER_SIZE,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority
 | 
					from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority
 | 
				
			||||||
from esphome.components.esp32 import get_esp32_variant
 | 
					from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
 | 
				
			||||||
from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3
 | 
					from esphome.components.esp32.const import (
 | 
				
			||||||
 | 
					    VARIANT_ESP32,
 | 
				
			||||||
 | 
					    VARIANT_ESP32S2,
 | 
				
			||||||
 | 
					    VARIANT_ESP32C3,
 | 
				
			||||||
 | 
					    VARIANT_ESP32S3,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CODEOWNERS = ["@esphome/core"]
 | 
					CODEOWNERS = ["@esphome/core"]
 | 
				
			||||||
logger_ns = cg.esphome_ns.namespace("logger")
 | 
					logger_ns = cg.esphome_ns.namespace("logger")
 | 
				
			||||||
@@ -54,36 +59,51 @@ LOG_LEVEL_SEVERITY = [
 | 
				
			|||||||
    "VERY_VERBOSE",
 | 
					    "VERY_VERBOSE",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2]
 | 
					UART0 = "UART0"
 | 
				
			||||||
 | 
					UART1 = "UART1"
 | 
				
			||||||
 | 
					UART2 = "UART2"
 | 
				
			||||||
 | 
					UART0_SWAP = "UART0_SWAP"
 | 
				
			||||||
 | 
					USB_SERIAL_JTAG = "USB_SERIAL_JTAG"
 | 
				
			||||||
 | 
					USB_CDC = "USB_CDC"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"]
 | 
					UART_SELECTION_ESP32 = {
 | 
				
			||||||
 | 
					    VARIANT_ESP32: [UART0, UART1, UART2],
 | 
				
			||||||
 | 
					    VARIANT_ESP32S2: [UART0, UART1, USB_CDC],
 | 
				
			||||||
 | 
					    VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
 | 
				
			||||||
 | 
					    VARIANT_ESP32C3: [UART0, UART1, USB_SERIAL_JTAG],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"]
 | 
					UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"]
 | 
					ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
HARDWARE_UART_TO_UART_SELECTION = {
 | 
					HARDWARE_UART_TO_UART_SELECTION = {
 | 
				
			||||||
    "UART0": logger_ns.UART_SELECTION_UART0,
 | 
					    UART0: logger_ns.UART_SELECTION_UART0,
 | 
				
			||||||
    "UART0_SWAP": logger_ns.UART_SELECTION_UART0_SWAP,
 | 
					    UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
 | 
				
			||||||
    "UART1": logger_ns.UART_SELECTION_UART1,
 | 
					    UART1: logger_ns.UART_SELECTION_UART1,
 | 
				
			||||||
    "UART2": logger_ns.UART_SELECTION_UART2,
 | 
					    UART2: logger_ns.UART_SELECTION_UART2,
 | 
				
			||||||
 | 
					    USB_CDC: logger_ns.UART_SELECTION_USB_CDC,
 | 
				
			||||||
 | 
					    USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
HARDWARE_UART_TO_SERIAL = {
 | 
					HARDWARE_UART_TO_SERIAL = {
 | 
				
			||||||
    "UART0": cg.global_ns.Serial,
 | 
					    UART0: cg.global_ns.Serial,
 | 
				
			||||||
    "UART0_SWAP": cg.global_ns.Serial,
 | 
					    UART0_SWAP: cg.global_ns.Serial,
 | 
				
			||||||
    "UART1": cg.global_ns.Serial1,
 | 
					    UART1: cg.global_ns.Serial1,
 | 
				
			||||||
    "UART2": cg.global_ns.Serial2,
 | 
					    UART2: cg.global_ns.Serial2,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
 | 
					is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def uart_selection(value):
 | 
					def uart_selection(value):
 | 
				
			||||||
 | 
					    if value.upper() in ESP_IDF_UARTS:
 | 
				
			||||||
 | 
					        if not CORE.using_esp_idf:
 | 
				
			||||||
 | 
					            raise cv.Invalid(f"Only esp-idf framework supports {value}.")
 | 
				
			||||||
    if CORE.is_esp32:
 | 
					    if CORE.is_esp32:
 | 
				
			||||||
        if get_esp32_variant() in ESP32_REDUCED_VARIANTS:
 | 
					        variant = get_esp32_variant()
 | 
				
			||||||
            return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value)
 | 
					        if variant in UART_SELECTION_ESP32:
 | 
				
			||||||
        return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value)
 | 
					            return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value)
 | 
				
			||||||
    if CORE.is_esp8266:
 | 
					    if CORE.is_esp8266:
 | 
				
			||||||
        return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value)
 | 
					        return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value)
 | 
				
			||||||
    raise NotImplementedError
 | 
					    raise NotImplementedError
 | 
				
			||||||
@@ -113,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
 | 
				
			|||||||
            cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int,
 | 
					            cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int,
 | 
				
			||||||
            cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes,
 | 
					            cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes,
 | 
				
			||||||
            cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean,
 | 
					            cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean,
 | 
				
			||||||
            cv.Optional(CONF_HARDWARE_UART, default="UART0"): uart_selection,
 | 
					            cv.Optional(CONF_HARDWARE_UART, default=UART0): uart_selection,
 | 
				
			||||||
            cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level,
 | 
					            cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level,
 | 
				
			||||||
            cv.Optional(CONF_LOGS, default={}): cv.Schema(
 | 
					            cv.Optional(CONF_LOGS, default={}): cv.Schema(
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -185,6 +205,12 @@ async def to_code(config):
 | 
				
			|||||||
    if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH):
 | 
					    if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH):
 | 
				
			||||||
        cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH")
 | 
					        cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if CORE.using_esp_idf:
 | 
				
			||||||
 | 
					        if config[CONF_HARDWARE_UART] == USB_CDC:
 | 
				
			||||||
 | 
					            add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
 | 
				
			||||||
 | 
					        elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
 | 
				
			||||||
 | 
					            add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Register at end for safe mode
 | 
					    # Register at end for safe mode
 | 
				
			||||||
    await cg.register_component(log, config)
 | 
					    await cg.register_component(log, config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -116,8 +116,22 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) {
 | 
				
			|||||||
    this->hw_serial_->println(msg);
 | 
					    this->hw_serial_->println(msg);
 | 
				
			||||||
#endif  // USE_ARDUINO
 | 
					#endif  // USE_ARDUINO
 | 
				
			||||||
#ifdef USE_ESP_IDF
 | 
					#ifdef USE_ESP_IDF
 | 
				
			||||||
    uart_write_bytes(uart_num_, msg, strlen(msg));
 | 
					    if (
 | 
				
			||||||
    uart_write_bytes(uart_num_, "\n", 1);
 | 
					#if defined(USE_ESP32_VARIANT_ESP32S2)
 | 
				
			||||||
 | 
					        uart_ == UART_SELECTION_USB_CDC
 | 
				
			||||||
 | 
					#elif defined(USE_ESP32_VARIANT_ESP32C3)
 | 
				
			||||||
 | 
					        uart_ == UART_SELECTION_USB_SERIAL_JTAG
 | 
				
			||||||
 | 
					#elif defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
 | 
					        uart_ == UART_SELECTION_USB_CDC || uart_ == UART_SELECTION_USB_SERIAL_JTAG
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
					        /* DISABLES CODE */ (false)
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      puts(msg);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      uart_write_bytes(uart_num_, msg, strlen(msg));
 | 
				
			||||||
 | 
					      uart_write_bytes(uart_num_, "\n", 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -149,13 +163,25 @@ void Logger::pre_setup() {
 | 
				
			|||||||
      case UART_SELECTION_UART0_SWAP:
 | 
					      case UART_SELECTION_UART0_SWAP:
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
        this->hw_serial_ = &Serial;
 | 
					        this->hw_serial_ = &Serial;
 | 
				
			||||||
 | 
					        Serial.begin(this->baud_rate_);
 | 
				
			||||||
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
 | 
					        if (this->uart_ == UART_SELECTION_UART0_SWAP) {
 | 
				
			||||||
 | 
					          Serial.swap();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      case UART_SELECTION_UART1:
 | 
					      case UART_SELECTION_UART1:
 | 
				
			||||||
        this->hw_serial_ = &Serial1;
 | 
					        this->hw_serial_ = &Serial1;
 | 
				
			||||||
 | 
					        Serial1.begin(this->baud_rate_);
 | 
				
			||||||
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
 | 
					        Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
 | 
					#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
 | 
				
			||||||
      case UART_SELECTION_UART2:
 | 
					      case UART_SELECTION_UART2:
 | 
				
			||||||
        this->hw_serial_ = &Serial2;
 | 
					        this->hw_serial_ = &Serial2;
 | 
				
			||||||
 | 
					        Serial2.begin(this->baud_rate_);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -169,39 +195,41 @@ void Logger::pre_setup() {
 | 
				
			|||||||
      case UART_SELECTION_UART1:
 | 
					      case UART_SELECTION_UART1:
 | 
				
			||||||
        uart_num_ = UART_NUM_1;
 | 
					        uart_num_ = UART_NUM_1;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
 | 
					#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
      case UART_SELECTION_UART2:
 | 
					      case UART_SELECTION_UART2:
 | 
				
			||||||
        uart_num_ = UART_NUM_2;
 | 
					        uart_num_ = UART_NUM_2;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
#endif
 | 
					#endif  // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
 | 
				
			||||||
 | 
					#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
 | 
					      case UART_SELECTION_USB_CDC:
 | 
				
			||||||
 | 
					        uart_num_ = -1;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
				
			||||||
 | 
					#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
 | 
					      case UART_SELECTION_USB_SERIAL_JTAG:
 | 
				
			||||||
 | 
					        uart_num_ = -1;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					#endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    uart_config_t uart_config{};
 | 
					    if (uart_num_ >= 0) {
 | 
				
			||||||
    uart_config.baud_rate = (int) baud_rate_;
 | 
					      uart_config_t uart_config{};
 | 
				
			||||||
    uart_config.data_bits = UART_DATA_8_BITS;
 | 
					      uart_config.baud_rate = (int) baud_rate_;
 | 
				
			||||||
    uart_config.parity = UART_PARITY_DISABLE;
 | 
					      uart_config.data_bits = UART_DATA_8_BITS;
 | 
				
			||||||
    uart_config.stop_bits = UART_STOP_BITS_1;
 | 
					      uart_config.parity = UART_PARITY_DISABLE;
 | 
				
			||||||
    uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
 | 
					      uart_config.stop_bits = UART_STOP_BITS_1;
 | 
				
			||||||
    uart_param_config(uart_num_, &uart_config);
 | 
					      uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
 | 
				
			||||||
    const int uart_buffer_size = tx_buffer_size_;
 | 
					      uart_param_config(uart_num_, &uart_config);
 | 
				
			||||||
    // Install UART driver using an event queue here
 | 
					      const int uart_buffer_size = tx_buffer_size_;
 | 
				
			||||||
    uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
 | 
					      // Install UART driver using an event queue here
 | 
				
			||||||
#endif
 | 
					      uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
 | 
				
			||||||
 | 
					 | 
				
			||||||
#ifdef USE_ARDUINO
 | 
					 | 
				
			||||||
    this->hw_serial_->begin(this->baud_rate_);
 | 
					 | 
				
			||||||
#ifdef USE_ESP8266
 | 
					 | 
				
			||||||
    if (this->uart_ == UART_SELECTION_UART0_SWAP) {
 | 
					 | 
				
			||||||
      this->hw_serial_->swap();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
 | 
					#endif  // USE_ESP_IDF
 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
#endif  // USE_ARDUINO
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
#ifdef USE_ESP8266
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
  else {
 | 
					  else {
 | 
				
			||||||
    uart_set_debug(UART_NO);
 | 
					    uart_set_debug(UART_NO);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
#endif
 | 
					#endif  // USE_ESP8266
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  global_logger = this;
 | 
					  global_logger = this;
 | 
				
			||||||
#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO)
 | 
					#if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO)
 | 
				
			||||||
@@ -209,7 +237,7 @@ void Logger::pre_setup() {
 | 
				
			|||||||
  if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
 | 
					  if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
 | 
				
			||||||
    esp_log_level_set("*", ESP_LOG_VERBOSE);
 | 
					    esp_log_level_set("*", ESP_LOG_VERBOSE);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
#endif
 | 
					#endif  // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ESP_LOGI(TAG, "Log initialized");
 | 
					  ESP_LOGI(TAG, "Log initialized");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -224,11 +252,24 @@ void Logger::add_on_log_callback(std::function<void(int, const char *, const cha
 | 
				
			|||||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
 | 
					float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
 | 
				
			||||||
const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
 | 
					const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
 | 
				
			||||||
#ifdef USE_ESP32
 | 
					#ifdef USE_ESP32
 | 
				
			||||||
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART2"};
 | 
					const char *const UART_SELECTIONS[] = {
 | 
				
			||||||
#endif
 | 
					    "UART0",           "UART1",
 | 
				
			||||||
 | 
					#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
 | 
					    "UART2",
 | 
				
			||||||
 | 
					#endif  // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
 | 
				
			||||||
 | 
					#if defined(USE_ESP_IDF)
 | 
				
			||||||
 | 
					#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
 | 
					    "USB_CDC",
 | 
				
			||||||
 | 
					#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
				
			||||||
 | 
					#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
 | 
					    "USB_SERIAL_JTAG",
 | 
				
			||||||
 | 
					#endif  // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3
 | 
				
			||||||
 | 
					#endif  // USE_ESP_IDF
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					#endif  // USE_ESP32
 | 
				
			||||||
#ifdef USE_ESP8266
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
 | 
					const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
 | 
				
			||||||
#endif
 | 
					#endif  // USE_ESP8266
 | 
				
			||||||
void Logger::dump_config() {
 | 
					void Logger::dump_config() {
 | 
				
			||||||
  ESP_LOGCONFIG(TAG, "Logger:");
 | 
					  ESP_LOGCONFIG(TAG, "Logger:");
 | 
				
			||||||
  ESP_LOGCONFIG(TAG, "  Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
 | 
					  ESP_LOGCONFIG(TAG, "  Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,9 +24,19 @@ namespace logger {
 | 
				
			|||||||
enum UARTSelection {
 | 
					enum UARTSelection {
 | 
				
			||||||
  UART_SELECTION_UART0 = 0,
 | 
					  UART_SELECTION_UART0 = 0,
 | 
				
			||||||
  UART_SELECTION_UART1,
 | 
					  UART_SELECTION_UART1,
 | 
				
			||||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
 | 
					#if defined(USE_ESP32)
 | 
				
			||||||
 | 
					#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
  UART_SELECTION_UART2,
 | 
					  UART_SELECTION_UART2,
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
					#ifdef USE_ESP_IDF
 | 
				
			||||||
 | 
					#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
 | 
					  UART_SELECTION_USB_CDC,
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
				
			||||||
 | 
					  UART_SELECTION_USB_SERIAL_JTAG,
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
#ifdef USE_ESP8266
 | 
					#ifdef USE_ESP8266
 | 
				
			||||||
  UART_SELECTION_UART0_SWAP,
 | 
					  UART_SELECTION_UART0_SWAP,
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
@@ -40,7 +50,7 @@ class Logger : public Component {
 | 
				
			|||||||
  void set_baud_rate(uint32_t baud_rate);
 | 
					  void set_baud_rate(uint32_t baud_rate);
 | 
				
			||||||
  uint32_t get_baud_rate() const { return baud_rate_; }
 | 
					  uint32_t get_baud_rate() const { return baud_rate_; }
 | 
				
			||||||
#ifdef USE_ARDUINO
 | 
					#ifdef USE_ARDUINO
 | 
				
			||||||
  HardwareSerial *get_hw_serial() const { return hw_serial_; }
 | 
					  Stream *get_hw_serial() const { return hw_serial_; }
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#ifdef USE_ESP_IDF
 | 
					#ifdef USE_ESP_IDF
 | 
				
			||||||
  uart_port_t get_uart_num() const { return uart_num_; }
 | 
					  uart_port_t get_uart_num() const { return uart_num_; }
 | 
				
			||||||
@@ -119,7 +129,7 @@ class Logger : public Component {
 | 
				
			|||||||
  int tx_buffer_size_{0};
 | 
					  int tx_buffer_size_{0};
 | 
				
			||||||
  UARTSelection uart_{UART_SELECTION_UART0};
 | 
					  UARTSelection uart_{UART_SELECTION_UART0};
 | 
				
			||||||
#ifdef USE_ARDUINO
 | 
					#ifdef USE_ARDUINO
 | 
				
			||||||
  HardwareSerial *hw_serial_{nullptr};
 | 
					  Stream *hw_serial_{nullptr};
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
#ifdef USE_ESP_IDF
 | 
					#ifdef USE_ESP_IDF
 | 
				
			||||||
  uart_port_t uart_num_;
 | 
					  uart_port_t uart_num_;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,7 +52,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Now parse the data - See Datasheet for definition
 | 
					  // Now parse the data - See Datasheet for definition
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (static_cast<SensorType>(manu_data.data[0]) != STANDARD_BOTTOM_UP) {
 | 
					  if (static_cast<SensorType>(manu_data.data[0]) != STANDARD_BOTTOM_UP &&
 | 
				
			||||||
 | 
					      static_cast<SensorType>(manu_data.data[0]) != PLUS_BOTTOM_UP) {
 | 
				
			||||||
    ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]);
 | 
					    ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]);
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,8 @@ namespace mopeka_pro_check {
 | 
				
			|||||||
enum SensorType {
 | 
					enum SensorType {
 | 
				
			||||||
  STANDARD_BOTTOM_UP = 0x03,
 | 
					  STANDARD_BOTTOM_UP = 0x03,
 | 
				
			||||||
  TOP_DOWN_AIR_ABOVE = 0x04,
 | 
					  TOP_DOWN_AIR_ABOVE = 0x04,
 | 
				
			||||||
  BOTTOM_UP_WATER = 0x05
 | 
					  BOTTOM_UP_WATER = 0x05,
 | 
				
			||||||
 | 
					  PLUS_BOTTOM_UP = 0x08
 | 
				
			||||||
  // all other values are reserved
 | 
					  // all other values are reserved
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -556,7 +556,12 @@ void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; }
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
void MQTTClientComponent::disable_discovery() {
 | 
					void MQTTClientComponent::disable_discovery() {
 | 
				
			||||||
  this->discovery_info_ = MQTTDiscoveryInfo{
 | 
					  this->discovery_info_ = MQTTDiscoveryInfo{
 | 
				
			||||||
      .prefix = "", .retain = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR};
 | 
					      .prefix = "",
 | 
				
			||||||
 | 
					      .retain = false,
 | 
				
			||||||
 | 
					      .clean = false,
 | 
				
			||||||
 | 
					      .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
 | 
				
			||||||
 | 
					      .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
void MQTTClientComponent::on_shutdown() {
 | 
					void MQTTClientComponent::on_shutdown() {
 | 
				
			||||||
  if (!this->shutdown_message_.topic.empty()) {
 | 
					  if (!this->shutdown_message_.topic.empty()) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -277,6 +277,7 @@ class MQTTClientComponent : public Component {
 | 
				
			|||||||
      .retain = true,
 | 
					      .retain = true,
 | 
				
			||||||
      .clean = false,
 | 
					      .clean = false,
 | 
				
			||||||
      .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
 | 
					      .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
 | 
				
			||||||
 | 
					      .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  std::string topic_prefix_{};
 | 
					  std::string topic_prefix_{};
 | 
				
			||||||
  MQTTMessage log_message_;
 | 
					  MQTTMessage log_message_;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ void MQTTSelectComponent::setup() {
 | 
				
			|||||||
    call.set_option(state);
 | 
					    call.set_option(state);
 | 
				
			||||||
    call.perform();
 | 
					    call.perform();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); });
 | 
					  this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void MQTTSelectComponent::dump_config() {
 | 
					void MQTTSelectComponent::dump_config() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,8 @@ from esphome.const import (
 | 
				
			|||||||
    CONF_UNIT_OF_MEASUREMENT,
 | 
					    CONF_UNIT_OF_MEASUREMENT,
 | 
				
			||||||
    CONF_MQTT_ID,
 | 
					    CONF_MQTT_ID,
 | 
				
			||||||
    CONF_VALUE,
 | 
					    CONF_VALUE,
 | 
				
			||||||
 | 
					    CONF_OPERATION,
 | 
				
			||||||
 | 
					    CONF_CYCLE,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from esphome.core import CORE, coroutine_with_priority
 | 
					from esphome.core import CORE, coroutine_with_priority
 | 
				
			||||||
from esphome.cpp_helpers import setup_entity
 | 
					from esphome.cpp_helpers import setup_entity
 | 
				
			||||||
@@ -35,6 +37,7 @@ ValueRangeTrigger = number_ns.class_(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Actions
 | 
					# Actions
 | 
				
			||||||
NumberSetAction = number_ns.class_("NumberSetAction", automation.Action)
 | 
					NumberSetAction = number_ns.class_("NumberSetAction", automation.Action)
 | 
				
			||||||
 | 
					NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Conditions
 | 
					# Conditions
 | 
				
			||||||
NumberInRangeCondition = number_ns.class_(
 | 
					NumberInRangeCondition = number_ns.class_(
 | 
				
			||||||
@@ -49,6 +52,15 @@ NUMBER_MODES = {
 | 
				
			|||||||
    "SLIDER": NumberMode.NUMBER_MODE_SLIDER,
 | 
					    "SLIDER": NumberMode.NUMBER_MODE_SLIDER,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberOperation = number_ns.enum("NumberOperation")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NUMBER_OPERATION_OPTIONS = {
 | 
				
			||||||
 | 
					    "INCREMENT": NumberOperation.NUMBER_OP_INCREMENT,
 | 
				
			||||||
 | 
					    "DECREMENT": NumberOperation.NUMBER_OP_DECREMENT,
 | 
				
			||||||
 | 
					    "TO_MIN": NumberOperation.NUMBER_OP_TO_MIN,
 | 
				
			||||||
 | 
					    "TO_MAX": NumberOperation.NUMBER_OP_TO_MAX,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
icon = cv.icon
 | 
					icon = cv.icon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
 | 
					NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
 | 
				
			||||||
@@ -159,12 +171,18 @@ async def to_code(config):
 | 
				
			|||||||
    cg.add_global(number_ns.using)
 | 
					    cg.add_global(number_ns.using)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OPERATION_BASE_SCHEMA = cv.Schema(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        cv.Required(CONF_ID): cv.use_id(Number),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@automation.register_action(
 | 
					@automation.register_action(
 | 
				
			||||||
    "number.set",
 | 
					    "number.set",
 | 
				
			||||||
    NumberSetAction,
 | 
					    NumberSetAction,
 | 
				
			||||||
    cv.Schema(
 | 
					    OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            cv.Required(CONF_ID): cv.use_id(Number),
 | 
					 | 
				
			||||||
            cv.Required(CONF_VALUE): cv.templatable(cv.float_),
 | 
					            cv.Required(CONF_VALUE): cv.templatable(cv.float_),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
@@ -175,3 +193,85 @@ async def number_set_to_code(config, action_id, template_arg, args):
 | 
				
			|||||||
    template_ = await cg.templatable(config[CONF_VALUE], args, float)
 | 
					    template_ = await cg.templatable(config[CONF_VALUE], args, float)
 | 
				
			||||||
    cg.add(var.set_value(template_))
 | 
					    cg.add(var.set_value(template_))
 | 
				
			||||||
    return var
 | 
					    return var
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "number.increment",
 | 
				
			||||||
 | 
					    NumberOperationAction,
 | 
				
			||||||
 | 
					    automation.maybe_simple_id(
 | 
				
			||||||
 | 
					        OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_MODE, default="INCREMENT"): cv.one_of(
 | 
				
			||||||
 | 
					                    "INCREMENT", upper=True
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                cv.Optional(CONF_CYCLE, default=True): cv.boolean,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "number.decrement",
 | 
				
			||||||
 | 
					    NumberOperationAction,
 | 
				
			||||||
 | 
					    automation.maybe_simple_id(
 | 
				
			||||||
 | 
					        OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_MODE, default="DECREMENT"): cv.one_of(
 | 
				
			||||||
 | 
					                    "DECREMENT", upper=True
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                cv.Optional(CONF_CYCLE, default=True): cv.boolean,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "number.to_min",
 | 
				
			||||||
 | 
					    NumberOperationAction,
 | 
				
			||||||
 | 
					    automation.maybe_simple_id(
 | 
				
			||||||
 | 
					        OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_MODE, default="TO_MIN"): cv.one_of(
 | 
				
			||||||
 | 
					                    "TO_MIN", upper=True
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "number.to_max",
 | 
				
			||||||
 | 
					    NumberOperationAction,
 | 
				
			||||||
 | 
					    automation.maybe_simple_id(
 | 
				
			||||||
 | 
					        OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_MODE, default="TO_MAX"): cv.one_of(
 | 
				
			||||||
 | 
					                    "TO_MAX", upper=True
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "number.operation",
 | 
				
			||||||
 | 
					    NumberOperationAction,
 | 
				
			||||||
 | 
					    OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            cv.Required(CONF_OPERATION): cv.templatable(
 | 
				
			||||||
 | 
					                cv.enum(NUMBER_OPERATION_OPTIONS, upper=True)
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def number_to_to_code(config, action_id, template_arg, args):
 | 
				
			||||||
 | 
					    paren = await cg.get_variable(config[CONF_ID])
 | 
				
			||||||
 | 
					    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
				
			||||||
 | 
					    if CONF_OPERATION in config:
 | 
				
			||||||
 | 
					        to_ = await cg.templatable(config[CONF_OPERATION], args, NumberOperation)
 | 
				
			||||||
 | 
					        cg.add(var.set_operation(to_))
 | 
				
			||||||
 | 
					        if CONF_CYCLE in config:
 | 
				
			||||||
 | 
					            cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool)
 | 
				
			||||||
 | 
					            cg.add(var.set_cycle(cycle_))
 | 
				
			||||||
 | 
					    if CONF_MODE in config:
 | 
				
			||||||
 | 
					        cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[config[CONF_MODE]]))
 | 
				
			||||||
 | 
					        if CONF_CYCLE in config:
 | 
				
			||||||
 | 
					            cg.add(var.set_cycle(config[CONF_CYCLE]))
 | 
				
			||||||
 | 
					    return var
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,25 @@ template<typename... Ts> class NumberSetAction : public Action<Ts...> {
 | 
				
			|||||||
  Number *number_;
 | 
					  Number *number_;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename... Ts> class NumberOperationAction : public Action<Ts...> {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  explicit NumberOperationAction(Number *number) : number_(number) {}
 | 
				
			||||||
 | 
					  TEMPLATABLE_VALUE(NumberOperation, operation)
 | 
				
			||||||
 | 
					  TEMPLATABLE_VALUE(bool, cycle)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void play(Ts... x) override {
 | 
				
			||||||
 | 
					    auto call = this->number_->make_call();
 | 
				
			||||||
 | 
					    call.with_operation(this->operation_.value(x...));
 | 
				
			||||||
 | 
					    if (this->cycle_.has_value()) {
 | 
				
			||||||
 | 
					      call.with_cycle(this->cycle_.value(x...));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    call.perform();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  Number *number_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ValueRangeTrigger : public Trigger<float>, public Component {
 | 
					class ValueRangeTrigger : public Trigger<float>, public Component {
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  explicit ValueRangeTrigger(Number *parent) : parent_(parent) {}
 | 
					  explicit ValueRangeTrigger(Number *parent) : parent_(parent) {}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,30 +6,6 @@ namespace number {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
static const char *const TAG = "number";
 | 
					static const char *const TAG = "number";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void NumberCall::perform() {
 | 
					 | 
				
			||||||
  ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
 | 
					 | 
				
			||||||
  if (!this->value_.has_value() || std::isnan(*this->value_)) {
 | 
					 | 
				
			||||||
    ESP_LOGW(TAG, "No value set for NumberCall");
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const auto &traits = this->parent_->traits;
 | 
					 | 
				
			||||||
  auto value = *this->value_;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  float min_value = traits.get_min_value();
 | 
					 | 
				
			||||||
  if (value < min_value) {
 | 
					 | 
				
			||||||
    ESP_LOGW(TAG, "  Value %f must not be less than minimum %f", value, min_value);
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  float max_value = traits.get_max_value();
 | 
					 | 
				
			||||||
  if (value > max_value) {
 | 
					 | 
				
			||||||
    ESP_LOGW(TAG, "  Value %f must not be greater than maximum %f", value, max_value);
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  ESP_LOGD(TAG, "  Value: %f", *this->value_);
 | 
					 | 
				
			||||||
  this->parent_->control(*this->value_);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void Number::publish_state(float state) {
 | 
					void Number::publish_state(float state) {
 | 
				
			||||||
  this->has_state_ = true;
 | 
					  this->has_state_ = true;
 | 
				
			||||||
  this->state = state;
 | 
					  this->state = state;
 | 
				
			||||||
@@ -41,15 +17,6 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
 | 
				
			|||||||
  this->state_callback_.add(std::move(callback));
 | 
					  this->state_callback_.add(std::move(callback));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
std::string NumberTraits::get_unit_of_measurement() {
 | 
					 | 
				
			||||||
  if (this->unit_of_measurement_.has_value())
 | 
					 | 
				
			||||||
    return *this->unit_of_measurement_;
 | 
					 | 
				
			||||||
  return "";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) {
 | 
					 | 
				
			||||||
  this->unit_of_measurement_ = unit_of_measurement;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
uint32_t Number::hash_base() { return 2282307003UL; }
 | 
					uint32_t Number::hash_base() { return 2282307003UL; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace number
 | 
					}  // namespace number
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@
 | 
				
			|||||||
#include "esphome/core/component.h"
 | 
					#include "esphome/core/component.h"
 | 
				
			||||||
#include "esphome/core/entity_base.h"
 | 
					#include "esphome/core/entity_base.h"
 | 
				
			||||||
#include "esphome/core/helpers.h"
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
 | 
					#include "number_call.h"
 | 
				
			||||||
 | 
					#include "number_traits.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace esphome {
 | 
					namespace esphome {
 | 
				
			||||||
namespace number {
 | 
					namespace number {
 | 
				
			||||||
@@ -20,54 +22,6 @@ namespace number {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class Number;
 | 
					class Number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NumberCall {
 | 
					 | 
				
			||||||
 public:
 | 
					 | 
				
			||||||
  explicit NumberCall(Number *parent) : parent_(parent) {}
 | 
					 | 
				
			||||||
  void perform();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  NumberCall &set_value(float value) {
 | 
					 | 
				
			||||||
    value_ = value;
 | 
					 | 
				
			||||||
    return *this;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const optional<float> &get_value() const { return value_; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 protected:
 | 
					 | 
				
			||||||
  Number *const parent_;
 | 
					 | 
				
			||||||
  optional<float> value_;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum NumberMode : uint8_t {
 | 
					 | 
				
			||||||
  NUMBER_MODE_AUTO = 0,
 | 
					 | 
				
			||||||
  NUMBER_MODE_BOX = 1,
 | 
					 | 
				
			||||||
  NUMBER_MODE_SLIDER = 2,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NumberTraits {
 | 
					 | 
				
			||||||
 public:
 | 
					 | 
				
			||||||
  void set_min_value(float min_value) { min_value_ = min_value; }
 | 
					 | 
				
			||||||
  float get_min_value() const { return min_value_; }
 | 
					 | 
				
			||||||
  void set_max_value(float max_value) { max_value_ = max_value; }
 | 
					 | 
				
			||||||
  float get_max_value() const { return max_value_; }
 | 
					 | 
				
			||||||
  void set_step(float step) { step_ = step; }
 | 
					 | 
				
			||||||
  float get_step() const { return step_; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Get the unit of measurement, using the manual override if set.
 | 
					 | 
				
			||||||
  std::string get_unit_of_measurement();
 | 
					 | 
				
			||||||
  /// Manually set the unit of measurement.
 | 
					 | 
				
			||||||
  void set_unit_of_measurement(const std::string &unit_of_measurement);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Get/set the frontend mode.
 | 
					 | 
				
			||||||
  NumberMode get_mode() const { return this->mode_; }
 | 
					 | 
				
			||||||
  void set_mode(NumberMode mode) { this->mode_ = mode; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 protected:
 | 
					 | 
				
			||||||
  float min_value_ = NAN;
 | 
					 | 
				
			||||||
  float max_value_ = NAN;
 | 
					 | 
				
			||||||
  float step_ = NAN;
 | 
					 | 
				
			||||||
  optional<std::string> unit_of_measurement_;  ///< Unit of measurement override
 | 
					 | 
				
			||||||
  NumberMode mode_{NUMBER_MODE_AUTO};
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/** Base-class for all numbers.
 | 
					/** Base-class for all numbers.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * A number can use publish_state to send out a new value.
 | 
					 * A number can use publish_state to send out a new value.
 | 
				
			||||||
@@ -79,7 +33,6 @@ class Number : public EntityBase {
 | 
				
			|||||||
  void publish_state(float state);
 | 
					  void publish_state(float state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  NumberCall make_call() { return NumberCall(this); }
 | 
					  NumberCall make_call() { return NumberCall(this); }
 | 
				
			||||||
  void set(float value) { make_call().set_value(value).perform(); }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void add_on_state_callback(std::function<void(float)> &&callback);
 | 
					  void add_on_state_callback(std::function<void(float)> &&callback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										118
									
								
								esphome/components/number/number_call.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								esphome/components/number/number_call.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					#include "number_call.h"
 | 
				
			||||||
 | 
					#include "number.h"
 | 
				
			||||||
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace number {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *const TAG = "number";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberCall &NumberCall::number_increment(bool cycle) {
 | 
				
			||||||
 | 
					  return this->with_operation(NUMBER_OP_INCREMENT).with_cycle(cycle);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberCall &NumberCall::number_decrement(bool cycle) {
 | 
				
			||||||
 | 
					  return this->with_operation(NUMBER_OP_DECREMENT).with_cycle(cycle);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberCall &NumberCall::number_to_min() { return this->with_operation(NUMBER_OP_TO_MIN); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberCall &NumberCall::number_to_max() { return this->with_operation(NUMBER_OP_TO_MAX); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberCall &NumberCall::with_operation(NumberOperation operation) {
 | 
				
			||||||
 | 
					  this->operation_ = operation;
 | 
				
			||||||
 | 
					  return *this;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberCall &NumberCall::with_value(float value) {
 | 
				
			||||||
 | 
					  this->value_ = value;
 | 
				
			||||||
 | 
					  return *this;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NumberCall &NumberCall::with_cycle(bool cycle) {
 | 
				
			||||||
 | 
					  this->cycle_ = cycle;
 | 
				
			||||||
 | 
					  return *this;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void NumberCall::perform() {
 | 
				
			||||||
 | 
					  auto *parent = this->parent_;
 | 
				
			||||||
 | 
					  const auto *name = parent->get_name().c_str();
 | 
				
			||||||
 | 
					  const auto &traits = parent->traits;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (this->operation_ == NUMBER_OP_NONE) {
 | 
				
			||||||
 | 
					    ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  float target_value = NAN;
 | 
				
			||||||
 | 
					  float min_value = traits.get_min_value();
 | 
				
			||||||
 | 
					  float max_value = traits.get_max_value();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (this->operation_ == NUMBER_OP_SET) {
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "'%s' - Setting number value", name);
 | 
				
			||||||
 | 
					    if (!this->value_.has_value() || std::isnan(*this->value_)) {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    target_value = this->value_.value();
 | 
				
			||||||
 | 
					  } else if (this->operation_ == NUMBER_OP_TO_MIN) {
 | 
				
			||||||
 | 
					    if (std::isnan(min_value)) {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      target_value = min_value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else if (this->operation_ == NUMBER_OP_TO_MAX) {
 | 
				
			||||||
 | 
					    if (std::isnan(max_value)) {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      target_value = max_value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else if (this->operation_ == NUMBER_OP_INCREMENT) {
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out");
 | 
				
			||||||
 | 
					    if (!parent->has_state()) {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    auto step = traits.get_step();
 | 
				
			||||||
 | 
					    target_value = parent->state + (std::isnan(step) ? 1 : step);
 | 
				
			||||||
 | 
					    if (target_value > max_value) {
 | 
				
			||||||
 | 
					      if (this->cycle_ && !std::isnan(min_value)) {
 | 
				
			||||||
 | 
					        target_value = min_value;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        target_value = max_value;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else if (this->operation_ == NUMBER_OP_DECREMENT) {
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out");
 | 
				
			||||||
 | 
					    if (!parent->has_state()) {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    auto step = traits.get_step();
 | 
				
			||||||
 | 
					    target_value = parent->state - (std::isnan(step) ? 1 : step);
 | 
				
			||||||
 | 
					    if (target_value < min_value) {
 | 
				
			||||||
 | 
					      if (this->cycle_ && !std::isnan(max_value)) {
 | 
				
			||||||
 | 
					        target_value = max_value;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        target_value = min_value;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (target_value < min_value) {
 | 
				
			||||||
 | 
					    ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (target_value > max_value) {
 | 
				
			||||||
 | 
					    ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ESP_LOGD(TAG, "  New number value: %f", target_value);
 | 
				
			||||||
 | 
					  this->parent_->control(target_value);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace number
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										43
									
								
								esphome/components/number/number_call.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/number/number_call.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
 | 
					#include "number_traits.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace number {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum NumberOperation {
 | 
				
			||||||
 | 
					  NUMBER_OP_NONE,
 | 
				
			||||||
 | 
					  NUMBER_OP_SET,
 | 
				
			||||||
 | 
					  NUMBER_OP_INCREMENT,
 | 
				
			||||||
 | 
					  NUMBER_OP_DECREMENT,
 | 
				
			||||||
 | 
					  NUMBER_OP_TO_MIN,
 | 
				
			||||||
 | 
					  NUMBER_OP_TO_MAX,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NumberCall {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  explicit NumberCall(Number *parent) : parent_(parent) {}
 | 
				
			||||||
 | 
					  void perform();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  NumberCall &set_value(float value);
 | 
				
			||||||
 | 
					  NumberCall &number_increment(bool cycle);
 | 
				
			||||||
 | 
					  NumberCall &number_decrement(bool cycle);
 | 
				
			||||||
 | 
					  NumberCall &number_to_min();
 | 
				
			||||||
 | 
					  NumberCall &number_to_max();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  NumberCall &with_operation(NumberOperation operation);
 | 
				
			||||||
 | 
					  NumberCall &with_value(float value);
 | 
				
			||||||
 | 
					  NumberCall &with_cycle(bool cycle);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  Number *const parent_;
 | 
				
			||||||
 | 
					  NumberOperation operation_{NUMBER_OP_NONE};
 | 
				
			||||||
 | 
					  optional<float> value_;
 | 
				
			||||||
 | 
					  bool cycle_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace number
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										20
									
								
								esphome/components/number/number_traits.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/number/number_traits.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					#include "number_traits.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace number {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *const TAG = "number";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) {
 | 
				
			||||||
 | 
					  this->unit_of_measurement_ = unit_of_measurement;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::string NumberTraits::get_unit_of_measurement() {
 | 
				
			||||||
 | 
					  if (this->unit_of_measurement_.has_value())
 | 
				
			||||||
 | 
					    return *this->unit_of_measurement_;
 | 
				
			||||||
 | 
					  return "";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace number
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										44
									
								
								esphome/components/number/number_traits.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/number/number_traits.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace number {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum NumberMode : uint8_t {
 | 
				
			||||||
 | 
					  NUMBER_MODE_AUTO = 0,
 | 
				
			||||||
 | 
					  NUMBER_MODE_BOX = 1,
 | 
				
			||||||
 | 
					  NUMBER_MODE_SLIDER = 2,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NumberTraits {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  // Set/get the number value boundaries.
 | 
				
			||||||
 | 
					  void set_min_value(float min_value) { min_value_ = min_value; }
 | 
				
			||||||
 | 
					  float get_min_value() const { return min_value_; }
 | 
				
			||||||
 | 
					  void set_max_value(float max_value) { max_value_ = max_value; }
 | 
				
			||||||
 | 
					  float get_max_value() const { return max_value_; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Set/get the step size for incrementing or decrementing the number value.
 | 
				
			||||||
 | 
					  void set_step(float step) { step_ = step; }
 | 
				
			||||||
 | 
					  float get_step() const { return step_; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Manually set the unit of measurement.
 | 
				
			||||||
 | 
					  void set_unit_of_measurement(const std::string &unit_of_measurement);
 | 
				
			||||||
 | 
					  /// Get the unit of measurement, using the manual override if set.
 | 
				
			||||||
 | 
					  std::string get_unit_of_measurement();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Set/get the frontend mode.
 | 
				
			||||||
 | 
					  void set_mode(NumberMode mode) { this->mode_ = mode; }
 | 
				
			||||||
 | 
					  NumberMode get_mode() const { return this->mode_; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  float min_value_ = NAN;
 | 
				
			||||||
 | 
					  float max_value_ = NAN;
 | 
				
			||||||
 | 
					  float step_ = NAN;
 | 
				
			||||||
 | 
					  optional<std::string> unit_of_measurement_;  ///< Unit of measurement override
 | 
				
			||||||
 | 
					  NumberMode mode_{NUMBER_MODE_AUTO};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace number
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
@@ -49,6 +49,47 @@ void PMSX003Component::set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sens
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
void PMSX003Component::loop() {
 | 
					void PMSX003Component::loop() {
 | 
				
			||||||
  const uint32_t now = millis();
 | 
					  const uint32_t now = millis();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // If we update less often than it takes the device to stabilise, spin the fan down
 | 
				
			||||||
 | 
					  // rather than running it constantly. It does take some time to stabilise, so we
 | 
				
			||||||
 | 
					  // need to keep track of what state we're in.
 | 
				
			||||||
 | 
					  if (this->update_interval_ > PMS_STABILISING_MS) {
 | 
				
			||||||
 | 
					    if (this->initialised_ == 0) {
 | 
				
			||||||
 | 
					      this->send_command_(PMS_CMD_AUTO_MANUAL, 0);
 | 
				
			||||||
 | 
					      this->send_command_(PMS_CMD_ON_STANDBY, 1);
 | 
				
			||||||
 | 
					      this->initialised_ = 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    switch (this->state_) {
 | 
				
			||||||
 | 
					      case PMSX003_STATE_IDLE:
 | 
				
			||||||
 | 
					        // Power on the sensor now so it'll be ready when we hit the update time
 | 
				
			||||||
 | 
					        if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS))
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this->state_ = PMSX003_STATE_STABILISING;
 | 
				
			||||||
 | 
					        this->send_command_(PMS_CMD_ON_STANDBY, 1);
 | 
				
			||||||
 | 
					        this->fan_on_time_ = now;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      case PMSX003_STATE_STABILISING:
 | 
				
			||||||
 | 
					        // wait for the sensor to be stable
 | 
				
			||||||
 | 
					        if (now - this->fan_on_time_ < PMS_STABILISING_MS)
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        // consume any command responses that are in the serial buffer
 | 
				
			||||||
 | 
					        while (this->available())
 | 
				
			||||||
 | 
					          this->read_byte(&this->data_[0]);
 | 
				
			||||||
 | 
					        // Trigger a new read
 | 
				
			||||||
 | 
					        this->send_command_(PMS_CMD_TRIG_MANUAL, 0);
 | 
				
			||||||
 | 
					        this->state_ = PMSX003_STATE_WAITING;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case PMSX003_STATE_WAITING:
 | 
				
			||||||
 | 
					        // Just go ahead and read stuff
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else if (now - this->last_update_ < this->update_interval_) {
 | 
				
			||||||
 | 
					    // Otherwise just leave the sensor powered up and come back when we hit the update
 | 
				
			||||||
 | 
					    // time
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (now - this->last_transmission_ >= 500) {
 | 
					  if (now - this->last_transmission_ >= 500) {
 | 
				
			||||||
    // last transmission too long ago. Reset RX index.
 | 
					    // last transmission too long ago. Reset RX index.
 | 
				
			||||||
    this->data_index_ = 0;
 | 
					    this->data_index_ = 0;
 | 
				
			||||||
@@ -65,6 +106,7 @@ void PMSX003Component::loop() {
 | 
				
			|||||||
      // finished
 | 
					      // finished
 | 
				
			||||||
      this->parse_data_();
 | 
					      this->parse_data_();
 | 
				
			||||||
      this->data_index_ = 0;
 | 
					      this->data_index_ = 0;
 | 
				
			||||||
 | 
					      this->last_update_ = now;
 | 
				
			||||||
    } else if (!*check) {
 | 
					    } else if (!*check) {
 | 
				
			||||||
      // wrong data
 | 
					      // wrong data
 | 
				
			||||||
      this->data_index_ = 0;
 | 
					      this->data_index_ = 0;
 | 
				
			||||||
@@ -131,6 +173,25 @@ optional<bool> PMSX003Component::check_byte_() {
 | 
				
			|||||||
  return {};
 | 
					  return {};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) {
 | 
				
			||||||
 | 
					  this->data_index_ = 0;
 | 
				
			||||||
 | 
					  this->data_[data_index_++] = 0x42;
 | 
				
			||||||
 | 
					  this->data_[data_index_++] = 0x4D;
 | 
				
			||||||
 | 
					  this->data_[data_index_++] = cmd;
 | 
				
			||||||
 | 
					  this->data_[data_index_++] = (data >> 8) & 0xFF;
 | 
				
			||||||
 | 
					  this->data_[data_index_++] = (data >> 0) & 0xFF;
 | 
				
			||||||
 | 
					  int sum = 0;
 | 
				
			||||||
 | 
					  for (int i = 0; i < data_index_; i++) {
 | 
				
			||||||
 | 
					    sum += this->data_[i];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  this->data_[data_index_++] = (sum >> 8) & 0xFF;
 | 
				
			||||||
 | 
					  this->data_[data_index_++] = (sum >> 0) & 0xFF;
 | 
				
			||||||
 | 
					  for (int i = 0; i < data_index_; i++) {
 | 
				
			||||||
 | 
					    this->write_byte(this->data_[i]);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  this->data_index_ = 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void PMSX003Component::parse_data_() {
 | 
					void PMSX003Component::parse_data_() {
 | 
				
			||||||
  switch (this->type_) {
 | 
					  switch (this->type_) {
 | 
				
			||||||
    case PMSX003_TYPE_5003ST: {
 | 
					    case PMSX003_TYPE_5003ST: {
 | 
				
			||||||
@@ -218,6 +279,13 @@ void PMSX003Component::parse_data_() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Spin down the sensor again if we aren't going to need it until more time has
 | 
				
			||||||
 | 
					  // passed than it takes to stabilise
 | 
				
			||||||
 | 
					  if (this->update_interval_ > PMS_STABILISING_MS) {
 | 
				
			||||||
 | 
					    this->send_command_(PMS_CMD_ON_STANDBY, 0);
 | 
				
			||||||
 | 
					    this->state_ = PMSX003_STATE_IDLE;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  this->status_clear_warning();
 | 
					  this->status_clear_warning();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) {
 | 
					uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,13 @@
 | 
				
			|||||||
namespace esphome {
 | 
					namespace esphome {
 | 
				
			||||||
namespace pmsx003 {
 | 
					namespace pmsx003 {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// known command bytes
 | 
				
			||||||
 | 
					#define PMS_CMD_AUTO_MANUAL 0xE1  // data=0: perform measurement manually, data=1: perform measurement automatically
 | 
				
			||||||
 | 
					#define PMS_CMD_TRIG_MANUAL 0xE2  // trigger a manual measurement
 | 
				
			||||||
 | 
					#define PMS_CMD_ON_STANDBY 0xE4   // data=0: go to standby mode, data=1: go to normal mode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const uint16_t PMS_STABILISING_MS = 30000;  // time taken for the sensor to become stable after power on
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum PMSX003Type {
 | 
					enum PMSX003Type {
 | 
				
			||||||
  PMSX003_TYPE_X003 = 0,
 | 
					  PMSX003_TYPE_X003 = 0,
 | 
				
			||||||
  PMSX003_TYPE_5003T,
 | 
					  PMSX003_TYPE_5003T,
 | 
				
			||||||
@@ -14,6 +21,12 @@ enum PMSX003Type {
 | 
				
			|||||||
  PMSX003_TYPE_5003S,
 | 
					  PMSX003_TYPE_5003S,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum PMSX003State {
 | 
				
			||||||
 | 
					  PMSX003_STATE_IDLE = 0,
 | 
				
			||||||
 | 
					  PMSX003_STATE_STABILISING,
 | 
				
			||||||
 | 
					  PMSX003_STATE_WAITING,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PMSX003Component : public uart::UARTDevice, public Component {
 | 
					class PMSX003Component : public uart::UARTDevice, public Component {
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  PMSX003Component() = default;
 | 
					  PMSX003Component() = default;
 | 
				
			||||||
@@ -23,6 +36,8 @@ class PMSX003Component : public uart::UARTDevice, public Component {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void set_type(PMSX003Type type) { type_ = type; }
 | 
					  void set_type(PMSX003Type type) { type_ = type; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void set_update_interval(uint32_t val) { update_interval_ = val; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor);
 | 
					  void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor);
 | 
				
			||||||
  void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor);
 | 
					  void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor);
 | 
				
			||||||
  void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor);
 | 
					  void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor);
 | 
				
			||||||
@@ -45,11 +60,17 @@ class PMSX003Component : public uart::UARTDevice, public Component {
 | 
				
			|||||||
 protected:
 | 
					 protected:
 | 
				
			||||||
  optional<bool> check_byte_();
 | 
					  optional<bool> check_byte_();
 | 
				
			||||||
  void parse_data_();
 | 
					  void parse_data_();
 | 
				
			||||||
 | 
					  void send_command_(uint8_t cmd, uint16_t data);
 | 
				
			||||||
  uint16_t get_16_bit_uint_(uint8_t start_index);
 | 
					  uint16_t get_16_bit_uint_(uint8_t start_index);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  uint8_t data_[64];
 | 
					  uint8_t data_[64];
 | 
				
			||||||
  uint8_t data_index_{0};
 | 
					  uint8_t data_index_{0};
 | 
				
			||||||
 | 
					  uint8_t initialised_{0};
 | 
				
			||||||
 | 
					  uint32_t fan_on_time_{0};
 | 
				
			||||||
 | 
					  uint32_t last_update_{0};
 | 
				
			||||||
  uint32_t last_transmission_{0};
 | 
					  uint32_t last_transmission_{0};
 | 
				
			||||||
 | 
					  uint32_t update_interval_{0};
 | 
				
			||||||
 | 
					  PMSX003State state_{PMSX003_STATE_IDLE};
 | 
				
			||||||
  PMSX003Type type_;
 | 
					  PMSX003Type type_;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // "Standard Particle"
 | 
					  // "Standard Particle"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import esphome.codegen as cg
 | 
					import esphome.codegen as cg
 | 
				
			||||||
import esphome.config_validation as cv
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
from esphome.components import sensor, uart
 | 
					from esphome.components import sensor, uart
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from esphome.const import (
 | 
					from esphome.const import (
 | 
				
			||||||
    CONF_FORMALDEHYDE,
 | 
					    CONF_FORMALDEHYDE,
 | 
				
			||||||
    CONF_HUMIDITY,
 | 
					    CONF_HUMIDITY,
 | 
				
			||||||
@@ -17,6 +18,7 @@ from esphome.const import (
 | 
				
			|||||||
    CONF_PM_2_5UM,
 | 
					    CONF_PM_2_5UM,
 | 
				
			||||||
    CONF_PM_5_0UM,
 | 
					    CONF_PM_5_0UM,
 | 
				
			||||||
    CONF_PM_10_0UM,
 | 
					    CONF_PM_10_0UM,
 | 
				
			||||||
 | 
					    CONF_UPDATE_INTERVAL,
 | 
				
			||||||
    CONF_TEMPERATURE,
 | 
					    CONF_TEMPERATURE,
 | 
				
			||||||
    CONF_TYPE,
 | 
					    CONF_TYPE,
 | 
				
			||||||
    DEVICE_CLASS_PM1,
 | 
					    DEVICE_CLASS_PM1,
 | 
				
			||||||
@@ -44,6 +46,7 @@ TYPE_PMS5003ST = "PMS5003ST"
 | 
				
			|||||||
TYPE_PMS5003S = "PMS5003S"
 | 
					TYPE_PMS5003S = "PMS5003S"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PMSX003Type = pmsx003_ns.enum("PMSX003Type")
 | 
					PMSX003Type = pmsx003_ns.enum("PMSX003Type")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PMSX003_TYPES = {
 | 
					PMSX003_TYPES = {
 | 
				
			||||||
    TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
 | 
					    TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
 | 
				
			||||||
    TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
 | 
					    TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
 | 
				
			||||||
@@ -68,6 +71,17 @@ def validate_pmsx003_sensors(value):
 | 
				
			|||||||
    return value
 | 
					    return value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def validate_update_interval(value):
 | 
				
			||||||
 | 
					    value = cv.positive_time_period_milliseconds(value)
 | 
				
			||||||
 | 
					    if value == cv.time_period("0s"):
 | 
				
			||||||
 | 
					        return value
 | 
				
			||||||
 | 
					    if value < cv.time_period("30s"):
 | 
				
			||||||
 | 
					        raise cv.Invalid(
 | 
				
			||||||
 | 
					            "Update interval must be greater than or equal to 30 seconds if set."
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    return value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CONFIG_SCHEMA = (
 | 
					CONFIG_SCHEMA = (
 | 
				
			||||||
    cv.Schema(
 | 
					    cv.Schema(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -157,6 +171,7 @@ CONFIG_SCHEMA = (
 | 
				
			|||||||
                accuracy_decimals=0,
 | 
					                accuracy_decimals=0,
 | 
				
			||||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
					    .extend(cv.COMPONENT_SCHEMA)
 | 
				
			||||||
@@ -164,6 +179,17 @@ CONFIG_SCHEMA = (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def final_validate(config):
 | 
				
			||||||
 | 
					    require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s")
 | 
				
			||||||
 | 
					    schema = uart.final_validate_device_schema(
 | 
				
			||||||
 | 
					        "pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    schema(config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FINAL_VALIDATE_SCHEMA = final_validate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async def to_code(config):
 | 
					async def to_code(config):
 | 
				
			||||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
					    var = cg.new_Pvariable(config[CONF_ID])
 | 
				
			||||||
    await cg.register_component(var, config)
 | 
					    await cg.register_component(var, config)
 | 
				
			||||||
@@ -230,3 +256,5 @@ async def to_code(config):
 | 
				
			|||||||
    if CONF_FORMALDEHYDE in config:
 | 
					    if CONF_FORMALDEHYDE in config:
 | 
				
			||||||
        sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
 | 
					        sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
 | 
				
			||||||
        cg.add(var.set_formaldehyde_sensor(sens))
 | 
					        cg.add(var.set_formaldehyde_sensor(sens))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										28
									
								
								esphome/components/scd4x/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								esphome/components/scd4x/automation.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/core/component.h"
 | 
				
			||||||
 | 
					#include "esphome/core/automation.h"
 | 
				
			||||||
 | 
					#include "scd4x.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace scd4x {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename... Ts> class PerformForcedCalibrationAction : public Action<Ts...>, public Parented<SCD4XComponent> {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  void play(Ts... x) override {
 | 
				
			||||||
 | 
					    if (this->value_.has_value()) {
 | 
				
			||||||
 | 
					      this->parent_->perform_forced_calibration(value_.value());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  TEMPLATABLE_VALUE(uint16_t, value)
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename... Ts> class FactoryResetAction : public Action<Ts...>, public Parented<SCD4XComponent> {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  void play(Ts... x) override { this->parent_->factory_reset(); }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace scd4x
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
@@ -13,39 +13,32 @@ static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427;
 | 
				
			|||||||
static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000;
 | 
					static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000;
 | 
				
			||||||
static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416;
 | 
					static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416;
 | 
				
			||||||
static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1;
 | 
					static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1;
 | 
				
			||||||
 | 
					static const uint16_t SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS = 0x21ac;
 | 
				
			||||||
 | 
					static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT = 0x219d;  // SCD41 only
 | 
				
			||||||
 | 
					static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY = 0x2196;
 | 
				
			||||||
static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8;
 | 
					static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8;
 | 
				
			||||||
static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05;
 | 
					static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05;
 | 
				
			||||||
static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f;
 | 
					static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f;
 | 
				
			||||||
static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86;
 | 
					static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86;
 | 
				
			||||||
 | 
					static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632;
 | 
				
			||||||
 | 
					static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f;
 | 
				
			||||||
static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f;
 | 
					static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f;
 | 
				
			||||||
 | 
					static const uint16_t SCD41_ID = 0x1408;
 | 
				
			||||||
 | 
					static const uint16_t SCD40_ID = 0x440;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void SCD4XComponent::setup() {
 | 
					void SCD4XComponent::setup() {
 | 
				
			||||||
  ESP_LOGCONFIG(TAG, "Setting up scd4x...");
 | 
					  ESP_LOGCONFIG(TAG, "Setting up scd4x...");
 | 
				
			||||||
 | 
					 | 
				
			||||||
  // the sensor needs 1000 ms to enter the idle state
 | 
					  // the sensor needs 1000 ms to enter the idle state
 | 
				
			||||||
  this->set_timeout(1000, [this]() {
 | 
					  this->set_timeout(1000, [this]() {
 | 
				
			||||||
    uint16_t raw_read_status;
 | 
					    this->status_clear_error();
 | 
				
			||||||
    if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) {
 | 
					    if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
 | 
				
			||||||
      ESP_LOGE(TAG, "Failed to read data ready status");
 | 
					      ESP_LOGE(TAG, "Failed to stop measurements");
 | 
				
			||||||
      this->mark_failed();
 | 
					      this->mark_failed();
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
 | 
				
			||||||
    uint32_t stop_measurement_delay = 0;
 | 
					    // issuing the stop_periodic_measurement command
 | 
				
			||||||
    // In order to query the device periodic measurement must be ceased
 | 
					    this->set_timeout(500, [this]() {
 | 
				
			||||||
    if (raw_read_status) {
 | 
					 | 
				
			||||||
      ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement");
 | 
					 | 
				
			||||||
      if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
 | 
					 | 
				
			||||||
        ESP_LOGE(TAG, "Failed to stop measurements");
 | 
					 | 
				
			||||||
        this->mark_failed();
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
 | 
					 | 
				
			||||||
      // issuing the stop_periodic_measurement command
 | 
					 | 
				
			||||||
      stop_measurement_delay = 500;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this->set_timeout(stop_measurement_delay, [this]() {
 | 
					 | 
				
			||||||
      uint16_t raw_serial_number[3];
 | 
					      uint16_t raw_serial_number[3];
 | 
				
			||||||
      if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) {
 | 
					      if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) {
 | 
				
			||||||
        ESP_LOGE(TAG, "Failed to read serial number");
 | 
					        ESP_LOGE(TAG, "Failed to read serial number");
 | 
				
			||||||
@@ -89,15 +82,9 @@ void SCD4XComponent::setup() {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Finally start sensor measurements
 | 
					 | 
				
			||||||
      if (!this->write_command(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) {
 | 
					 | 
				
			||||||
        ESP_LOGE(TAG, "Error starting continuous measurements.");
 | 
					 | 
				
			||||||
        this->error_code_ = MEASUREMENT_INIT_FAILED;
 | 
					 | 
				
			||||||
        this->mark_failed();
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      initialized_ = true;
 | 
					      initialized_ = true;
 | 
				
			||||||
 | 
					      // Finally start sensor measurements
 | 
				
			||||||
 | 
					      this->start_measurement_();
 | 
				
			||||||
      ESP_LOGD(TAG, "Sensor initialized");
 | 
					      ESP_LOGD(TAG, "Sensor initialized");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -123,12 +110,31 @@ void SCD4XComponent::dump_config() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  ESP_LOGCONFIG(TAG, "  Automatic self calibration: %s", ONOFF(this->enable_asc_));
 | 
					  ESP_LOGCONFIG(TAG, "  Automatic self calibration: %s", ONOFF(this->enable_asc_));
 | 
				
			||||||
  if (this->ambient_pressure_compensation_) {
 | 
					  if (this->ambient_pressure_source_ != nullptr) {
 | 
				
			||||||
    ESP_LOGCONFIG(TAG, "  Altitude compensation disabled");
 | 
					    ESP_LOGCONFIG(TAG, "  Dynamic ambient pressure compensation using sensor '%s'",
 | 
				
			||||||
    ESP_LOGCONFIG(TAG, "  Ambient pressure compensation: %dmBar", this->ambient_pressure_);
 | 
					                  this->ambient_pressure_source_->get_name().c_str());
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    ESP_LOGCONFIG(TAG, "  Ambient pressure compensation disabled");
 | 
					    if (this->ambient_pressure_compensation_) {
 | 
				
			||||||
    ESP_LOGCONFIG(TAG, "  Altitude compensation: %dm", this->altitude_compensation_);
 | 
					      ESP_LOGCONFIG(TAG, "  Altitude compensation disabled");
 | 
				
			||||||
 | 
					      ESP_LOGCONFIG(TAG, "  Ambient pressure compensation: %dmBar", this->ambient_pressure_);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      ESP_LOGCONFIG(TAG, "  Ambient pressure compensation disabled");
 | 
				
			||||||
 | 
					      ESP_LOGCONFIG(TAG, "  Altitude compensation: %dm", this->altitude_compensation_);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  switch (this->measurement_mode_) {
 | 
				
			||||||
 | 
					    case PERIODIC:
 | 
				
			||||||
 | 
					      ESP_LOGCONFIG(TAG, "  Measurement mode: periodic (5s)");
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case LOW_POWER_PERIODIC:
 | 
				
			||||||
 | 
					      ESP_LOGCONFIG(TAG, "  Measurement mode: low power periodic (30s)");
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case SINGLE_SHOT:
 | 
				
			||||||
 | 
					      ESP_LOGCONFIG(TAG, "  Measurement mode: single shot");
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case SINGLE_SHOT_RHT_ONLY:
 | 
				
			||||||
 | 
					      ESP_LOGCONFIG(TAG, "  Measurement mode: single shot rht only");
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  ESP_LOGCONFIG(TAG, "  Temperature offset: %.2f °C", this->temperature_offset_);
 | 
					  ESP_LOGCONFIG(TAG, "  Temperature offset: %.2f °C", this->temperature_offset_);
 | 
				
			||||||
  LOG_UPDATE_INTERVAL(this);
 | 
					  LOG_UPDATE_INTERVAL(this);
 | 
				
			||||||
@@ -149,47 +155,105 @@ void SCD4XComponent::update() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check if data is ready
 | 
					  uint32_t wait_time = 0;
 | 
				
			||||||
  if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
 | 
					  if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) {
 | 
				
			||||||
    this->status_set_warning();
 | 
					    start_measurement_();
 | 
				
			||||||
    return;
 | 
					    wait_time =
 | 
				
			||||||
 | 
					        this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50;  // Single shot measurement takes 5 secs rht mode 50 ms
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  this->set_timeout(wait_time, [this]() {
 | 
				
			||||||
 | 
					    // Check if data is ready
 | 
				
			||||||
 | 
					    if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
 | 
				
			||||||
 | 
					      this->status_set_warning();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  uint16_t raw_read_status;
 | 
					    uint16_t raw_read_status;
 | 
				
			||||||
  if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
 | 
					 | 
				
			||||||
    this->status_set_warning();
 | 
					 | 
				
			||||||
    ESP_LOGW(TAG, "Data not ready yet!");
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
 | 
					    if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
 | 
				
			||||||
    ESP_LOGW(TAG, "Error reading measurement!");
 | 
					      this->status_set_warning();
 | 
				
			||||||
    this->status_set_warning();
 | 
					      ESP_LOGW(TAG, "Data not ready yet!");
 | 
				
			||||||
    return;
 | 
					      return;
 | 
				
			||||||
  }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Read off sensor data
 | 
					    if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
 | 
				
			||||||
  uint16_t raw_data[3];
 | 
					      ESP_LOGW(TAG, "Error reading measurement!");
 | 
				
			||||||
  if (!this->read_data(raw_data, 3)) {
 | 
					      this->status_set_warning();
 | 
				
			||||||
    this->status_set_warning();
 | 
					      return;  // NO RETRY
 | 
				
			||||||
    return;
 | 
					    }
 | 
				
			||||||
  }
 | 
					    // Read off sensor data
 | 
				
			||||||
 | 
					    uint16_t raw_data[3];
 | 
				
			||||||
 | 
					    if (!this->read_data(raw_data, 3)) {
 | 
				
			||||||
 | 
					      this->status_set_warning();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (this->co2_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->co2_sensor_->publish_state(raw_data[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (this->co2_sensor_ != nullptr)
 | 
					    if (this->temperature_sensor_ != nullptr) {
 | 
				
			||||||
    this->co2_sensor_->publish_state(raw_data[0]);
 | 
					      const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
 | 
				
			||||||
 | 
					      this->temperature_sensor_->publish_state(temperature);
 | 
				
			||||||
  if (this->temperature_sensor_ != nullptr) {
 | 
					    }
 | 
				
			||||||
    const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
 | 
					    if (this->humidity_sensor_ != nullptr) {
 | 
				
			||||||
    this->temperature_sensor_->publish_state(temperature);
 | 
					      const float humidity = (100.0f * raw_data[2]) / (1 << 16);
 | 
				
			||||||
  }
 | 
					      this->humidity_sensor_->publish_state(humidity);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  if (this->humidity_sensor_ != nullptr) {
 | 
					    this->status_clear_warning();
 | 
				
			||||||
    const float humidity = (100.0f * raw_data[2]) / (1 << 16);
 | 
					  });  // set_timeout
 | 
				
			||||||
    this->humidity_sensor_->publish_state(humidity);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  this->status_clear_warning();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) {
 | 
				
			||||||
 | 
					  /*
 | 
				
			||||||
 | 
					    Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power
 | 
				
			||||||
 | 
					    periodic measurement or single shot) for > 3 minutes in an environment with homogenous and constant CO2
 | 
				
			||||||
 | 
					    concentration before performing a forced recalibration.
 | 
				
			||||||
 | 
					  */
 | 
				
			||||||
 | 
					  if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
 | 
				
			||||||
 | 
					    ESP_LOGE(TAG, "Failed to stop measurements");
 | 
				
			||||||
 | 
					    this->status_set_warning();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  this->set_timeout(500, [this, current_co2_concentration]() {
 | 
				
			||||||
 | 
					    if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) {
 | 
				
			||||||
 | 
					      ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration);
 | 
				
			||||||
 | 
					      // frc takes 400 ms
 | 
				
			||||||
 | 
					      // because this method will be used very rarly
 | 
				
			||||||
 | 
					      // the simple aproach with delay is ok
 | 
				
			||||||
 | 
					      delay(400);  // NOLINT'
 | 
				
			||||||
 | 
					      if (!this->start_measurement_()) {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        ESP_LOGD(TAG, "forced calibration complete");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "force calibration failed");
 | 
				
			||||||
 | 
					      this->error_code_ = FRC_FAILED;
 | 
				
			||||||
 | 
					      this->status_set_warning();
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool SCD4XComponent::factory_reset() {
 | 
				
			||||||
 | 
					  if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
 | 
				
			||||||
 | 
					    ESP_LOGE(TAG, "Failed to stop measurements");
 | 
				
			||||||
 | 
					    this->status_set_warning();
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  this->set_timeout(500, [this]() {
 | 
				
			||||||
 | 
					    if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "Failed to send factory reset command");
 | 
				
			||||||
 | 
					      this->status_set_warning();
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "Factory reset complete");
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Note pressure in bar here. Convert to hPa
 | 
					// Note pressure in bar here. Convert to hPa
 | 
				
			||||||
void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) {
 | 
					void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) {
 | 
				
			||||||
  ambient_pressure_compensation_ = true;
 | 
					  ambient_pressure_compensation_ = true;
 | 
				
			||||||
@@ -213,5 +277,38 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool SCD4XComponent::start_measurement_() {
 | 
				
			||||||
 | 
					  uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
 | 
				
			||||||
 | 
					  switch (this->measurement_mode_) {
 | 
				
			||||||
 | 
					    case PERIODIC:
 | 
				
			||||||
 | 
					      measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case LOW_POWER_PERIODIC:
 | 
				
			||||||
 | 
					      measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case SINGLE_SHOT:
 | 
				
			||||||
 | 
					      measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case SINGLE_SHOT_RHT_ONLY:
 | 
				
			||||||
 | 
					      measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static uint8_t remaining_retries = 3;
 | 
				
			||||||
 | 
					  while (remaining_retries) {
 | 
				
			||||||
 | 
					    if (!this->write_command(measurement_command)) {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "Error starting measurements.");
 | 
				
			||||||
 | 
					      this->error_code_ = MEASUREMENT_INIT_FAILED;
 | 
				
			||||||
 | 
					      this->status_set_warning();
 | 
				
			||||||
 | 
					      if (--remaining_retries == 0)
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      delay(50);  // NOLINT wait 50 ms and try again
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this->status_clear_warning();
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace scd4x
 | 
					}  // namespace scd4x
 | 
				
			||||||
}  // namespace esphome
 | 
					}  // namespace esphome
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
 | 
					#include <vector>
 | 
				
			||||||
 | 
					#include "esphome/core/application.h"
 | 
				
			||||||
#include "esphome/core/component.h"
 | 
					#include "esphome/core/component.h"
 | 
				
			||||||
#include "esphome/components/sensor/sensor.h"
 | 
					#include "esphome/components/sensor/sensor.h"
 | 
				
			||||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
 | 
					#include "esphome/components/sensirion_common/i2c_sensirion.h"
 | 
				
			||||||
@@ -7,7 +8,14 @@
 | 
				
			|||||||
namespace esphome {
 | 
					namespace esphome {
 | 
				
			||||||
namespace scd4x {
 | 
					namespace scd4x {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN };
 | 
					enum ERRORCODE {
 | 
				
			||||||
 | 
					  COMMUNICATION_FAILED,
 | 
				
			||||||
 | 
					  SERIAL_NUMBER_IDENTIFICATION_FAILED,
 | 
				
			||||||
 | 
					  MEASUREMENT_INIT_FAILED,
 | 
				
			||||||
 | 
					  FRC_FAILED,
 | 
				
			||||||
 | 
					  UNKNOWN
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
 | 
					class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
@@ -25,10 +33,13 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri
 | 
				
			|||||||
  void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
 | 
					  void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
 | 
				
			||||||
  void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; };
 | 
					  void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; };
 | 
				
			||||||
  void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
 | 
					  void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
 | 
				
			||||||
 | 
					  void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; }
 | 
				
			||||||
 | 
					  bool perform_forced_calibration(uint16_t current_co2_concentration);
 | 
				
			||||||
 | 
					  bool factory_reset();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 protected:
 | 
					 protected:
 | 
				
			||||||
  bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa);
 | 
					  bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa);
 | 
				
			||||||
 | 
					  bool start_measurement_();
 | 
				
			||||||
  ERRORCODE error_code_;
 | 
					  ERRORCODE error_code_;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool initialized_{false};
 | 
					  bool initialized_{false};
 | 
				
			||||||
@@ -38,7 +49,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri
 | 
				
			|||||||
  bool ambient_pressure_compensation_;
 | 
					  bool ambient_pressure_compensation_;
 | 
				
			||||||
  uint16_t ambient_pressure_;
 | 
					  uint16_t ambient_pressure_;
 | 
				
			||||||
  bool enable_asc_;
 | 
					  bool enable_asc_;
 | 
				
			||||||
 | 
					  MeasurementMode measurement_mode_{PERIODIC};
 | 
				
			||||||
  sensor::Sensor *co2_sensor_{nullptr};
 | 
					  sensor::Sensor *co2_sensor_{nullptr};
 | 
				
			||||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
					  sensor::Sensor *temperature_sensor_{nullptr};
 | 
				
			||||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
					  sensor::Sensor *humidity_sensor_{nullptr};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,11 +2,15 @@ import esphome.codegen as cg
 | 
				
			|||||||
import esphome.config_validation as cv
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
from esphome.components import i2c, sensor
 | 
					from esphome.components import i2c, sensor
 | 
				
			||||||
from esphome.components import sensirion_common
 | 
					from esphome.components import sensirion_common
 | 
				
			||||||
 | 
					from esphome import automation
 | 
				
			||||||
 | 
					from esphome.automation import maybe_simple_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from esphome.const import (
 | 
					from esphome.const import (
 | 
				
			||||||
    CONF_ID,
 | 
					    CONF_ID,
 | 
				
			||||||
    CONF_CO2,
 | 
					    CONF_CO2,
 | 
				
			||||||
    CONF_HUMIDITY,
 | 
					    CONF_HUMIDITY,
 | 
				
			||||||
    CONF_TEMPERATURE,
 | 
					    CONF_TEMPERATURE,
 | 
				
			||||||
 | 
					    CONF_VALUE,
 | 
				
			||||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
					    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
				
			||||||
    DEVICE_CLASS_HUMIDITY,
 | 
					    DEVICE_CLASS_HUMIDITY,
 | 
				
			||||||
    DEVICE_CLASS_TEMPERATURE,
 | 
					    DEVICE_CLASS_TEMPERATURE,
 | 
				
			||||||
@@ -19,7 +23,7 @@ from esphome.const import (
 | 
				
			|||||||
    UNIT_PERCENT,
 | 
					    UNIT_PERCENT,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CODEOWNERS = ["@sjtrny"]
 | 
					CODEOWNERS = ["@sjtrny", "@martgras"]
 | 
				
			||||||
DEPENDENCIES = ["i2c"]
 | 
					DEPENDENCIES = ["i2c"]
 | 
				
			||||||
AUTO_LOAD = ["sensirion_common"]
 | 
					AUTO_LOAD = ["sensirion_common"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,12 +31,29 @@ scd4x_ns = cg.esphome_ns.namespace("scd4x")
 | 
				
			|||||||
SCD4XComponent = scd4x_ns.class_(
 | 
					SCD4XComponent = scd4x_ns.class_(
 | 
				
			||||||
    "SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
 | 
					    "SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					MeasurementMode = scd4x_ns.enum("MEASUREMENT_MODE")
 | 
				
			||||||
 | 
					MEASUREMENT_MODE_OPTIONS = {
 | 
				
			||||||
 | 
					    "periodic": MeasurementMode.PERIODIC,
 | 
				
			||||||
 | 
					    "low_power_periodic": MeasurementMode.LOW_POWER_PERIODIC,
 | 
				
			||||||
 | 
					    "single_shot": MeasurementMode.SINGLE_SHOT,
 | 
				
			||||||
 | 
					    "single_shot_rht_only": MeasurementMode.SINGLE_SHOT_RHT_ONLY,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Actions
 | 
				
			||||||
 | 
					PerformForcedCalibrationAction = scd4x_ns.class_(
 | 
				
			||||||
 | 
					    "PerformForcedCalibrationAction", automation.Action
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
 | 
					 | 
				
			||||||
CONF_ALTITUDE_COMPENSATION = "altitude_compensation"
 | 
					CONF_ALTITUDE_COMPENSATION = "altitude_compensation"
 | 
				
			||||||
CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
 | 
					CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
 | 
				
			||||||
CONF_TEMPERATURE_OFFSET = "temperature_offset"
 | 
					 | 
				
			||||||
CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source"
 | 
					CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source"
 | 
				
			||||||
 | 
					CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration"
 | 
				
			||||||
 | 
					CONF_MEASUREMENT_MODE = "measurement_mode"
 | 
				
			||||||
 | 
					CONF_TEMPERATURE_OFFSET = "temperature_offset"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CONFIG_SCHEMA = (
 | 
					CONFIG_SCHEMA = (
 | 
				
			||||||
    cv.Schema(
 | 
					    cv.Schema(
 | 
				
			||||||
@@ -69,6 +90,9 @@ CONFIG_SCHEMA = (
 | 
				
			|||||||
            cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
 | 
					            cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
 | 
				
			||||||
                sensor.Sensor
 | 
					                sensor.Sensor
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_MEASUREMENT_MODE, default="periodic"): cv.enum(
 | 
				
			||||||
 | 
					                MEASUREMENT_MODE_OPTIONS, lower=True
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .extend(cv.polling_component_schema("60s"))
 | 
					    .extend(cv.polling_component_schema("60s"))
 | 
				
			||||||
@@ -106,3 +130,42 @@ async def to_code(config):
 | 
				
			|||||||
    if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config:
 | 
					    if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config:
 | 
				
			||||||
        sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE])
 | 
					        sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE])
 | 
				
			||||||
        cg.add(var.set_ambient_pressure_source(sens))
 | 
					        cg.add(var.set_ambient_pressure_source(sens))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SCD4X_ACTION_SCHEMA = maybe_simple_id(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        cv.GenerateID(): cv.use_id(SCD4XComponent),
 | 
				
			||||||
 | 
					        cv.Required(CONF_VALUE): cv.templatable(cv.positive_int),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "scd4x.perform_forced_calibration",
 | 
				
			||||||
 | 
					    PerformForcedCalibrationAction,
 | 
				
			||||||
 | 
					    SCD4X_ACTION_SCHEMA,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def scd4x_frc_to_code(config, action_id, template_arg, args):
 | 
				
			||||||
 | 
					    var = cg.new_Pvariable(action_id, template_arg)
 | 
				
			||||||
 | 
					    await cg.register_parented(var, config[CONF_ID])
 | 
				
			||||||
 | 
					    template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
 | 
				
			||||||
 | 
					    cg.add(var.set_value(template_))
 | 
				
			||||||
 | 
					    return var
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        cv.Required(CONF_ID): cv.use_id(SCD4XComponent),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def scd4x_reset_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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,10 @@ from esphome.const import (
 | 
				
			|||||||
    CONF_OPTION,
 | 
					    CONF_OPTION,
 | 
				
			||||||
    CONF_TRIGGER_ID,
 | 
					    CONF_TRIGGER_ID,
 | 
				
			||||||
    CONF_MQTT_ID,
 | 
					    CONF_MQTT_ID,
 | 
				
			||||||
 | 
					    CONF_CYCLE,
 | 
				
			||||||
 | 
					    CONF_MODE,
 | 
				
			||||||
 | 
					    CONF_OPERATION,
 | 
				
			||||||
 | 
					    CONF_INDEX,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from esphome.core import CORE, coroutine_with_priority
 | 
					from esphome.core import CORE, coroutine_with_priority
 | 
				
			||||||
from esphome.cpp_helpers import setup_entity
 | 
					from esphome.cpp_helpers import setup_entity
 | 
				
			||||||
@@ -22,14 +26,27 @@ SelectPtr = Select.operator("ptr")
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Triggers
 | 
					# Triggers
 | 
				
			||||||
SelectStateTrigger = select_ns.class_(
 | 
					SelectStateTrigger = select_ns.class_(
 | 
				
			||||||
    "SelectStateTrigger", automation.Trigger.template(cg.float_)
 | 
					    "SelectStateTrigger",
 | 
				
			||||||
 | 
					    automation.Trigger.template(cg.std_string, cg.size_t),
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Actions
 | 
					# Actions
 | 
				
			||||||
SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
 | 
					SelectSetAction = select_ns.class_("SelectSetAction", automation.Action)
 | 
				
			||||||
 | 
					SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action)
 | 
				
			||||||
 | 
					SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Enums
 | 
				
			||||||
 | 
					SelectOperation = select_ns.enum("SelectOperation")
 | 
				
			||||||
 | 
					SELECT_OPERATION_OPTIONS = {
 | 
				
			||||||
 | 
					    "NEXT": SelectOperation.SELECT_OP_NEXT,
 | 
				
			||||||
 | 
					    "PREVIOUS": SelectOperation.SELECT_OP_PREVIOUS,
 | 
				
			||||||
 | 
					    "FIRST": SelectOperation.SELECT_OP_FIRST,
 | 
				
			||||||
 | 
					    "LAST": SelectOperation.SELECT_OP_LAST,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
icon = cv.icon
 | 
					icon = cv.icon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
 | 
					SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent),
 | 
					        cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent),
 | 
				
			||||||
@@ -50,7 +67,9 @@ async def setup_select_core_(var, config, *, options: List[str]):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    for conf in config.get(CONF_ON_VALUE, []):
 | 
					    for conf in config.get(CONF_ON_VALUE, []):
 | 
				
			||||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
					        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
				
			||||||
        await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
 | 
					        await automation.build_automation(
 | 
				
			||||||
 | 
					            trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if CONF_MQTT_ID in config:
 | 
					    if CONF_MQTT_ID in config:
 | 
				
			||||||
        mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
 | 
					        mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
 | 
				
			||||||
@@ -76,12 +95,18 @@ async def to_code(config):
 | 
				
			|||||||
    cg.add_global(select_ns.using)
 | 
					    cg.add_global(select_ns.using)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					OPERATION_BASE_SCHEMA = cv.Schema(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        cv.Required(CONF_ID): cv.use_id(Select),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@automation.register_action(
 | 
					@automation.register_action(
 | 
				
			||||||
    "select.set",
 | 
					    "select.set",
 | 
				
			||||||
    SelectSetAction,
 | 
					    SelectSetAction,
 | 
				
			||||||
    cv.Schema(
 | 
					    OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            cv.Required(CONF_ID): cv.use_id(Select),
 | 
					 | 
				
			||||||
            cv.Required(CONF_OPTION): cv.templatable(cv.string_strict),
 | 
					            cv.Required(CONF_OPTION): cv.templatable(cv.string_strict),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
@@ -92,3 +117,96 @@ async def select_set_to_code(config, action_id, template_arg, args):
 | 
				
			|||||||
    template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string)
 | 
					    template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string)
 | 
				
			||||||
    cg.add(var.set_option(template_))
 | 
					    cg.add(var.set_option(template_))
 | 
				
			||||||
    return var
 | 
					    return var
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "select.set_index",
 | 
				
			||||||
 | 
					    SelectSetIndexAction,
 | 
				
			||||||
 | 
					    OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            cv.Required(CONF_INDEX): cv.templatable(cv.positive_int),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def select_set_index_to_code(config, action_id, template_arg, args):
 | 
				
			||||||
 | 
					    paren = await cg.get_variable(config[CONF_ID])
 | 
				
			||||||
 | 
					    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
				
			||||||
 | 
					    template_ = await cg.templatable(config[CONF_INDEX], args, cg.size_t)
 | 
				
			||||||
 | 
					    cg.add(var.set_index(template_))
 | 
				
			||||||
 | 
					    return var
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "select.operation",
 | 
				
			||||||
 | 
					    SelectOperationAction,
 | 
				
			||||||
 | 
					    OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            cv.Required(CONF_OPERATION): cv.templatable(
 | 
				
			||||||
 | 
					                cv.enum(SELECT_OPERATION_OPTIONS, upper=True)
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "select.next",
 | 
				
			||||||
 | 
					    SelectOperationAction,
 | 
				
			||||||
 | 
					    automation.maybe_simple_id(
 | 
				
			||||||
 | 
					        OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_MODE, default="NEXT"): cv.one_of("NEXT", upper=True),
 | 
				
			||||||
 | 
					                cv.Optional(CONF_CYCLE, default=True): cv.boolean,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "select.previous",
 | 
				
			||||||
 | 
					    SelectOperationAction,
 | 
				
			||||||
 | 
					    automation.maybe_simple_id(
 | 
				
			||||||
 | 
					        OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_MODE, default="PREVIOUS"): cv.one_of(
 | 
				
			||||||
 | 
					                    "PREVIOUS", upper=True
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                cv.Optional(CONF_CYCLE, default=True): cv.boolean,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "select.first",
 | 
				
			||||||
 | 
					    SelectOperationAction,
 | 
				
			||||||
 | 
					    automation.maybe_simple_id(
 | 
				
			||||||
 | 
					        OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_MODE, default="FIRST"): cv.one_of("FIRST", upper=True),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "select.last",
 | 
				
			||||||
 | 
					    SelectOperationAction,
 | 
				
			||||||
 | 
					    automation.maybe_simple_id(
 | 
				
			||||||
 | 
					        OPERATION_BASE_SCHEMA.extend(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_MODE, default="LAST"): cv.one_of("LAST", upper=True),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def select_operation_to_code(config, action_id, template_arg, args):
 | 
				
			||||||
 | 
					    paren = await cg.get_variable(config[CONF_ID])
 | 
				
			||||||
 | 
					    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
				
			||||||
 | 
					    if CONF_OPERATION in config:
 | 
				
			||||||
 | 
					        op_ = await cg.templatable(config[CONF_OPERATION], args, SelectOperation)
 | 
				
			||||||
 | 
					        cg.add(var.set_operation(op_))
 | 
				
			||||||
 | 
					        if CONF_CYCLE in config:
 | 
				
			||||||
 | 
					            cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool)
 | 
				
			||||||
 | 
					            cg.add(var.set_cycle(cycle_))
 | 
				
			||||||
 | 
					    if CONF_MODE in config:
 | 
				
			||||||
 | 
					        cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[config[CONF_MODE]]))
 | 
				
			||||||
 | 
					        if CONF_CYCLE in config:
 | 
				
			||||||
 | 
					            cg.add(var.set_cycle(config[CONF_CYCLE]))
 | 
				
			||||||
 | 
					    return var
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,16 +7,16 @@
 | 
				
			|||||||
namespace esphome {
 | 
					namespace esphome {
 | 
				
			||||||
namespace select {
 | 
					namespace select {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SelectStateTrigger : public Trigger<std::string> {
 | 
					class SelectStateTrigger : public Trigger<std::string, size_t> {
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  explicit SelectStateTrigger(Select *parent) {
 | 
					  explicit SelectStateTrigger(Select *parent) {
 | 
				
			||||||
    parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); });
 | 
					    parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
template<typename... Ts> class SelectSetAction : public Action<Ts...> {
 | 
					template<typename... Ts> class SelectSetAction : public Action<Ts...> {
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  SelectSetAction(Select *select) : select_(select) {}
 | 
					  explicit SelectSetAction(Select *select) : select_(select) {}
 | 
				
			||||||
  TEMPLATABLE_VALUE(std::string, option)
 | 
					  TEMPLATABLE_VALUE(std::string, option)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void play(Ts... x) override {
 | 
					  void play(Ts... x) override {
 | 
				
			||||||
@@ -29,5 +29,39 @@ template<typename... Ts> class SelectSetAction : public Action<Ts...> {
 | 
				
			|||||||
  Select *select_;
 | 
					  Select *select_;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename... Ts> class SelectSetIndexAction : public Action<Ts...> {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  explicit SelectSetIndexAction(Select *select) : select_(select) {}
 | 
				
			||||||
 | 
					  TEMPLATABLE_VALUE(size_t, index)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void play(Ts... x) override {
 | 
				
			||||||
 | 
					    auto call = this->select_->make_call();
 | 
				
			||||||
 | 
					    call.set_index(this->index_.value(x...));
 | 
				
			||||||
 | 
					    call.perform();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  Select *select_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename... Ts> class SelectOperationAction : public Action<Ts...> {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  explicit SelectOperationAction(Select *select) : select_(select) {}
 | 
				
			||||||
 | 
					  TEMPLATABLE_VALUE(bool, cycle)
 | 
				
			||||||
 | 
					  TEMPLATABLE_VALUE(SelectOperation, operation)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void play(Ts... x) override {
 | 
				
			||||||
 | 
					    auto call = this->select_->make_call();
 | 
				
			||||||
 | 
					    call.with_operation(this->operation_.value(x...));
 | 
				
			||||||
 | 
					    if (this->cycle_.has_value()) {
 | 
				
			||||||
 | 
					      call.with_cycle(this->cycle_.value(x...));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    call.perform();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  Select *select_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace select
 | 
					}  // namespace select
 | 
				
			||||||
}  // namespace esphome
 | 
					}  // namespace esphome
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,37 +6,58 @@ namespace select {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
static const char *const TAG = "select";
 | 
					static const char *const TAG = "select";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void SelectCall::perform() {
 | 
					 | 
				
			||||||
  ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
 | 
					 | 
				
			||||||
  if (!this->option_.has_value()) {
 | 
					 | 
				
			||||||
    ESP_LOGW(TAG, "No value set for SelectCall");
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const auto &traits = this->parent_->traits;
 | 
					 | 
				
			||||||
  auto value = *this->option_;
 | 
					 | 
				
			||||||
  auto options = traits.get_options();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (std::find(options.begin(), options.end(), value) == options.end()) {
 | 
					 | 
				
			||||||
    ESP_LOGW(TAG, "  Option %s is not a valid option.", value.c_str());
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ESP_LOGD(TAG, "  Option: %s", (*this->option_).c_str());
 | 
					 | 
				
			||||||
  this->parent_->control(*this->option_);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void Select::publish_state(const std::string &state) {
 | 
					void Select::publish_state(const std::string &state) {
 | 
				
			||||||
  this->has_state_ = true;
 | 
					  auto index = this->index_of(state);
 | 
				
			||||||
  this->state = state;
 | 
					  const auto *name = this->get_name().c_str();
 | 
				
			||||||
  ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str());
 | 
					  if (index.has_value()) {
 | 
				
			||||||
  this->state_callback_.call(state);
 | 
					    this->has_state_ = true;
 | 
				
			||||||
 | 
					    this->state = state;
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "'%s': Sending state %s (index %d)", name, state.c_str(), index.value());
 | 
				
			||||||
 | 
					    this->state_callback_.call(state, index.value());
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void Select::add_on_state_callback(std::function<void(std::string)> &&callback) {
 | 
					void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) {
 | 
				
			||||||
  this->state_callback_.add(std::move(callback));
 | 
					  this->state_callback_.add(std::move(callback));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool Select::has_index(size_t index) const { return index < this->size(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					size_t Select::size() const {
 | 
				
			||||||
 | 
					  auto options = traits.get_options();
 | 
				
			||||||
 | 
					  return options.size();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					optional<size_t> Select::index_of(const std::string &option) const {
 | 
				
			||||||
 | 
					  auto options = traits.get_options();
 | 
				
			||||||
 | 
					  auto it = std::find(options.begin(), options.end(), option);
 | 
				
			||||||
 | 
					  if (it == options.end()) {
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return std::distance(options.begin(), it);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					optional<size_t> Select::active_index() const {
 | 
				
			||||||
 | 
					  if (this->has_state()) {
 | 
				
			||||||
 | 
					    return this->index_of(this->state);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					optional<std::string> Select::at(size_t index) const {
 | 
				
			||||||
 | 
					  if (this->has_index(index)) {
 | 
				
			||||||
 | 
					    auto options = traits.get_options();
 | 
				
			||||||
 | 
					    return options.at(index);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    return {};
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
uint32_t Select::hash_base() { return 2812997003UL; }
 | 
					uint32_t Select::hash_base() { return 2812997003UL; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace select
 | 
					}  // namespace select
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,10 @@
 | 
				
			|||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <set>
 | 
					 | 
				
			||||||
#include <utility>
 | 
					 | 
				
			||||||
#include "esphome/core/component.h"
 | 
					#include "esphome/core/component.h"
 | 
				
			||||||
#include "esphome/core/entity_base.h"
 | 
					#include "esphome/core/entity_base.h"
 | 
				
			||||||
#include "esphome/core/helpers.h"
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
 | 
					#include "select_call.h"
 | 
				
			||||||
 | 
					#include "select_traits.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace esphome {
 | 
					namespace esphome {
 | 
				
			||||||
namespace select {
 | 
					namespace select {
 | 
				
			||||||
@@ -17,33 +17,6 @@ namespace select {
 | 
				
			|||||||
    } \
 | 
					    } \
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Select;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SelectCall {
 | 
					 | 
				
			||||||
 public:
 | 
					 | 
				
			||||||
  explicit SelectCall(Select *parent) : parent_(parent) {}
 | 
					 | 
				
			||||||
  void perform();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  SelectCall &set_option(const std::string &option) {
 | 
					 | 
				
			||||||
    option_ = option;
 | 
					 | 
				
			||||||
    return *this;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  const optional<std::string> &get_option() const { return option_; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 protected:
 | 
					 | 
				
			||||||
  Select *const parent_;
 | 
					 | 
				
			||||||
  optional<std::string> option_;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SelectTraits {
 | 
					 | 
				
			||||||
 public:
 | 
					 | 
				
			||||||
  void set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
 | 
					 | 
				
			||||||
  std::vector<std::string> get_options() const { return this->options_; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 protected:
 | 
					 | 
				
			||||||
  std::vector<std::string> options_;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/** Base-class for all selects.
 | 
					/** Base-class for all selects.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * A select can use publish_state to send out a new value.
 | 
					 * A select can use publish_state to send out a new value.
 | 
				
			||||||
@@ -51,19 +24,36 @@ class SelectTraits {
 | 
				
			|||||||
class Select : public EntityBase {
 | 
					class Select : public EntityBase {
 | 
				
			||||||
 public:
 | 
					 public:
 | 
				
			||||||
  std::string state;
 | 
					  std::string state;
 | 
				
			||||||
 | 
					  SelectTraits traits;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void publish_state(const std::string &state);
 | 
					  void publish_state(const std::string &state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SelectCall make_call() { return SelectCall(this); }
 | 
					  /// Return whether this select component has gotten a full state yet.
 | 
				
			||||||
  void set(const std::string &value) { make_call().set_option(value).perform(); }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void add_on_state_callback(std::function<void(std::string)> &&callback);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  SelectTraits traits;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Return whether this select has gotten a full state yet.
 | 
					 | 
				
			||||||
  bool has_state() const { return has_state_; }
 | 
					  bool has_state() const { return has_state_; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Instantiate a SelectCall object to modify this select component's state.
 | 
				
			||||||
 | 
					  SelectCall make_call() { return SelectCall(this); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Return whether this select component contains the provided option.
 | 
				
			||||||
 | 
					  bool has_option(const std::string &option) const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Return whether this select component contains the provided index offset.
 | 
				
			||||||
 | 
					  bool has_index(size_t index) const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Return the number of options in this select component.
 | 
				
			||||||
 | 
					  size_t size() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Find the (optional) index offset of the provided option value.
 | 
				
			||||||
 | 
					  optional<size_t> index_of(const std::string &option) const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Return the (optional) index offset of the currently active option.
 | 
				
			||||||
 | 
					  optional<size_t> active_index() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Return the (optional) option value at the provided index offset.
 | 
				
			||||||
 | 
					  optional<std::string> at(size_t index) const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 protected:
 | 
					 protected:
 | 
				
			||||||
  friend class SelectCall;
 | 
					  friend class SelectCall;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -77,7 +67,7 @@ class Select : public EntityBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  uint32_t hash_base() override;
 | 
					  uint32_t hash_base() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  CallbackManager<void(std::string)> state_callback_;
 | 
					  CallbackManager<void(std::string, size_t)> state_callback_;
 | 
				
			||||||
  bool has_state_{false};
 | 
					  bool has_state_{false};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										120
									
								
								esphome/components/select/select_call.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								esphome/components/select/select_call.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					#include "select_call.h"
 | 
				
			||||||
 | 
					#include "select.h"
 | 
				
			||||||
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace select {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *const TAG = "select";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::set_option(const std::string &option) {
 | 
				
			||||||
 | 
					  return with_operation(SELECT_OP_SET).with_option(option);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::with_operation(SelectOperation operation) {
 | 
				
			||||||
 | 
					  this->operation_ = operation;
 | 
				
			||||||
 | 
					  return *this;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::with_cycle(bool cycle) {
 | 
				
			||||||
 | 
					  this->cycle_ = cycle;
 | 
				
			||||||
 | 
					  return *this;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::with_option(const std::string &option) {
 | 
				
			||||||
 | 
					  this->option_ = option;
 | 
				
			||||||
 | 
					  return *this;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SelectCall &SelectCall::with_index(size_t index) {
 | 
				
			||||||
 | 
					  this->index_ = index;
 | 
				
			||||||
 | 
					  return *this;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SelectCall::perform() {
 | 
				
			||||||
 | 
					  auto *parent = this->parent_;
 | 
				
			||||||
 | 
					  const auto *name = parent->get_name().c_str();
 | 
				
			||||||
 | 
					  const auto &traits = parent->traits;
 | 
				
			||||||
 | 
					  auto options = traits.get_options();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (this->operation_ == SELECT_OP_NONE) {
 | 
				
			||||||
 | 
					    ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (options.empty()) {
 | 
				
			||||||
 | 
					    ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  std::string target_value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (this->operation_ == SELECT_OP_SET) {
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "'%s' - Setting", name);
 | 
				
			||||||
 | 
					    if (!this->option_.has_value()) {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    target_value = this->option_.value();
 | 
				
			||||||
 | 
					  } else if (this->operation_ == SELECT_OP_SET_INDEX) {
 | 
				
			||||||
 | 
					    if (!this->index_.has_value()) {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (this->index_.value() >= options.size()) {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "'%s' - Index value %d out of bounds", name, this->index_.value());
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    target_value = options[this->index_.value()];
 | 
				
			||||||
 | 
					  } else if (this->operation_ == SELECT_OP_FIRST) {
 | 
				
			||||||
 | 
					    target_value = options.front();
 | 
				
			||||||
 | 
					  } else if (this->operation_ == SELECT_OP_LAST) {
 | 
				
			||||||
 | 
					    target_value = options.back();
 | 
				
			||||||
 | 
					  } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) {
 | 
				
			||||||
 | 
					    auto cycle = this->cycle_;
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous",
 | 
				
			||||||
 | 
					             cycle ? "" : "out");
 | 
				
			||||||
 | 
					    if (!parent->has_state()) {
 | 
				
			||||||
 | 
					      target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      auto index = parent->index_of(parent->state);
 | 
				
			||||||
 | 
					      if (index.has_value()) {
 | 
				
			||||||
 | 
					        auto size = options.size();
 | 
				
			||||||
 | 
					        if (cycle) {
 | 
				
			||||||
 | 
					          auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size;
 | 
				
			||||||
 | 
					          target_value = options[use_index];
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) {
 | 
				
			||||||
 | 
					            target_value = options[index.value() - 1];
 | 
				
			||||||
 | 
					          } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) {
 | 
				
			||||||
 | 
					            target_value = options[index.value() + 1];
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (std::find(options.begin(), options.end(), target_value) == options.end()) {
 | 
				
			||||||
 | 
					    ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str());
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str());
 | 
				
			||||||
 | 
					  parent->control(target_value);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace select
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										47
									
								
								esphome/components/select/select_call.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/select/select_call.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace select {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Select;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum SelectOperation {
 | 
				
			||||||
 | 
					  SELECT_OP_NONE,
 | 
				
			||||||
 | 
					  SELECT_OP_SET,
 | 
				
			||||||
 | 
					  SELECT_OP_SET_INDEX,
 | 
				
			||||||
 | 
					  SELECT_OP_NEXT,
 | 
				
			||||||
 | 
					  SELECT_OP_PREVIOUS,
 | 
				
			||||||
 | 
					  SELECT_OP_FIRST,
 | 
				
			||||||
 | 
					  SELECT_OP_LAST
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SelectCall {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  explicit SelectCall(Select *parent) : parent_(parent) {}
 | 
				
			||||||
 | 
					  void perform();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SelectCall &set_option(const std::string &option);
 | 
				
			||||||
 | 
					  SelectCall &set_index(size_t index);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SelectCall &select_next(bool cycle);
 | 
				
			||||||
 | 
					  SelectCall &select_previous(bool cycle);
 | 
				
			||||||
 | 
					  SelectCall &select_first();
 | 
				
			||||||
 | 
					  SelectCall &select_last();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SelectCall &with_operation(SelectOperation operation);
 | 
				
			||||||
 | 
					  SelectCall &with_cycle(bool cycle);
 | 
				
			||||||
 | 
					  SelectCall &with_option(const std::string &option);
 | 
				
			||||||
 | 
					  SelectCall &with_index(size_t index);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  Select *const parent_;
 | 
				
			||||||
 | 
					  optional<std::string> option_;
 | 
				
			||||||
 | 
					  optional<size_t> index_;
 | 
				
			||||||
 | 
					  SelectOperation operation_{SELECT_OP_NONE};
 | 
				
			||||||
 | 
					  bool cycle_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace select
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										11
									
								
								esphome/components/select/select_traits.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								esphome/components/select/select_traits.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					#include "select_traits.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace select {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::vector<std::string> SelectTraits::get_options() const { return this->options_; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace select
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										19
									
								
								esphome/components/select/select_traits.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/select/select_traits.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <vector>
 | 
				
			||||||
 | 
					#include <string>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace select {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SelectTraits {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  void set_options(std::vector<std::string> options);
 | 
				
			||||||
 | 
					  std::vector<std::string> get_options() const;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  std::vector<std::string> options_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace select
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										0
									
								
								esphome/components/sen5x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/sen5x/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								esphome/components/sen5x/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/sen5x/automation.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/core/component.h"
 | 
				
			||||||
 | 
					#include "esphome/core/automation.h"
 | 
				
			||||||
 | 
					#include "sen5x.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sen5x {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename... Ts> class StartFanAction : public Action<Ts...> {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  SEN5XComponent *sen5x_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace sen5x
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										413
									
								
								esphome/components/sen5x/sen5x.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										413
									
								
								esphome/components/sen5x/sen5x.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,413 @@
 | 
				
			|||||||
 | 
					#include "sen5x.h"
 | 
				
			||||||
 | 
					#include "esphome/core/hal.h"
 | 
				
			||||||
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sen5x {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *const TAG = "sen5x";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181;
 | 
				
			||||||
 | 
					static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SEN5XComponent::setup() {
 | 
				
			||||||
 | 
					  ESP_LOGCONFIG(TAG, "Setting up sen5x...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // the sensor needs 1000 ms to enter the idle state
 | 
				
			||||||
 | 
					  this->set_timeout(1000, [this]() {
 | 
				
			||||||
 | 
					    // Check if measurement is ready before reading the value
 | 
				
			||||||
 | 
					    if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "Failed to write data ready status command");
 | 
				
			||||||
 | 
					      this->mark_failed();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uint16_t raw_read_status;
 | 
				
			||||||
 | 
					    if (!this->read_data(raw_read_status)) {
 | 
				
			||||||
 | 
					      ESP_LOGE(TAG, "Failed to read data ready status");
 | 
				
			||||||
 | 
					      this->mark_failed();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    uint32_t stop_measurement_delay = 0;
 | 
				
			||||||
 | 
					    // In order to query the device periodic measurement must be ceased
 | 
				
			||||||
 | 
					    if (raw_read_status) {
 | 
				
			||||||
 | 
					      ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement");
 | 
				
			||||||
 | 
					      if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "Failed to stop measurements");
 | 
				
			||||||
 | 
					        this->mark_failed();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after
 | 
				
			||||||
 | 
					      // issuing the stop_periodic_measurement command
 | 
				
			||||||
 | 
					      stop_measurement_delay = 200;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this->set_timeout(stop_measurement_delay, [this]() {
 | 
				
			||||||
 | 
					      uint16_t raw_serial_number[3];
 | 
				
			||||||
 | 
					      if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "Failed to read serial number");
 | 
				
			||||||
 | 
					        this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED;
 | 
				
			||||||
 | 
					        this->mark_failed();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this->serial_number_[0] = static_cast<bool>(uint16_t(raw_serial_number[0]) & 0xFF);
 | 
				
			||||||
 | 
					      this->serial_number_[1] = static_cast<uint16_t>(raw_serial_number[0] & 0xFF);
 | 
				
			||||||
 | 
					      this->serial_number_[2] = static_cast<uint16_t>(raw_serial_number[1] >> 8);
 | 
				
			||||||
 | 
					      ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      uint16_t raw_product_name[16];
 | 
				
			||||||
 | 
					      if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "Failed to read product name");
 | 
				
			||||||
 | 
					        this->error_code_ = PRODUCT_NAME_FAILED;
 | 
				
			||||||
 | 
					        this->mark_failed();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // 2 ASCII bytes are encoded in an int
 | 
				
			||||||
 | 
					      const uint16_t *current_int = raw_product_name;
 | 
				
			||||||
 | 
					      char current_char;
 | 
				
			||||||
 | 
					      uint8_t max = 16;
 | 
				
			||||||
 | 
					      do {
 | 
				
			||||||
 | 
					        // first char
 | 
				
			||||||
 | 
					        current_char = *current_int >> 8;
 | 
				
			||||||
 | 
					        if (current_char) {
 | 
				
			||||||
 | 
					          product_name_.push_back(current_char);
 | 
				
			||||||
 | 
					          // second char
 | 
				
			||||||
 | 
					          current_char = *current_int & 0xFF;
 | 
				
			||||||
 | 
					          if (current_char)
 | 
				
			||||||
 | 
					            product_name_.push_back(current_char);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        current_int++;
 | 
				
			||||||
 | 
					      } while (current_char && --max);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Sen5xType sen5x_type = UNKNOWN;
 | 
				
			||||||
 | 
					      if (product_name_ == "SEN50") {
 | 
				
			||||||
 | 
					        sen5x_type = SEN50;
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        if (product_name_ == "SEN54") {
 | 
				
			||||||
 | 
					          sen5x_type = SEN54;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          if (product_name_ == "SEN55") {
 | 
				
			||||||
 | 
					            sen5x_type = SEN55;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ESP_LOGD(TAG, "Productname %s", product_name_.c_str());
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this->humidity_sensor_ && sen5x_type == SEN50) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor",
 | 
				
			||||||
 | 
					                 this->product_name_.c_str());
 | 
				
			||||||
 | 
					        this->humidity_sensor_ = nullptr;  // mark as not used
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this->temperature_sensor_ && sen5x_type == SEN50) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor",
 | 
				
			||||||
 | 
					                 this->product_name_.c_str());
 | 
				
			||||||
 | 
					        this->temperature_sensor_ = nullptr;  // mark as not used
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this->voc_sensor_ && sen5x_type == SEN50) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str());
 | 
				
			||||||
 | 
					        this->voc_sensor_ = nullptr;  // mark as not used
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this->nox_sensor_ && sen5x_type != SEN55) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str());
 | 
				
			||||||
 | 
					        this->nox_sensor_ = nullptr;  // mark as not used
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "Failed to read firmware version");
 | 
				
			||||||
 | 
					        this->error_code_ = FIRMWARE_FAILED;
 | 
				
			||||||
 | 
					        this->mark_failed();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this->firmware_version_ >>= 8;
 | 
				
			||||||
 | 
					      ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this->voc_sensor_ && this->store_baseline_) {
 | 
				
			||||||
 | 
					        // Hash with compilation time
 | 
				
			||||||
 | 
					        // This ensures the baseline storage is cleared after OTA
 | 
				
			||||||
 | 
					        uint32_t hash = fnv1_hash(App.get_compilation_time());
 | 
				
			||||||
 | 
					        this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this->pref_.load(&this->voc_baselines_storage_)) {
 | 
				
			||||||
 | 
					          ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0,
 | 
				
			||||||
 | 
					                   voc_baselines_storage_.state1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Initialize storage timestamp
 | 
				
			||||||
 | 
					        this->seconds_since_last_store_ = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
 | 
				
			||||||
 | 
					          ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X",
 | 
				
			||||||
 | 
					                   this->voc_baselines_storage_.state0, voc_baselines_storage_.state1);
 | 
				
			||||||
 | 
					          uint16_t states[4];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          states[0] = voc_baselines_storage_.state0 >> 16;
 | 
				
			||||||
 | 
					          states[1] = voc_baselines_storage_.state0 & 0xFFFF;
 | 
				
			||||||
 | 
					          states[2] = voc_baselines_storage_.state1 >> 16;
 | 
				
			||||||
 | 
					          states[3] = voc_baselines_storage_.state1 & 0xFFFF;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
 | 
				
			||||||
 | 
					            ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      bool result;
 | 
				
			||||||
 | 
					      if (this->auto_cleaning_interval_.has_value()) {
 | 
				
			||||||
 | 
					        // override default value
 | 
				
			||||||
 | 
					        result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value());
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (result) {
 | 
				
			||||||
 | 
					        delay(20);
 | 
				
			||||||
 | 
					        uint16_t secs[2];
 | 
				
			||||||
 | 
					        if (this->read_data(secs, 2)) {
 | 
				
			||||||
 | 
					          auto_cleaning_interval_ = secs[0] << 16 | secs[1];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (acceleration_mode_.has_value()) {
 | 
				
			||||||
 | 
					        result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value());
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!result) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "Failed to set rh/t acceleration mode");
 | 
				
			||||||
 | 
					        this->error_code_ = COMMUNICATION_FAILED;
 | 
				
			||||||
 | 
					        this->mark_failed();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      delay(20);
 | 
				
			||||||
 | 
					      if (!acceleration_mode_.has_value()) {
 | 
				
			||||||
 | 
					        uint16_t mode;
 | 
				
			||||||
 | 
					        if (this->read_data(mode)) {
 | 
				
			||||||
 | 
					          this->acceleration_mode_ = RhtAccelerationMode(mode);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          ESP_LOGE(TAG, "Failed to read RHT Acceleration mode");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (this->voc_tuning_params_.has_value())
 | 
				
			||||||
 | 
					        this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
 | 
				
			||||||
 | 
					      if (this->nox_tuning_params_.has_value())
 | 
				
			||||||
 | 
					        this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (this->temperature_compensation_.has_value())
 | 
				
			||||||
 | 
					        this->write_temperature_compensation_(this->temperature_compensation_.value());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Finally start sensor measurements
 | 
				
			||||||
 | 
					      auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY;
 | 
				
			||||||
 | 
					      if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) {
 | 
				
			||||||
 | 
					        // if any of the gas sensors are active we need a full measurement
 | 
				
			||||||
 | 
					        cmd = SEN5X_CMD_START_MEASUREMENTS;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!this->write_command(cmd)) {
 | 
				
			||||||
 | 
					        ESP_LOGE(TAG, "Error starting continuous measurements.");
 | 
				
			||||||
 | 
					        this->error_code_ = MEASUREMENT_INIT_FAILED;
 | 
				
			||||||
 | 
					        this->mark_failed();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      initialized_ = true;
 | 
				
			||||||
 | 
					      ESP_LOGD(TAG, "Sensor initialized");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SEN5XComponent::dump_config() {
 | 
				
			||||||
 | 
					  ESP_LOGCONFIG(TAG, "sen5x:");
 | 
				
			||||||
 | 
					  LOG_I2C_DEVICE(this);
 | 
				
			||||||
 | 
					  if (this->is_failed()) {
 | 
				
			||||||
 | 
					    switch (this->error_code_) {
 | 
				
			||||||
 | 
					      case COMMUNICATION_FAILED:
 | 
				
			||||||
 | 
					        ESP_LOGW(TAG, "Communication failed! Is the sensor connected?");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case MEASUREMENT_INIT_FAILED:
 | 
				
			||||||
 | 
					        ESP_LOGW(TAG, "Measurement Initialization failed!");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case SERIAL_NUMBER_IDENTIFICATION_FAILED:
 | 
				
			||||||
 | 
					        ESP_LOGW(TAG, "Unable to read sensor serial id");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case PRODUCT_NAME_FAILED:
 | 
				
			||||||
 | 
					        ESP_LOGW(TAG, "Unable to read product name");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case FIRMWARE_FAILED:
 | 
				
			||||||
 | 
					        ESP_LOGW(TAG, "Unable to read sensor firmware version");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        ESP_LOGW(TAG, "Unknown setup error!");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  ESP_LOGCONFIG(TAG, "  Productname: %s", this->product_name_.c_str());
 | 
				
			||||||
 | 
					  ESP_LOGCONFIG(TAG, "  Firmware version: %d", this->firmware_version_);
 | 
				
			||||||
 | 
					  ESP_LOGCONFIG(TAG, "  Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]);
 | 
				
			||||||
 | 
					  if (this->auto_cleaning_interval_.has_value()) {
 | 
				
			||||||
 | 
					    ESP_LOGCONFIG(TAG, "  Auto auto cleaning interval %d seconds", auto_cleaning_interval_.value());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (this->acceleration_mode_.has_value()) {
 | 
				
			||||||
 | 
					    switch (this->acceleration_mode_.value()) {
 | 
				
			||||||
 | 
					      case LOW_ACCELERATION:
 | 
				
			||||||
 | 
					        ESP_LOGCONFIG(TAG, "  Low RH/T acceleration mode");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case MEDIUM_ACCELERATION:
 | 
				
			||||||
 | 
					        ESP_LOGCONFIG(TAG, "  Medium RH/T accelertion mode");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      case HIGH_ACCELERATION:
 | 
				
			||||||
 | 
					        ESP_LOGCONFIG(TAG, "  High RH/T accelertion mode");
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  LOG_UPDATE_INTERVAL(this);
 | 
				
			||||||
 | 
					  LOG_SENSOR("  ", "PM  1.0", this->pm_1_0_sensor_);
 | 
				
			||||||
 | 
					  LOG_SENSOR("  ", "PM  2.5", this->pm_2_5_sensor_);
 | 
				
			||||||
 | 
					  LOG_SENSOR("  ", "PM  4.0", this->pm_4_0_sensor_);
 | 
				
			||||||
 | 
					  LOG_SENSOR("  ", "PM 10.0", this->pm_10_0_sensor_);
 | 
				
			||||||
 | 
					  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
				
			||||||
 | 
					  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
				
			||||||
 | 
					  LOG_SENSOR("  ", "VOC", this->voc_sensor_);  // SEN54 and SEN55 only
 | 
				
			||||||
 | 
					  LOG_SENSOR("  ", "NOx", this->nox_sensor_);  // SEN55 only
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SEN5XComponent::update() {
 | 
				
			||||||
 | 
					  if (!initialized_) {
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Store baselines after defined interval or if the difference between current and stored baseline becomes too
 | 
				
			||||||
 | 
					  // much
 | 
				
			||||||
 | 
					  if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
 | 
				
			||||||
 | 
					    if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
 | 
				
			||||||
 | 
					      // run it a bit later to avoid adding a delay here
 | 
				
			||||||
 | 
					      this->set_timeout(550, [this]() {
 | 
				
			||||||
 | 
					        uint16_t states[4];
 | 
				
			||||||
 | 
					        if (this->read_data(states, 4)) {
 | 
				
			||||||
 | 
					          uint32_t state0 = states[0] << 16 | states[1];
 | 
				
			||||||
 | 
					          uint32_t state1 = states[2] << 16 | states[3];
 | 
				
			||||||
 | 
					          if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
 | 
				
			||||||
 | 
					                  MAXIMUM_STORAGE_DIFF ||
 | 
				
			||||||
 | 
					              (uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
 | 
				
			||||||
 | 
					                  MAXIMUM_STORAGE_DIFF) {
 | 
				
			||||||
 | 
					            this->seconds_since_last_store_ = 0;
 | 
				
			||||||
 | 
					            this->voc_baselines_storage_.state0 = state0;
 | 
				
			||||||
 | 
					            this->voc_baselines_storage_.state1 = state1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this->pref_.save(&this->voc_baselines_storage_)) {
 | 
				
			||||||
 | 
					              ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0,
 | 
				
			||||||
 | 
					                       voc_baselines_storage_.state1);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              ESP_LOGW(TAG, "Could not store VOC baselines");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
 | 
				
			||||||
 | 
					    this->status_set_warning();
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  this->set_timeout(20, [this]() {
 | 
				
			||||||
 | 
					    uint16_t measurements[8];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this->read_data(measurements, 8)) {
 | 
				
			||||||
 | 
					      this->status_set_warning();
 | 
				
			||||||
 | 
					      ESP_LOGD(TAG, "read data error (%d)", this->last_error_);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    float pm_1_0 = measurements[0] / 10.0;
 | 
				
			||||||
 | 
					    if (measurements[0] == 0xFFFF)
 | 
				
			||||||
 | 
					      pm_1_0 = NAN;
 | 
				
			||||||
 | 
					    float pm_2_5 = measurements[1] / 10.0;
 | 
				
			||||||
 | 
					    if (measurements[1] == 0xFFFF)
 | 
				
			||||||
 | 
					      pm_2_5 = NAN;
 | 
				
			||||||
 | 
					    float pm_4_0 = measurements[2] / 10.0;
 | 
				
			||||||
 | 
					    if (measurements[2] == 0xFFFF)
 | 
				
			||||||
 | 
					      pm_4_0 = NAN;
 | 
				
			||||||
 | 
					    float pm_10_0 = measurements[3] / 10.0;
 | 
				
			||||||
 | 
					    if (measurements[3] == 0xFFFF)
 | 
				
			||||||
 | 
					      pm_10_0 = NAN;
 | 
				
			||||||
 | 
					    float humidity = measurements[4] / 100.0;
 | 
				
			||||||
 | 
					    if (measurements[4] == 0xFFFF)
 | 
				
			||||||
 | 
					      humidity = NAN;
 | 
				
			||||||
 | 
					    float temperature = measurements[5] / 200.0;
 | 
				
			||||||
 | 
					    if (measurements[5] == 0xFFFF)
 | 
				
			||||||
 | 
					      temperature = NAN;
 | 
				
			||||||
 | 
					    float voc = measurements[6] / 10.0;
 | 
				
			||||||
 | 
					    if (measurements[6] == 0xFFFF)
 | 
				
			||||||
 | 
					      voc = NAN;
 | 
				
			||||||
 | 
					    float nox = measurements[7] / 10.0;
 | 
				
			||||||
 | 
					    if (measurements[7] == 0xFFFF)
 | 
				
			||||||
 | 
					      nox = NAN;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this->pm_1_0_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->pm_1_0_sensor_->publish_state(pm_1_0);
 | 
				
			||||||
 | 
					    if (this->pm_2_5_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->pm_2_5_sensor_->publish_state(pm_2_5);
 | 
				
			||||||
 | 
					    if (this->pm_4_0_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->pm_4_0_sensor_->publish_state(pm_4_0);
 | 
				
			||||||
 | 
					    if (this->pm_10_0_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->pm_10_0_sensor_->publish_state(pm_10_0);
 | 
				
			||||||
 | 
					    if (this->temperature_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->temperature_sensor_->publish_state(temperature);
 | 
				
			||||||
 | 
					    if (this->humidity_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->humidity_sensor_->publish_state(humidity);
 | 
				
			||||||
 | 
					    if (this->voc_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->voc_sensor_->publish_state(voc);
 | 
				
			||||||
 | 
					    if (this->nox_sensor_ != nullptr)
 | 
				
			||||||
 | 
					      this->nox_sensor_->publish_state(nox);
 | 
				
			||||||
 | 
					    this->status_clear_warning();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
 | 
				
			||||||
 | 
					  uint16_t params[6];
 | 
				
			||||||
 | 
					  params[0] = tuning.index_offset;
 | 
				
			||||||
 | 
					  params[1] = tuning.learning_time_offset_hours;
 | 
				
			||||||
 | 
					  params[2] = tuning.learning_time_gain_hours;
 | 
				
			||||||
 | 
					  params[3] = tuning.gating_max_duration_minutes;
 | 
				
			||||||
 | 
					  params[4] = tuning.std_initial;
 | 
				
			||||||
 | 
					  params[5] = tuning.gain_factor;
 | 
				
			||||||
 | 
					  auto result = write_command(i2c_command, params, 6);
 | 
				
			||||||
 | 
					  if (!result) {
 | 
				
			||||||
 | 
					    ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensation &compensation) {
 | 
				
			||||||
 | 
					  uint16_t params[3];
 | 
				
			||||||
 | 
					  params[0] = compensation.offset;
 | 
				
			||||||
 | 
					  params[1] = compensation.normalized_offset_slope;
 | 
				
			||||||
 | 
					  params[2] = compensation.time_constant;
 | 
				
			||||||
 | 
					  if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) {
 | 
				
			||||||
 | 
					    ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_);
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool SEN5XComponent::start_fan_cleaning() {
 | 
				
			||||||
 | 
					  if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) {
 | 
				
			||||||
 | 
					    this->status_set_warning();
 | 
				
			||||||
 | 
					    ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_);
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "Fan auto clean started");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace sen5x
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										128
									
								
								esphome/components/sen5x/sen5x.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								esphome/components/sen5x/sen5x.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "esphome/core/component.h"
 | 
				
			||||||
 | 
					#include "esphome/components/sensor/sensor.h"
 | 
				
			||||||
 | 
					#include "esphome/components/sensirion_common/i2c_sensirion.h"
 | 
				
			||||||
 | 
					#include "esphome/core/application.h"
 | 
				
			||||||
 | 
					#include "esphome/core/preferences.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sen5x {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum ERRORCODE {
 | 
				
			||||||
 | 
					  COMMUNICATION_FAILED,
 | 
				
			||||||
 | 
					  SERIAL_NUMBER_IDENTIFICATION_FAILED,
 | 
				
			||||||
 | 
					  MEASUREMENT_INIT_FAILED,
 | 
				
			||||||
 | 
					  PRODUCT_NAME_FAILED,
 | 
				
			||||||
 | 
					  FIRMWARE_FAILED,
 | 
				
			||||||
 | 
					  UNKNOWN
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Shortest time interval of 3H for storing baseline values.
 | 
				
			||||||
 | 
					// Prevents wear of the flash because of too many write operations
 | 
				
			||||||
 | 
					const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
 | 
				
			||||||
 | 
					// Store anyway if the baseline difference exceeds the max storage diff value
 | 
				
			||||||
 | 
					const uint32_t MAXIMUM_STORAGE_DIFF = 50;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Sen5xBaselines {
 | 
				
			||||||
 | 
					  int32_t state0;
 | 
				
			||||||
 | 
					  int32_t state1;
 | 
				
			||||||
 | 
					} PACKED;  // NOLINT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct GasTuning {
 | 
				
			||||||
 | 
					  uint16_t index_offset;
 | 
				
			||||||
 | 
					  uint16_t learning_time_offset_hours;
 | 
				
			||||||
 | 
					  uint16_t learning_time_gain_hours;
 | 
				
			||||||
 | 
					  uint16_t gating_max_duration_minutes;
 | 
				
			||||||
 | 
					  uint16_t std_initial;
 | 
				
			||||||
 | 
					  uint16_t gain_factor;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct TemperatureCompensation {
 | 
				
			||||||
 | 
					  uint16_t offset;
 | 
				
			||||||
 | 
					  uint16_t normalized_offset_slope;
 | 
				
			||||||
 | 
					  uint16_t time_constant;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
				
			||||||
 | 
					  void setup() override;
 | 
				
			||||||
 | 
					  void dump_config() override;
 | 
				
			||||||
 | 
					  void update() override;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; }
 | 
				
			||||||
 | 
					  void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; }
 | 
				
			||||||
 | 
					  void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; }
 | 
				
			||||||
 | 
					  void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; }
 | 
				
			||||||
 | 
					  void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; }
 | 
				
			||||||
 | 
					  void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
 | 
				
			||||||
 | 
					  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
 | 
				
			||||||
 | 
					  void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; }
 | 
				
			||||||
 | 
					  void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; }
 | 
				
			||||||
 | 
					  void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; }
 | 
				
			||||||
 | 
					  void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
 | 
				
			||||||
 | 
					                                uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
 | 
				
			||||||
 | 
					                                uint16_t std_initial, uint16_t gain_factor) {
 | 
				
			||||||
 | 
					    voc_tuning_params_.value().index_offset = index_offset;
 | 
				
			||||||
 | 
					    voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
 | 
				
			||||||
 | 
					    voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
 | 
				
			||||||
 | 
					    voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
 | 
				
			||||||
 | 
					    voc_tuning_params_.value().std_initial = std_initial;
 | 
				
			||||||
 | 
					    voc_tuning_params_.value().gain_factor = gain_factor;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
 | 
				
			||||||
 | 
					                                uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
 | 
				
			||||||
 | 
					                                uint16_t gain_factor) {
 | 
				
			||||||
 | 
					    nox_tuning_params_.value().index_offset = index_offset;
 | 
				
			||||||
 | 
					    nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours;
 | 
				
			||||||
 | 
					    nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours;
 | 
				
			||||||
 | 
					    nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes;
 | 
				
			||||||
 | 
					    nox_tuning_params_.value().std_initial = 50;
 | 
				
			||||||
 | 
					    nox_tuning_params_.value().gain_factor = gain_factor;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) {
 | 
				
			||||||
 | 
					    temperature_compensation_.value().offset = offset * 200;
 | 
				
			||||||
 | 
					    temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100;
 | 
				
			||||||
 | 
					    temperature_compensation_.value().time_constant = time_constant;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  bool start_fan_cleaning();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
 | 
				
			||||||
 | 
					  bool write_temperature_compensation_(const TemperatureCompensation &compensation);
 | 
				
			||||||
 | 
					  ERRORCODE error_code_;
 | 
				
			||||||
 | 
					  bool initialized_{false};
 | 
				
			||||||
 | 
					  sensor::Sensor *pm_1_0_sensor_{nullptr};
 | 
				
			||||||
 | 
					  sensor::Sensor *pm_2_5_sensor_{nullptr};
 | 
				
			||||||
 | 
					  sensor::Sensor *pm_4_0_sensor_{nullptr};
 | 
				
			||||||
 | 
					  sensor::Sensor *pm_10_0_sensor_{nullptr};
 | 
				
			||||||
 | 
					  // SEN54 and SEN55 only
 | 
				
			||||||
 | 
					  sensor::Sensor *temperature_sensor_{nullptr};
 | 
				
			||||||
 | 
					  sensor::Sensor *humidity_sensor_{nullptr};
 | 
				
			||||||
 | 
					  sensor::Sensor *voc_sensor_{nullptr};
 | 
				
			||||||
 | 
					  // SEN55 only
 | 
				
			||||||
 | 
					  sensor::Sensor *nox_sensor_{nullptr};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  std::string product_name_;
 | 
				
			||||||
 | 
					  uint8_t serial_number_[4];
 | 
				
			||||||
 | 
					  uint16_t firmware_version_;
 | 
				
			||||||
 | 
					  Sen5xBaselines voc_baselines_storage_;
 | 
				
			||||||
 | 
					  bool store_baseline_;
 | 
				
			||||||
 | 
					  uint32_t seconds_since_last_store_;
 | 
				
			||||||
 | 
					  ESPPreferenceObject pref_;
 | 
				
			||||||
 | 
					  optional<RhtAccelerationMode> acceleration_mode_;
 | 
				
			||||||
 | 
					  optional<uint32_t> auto_cleaning_interval_;
 | 
				
			||||||
 | 
					  optional<GasTuning> voc_tuning_params_;
 | 
				
			||||||
 | 
					  optional<GasTuning> nox_tuning_params_;
 | 
				
			||||||
 | 
					  optional<TemperatureCompensation> temperature_compensation_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace sen5x
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										241
									
								
								esphome/components/sen5x/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								esphome/components/sen5x/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,241 @@
 | 
				
			|||||||
 | 
					import esphome.codegen as cg
 | 
				
			||||||
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
 | 
					from esphome.components import i2c, sensor, sensirion_common
 | 
				
			||||||
 | 
					from esphome import automation
 | 
				
			||||||
 | 
					from esphome.automation import maybe_simple_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from esphome.const import (
 | 
				
			||||||
 | 
					    CONF_HUMIDITY,
 | 
				
			||||||
 | 
					    CONF_ID,
 | 
				
			||||||
 | 
					    CONF_OFFSET,
 | 
				
			||||||
 | 
					    CONF_PM_1_0,
 | 
				
			||||||
 | 
					    CONF_PM_10_0,
 | 
				
			||||||
 | 
					    CONF_PM_2_5,
 | 
				
			||||||
 | 
					    CONF_PM_4_0,
 | 
				
			||||||
 | 
					    CONF_STORE_BASELINE,
 | 
				
			||||||
 | 
					    CONF_TEMPERATURE,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_HUMIDITY,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_NITROUS_OXIDE,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_PM1,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_PM10,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_PM25,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_TEMPERATURE,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
 | 
				
			||||||
 | 
					    ICON_CHEMICAL_WEAPON,
 | 
				
			||||||
 | 
					    ICON_RADIATOR,
 | 
				
			||||||
 | 
					    ICON_THERMOMETER,
 | 
				
			||||||
 | 
					    ICON_WATER_PERCENT,
 | 
				
			||||||
 | 
					    STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					    UNIT_CELSIUS,
 | 
				
			||||||
 | 
					    UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
				
			||||||
 | 
					    UNIT_PERCENT,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CODEOWNERS = ["@martgras"]
 | 
				
			||||||
 | 
					DEPENDENCIES = ["i2c"]
 | 
				
			||||||
 | 
					AUTO_LOAD = ["sensirion_common"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sen5x_ns = cg.esphome_ns.namespace("sen5x")
 | 
				
			||||||
 | 
					SEN5XComponent = sen5x_ns.class_(
 | 
				
			||||||
 | 
					    "SEN5XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONF_ACCELERATION_MODE = "acceleration_mode"
 | 
				
			||||||
 | 
					CONF_ALGORITHM_TUNING = "algorithm_tuning"
 | 
				
			||||||
 | 
					CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
 | 
				
			||||||
 | 
					CONF_GAIN_FACTOR = "gain_factor"
 | 
				
			||||||
 | 
					CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
 | 
				
			||||||
 | 
					CONF_INDEX_OFFSET = "index_offset"
 | 
				
			||||||
 | 
					CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
 | 
				
			||||||
 | 
					CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
 | 
				
			||||||
 | 
					CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope"
 | 
				
			||||||
 | 
					CONF_NOX = "nox"
 | 
				
			||||||
 | 
					CONF_STD_INITIAL = "std_initial"
 | 
				
			||||||
 | 
					CONF_TEMPERATURE_COMPENSATION = "temperature_compensation"
 | 
				
			||||||
 | 
					CONF_TIME_CONSTANT = "time_constant"
 | 
				
			||||||
 | 
					CONF_VOC = "voc"
 | 
				
			||||||
 | 
					CONF_VOC_BASELINE = "voc_baseline"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Actions
 | 
				
			||||||
 | 
					StartFanAction = sen5x_ns.class_("StartFanAction", automation.Action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ACCELERATION_MODES = {
 | 
				
			||||||
 | 
					    "low": RhtAccelerationMode.LOW_ACCELERATION,
 | 
				
			||||||
 | 
					    "medium": RhtAccelerationMode.MEDIUM_ACCELERATION,
 | 
				
			||||||
 | 
					    "high": RhtAccelerationMode.HIGH_ACCELERATION,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GAS_SENSOR = cv.Schema(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250),
 | 
				
			||||||
 | 
					                cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range(
 | 
				
			||||||
 | 
					                    1, 1000
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range(
 | 
				
			||||||
 | 
					                    1, 1000
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                cv.Optional(
 | 
				
			||||||
 | 
					                    CONF_GATING_MAX_DURATION_MINUTES, default=720
 | 
				
			||||||
 | 
					                ): cv.int_range(0, 3000),
 | 
				
			||||||
 | 
					                cv.Optional(CONF_STD_INITIAL, default=50): cv.int_,
 | 
				
			||||||
 | 
					                cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONFIG_SCHEMA = (
 | 
				
			||||||
 | 
					    cv.Schema(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            cv.GenerateID(): cv.declare_id(SEN5XComponent),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_PM_1_0): sensor.sensor_schema(
 | 
				
			||||||
 | 
					                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
				
			||||||
 | 
					                icon=ICON_CHEMICAL_WEAPON,
 | 
				
			||||||
 | 
					                accuracy_decimals=2,
 | 
				
			||||||
 | 
					                device_class=DEVICE_CLASS_PM1,
 | 
				
			||||||
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_PM_2_5): sensor.sensor_schema(
 | 
				
			||||||
 | 
					                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
				
			||||||
 | 
					                icon=ICON_CHEMICAL_WEAPON,
 | 
				
			||||||
 | 
					                accuracy_decimals=2,
 | 
				
			||||||
 | 
					                device_class=DEVICE_CLASS_PM25,
 | 
				
			||||||
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_PM_4_0): sensor.sensor_schema(
 | 
				
			||||||
 | 
					                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
				
			||||||
 | 
					                icon=ICON_CHEMICAL_WEAPON,
 | 
				
			||||||
 | 
					                accuracy_decimals=2,
 | 
				
			||||||
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_PM_10_0): sensor.sensor_schema(
 | 
				
			||||||
 | 
					                unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
 | 
				
			||||||
 | 
					                icon=ICON_CHEMICAL_WEAPON,
 | 
				
			||||||
 | 
					                accuracy_decimals=2,
 | 
				
			||||||
 | 
					                device_class=DEVICE_CLASS_PM10,
 | 
				
			||||||
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.time_period_in_seconds_,
 | 
				
			||||||
 | 
					            cv.Optional(CONF_VOC): sensor.sensor_schema(
 | 
				
			||||||
 | 
					                icon=ICON_RADIATOR,
 | 
				
			||||||
 | 
					                accuracy_decimals=0,
 | 
				
			||||||
 | 
					                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
 | 
				
			||||||
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					            ).extend(GAS_SENSOR),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_NOX): sensor.sensor_schema(
 | 
				
			||||||
 | 
					                icon=ICON_RADIATOR,
 | 
				
			||||||
 | 
					                accuracy_decimals=0,
 | 
				
			||||||
 | 
					                device_class=DEVICE_CLASS_NITROUS_OXIDE,
 | 
				
			||||||
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					            ).extend(GAS_SENSOR),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
 | 
				
			||||||
 | 
					            cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
 | 
				
			||||||
 | 
					            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
				
			||||||
 | 
					                unit_of_measurement=UNIT_CELSIUS,
 | 
				
			||||||
 | 
					                icon=ICON_THERMOMETER,
 | 
				
			||||||
 | 
					                accuracy_decimals=2,
 | 
				
			||||||
 | 
					                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
				
			||||||
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
				
			||||||
 | 
					                unit_of_measurement=UNIT_PERCENT,
 | 
				
			||||||
 | 
					                icon=ICON_WATER_PERCENT,
 | 
				
			||||||
 | 
					                accuracy_decimals=2,
 | 
				
			||||||
 | 
					                device_class=DEVICE_CLASS_HUMIDITY,
 | 
				
			||||||
 | 
					                state_class=STATE_CLASS_MEASUREMENT,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema(
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    cv.Optional(CONF_OFFSET, default=0): cv.float_,
 | 
				
			||||||
 | 
					                    cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage,
 | 
				
			||||||
 | 
					                    cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            cv.Optional(CONF_ACCELERATION_MODE): cv.enum(ACCELERATION_MODES),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .extend(cv.polling_component_schema("60s"))
 | 
				
			||||||
 | 
					    .extend(i2c.i2c_device_schema(0x69))
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SENSOR_MAP = {
 | 
				
			||||||
 | 
					    CONF_PM_1_0: "set_pm_1_0_sensor",
 | 
				
			||||||
 | 
					    CONF_PM_2_5: "set_pm_2_5_sensor",
 | 
				
			||||||
 | 
					    CONF_PM_4_0: "set_pm_4_0_sensor",
 | 
				
			||||||
 | 
					    CONF_PM_10_0: "set_pm_10_0_sensor",
 | 
				
			||||||
 | 
					    CONF_VOC: "set_voc_sensor",
 | 
				
			||||||
 | 
					    CONF_NOX: "set_nox_sensor",
 | 
				
			||||||
 | 
					    CONF_TEMPERATURE: "set_temperature_sensor",
 | 
				
			||||||
 | 
					    CONF_HUMIDITY: "set_humidity_sensor",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SETTING_MAP = {
 | 
				
			||||||
 | 
					    CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
 | 
				
			||||||
 | 
					    CONF_ACCELERATION_MODE: "set_acceleration_mode",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for key, funcName in SETTING_MAP.items():
 | 
				
			||||||
 | 
					        if key in config:
 | 
				
			||||||
 | 
					            cg.add(getattr(var, funcName)(config[key]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for key, funcName in SENSOR_MAP.items():
 | 
				
			||||||
 | 
					        if key in config:
 | 
				
			||||||
 | 
					            sens = await sensor.new_sensor(config[key])
 | 
				
			||||||
 | 
					            cg.add(getattr(var, funcName)(sens))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]:
 | 
				
			||||||
 | 
					        cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING]
 | 
				
			||||||
 | 
					        cg.add(
 | 
				
			||||||
 | 
					            var.set_voc_algorithm_tuning(
 | 
				
			||||||
 | 
					                cfg[CONF_INDEX_OFFSET],
 | 
				
			||||||
 | 
					                cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
 | 
				
			||||||
 | 
					                cfg[CONF_LEARNING_TIME_GAIN_HOURS],
 | 
				
			||||||
 | 
					                cfg[CONF_GATING_MAX_DURATION_MINUTES],
 | 
				
			||||||
 | 
					                cfg[CONF_STD_INITIAL],
 | 
				
			||||||
 | 
					                cfg[CONF_GAIN_FACTOR],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]:
 | 
				
			||||||
 | 
					        cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING]
 | 
				
			||||||
 | 
					        cg.add(
 | 
				
			||||||
 | 
					            var.set_nox_algorithm_tuning(
 | 
				
			||||||
 | 
					                cfg[CONF_INDEX_OFFSET],
 | 
				
			||||||
 | 
					                cfg[CONF_LEARNING_TIME_OFFSET_HOURS],
 | 
				
			||||||
 | 
					                cfg[CONF_LEARNING_TIME_GAIN_HOURS],
 | 
				
			||||||
 | 
					                cfg[CONF_GATING_MAX_DURATION_MINUTES],
 | 
				
			||||||
 | 
					                cfg[CONF_GAIN_FACTOR],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    if CONF_TEMPERATURE_COMPENSATION in config:
 | 
				
			||||||
 | 
					        cg.add(
 | 
				
			||||||
 | 
					            var.set_temperature_compensation(
 | 
				
			||||||
 | 
					                config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET],
 | 
				
			||||||
 | 
					                config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE],
 | 
				
			||||||
 | 
					                config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SEN5X_ACTION_SCHEMA = maybe_simple_id(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        cv.Required(CONF_ID): cv.use_id(SEN5XComponent),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@automation.register_action(
 | 
				
			||||||
 | 
					    "sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def sen54_fan_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)
 | 
				
			||||||
@@ -29,6 +29,7 @@ from esphome.const import (
 | 
				
			|||||||
    CONF_WINDOW_SIZE,
 | 
					    CONF_WINDOW_SIZE,
 | 
				
			||||||
    CONF_MQTT_ID,
 | 
					    CONF_MQTT_ID,
 | 
				
			||||||
    CONF_FORCE_UPDATE,
 | 
					    CONF_FORCE_UPDATE,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_DURATION,
 | 
				
			||||||
    DEVICE_CLASS_EMPTY,
 | 
					    DEVICE_CLASS_EMPTY,
 | 
				
			||||||
    DEVICE_CLASS_AQI,
 | 
					    DEVICE_CLASS_AQI,
 | 
				
			||||||
    DEVICE_CLASS_BATTERY,
 | 
					    DEVICE_CLASS_BATTERY,
 | 
				
			||||||
@@ -70,6 +71,7 @@ DEVICE_CLASSES = [
 | 
				
			|||||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
					    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
				
			||||||
    DEVICE_CLASS_CARBON_MONOXIDE,
 | 
					    DEVICE_CLASS_CARBON_MONOXIDE,
 | 
				
			||||||
    DEVICE_CLASS_CURRENT,
 | 
					    DEVICE_CLASS_CURRENT,
 | 
				
			||||||
 | 
					    DEVICE_CLASS_DURATION,
 | 
				
			||||||
    DEVICE_CLASS_ENERGY,
 | 
					    DEVICE_CLASS_ENERGY,
 | 
				
			||||||
    DEVICE_CLASS_GAS,
 | 
					    DEVICE_CLASS_GAS,
 | 
				
			||||||
    DEVICE_CLASS_HUMIDITY,
 | 
					    DEVICE_CLASS_HUMIDITY,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -158,11 +158,8 @@ bool ShellyDimmer::upgrade_firmware_() {
 | 
				
			|||||||
  ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
 | 
					  ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
 | 
				
			||||||
  this->reset_dfu_boot_();
 | 
					  this->reset_dfu_boot_();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Could be constexpr in c++17
 | 
					 | 
				
			||||||
  static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Cleanup with RAII
 | 
					  // Cleanup with RAII
 | 
				
			||||||
  std::unique_ptr<stm32_t, decltype(CLOSE)> stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE};
 | 
					  auto stm32 = stm32_init(this, STREAM_SERIAL, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!stm32) {
 | 
					  if (!stm32) {
 | 
				
			||||||
    ESP_LOGW(TAG, "Failed to initialize STM32");
 | 
					    ESP_LOGW(TAG, "Failed to initialize STM32");
 | 
				
			||||||
@@ -170,7 +167,7 @@ bool ShellyDimmer::upgrade_firmware_() {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Erase STM32 flash.
 | 
					  // Erase STM32 flash.
 | 
				
			||||||
  if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
 | 
					  if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
 | 
				
			||||||
    ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
 | 
					    ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -196,7 +193,7 @@ bool ShellyDimmer::upgrade_firmware_() {
 | 
				
			|||||||
    std::memcpy(buffer, p, BUFFER_SIZE);
 | 
					    std::memcpy(buffer, p, BUFFER_SIZE);
 | 
				
			||||||
    p += BUFFER_SIZE;
 | 
					    p += BUFFER_SIZE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) {
 | 
					    if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) {
 | 
				
			||||||
      ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
 | 
					      ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -117,7 +117,7 @@ namespace shelly_dimmer {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace {
 | 
					namespace {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) {
 | 
					int flash_addr_to_page_ceil(const stm32_unique_ptr &stm, uint32_t addr) {
 | 
				
			||||||
  if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end))
 | 
					  if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end))
 | 
				
			||||||
    return 0;
 | 
					    return 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -135,7 +135,7 @@ int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) {
 | 
				
			|||||||
  return addr ? page + 1 : page;
 | 
					  return addr ? page + 1 : page;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) {
 | 
					stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) {
 | 
				
			||||||
  auto *stream = stm->stream;
 | 
					  auto *stream = stm->stream;
 | 
				
			||||||
  uint8_t rxbyte;
 | 
					  uint8_t rxbyte;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -168,9 +168,9 @@ stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) {
 | 
				
			|||||||
  } while (true);
 | 
					  } while (true);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); }
 | 
					stm32_err_t stm32_get_ack(const stm32_unique_ptr &stm) { return stm32_get_ack_timeout(stm, 0); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) {
 | 
					stm32_err_t stm32_send_command_timeout(const stm32_unique_ptr &stm, const uint8_t cmd, const uint32_t timeout) {
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static constexpr auto BUFFER_SIZE = 2;
 | 
					  static constexpr auto BUFFER_SIZE = 2;
 | 
				
			||||||
@@ -194,12 +194,12 @@ stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, co
 | 
				
			|||||||
  return STM32_ERR_UNKNOWN;
 | 
					  return STM32_ERR_UNKNOWN;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) {
 | 
					stm32_err_t stm32_send_command(const stm32_unique_ptr &stm, const uint8_t cmd) {
 | 
				
			||||||
  return stm32_send_command_timeout(stm, cmd, 0);
 | 
					  return stm32_send_command_timeout(stm, cmd, 0);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* if we have lost sync, send a wrong command and expect a NACK */
 | 
					/* if we have lost sync, send a wrong command and expect a NACK */
 | 
				
			||||||
stm32_err_t stm32_resync(const stm32_t *stm) {
 | 
					stm32_err_t stm32_resync(const stm32_unique_ptr &stm) {
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
  uint32_t t0 = millis();
 | 
					  uint32_t t0 = millis();
 | 
				
			||||||
  auto t1 = t0;
 | 
					  auto t1 = t0;
 | 
				
			||||||
@@ -238,7 +238,7 @@ stm32_err_t stm32_resync(const stm32_t *stm) {
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * len is value of the first byte in the frame.
 | 
					 * len is value of the first byte in the frame.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) {
 | 
					stm32_err_t stm32_guess_len_cmd(const stm32_unique_ptr &stm, const uint8_t cmd, uint8_t *const data, unsigned int len) {
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
 | 
					  if (stm32_send_command(stm, cmd) != STM32_ERR_OK)
 | 
				
			||||||
@@ -286,7 +286,7 @@ stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *
 | 
				
			|||||||
 * This function sends the init sequence and, in case of timeout, recovers
 | 
					 * This function sends the init sequence and, in case of timeout, recovers
 | 
				
			||||||
 * the interface.
 | 
					 * the interface.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
stm32_err_t stm32_send_init_seq(const stm32_t *stm) {
 | 
					stm32_err_t stm32_send_init_seq(const stm32_unique_ptr &stm) {
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  stream->write_array(&STM32_CMD_INIT, 1);
 | 
					  stream->write_array(&STM32_CMD_INIT, 1);
 | 
				
			||||||
@@ -320,7 +320,7 @@ stm32_err_t stm32_send_init_seq(const stm32_t *stm) {
 | 
				
			|||||||
  return STM32_ERR_UNKNOWN;
 | 
					  return STM32_ERR_UNKNOWN;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_mass_erase(const stm32_t *stm) {
 | 
					stm32_err_t stm32_mass_erase(const stm32_unique_ptr &stm) {
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) {
 | 
					  if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) {
 | 
				
			||||||
@@ -364,7 +364,7 @@ template<typename T> std::unique_ptr<T[], void (*)(T *memory)> malloc_array_raii
 | 
				
			|||||||
                                                 DELETOR};
 | 
					                                                 DELETOR};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) {
 | 
					stm32_err_t stm32_pages_erase(const stm32_unique_ptr &stm, const uint32_t spage, const uint32_t pages) {
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
  uint8_t cs = 0;
 | 
					  uint8_t cs = 0;
 | 
				
			||||||
  int i = 0;
 | 
					  int i = 0;
 | 
				
			||||||
@@ -474,6 +474,18 @@ template<size_t N> void populate_buffer_with_address(uint8_t (&buffer)[N], uint3
 | 
				
			|||||||
  buffer[4] = static_cast<uint8_t>(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]);
 | 
					  buffer[4] = static_cast<uint8_t>(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					template<typename T> stm32_unique_ptr make_stm32_with_deletor(T ptr) {
 | 
				
			||||||
 | 
					  static const auto CLOSE = [](stm32_t *stm32) {
 | 
				
			||||||
 | 
					    if (stm32) {
 | 
				
			||||||
 | 
					      free(stm32->cmd);  // NOLINT
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    free(stm32);  // NOLINT
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Cleanup with RAII
 | 
				
			||||||
 | 
					  return std::unique_ptr<stm32_t, decltype(CLOSE)>{ptr, CLOSE};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // Anonymous namespace
 | 
					}  // Anonymous namespace
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace shelly_dimmer
 | 
					}  // namespace shelly_dimmer
 | 
				
			||||||
@@ -485,48 +497,44 @@ namespace shelly_dimmer {
 | 
				
			|||||||
/* find newer command by higher code */
 | 
					/* find newer command by higher code */
 | 
				
			||||||
#define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a)))
 | 
					#define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) {
 | 
					stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) {
 | 
				
			||||||
  uint8_t buf[257];
 | 
					  uint8_t buf[257];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Could be constexpr in c++17
 | 
					  auto stm = make_stm32_with_deletor(static_cast<stm32_t *>(calloc(sizeof(stm32_t), 1)));  // NOLINT
 | 
				
			||||||
  static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Cleanup with RAII
 | 
					 | 
				
			||||||
  std::unique_ptr<stm32_t, decltype(CLOSE)> stm{static_cast<stm32_t *>(calloc(sizeof(stm32_t), 1)),  // NOLINT
 | 
					 | 
				
			||||||
                                                CLOSE};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!stm) {
 | 
					  if (!stm) {
 | 
				
			||||||
    return nullptr;
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  stm->stream = stream;
 | 
					  stm->stream = stream;
 | 
				
			||||||
  stm->flags = flags;
 | 
					  stm->flags = flags;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  stm->cmd = static_cast<stm32_cmd_t *>(malloc(sizeof(stm32_cmd_t)));  // NOLINT
 | 
					  stm->cmd = static_cast<stm32_cmd_t *>(malloc(sizeof(stm32_cmd_t)));  // NOLINT
 | 
				
			||||||
  if (!stm->cmd) {
 | 
					  if (!stm->cmd) {
 | 
				
			||||||
    return nullptr;
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t));
 | 
					  memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if ((stm->flags & STREAM_OPT_CMD_INIT) && init) {
 | 
					  if ((stm->flags & STREAM_OPT_CMD_INIT) && init) {
 | 
				
			||||||
    if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK)
 | 
					    if (stm32_send_init_seq(stm) != STM32_ERR_OK)
 | 
				
			||||||
      return nullptr;  // NOLINT
 | 
					      return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* get the version and read protection status  */
 | 
					  /* get the version and read protection status  */
 | 
				
			||||||
  if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) {
 | 
					  if (stm32_send_command(stm, STM32_CMD_GVR) != STM32_ERR_OK) {
 | 
				
			||||||
    return nullptr;  // NOLINT
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* From AN, only UART bootloader returns 3 bytes */
 | 
					  /* From AN, only UART bootloader returns 3 bytes */
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1;
 | 
					    const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1;
 | 
				
			||||||
    if (!stream->read_array(buf, len))
 | 
					    if (!stream->read_array(buf, len))
 | 
				
			||||||
      return nullptr;  // NOLINT
 | 
					      return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    stm->version = buf[0];
 | 
					    stm->version = buf[0];
 | 
				
			||||||
    stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0;
 | 
					    stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0;
 | 
				
			||||||
    stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0;
 | 
					    stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0;
 | 
				
			||||||
    if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
 | 
					    if (stm32_get_ack(stm) != STM32_ERR_OK) {
 | 
				
			||||||
      return nullptr;
 | 
					      return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -544,8 +552,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
 | 
				
			|||||||
      return STM32_CMD_GET_LENGTH;
 | 
					      return STM32_CMD_GET_LENGTH;
 | 
				
			||||||
    })();
 | 
					    })();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK)
 | 
					    if (stm32_guess_len_cmd(stm, STM32_CMD_GET, buf, len) != STM32_ERR_OK)
 | 
				
			||||||
      return nullptr;
 | 
					      return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const auto stop = buf[0] + 1;
 | 
					  const auto stop = buf[0] + 1;
 | 
				
			||||||
@@ -607,23 +615,23 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  if (new_cmds)
 | 
					  if (new_cmds)
 | 
				
			||||||
    ESP_LOGD(TAG, ")");
 | 
					    ESP_LOGD(TAG, ")");
 | 
				
			||||||
  if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
 | 
					  if (stm32_get_ack(stm) != STM32_ERR_OK) {
 | 
				
			||||||
    return nullptr;
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) {
 | 
					  if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) {
 | 
				
			||||||
    ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command");
 | 
					    ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command");
 | 
				
			||||||
    return nullptr;
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /* get the device ID */
 | 
					  /* get the device ID */
 | 
				
			||||||
  if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) {
 | 
					  if (stm32_guess_len_cmd(stm, stm->cmd->gid, buf, 1) != STM32_ERR_OK) {
 | 
				
			||||||
    return nullptr;
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const auto returned = buf[0] + 1;
 | 
					  const auto returned = buf[0] + 1;
 | 
				
			||||||
  if (returned < 2) {
 | 
					  if (returned < 2) {
 | 
				
			||||||
    ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned);
 | 
					    ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned);
 | 
				
			||||||
    return nullptr;
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  stm->pid = (buf[1] << 8) | buf[2];
 | 
					  stm->pid = (buf[1] << 8) | buf[2];
 | 
				
			||||||
  if (returned > 2) {
 | 
					  if (returned > 2) {
 | 
				
			||||||
@@ -631,8 +639,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
 | 
				
			|||||||
    for (auto i = 2; i <= returned; i++)
 | 
					    for (auto i = 2; i <= returned; i++)
 | 
				
			||||||
      ESP_LOGD(TAG, " %02x", buf[i]);
 | 
					      ESP_LOGD(TAG, " %02x", buf[i]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (stm32_get_ack(stm.get()) != STM32_ERR_OK) {
 | 
					  if (stm32_get_ack(stm) != STM32_ERR_OK) {
 | 
				
			||||||
    return nullptr;
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  stm->dev = DEVICES;
 | 
					  stm->dev = DEVICES;
 | 
				
			||||||
@@ -641,21 +649,14 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  if (!stm->dev->id) {
 | 
					  if (!stm->dev->id) {
 | 
				
			||||||
    ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid);
 | 
					    ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid);
 | 
				
			||||||
    return nullptr;
 | 
					    return make_stm32_with_deletor(nullptr);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // TODO: Would be much better if the unique_ptr was returned from this function
 | 
					  return stm;
 | 
				
			||||||
  // Release ownership of unique_ptr
 | 
					 | 
				
			||||||
  return stm.release();  // NOLINT
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void stm32_close(stm32_t *stm) {
 | 
					stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, const uint32_t address, uint8_t *data,
 | 
				
			||||||
  if (stm)
 | 
					                              const unsigned int len) {
 | 
				
			||||||
    free(stm->cmd);  // NOLINT
 | 
					 | 
				
			||||||
  free(stm);         // NOLINT
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) {
 | 
					 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!len)
 | 
					  if (!len)
 | 
				
			||||||
@@ -693,7 +694,8 @@ stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_
 | 
				
			|||||||
  return STM32_ERR_OK;
 | 
					  return STM32_ERR_OK;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) {
 | 
					stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data,
 | 
				
			||||||
 | 
					                               const unsigned int len) {
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!len)
 | 
					  if (!len)
 | 
				
			||||||
@@ -753,7 +755,7 @@ stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8
 | 
				
			|||||||
  return STM32_ERR_OK;
 | 
					  return STM32_ERR_OK;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_wunprot_memory(const stm32_t *stm) {
 | 
					stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm) {
 | 
				
			||||||
  if (stm->cmd->uw == STM32_CMD_ERR) {
 | 
					  if (stm->cmd->uw == STM32_CMD_ERR) {
 | 
				
			||||||
    ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader.");
 | 
					    ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader.");
 | 
				
			||||||
    return STM32_ERR_NO_CMD;
 | 
					    return STM32_ERR_NO_CMD;
 | 
				
			||||||
@@ -766,7 +768,7 @@ stm32_err_t stm32_wunprot_memory(const stm32_t *stm) {
 | 
				
			|||||||
                                 []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); });
 | 
					                                 []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_wprot_memory(const stm32_t *stm) {
 | 
					stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm) {
 | 
				
			||||||
  if (stm->cmd->wp == STM32_CMD_ERR) {
 | 
					  if (stm->cmd->wp == STM32_CMD_ERR) {
 | 
				
			||||||
    ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader.");
 | 
					    ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader.");
 | 
				
			||||||
    return STM32_ERR_NO_CMD;
 | 
					    return STM32_ERR_NO_CMD;
 | 
				
			||||||
@@ -779,7 +781,7 @@ stm32_err_t stm32_wprot_memory(const stm32_t *stm) {
 | 
				
			|||||||
                                 []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); });
 | 
					                                 []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_runprot_memory(const stm32_t *stm) {
 | 
					stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm) {
 | 
				
			||||||
  if (stm->cmd->ur == STM32_CMD_ERR) {
 | 
					  if (stm->cmd->ur == STM32_CMD_ERR) {
 | 
				
			||||||
    ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader.");
 | 
					    ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader.");
 | 
				
			||||||
    return STM32_ERR_NO_CMD;
 | 
					    return STM32_ERR_NO_CMD;
 | 
				
			||||||
@@ -792,7 +794,7 @@ stm32_err_t stm32_runprot_memory(const stm32_t *stm) {
 | 
				
			|||||||
                                 []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); });
 | 
					                                 []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_readprot_memory(const stm32_t *stm) {
 | 
					stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm) {
 | 
				
			||||||
  if (stm->cmd->rp == STM32_CMD_ERR) {
 | 
					  if (stm->cmd->rp == STM32_CMD_ERR) {
 | 
				
			||||||
    ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader.");
 | 
					    ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader.");
 | 
				
			||||||
    return STM32_ERR_NO_CMD;
 | 
					    return STM32_ERR_NO_CMD;
 | 
				
			||||||
@@ -805,7 +807,7 @@ stm32_err_t stm32_readprot_memory(const stm32_t *stm) {
 | 
				
			|||||||
                                 []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); });
 | 
					                                 []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) {
 | 
					stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages) {
 | 
				
			||||||
  if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES)))
 | 
					  if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES)))
 | 
				
			||||||
    return STM32_ERR_OK;
 | 
					    return STM32_ERR_OK;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -847,7 +849,7 @@ stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t page
 | 
				
			|||||||
  return STM32_ERR_OK;
 | 
					  return STM32_ERR_OK;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code,
 | 
					static stm32_err_t stm32_run_raw_code(const stm32_unique_ptr &stm, uint32_t target_address, const uint8_t *code,
 | 
				
			||||||
                                      uint32_t code_size) {
 | 
					                                      uint32_t code_size) {
 | 
				
			||||||
  static constexpr uint32_t BUFFER_SIZE = 256;
 | 
					  static constexpr uint32_t BUFFER_SIZE = 256;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -893,7 +895,7 @@ static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_addres
 | 
				
			|||||||
  return stm32_go(stm, target_address);
 | 
					  return stm32_go(stm, target_address);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) {
 | 
					stm32_err_t stm32_go(const stm32_unique_ptr &stm, const uint32_t address) {
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (stm->cmd->go == STM32_CMD_ERR) {
 | 
					  if (stm->cmd->go == STM32_CMD_ERR) {
 | 
				
			||||||
@@ -916,7 +918,7 @@ stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) {
 | 
				
			|||||||
  return STM32_ERR_OK;
 | 
					  return STM32_ERR_OK;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_reset_device(const stm32_t *stm) {
 | 
					stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm) {
 | 
				
			||||||
  const auto target_address = stm->dev->ram_start;
 | 
					  const auto target_address = stm->dev->ram_start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (stm->dev->flags & F_OBLL) {
 | 
					  if (stm->dev->flags & F_OBLL) {
 | 
				
			||||||
@@ -927,7 +929,8 @@ stm32_err_t stm32_reset_device(const stm32_t *stm) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) {
 | 
					stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, const uint32_t address, const uint32_t length,
 | 
				
			||||||
 | 
					                             uint32_t *const crc) {
 | 
				
			||||||
  static constexpr auto BUFFER_SIZE = 5;
 | 
					  static constexpr auto BUFFER_SIZE = 5;
 | 
				
			||||||
  auto *const stream = stm->stream;
 | 
					  auto *const stream = stm->stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1022,7 +1025,7 @@ uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) {
 | 
				
			|||||||
  return crc;
 | 
					  return crc;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) {
 | 
					stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc) {
 | 
				
			||||||
  static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF;
 | 
					  static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF;
 | 
				
			||||||
  static constexpr uint32_t BUFFER_SIZE = 256;
 | 
					  static constexpr uint32_t BUFFER_SIZE = 256;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,7 @@
 | 
				
			|||||||
#ifdef USE_SHD_FIRMWARE_DATA
 | 
					#ifdef USE_SHD_FIRMWARE_DATA
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <cstdint>
 | 
					#include <cstdint>
 | 
				
			||||||
 | 
					#include <memory>
 | 
				
			||||||
#include "esphome/components/uart/uart.h"
 | 
					#include "esphome/components/uart/uart.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace esphome {
 | 
					namespace esphome {
 | 
				
			||||||
@@ -108,19 +109,20 @@ struct VarlenCmd {
 | 
				
			|||||||
  uint8_t length;
 | 
					  uint8_t length;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init);
 | 
					using stm32_unique_ptr = std::unique_ptr<stm32_t, void (*)(stm32_t *)>;
 | 
				
			||||||
void stm32_close(stm32_t *stm);
 | 
					
 | 
				
			||||||
stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len);
 | 
					stm32_unique_ptr stm32_init(uart::UARTDevice *stream, uint8_t flags, char init);
 | 
				
			||||||
stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len);
 | 
					stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, uint32_t address, uint8_t *data, unsigned int len);
 | 
				
			||||||
stm32_err_t stm32_wunprot_memory(const stm32_t *stm);
 | 
					stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, unsigned int len);
 | 
				
			||||||
stm32_err_t stm32_wprot_memory(const stm32_t *stm);
 | 
					stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm);
 | 
				
			||||||
stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages);
 | 
					stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm);
 | 
				
			||||||
stm32_err_t stm32_go(const stm32_t *stm, uint32_t address);
 | 
					stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages);
 | 
				
			||||||
stm32_err_t stm32_reset_device(const stm32_t *stm);
 | 
					stm32_err_t stm32_go(const stm32_unique_ptr &stm, uint32_t address);
 | 
				
			||||||
stm32_err_t stm32_readprot_memory(const stm32_t *stm);
 | 
					stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm);
 | 
				
			||||||
stm32_err_t stm32_runprot_memory(const stm32_t *stm);
 | 
					stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm);
 | 
				
			||||||
stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc);
 | 
					stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm);
 | 
				
			||||||
stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc);
 | 
					stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc);
 | 
				
			||||||
 | 
					stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc);
 | 
				
			||||||
uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len);
 | 
					uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}  // namespace shelly_dimmer
 | 
					}  // namespace shelly_dimmer
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										38
									
								
								esphome/components/sml/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/sml/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
				
			|||||||
 | 
					import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import esphome.codegen as cg
 | 
				
			||||||
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
 | 
					from esphome.components import uart
 | 
				
			||||||
 | 
					from esphome.const import CONF_ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CODEOWNERS = ["@alengwenus"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEPENDENCIES = ["uart"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sml_ns = cg.esphome_ns.namespace("sml")
 | 
				
			||||||
 | 
					Sml = sml_ns.class_("Sml", cg.Component, uart.UARTDevice)
 | 
				
			||||||
 | 
					MULTI_CONF = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONF_SML_ID = "sml_id"
 | 
				
			||||||
 | 
					CONF_OBIS_CODE = "obis_code"
 | 
				
			||||||
 | 
					CONF_SERVER_ID = "server_id"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONFIG_SCHEMA = cv.Schema(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        cv.GenerateID(): cv.declare_id(Sml),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					).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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def obis_code(value):
 | 
				
			||||||
 | 
					    value = cv.string(value)
 | 
				
			||||||
 | 
					    match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value)
 | 
				
			||||||
 | 
					    if match is None:
 | 
				
			||||||
 | 
					        raise cv.Invalid(f"{value} is not a valid OBIS code")
 | 
				
			||||||
 | 
					    return value
 | 
				
			||||||
							
								
								
									
										48
									
								
								esphome/components/sml/constants.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								esphome/components/sml/constants.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <cstdint>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sml {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum SmlType : uint8_t {
 | 
				
			||||||
 | 
					  SML_OCTET = 0,
 | 
				
			||||||
 | 
					  SML_BOOL = 4,
 | 
				
			||||||
 | 
					  SML_INT = 5,
 | 
				
			||||||
 | 
					  SML_UINT = 6,
 | 
				
			||||||
 | 
					  SML_LIST = 7,
 | 
				
			||||||
 | 
					  SML_HEX = 10,
 | 
				
			||||||
 | 
					  SML_UNDEFINED = 255
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES = 0x701 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum Crc16CheckResult : uint8_t { CHECK_CRC16_FAILED, CHECK_CRC16_X25_SUCCESS, CHECK_CRC16_KERMIT_SUCCESS };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// masks with two-bit mapping 0x1b -> 0b01; 0x01 -> 0b10; 0x1a -> 0b11
 | 
				
			||||||
 | 
					const uint16_t START_MASK = 0x55aa;  // 0x1b 1b 1b 1b 1b 01 01 01 01
 | 
				
			||||||
 | 
					const uint16_t END_MASK = 0x0157;    // 0x1b 1b 1b 1b 1a
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uint16_t CRC16_X25_TABLE[256] = {
 | 
				
			||||||
 | 
					    0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5,
 | 
				
			||||||
 | 
					    0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52,
 | 
				
			||||||
 | 
					    0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3,
 | 
				
			||||||
 | 
					    0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
 | 
				
			||||||
 | 
					    0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9,
 | 
				
			||||||
 | 
					    0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e,
 | 
				
			||||||
 | 
					    0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f,
 | 
				
			||||||
 | 
					    0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
 | 
				
			||||||
 | 
					    0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862,
 | 
				
			||||||
 | 
					    0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb,
 | 
				
			||||||
 | 
					    0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948,
 | 
				
			||||||
 | 
					    0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
 | 
				
			||||||
 | 
					    0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226,
 | 
				
			||||||
 | 
					    0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497,
 | 
				
			||||||
 | 
					    0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704,
 | 
				
			||||||
 | 
					    0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
 | 
				
			||||||
 | 
					    0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb,
 | 
				
			||||||
 | 
					    0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c,
 | 
				
			||||||
 | 
					    0x3de3, 0x2c6a, 0x1ef1, 0x0f78};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace sml
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										30
									
								
								esphome/components/sml/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/sml/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					import esphome.codegen as cg
 | 
				
			||||||
 | 
					import esphome.config_validation as cv
 | 
				
			||||||
 | 
					from esphome.components import sensor
 | 
				
			||||||
 | 
					from esphome.const import CONF_ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					AUTO_LOAD = ["sml"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SmlSensor = sml_ns.class_("SmlSensor", sensor.Sensor, cg.Component)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CONFIG_SCHEMA = sensor.sensor_schema().extend(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        cv.GenerateID(): cv.declare_id(SmlSensor),
 | 
				
			||||||
 | 
					        cv.GenerateID(CONF_SML_ID): cv.use_id(Sml),
 | 
				
			||||||
 | 
					        cv.Required(CONF_OBIS_CODE): obis_code,
 | 
				
			||||||
 | 
					        cv.Optional(CONF_SERVER_ID, default=""): cv.string,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async def to_code(config):
 | 
				
			||||||
 | 
					    var = cg.new_Pvariable(
 | 
				
			||||||
 | 
					        config[CONF_ID], config[CONF_SERVER_ID], config[CONF_OBIS_CODE]
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    await cg.register_component(var, config)
 | 
				
			||||||
 | 
					    await sensor.register_sensor(var, config)
 | 
				
			||||||
 | 
					    sml = await cg.get_variable(config[CONF_SML_ID])
 | 
				
			||||||
 | 
					    cg.add(sml.register_sml_listener(var))
 | 
				
			||||||
							
								
								
									
										41
									
								
								esphome/components/sml/sensor/sml_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/sml/sensor/sml_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					#include "sml_sensor.h"
 | 
				
			||||||
 | 
					#include "../sml_parser.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sml {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *const TAG = "sml_sensor";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SmlSensor::SmlSensor(std::string server_id, std::string obis_code)
 | 
				
			||||||
 | 
					    : SmlListener(std::move(server_id), std::move(obis_code)) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SmlSensor::publish_val(const ObisInfo &obis_info) {
 | 
				
			||||||
 | 
					  switch (obis_info.value_type) {
 | 
				
			||||||
 | 
					    case SML_INT: {
 | 
				
			||||||
 | 
					      publish_state(bytes_to_int(obis_info.value));
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case SML_BOOL:
 | 
				
			||||||
 | 
					    case SML_UINT: {
 | 
				
			||||||
 | 
					      publish_state(bytes_to_uint(obis_info.value));
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    case SML_OCTET: {
 | 
				
			||||||
 | 
					      ESP_LOGW(TAG, "No number conversion for (%s) %s. Consider using SML TextSensor instead.",
 | 
				
			||||||
 | 
					               bytes_repr(obis_info.server_id).c_str(), obis_info.code_repr().c_str());
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void SmlSensor::dump_config() {
 | 
				
			||||||
 | 
					  LOG_SENSOR("", "SML", this);
 | 
				
			||||||
 | 
					  if (!this->server_id.empty()) {
 | 
				
			||||||
 | 
					    ESP_LOGCONFIG(TAG, "  Server ID: %s", this->server_id.c_str());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  ESP_LOGCONFIG(TAG, "  OBIS Code: %s", this->obis_code.c_str());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace sml
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										16
									
								
								esphome/components/sml/sensor/sml_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								esphome/components/sml/sensor/sml_sensor.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					#include "esphome/components/sml/sml.h"
 | 
				
			||||||
 | 
					#include "esphome/components/sensor/sensor.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sml {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SmlSensor : public SmlListener, public sensor::Sensor, public Component {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  SmlSensor(std::string server_id, std::string obis_code);
 | 
				
			||||||
 | 
					  void publish_val(const ObisInfo &obis_info) override;
 | 
				
			||||||
 | 
					  void dump_config() override;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace sml
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										146
									
								
								esphome/components/sml/sml.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								esphome/components/sml/sml.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,146 @@
 | 
				
			|||||||
 | 
					#include "sml.h"
 | 
				
			||||||
 | 
					#include "esphome/core/log.h"
 | 
				
			||||||
 | 
					#include "sml_parser.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sml {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *const TAG = "sml";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const char START_BYTES_DETECTED = 1;
 | 
				
			||||||
 | 
					const char END_BYTES_DETECTED = 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SmlListener::SmlListener(std::string server_id, std::string obis_code)
 | 
				
			||||||
 | 
					    : server_id(std::move(server_id)), obis_code(std::move(obis_code)) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					char Sml::check_start_end_bytes_(uint8_t byte) {
 | 
				
			||||||
 | 
					  this->incoming_mask_ = (this->incoming_mask_ << 2) | get_code(byte);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (this->incoming_mask_ == START_MASK)
 | 
				
			||||||
 | 
					    return START_BYTES_DETECTED;
 | 
				
			||||||
 | 
					  if ((this->incoming_mask_ >> 6) == END_MASK)
 | 
				
			||||||
 | 
					    return END_BYTES_DETECTED;
 | 
				
			||||||
 | 
					  return 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void Sml::loop() {
 | 
				
			||||||
 | 
					  while (available()) {
 | 
				
			||||||
 | 
					    const char c = read();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this->record_)
 | 
				
			||||||
 | 
					      this->sml_data_.emplace_back(c);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    switch (this->check_start_end_bytes_(c)) {
 | 
				
			||||||
 | 
					      case START_BYTES_DETECTED: {
 | 
				
			||||||
 | 
					        this->record_ = true;
 | 
				
			||||||
 | 
					        this->sml_data_.clear();
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      case END_BYTES_DETECTED: {
 | 
				
			||||||
 | 
					        if (this->record_) {
 | 
				
			||||||
 | 
					          this->record_ = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!check_sml_data(this->sml_data_))
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // remove footer bytes
 | 
				
			||||||
 | 
					          this->sml_data_.resize(this->sml_data_.size() - 8);
 | 
				
			||||||
 | 
					          this->process_sml_file_(this->sml_data_);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void Sml::process_sml_file_(const bytes &sml_data) {
 | 
				
			||||||
 | 
					  SmlFile sml_file = SmlFile(sml_data);
 | 
				
			||||||
 | 
					  std::vector<ObisInfo> obis_info = sml_file.get_obis_info();
 | 
				
			||||||
 | 
					  this->publish_obis_info_(obis_info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  this->log_obis_info_(obis_info);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void Sml::log_obis_info_(const std::vector<ObisInfo> &obis_info_vec) {
 | 
				
			||||||
 | 
					  ESP_LOGD(TAG, "OBIS info:");
 | 
				
			||||||
 | 
					  for (auto const &obis_info : obis_info_vec) {
 | 
				
			||||||
 | 
					    std::string info;
 | 
				
			||||||
 | 
					    info += "  (" + bytes_repr(obis_info.server_id) + ") ";
 | 
				
			||||||
 | 
					    info += obis_info.code_repr();
 | 
				
			||||||
 | 
					    info += " [0x" + bytes_repr(obis_info.value) + "]";
 | 
				
			||||||
 | 
					    ESP_LOGD(TAG, "%s", info.c_str());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void Sml::publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec) {
 | 
				
			||||||
 | 
					  for (auto const &obis_info : obis_info_vec) {
 | 
				
			||||||
 | 
					    this->publish_value_(obis_info);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void Sml::publish_value_(const ObisInfo &obis_info) {
 | 
				
			||||||
 | 
					  for (auto const &sml_listener : sml_listeners_) {
 | 
				
			||||||
 | 
					    if ((!sml_listener->server_id.empty()) && (bytes_repr(obis_info.server_id) != sml_listener->server_id))
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    if (obis_info.code_repr() != sml_listener->obis_code)
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    sml_listener->publish_val(obis_info);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void Sml::dump_config() { ESP_LOGCONFIG(TAG, "SML:"); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void Sml::register_sml_listener(SmlListener *listener) { sml_listeners_.emplace_back(listener); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool check_sml_data(const bytes &buffer) {
 | 
				
			||||||
 | 
					  if (buffer.size() < 2) {
 | 
				
			||||||
 | 
					    ESP_LOGW(TAG, "Checksum error in received SML data.");
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  uint16_t crc_received = (buffer.at(buffer.size() - 2) << 8) | buffer.at(buffer.size() - 1);
 | 
				
			||||||
 | 
					  if (crc_received == calc_crc16_x25(buffer.begin(), buffer.end() - 2, 0x6e23)) {
 | 
				
			||||||
 | 
					    ESP_LOGV(TAG, "Checksum verification successful with CRC16/X25.");
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (crc_received == calc_crc16_kermit(buffer.begin(), buffer.end() - 2, 0xed50)) {
 | 
				
			||||||
 | 
					    ESP_LOGV(TAG, "Checksum verification successful with CRC16/KERMIT.");
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ESP_LOGW(TAG, "Checksum error in received SML data.");
 | 
				
			||||||
 | 
					  return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum) {
 | 
				
			||||||
 | 
					  for (auto it = begin; it != end; it++) {
 | 
				
			||||||
 | 
					    crcsum = (crcsum >> 8) ^ CRC16_X25_TABLE[(crcsum & 0xff) ^ *it];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return crcsum;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) {
 | 
				
			||||||
 | 
					  crcsum = calc_crc16_p1021(begin, end, crcsum ^ 0xffff) ^ 0xffff;
 | 
				
			||||||
 | 
					  return (crcsum >> 8) | ((crcsum & 0xff) << 8);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) {
 | 
				
			||||||
 | 
					  return calc_crc16_p1021(begin, end, crcsum);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint8_t get_code(uint8_t byte) {
 | 
				
			||||||
 | 
					  switch (byte) {
 | 
				
			||||||
 | 
					    case 0x1b:
 | 
				
			||||||
 | 
					      return 1;
 | 
				
			||||||
 | 
					    case 0x01:
 | 
				
			||||||
 | 
					      return 2;
 | 
				
			||||||
 | 
					    case 0x1a:
 | 
				
			||||||
 | 
					      return 3;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace sml
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										47
									
								
								esphome/components/sml/sml.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/sml/sml.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <string>
 | 
				
			||||||
 | 
					#include <vector>
 | 
				
			||||||
 | 
					#include "esphome/core/component.h"
 | 
				
			||||||
 | 
					#include "esphome/components/uart/uart.h"
 | 
				
			||||||
 | 
					#include "sml_parser.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sml {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SmlListener {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  std::string server_id;
 | 
				
			||||||
 | 
					  std::string obis_code;
 | 
				
			||||||
 | 
					  SmlListener(std::string server_id, std::string obis_code);
 | 
				
			||||||
 | 
					  virtual void publish_val(const ObisInfo &obis_info){};
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Sml : public Component, public uart::UARTDevice {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  void register_sml_listener(SmlListener *listener);
 | 
				
			||||||
 | 
					  void loop() override;
 | 
				
			||||||
 | 
					  void dump_config() override;
 | 
				
			||||||
 | 
					  std::vector<SmlListener *> sml_listeners_{};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  void process_sml_file_(const bytes &sml_data);
 | 
				
			||||||
 | 
					  void log_obis_info_(const std::vector<ObisInfo> &obis_info_vec);
 | 
				
			||||||
 | 
					  void publish_obis_info_(const std::vector<ObisInfo> &obis_info_vec);
 | 
				
			||||||
 | 
					  char check_start_end_bytes_(uint8_t byte);
 | 
				
			||||||
 | 
					  void publish_value_(const ObisInfo &obis_info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Serial parser
 | 
				
			||||||
 | 
					  bool record_ = false;
 | 
				
			||||||
 | 
					  uint16_t incoming_mask_ = 0;
 | 
				
			||||||
 | 
					  bytes sml_data_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool check_sml_data(const bytes &buffer);
 | 
				
			||||||
 | 
					uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum);
 | 
				
			||||||
 | 
					uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum);
 | 
				
			||||||
 | 
					uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint8_t get_code(uint8_t byte);
 | 
				
			||||||
 | 
					}  // namespace sml
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										131
									
								
								esphome/components/sml/sml_parser.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								esphome/components/sml/sml_parser.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					#include "esphome/core/helpers.h"
 | 
				
			||||||
 | 
					#include "constants.h"
 | 
				
			||||||
 | 
					#include "sml_parser.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sml {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) {
 | 
				
			||||||
 | 
					  // extract messages
 | 
				
			||||||
 | 
					  this->pos_ = 0;
 | 
				
			||||||
 | 
					  while (this->pos_ < this->buffer_.size()) {
 | 
				
			||||||
 | 
					    if (this->buffer_[this->pos_] == 0x00)
 | 
				
			||||||
 | 
					      break;  // fill byte detected -> no more messages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SmlNode message = SmlNode();
 | 
				
			||||||
 | 
					    if (!this->setup_node(&message))
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    this->messages.emplace_back(message);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bool SmlFile::setup_node(SmlNode *node) {
 | 
				
			||||||
 | 
					  uint8_t type = this->buffer_[this->pos_] >> 4;      // type including overlength info
 | 
				
			||||||
 | 
					  uint8_t length = this->buffer_[this->pos_] & 0x0f;  // length including TL bytes
 | 
				
			||||||
 | 
					  bool is_list = (type & 0x07) == SML_LIST;
 | 
				
			||||||
 | 
					  bool has_extended_length = type & 0x08;  // we have a long list/value (>15 entries)
 | 
				
			||||||
 | 
					  uint8_t parse_length = length;
 | 
				
			||||||
 | 
					  if (has_extended_length) {
 | 
				
			||||||
 | 
					    length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f);
 | 
				
			||||||
 | 
					    parse_length = length - 1;
 | 
				
			||||||
 | 
					    this->pos_ += 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (this->pos_ + parse_length >= this->buffer_.size())
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  node->type = type & 0x07;
 | 
				
			||||||
 | 
					  node->nodes.clear();
 | 
				
			||||||
 | 
					  node->value_bytes.clear();
 | 
				
			||||||
 | 
					  if (this->buffer_[this->pos_] == 0x00) {  // end of message
 | 
				
			||||||
 | 
					    this->pos_ += 1;
 | 
				
			||||||
 | 
					  } else if (is_list) {  // list
 | 
				
			||||||
 | 
					    this->pos_ += 1;
 | 
				
			||||||
 | 
					    node->nodes.reserve(parse_length);
 | 
				
			||||||
 | 
					    for (size_t i = 0; i != parse_length; i++) {
 | 
				
			||||||
 | 
					      SmlNode child_node = SmlNode();
 | 
				
			||||||
 | 
					      if (!this->setup_node(&child_node))
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      node->nodes.emplace_back(child_node);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {  // value
 | 
				
			||||||
 | 
					    node->value_bytes =
 | 
				
			||||||
 | 
					        bytes(this->buffer_.begin() + this->pos_ + 1, this->buffer_.begin() + this->pos_ + parse_length);
 | 
				
			||||||
 | 
					    this->pos_ += parse_length;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::vector<ObisInfo> SmlFile::get_obis_info() {
 | 
				
			||||||
 | 
					  std::vector<ObisInfo> obis_info;
 | 
				
			||||||
 | 
					  for (auto const &message : messages) {
 | 
				
			||||||
 | 
					    SmlNode message_body = message.nodes[3];
 | 
				
			||||||
 | 
					    uint16_t message_type = bytes_to_uint(message_body.nodes[0].value_bytes);
 | 
				
			||||||
 | 
					    if (message_type != SML_GET_LIST_RES)
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SmlNode get_list_response = message_body.nodes[1];
 | 
				
			||||||
 | 
					    bytes server_id = get_list_response.nodes[1].value_bytes;
 | 
				
			||||||
 | 
					    SmlNode val_list = get_list_response.nodes[4];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (auto const &val_list_entry : val_list.nodes) {
 | 
				
			||||||
 | 
					      obis_info.emplace_back(server_id, val_list_entry);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return obis_info;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::string bytes_repr(const bytes &buffer) {
 | 
				
			||||||
 | 
					  std::string repr;
 | 
				
			||||||
 | 
					  for (auto const value : buffer) {
 | 
				
			||||||
 | 
					    repr += str_sprintf("%02x", value & 0xff);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return repr;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint64_t bytes_to_uint(const bytes &buffer) {
 | 
				
			||||||
 | 
					  uint64_t val = 0;
 | 
				
			||||||
 | 
					  for (auto const value : buffer) {
 | 
				
			||||||
 | 
					    val = (val << 8) + value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return val;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int64_t bytes_to_int(const bytes &buffer) {
 | 
				
			||||||
 | 
					  uint64_t tmp = bytes_to_uint(buffer);
 | 
				
			||||||
 | 
					  int64_t val;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  switch (buffer.size()) {
 | 
				
			||||||
 | 
					    case 1:  // int8
 | 
				
			||||||
 | 
					      val = (int8_t) tmp;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 2:  // int16
 | 
				
			||||||
 | 
					      val = (int16_t) tmp;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    case 4:  // int32
 | 
				
			||||||
 | 
					      val = (int32_t) tmp;
 | 
				
			||||||
 | 
					      break;
 | 
				
			||||||
 | 
					    default:  // int64
 | 
				
			||||||
 | 
					      val = (int64_t) tmp;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return val;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::string bytes_to_string(const bytes &buffer) { return std::string(buffer.begin(), buffer.end()); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ObisInfo::ObisInfo(bytes server_id, SmlNode val_list_entry) : server_id(std::move(server_id)) {
 | 
				
			||||||
 | 
					  this->code = val_list_entry.nodes[0].value_bytes;
 | 
				
			||||||
 | 
					  this->status = val_list_entry.nodes[1].value_bytes;
 | 
				
			||||||
 | 
					  this->unit = bytes_to_uint(val_list_entry.nodes[3].value_bytes);
 | 
				
			||||||
 | 
					  this->scaler = bytes_to_int(val_list_entry.nodes[4].value_bytes);
 | 
				
			||||||
 | 
					  SmlNode value_node = val_list_entry.nodes[5];
 | 
				
			||||||
 | 
					  this->value = value_node.value_bytes;
 | 
				
			||||||
 | 
					  this->value_type = value_node.type;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::string ObisInfo::code_repr() const {
 | 
				
			||||||
 | 
					  return str_sprintf("%d-%d:%d.%d.%d", this->code[0], this->code[1], this->code[2], this->code[3], this->code[4]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}  // namespace sml
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
							
								
								
									
										54
									
								
								esphome/components/sml/sml_parser.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								esphome/components/sml/sml_parser.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <cstdint>
 | 
				
			||||||
 | 
					#include <cstdio>
 | 
				
			||||||
 | 
					#include <string>
 | 
				
			||||||
 | 
					#include <vector>
 | 
				
			||||||
 | 
					#include "constants.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace esphome {
 | 
				
			||||||
 | 
					namespace sml {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using bytes = std::vector<uint8_t>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SmlNode {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  uint8_t type;
 | 
				
			||||||
 | 
					  bytes value_bytes;
 | 
				
			||||||
 | 
					  std::vector<SmlNode> nodes;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ObisInfo {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  ObisInfo(bytes server_id, SmlNode val_list_entry);
 | 
				
			||||||
 | 
					  bytes server_id;
 | 
				
			||||||
 | 
					  bytes code;
 | 
				
			||||||
 | 
					  bytes status;
 | 
				
			||||||
 | 
					  char unit;
 | 
				
			||||||
 | 
					  char scaler;
 | 
				
			||||||
 | 
					  bytes value;
 | 
				
			||||||
 | 
					  uint16_t value_type;
 | 
				
			||||||
 | 
					  std::string code_repr() const;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SmlFile {
 | 
				
			||||||
 | 
					 public:
 | 
				
			||||||
 | 
					  SmlFile(bytes buffer);
 | 
				
			||||||
 | 
					  bool setup_node(SmlNode *node);
 | 
				
			||||||
 | 
					  std::vector<SmlNode> messages;
 | 
				
			||||||
 | 
					  std::vector<ObisInfo> get_obis_info();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 protected:
 | 
				
			||||||
 | 
					  const bytes buffer_;
 | 
				
			||||||
 | 
					  size_t pos_;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::string bytes_repr(const bytes &buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					uint64_t bytes_to_uint(const bytes &buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int64_t bytes_to_int(const bytes &buffer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					std::string bytes_to_string(const bytes &buffer);
 | 
				
			||||||
 | 
					}  // namespace sml
 | 
				
			||||||
 | 
					}  // namespace esphome
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user