mirror of
https://github.com/esphome/esphome.git
synced 2025-11-03 16:41:50 +00:00
Compare commits
86 Commits
2022.4.0b4
...
jesserockz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9268ef7665 | ||
|
|
cd35ead890 | ||
|
|
9dc804ee27 | ||
|
|
a8ceeaa7b0 | ||
|
|
7092f7663e | ||
|
|
d9d2edeb08 | ||
|
|
dda1ddcb26 | ||
|
|
f0c890f160 | ||
|
|
4f52d43347 | ||
|
|
0ed7db979b | ||
|
|
9c78049359 | ||
|
|
7882105661 | ||
|
|
c000e1d6dd | ||
|
|
9b6b9c1fa2 | ||
|
|
609a2ca592 | ||
|
|
6dabf24bf3 | ||
|
|
93e2506279 | ||
|
|
f62d5d3b9d | ||
|
|
0665acd190 | ||
|
|
fea05e9d33 | ||
|
|
7a03c7d56f | ||
|
|
2dc2aec954 | ||
|
|
39c6c2417a | ||
|
|
03d5a0ec1d | ||
|
|
1c873e0034 | ||
|
|
bcb47c306c | ||
|
|
01c4d3c225 | ||
|
|
c2aaae4818 | ||
|
|
3f678e218d | ||
|
|
f8a1bd4e79 | ||
|
|
40ad9f4911 | ||
|
|
4116caff6a | ||
|
|
0b69f72315 | ||
|
|
c569f5ddcf | ||
|
|
62f9e181e0 | ||
|
|
235a97ea10 | ||
|
|
e541ae400c | ||
|
|
4822abde86 | ||
|
|
b7e52812f8 | ||
|
|
69118120d9 | ||
|
|
7cba0c6fb0 | ||
|
|
5fac67ce15 | ||
|
|
98c733108e | ||
|
|
782186e13d | ||
|
|
4e1f6518e8 | ||
|
|
53e0fe8e51 | ||
|
|
0e547390da | ||
|
|
86b52df839 | ||
|
|
d685fdf54a | ||
|
|
d9caab4108 | ||
|
|
44b68f140e | ||
|
|
3a3d97dfa7 | ||
|
|
47898b527c | ||
|
|
a35f36ad39 | ||
|
|
d13a397f8e | ||
|
|
df999723f8 | ||
|
|
8236e840a7 | ||
|
|
e5b3625f73 | ||
|
|
2e4645310b | ||
|
|
50a32b387e | ||
|
|
2059283707 | ||
|
|
8e3af515c9 | ||
|
|
6f88f0ea3f | ||
|
|
d2f37cf3f9 | ||
|
|
7c30d6254e | ||
|
|
64fb39a653 | ||
|
|
91895aa70c | ||
|
|
68dfaf238b | ||
|
|
ebf13a0ba0 | ||
|
|
2bff9937b7 | ||
|
|
256395c28d | ||
|
|
3346bc8bba | ||
|
|
6fe22a7e62 | ||
|
|
757b98748b | ||
|
|
7a778f3f33 | ||
|
|
9576d246ee | ||
|
|
988d3ea8ba | ||
|
|
0767b92b62 | ||
|
|
2064abe16d | ||
|
|
b605982f94 | ||
|
|
93b628d9a8 | ||
|
|
6bac551d9f | ||
|
|
70a35656e4 | ||
|
|
047c18eac0 | ||
|
|
b4a86ce6cf | ||
|
|
b778eed419 |
11
CODEOWNERS
11
CODEOWNERS
@@ -28,8 +28,10 @@ esphome/components/atc_mithermometer/* @ahpohl
|
||||
esphome/components/b_parasite/* @rbaron
|
||||
esphome/components/ballu/* @bazuchan
|
||||
esphome/components/bang_bang/* @OttoWinter
|
||||
esphome/components/bedjet/* @jhansche
|
||||
esphome/components/bh1750/* @OttoWinter
|
||||
esphome/components/binary_sensor/* @esphome/core
|
||||
esphome/components/bl0939/* @ziceva
|
||||
esphome/components/bl0940/* @tobias-
|
||||
esphome/components/ble_client/* @buxtronix
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
@@ -53,11 +55,13 @@ esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/daly_bms/* @s1lvi0
|
||||
esphome/components/dashboard_import/* @esphome/core
|
||||
esphome/components/debug/* @OttoWinter
|
||||
esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dht/* @OttoWinter
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/dsmr/* @glmnet @zuidwijk
|
||||
esphome/components/ektf2232/* @jesserockz
|
||||
esphome/components/ens210/* @itn3rd77
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @jesserockz
|
||||
esphome/components/esp32_ble_server/* @jesserockz
|
||||
@@ -164,23 +168,27 @@ esphome/components/rf_bridge/* @jesserockz
|
||||
esphome/components/rgbct/* @jesserockz
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/safe_mode/* @jsuanet @paulmonigatti
|
||||
esphome/components/scd4x/* @sjtrny
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
esphome/components/script/* @esphome/core
|
||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
||||
esphome/components/sdp3x/* @Azimath
|
||||
esphome/components/selec_meter/* @sourabhjaiswal
|
||||
esphome/components/select/* @esphome/core
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/sgp40/* @SenexCrenshaw
|
||||
esphome/components/sgp4x/* @SenexCrenshaw @martgras
|
||||
esphome/components/shelly_dimmer/* @edge90 @rnauber
|
||||
esphome/components/sht4x/* @sjtrny
|
||||
esphome/components/shutdown/* @esphome/core @jsuanet
|
||||
esphome/components/sim800l/* @glmnet
|
||||
esphome/components/sm2135/* @BoukeHaarsma23
|
||||
esphome/components/sml/* @alengwenus
|
||||
esphome/components/socket/* @esphome/core
|
||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/spi/* @esphome/core
|
||||
esphome/components/sps30/* @martgras
|
||||
esphome/components/ssd1322_base/* @kbx81
|
||||
esphome/components/ssd1322_spi/* @kbx81
|
||||
esphome/components/ssd1325_base/* @kbx81
|
||||
@@ -215,6 +223,7 @@ esphome/components/tsl2591/* @wjcarpenter
|
||||
esphome/components/tuya/binary_sensor/* @jesserockz
|
||||
esphome/components/tuya/climate/* @jesserockz
|
||||
esphome/components/tuya/number/* @frankiboy1
|
||||
esphome/components/tuya/select/* @bearpawmaxim
|
||||
esphome/components/tuya/sensor/* @jesserockz
|
||||
esphome/components/tuya/switch/* @jesserockz
|
||||
esphome/components/tuya/text_sensor/* @dentra
|
||||
|
||||
@@ -30,6 +30,7 @@ RUN \
|
||||
iputils-ping=3:20210202-1 \
|
||||
git=1:2.30.2-1 \
|
||||
curl=7.74.0-1.3+deb11u1 \
|
||||
openssh-client=1:8.4p1-5 \
|
||||
&& rm -rf \
|
||||
/tmp/* \
|
||||
/var/{cache,log}/* \
|
||||
|
||||
@@ -2,6 +2,7 @@ import argparse
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
@@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util
|
||||
import esphome.codegen as cg
|
||||
from esphome.config import iter_components, read_config, strip_default_ids
|
||||
from esphome.const import (
|
||||
ALLOWED_NAME_CHARS,
|
||||
CONF_BAUD_RATE,
|
||||
CONF_BROKER,
|
||||
CONF_DEASSERT_RTS_DTR,
|
||||
CONF_LOGGER,
|
||||
CONF_NAME,
|
||||
CONF_OTA,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_ESPHOME,
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
CONF_SUBSTITUTIONS,
|
||||
SECRETS_FILES,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
@@ -481,6 +485,98 @@ def command_idedata(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_rename(args, config):
|
||||
for c in args.name:
|
||||
if c not in ALLOWED_NAME_CHARS:
|
||||
print(
|
||||
color(
|
||||
Fore.BOLD_RED,
|
||||
f"'{c}' is an invalid character for names. Valid characters are: "
|
||||
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
# Load existing yaml file
|
||||
with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file:
|
||||
raw_contents = raw_file.read()
|
||||
|
||||
yaml = yaml_util.load_yaml(CORE.config_path)
|
||||
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
|
||||
print(
|
||||
color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
|
||||
)
|
||||
return 1
|
||||
old_name = yaml[CONF_ESPHOME][CONF_NAME]
|
||||
match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name)
|
||||
if match is None:
|
||||
new_raw = re.sub(
|
||||
rf"name:\s+[\"']?{old_name}[\"']?",
|
||||
f'name: "{args.name}"',
|
||||
raw_contents,
|
||||
)
|
||||
else:
|
||||
old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)]
|
||||
if (
|
||||
len(
|
||||
re.findall(
|
||||
rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?",
|
||||
raw_contents,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
)
|
||||
> 1
|
||||
):
|
||||
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
|
||||
return 1
|
||||
|
||||
new_raw = re.sub(
|
||||
rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?",
|
||||
f'\\1: "{args.name}"',
|
||||
raw_contents,
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
|
||||
new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
|
||||
print(
|
||||
f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
|
||||
)
|
||||
print()
|
||||
|
||||
with open(new_path, mode="w", encoding="utf-8") as new_file:
|
||||
new_file.write(new_raw)
|
||||
|
||||
rc = run_external_process("esphome", "config", new_path)
|
||||
if rc != 0:
|
||||
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
|
||||
os.remove(new_path)
|
||||
return 1
|
||||
|
||||
cli_args = [
|
||||
"run",
|
||||
new_path,
|
||||
"--no-logs",
|
||||
"--device",
|
||||
CORE.address,
|
||||
]
|
||||
|
||||
if args.dashboard:
|
||||
cli_args.insert(0, "--dashboard")
|
||||
|
||||
try:
|
||||
rc = run_external_process("esphome", *cli_args)
|
||||
except KeyboardInterrupt:
|
||||
rc = 1
|
||||
if rc != 0:
|
||||
os.remove(new_path)
|
||||
return 1
|
||||
|
||||
os.remove(CORE.config_path)
|
||||
|
||||
print(color(Fore.BOLD_GREEN, "SUCCESS"))
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
PRE_CONFIG_ACTIONS = {
|
||||
"wizard": command_wizard,
|
||||
"version": command_version,
|
||||
@@ -499,6 +595,7 @@ POST_CONFIG_ACTIONS = {
|
||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||
"clean": command_clean,
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
}
|
||||
|
||||
|
||||
@@ -681,6 +778,15 @@ def parse_args(argv):
|
||||
"configuration", help="Your YAML configuration file(s).", nargs=1
|
||||
)
|
||||
|
||||
parser_rename = subparsers.add_parser(
|
||||
"rename",
|
||||
help="Rename a device in YAML, compile the binary and upload it.",
|
||||
)
|
||||
parser_rename.add_argument(
|
||||
"configuration", help="Your YAML configuration file.", nargs=1
|
||||
)
|
||||
parser_rename.add_argument("name", help="The new name for the device.", type=str)
|
||||
|
||||
# Keep backward compatibility with the old command line format of
|
||||
# esphome <config> <command>.
|
||||
#
|
||||
|
||||
@@ -64,6 +64,7 @@ from esphome.cpp_types import ( # noqa
|
||||
uint64,
|
||||
int32,
|
||||
int64,
|
||||
size_t,
|
||||
const_char_ptr,
|
||||
NAN,
|
||||
esphome_ns,
|
||||
|
||||
@@ -121,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
|
||||
// calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
|
||||
// also take into account min_power
|
||||
auto min_us = this->cycle_time_us * this->min_power / 1000;
|
||||
this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
|
||||
// calculate required value to provide a true RMS voltage output
|
||||
this->enable_time_us =
|
||||
std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) *
|
||||
(this->cycle_time_us - min_us)) /
|
||||
65535);
|
||||
if (this->method == DIM_METHOD_LEADING_PULSE) {
|
||||
// Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone
|
||||
// this is for brightness near 99%
|
||||
|
||||
@@ -51,8 +51,8 @@ void ADCSensor::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
// adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
// adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_);
|
||||
#endif
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -76,8 +76,6 @@ async def to_code(config):
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
if CONF_RESIZE in config:
|
||||
image.thumbnail(config[CONF_RESIZE])
|
||||
frame = image.convert("RGB")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
@@ -94,6 +92,29 @@ async def to_code(config):
|
||||
data[pos] = pix[2]
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGB565":
|
||||
data = [0 for _ in range(height * width * 2 * frames)]
|
||||
pos = 0
|
||||
for frameIndex in range(frames):
|
||||
image.seek(frameIndex)
|
||||
frame = image.convert("RGB")
|
||||
if CONF_RESIZE in config:
|
||||
frame = frame.resize([width, height])
|
||||
pixels = list(frame.getdata())
|
||||
if len(pixels) != height * width:
|
||||
raise core.EsphomeError(
|
||||
f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
|
||||
)
|
||||
for pix in pixels:
|
||||
R = pix[0] >> 3
|
||||
G = pix[1] >> 2
|
||||
B = pix[2] >> 3
|
||||
rgb = (R << 11) | (G << 5) | B
|
||||
data[pos] = rgb >> 8
|
||||
pos += 1
|
||||
data[pos] = rgb & 255
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "BINARY":
|
||||
width8 = ((width + 7) // 8) * 8
|
||||
data = [0 for _ in range((height * width8 // 8) * frames)]
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
#include "esphome/components/homeassistant/time/homeassistant_time.h"
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
#include "esphome/components/fan/fan_helpers.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
@@ -253,9 +250,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
bool APIConnection::send_fan_state(fan::Fan *fan) {
|
||||
if (!this->state_subscription_)
|
||||
return false;
|
||||
@@ -268,7 +262,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
|
||||
resp.oscillating = fan->oscillating;
|
||||
if (traits.supports_speed()) {
|
||||
resp.speed_level = fan->speed;
|
||||
resp.speed = static_cast<enums::FanSpeed>(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count()));
|
||||
}
|
||||
if (traits.supports_direction())
|
||||
resp.direction = static_cast<enums::FanDirection>(fan->direction);
|
||||
@@ -295,8 +288,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
if (fan == nullptr)
|
||||
return;
|
||||
|
||||
auto traits = fan->get_traits();
|
||||
|
||||
auto call = fan->make_call();
|
||||
if (msg.has_state)
|
||||
call.set_state(msg.state);
|
||||
@@ -305,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
if (msg.has_speed_level) {
|
||||
// Prefer level
|
||||
call.set_speed(msg.speed_level);
|
||||
} else if (msg.has_speed) {
|
||||
call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count()));
|
||||
}
|
||||
if (msg.has_direction)
|
||||
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
|
||||
call.perform();
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
|
||||
@@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void APIServer::on_select_update(select::Select *obj, const std::string &state) {
|
||||
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
|
||||
@@ -64,7 +64,7 @@ class APIServer : public Component, public Controller {
|
||||
void on_number_update(number::Number *obj, float state) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_update(select::Select *obj, const std::string &state) override;
|
||||
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_update(lock::Lock *obj) override;
|
||||
|
||||
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() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up BME280...");
|
||||
uint8_t chip_id = 0;
|
||||
|
||||
// Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
|
||||
// and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component.
|
||||
this->component_state_ &= ~COMPONENT_STATE_FAILED;
|
||||
|
||||
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
|
||||
@@ -169,6 +169,14 @@ void BME680BSECComponent::loop() {
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
// Process a single action from the queue. These are primarily sensor state publishes
|
||||
// that in totality take too long to send in a single call.
|
||||
if (this->queue_.size()) {
|
||||
auto action = std::move(this->queue_.front());
|
||||
this->queue_.pop();
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
void BME680BSECComponent::run_() {
|
||||
@@ -306,37 +314,39 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) {
|
||||
ESP_LOGV(TAG, "Publishing sensor states");
|
||||
ESP_LOGV(TAG, "Queuing sensor state publish actions");
|
||||
for (uint8_t i = 0; i < num_outputs; i++) {
|
||||
float signal = outputs[i].signal;
|
||||
switch (outputs[i].sensor_id) {
|
||||
case BSEC_OUTPUT_IAQ:
|
||||
case BSEC_OUTPUT_STATIC_IAQ:
|
||||
uint8_t accuracy;
|
||||
accuracy = outputs[i].accuracy;
|
||||
this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal);
|
||||
this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
|
||||
this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true);
|
||||
case BSEC_OUTPUT_STATIC_IAQ: {
|
||||
uint8_t accuracy = outputs[i].accuracy;
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
|
||||
this->queue_push_([this, accuracy]() {
|
||||
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
|
||||
});
|
||||
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
|
||||
|
||||
// Queue up an opportunity to save state
|
||||
this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); });
|
||||
break;
|
||||
this->queue_push_([this, accuracy]() { this->save_state_(accuracy); });
|
||||
} break;
|
||||
case BSEC_OUTPUT_CO2_EQUIVALENT:
|
||||
this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
|
||||
break;
|
||||
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
|
||||
this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
|
||||
break;
|
||||
case BSEC_OUTPUT_RAW_PRESSURE:
|
||||
this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
|
||||
break;
|
||||
case BSEC_OUTPUT_RAW_GAS:
|
||||
this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
|
||||
break;
|
||||
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
|
||||
this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
|
||||
break;
|
||||
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
|
||||
this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal);
|
||||
this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() {
|
||||
return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) {
|
||||
void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) {
|
||||
if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) {
|
||||
return;
|
||||
}
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
|
||||
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
|
||||
if (!sensor || (sensor->has_state() && sensor->state == value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
|
||||
void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
|
||||
int64_t get_time_ns_();
|
||||
|
||||
void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
|
||||
void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
|
||||
void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
|
||||
void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
|
||||
|
||||
void load_state_();
|
||||
void save_state_(uint8_t accuracy);
|
||||
|
||||
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
|
||||
|
||||
struct bme680_dev bme680_;
|
||||
bsec_library_return_t bsec_status_{BSEC_OK};
|
||||
int8_t bme680_status_{BME680_OK};
|
||||
@@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
|
||||
uint32_t millis_overflow_counter_{0};
|
||||
int64_t next_call_ns_{0};
|
||||
|
||||
std::queue<std::function<void()>> queue_;
|
||||
|
||||
ESPPreferenceObject bsec_state_;
|
||||
uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day
|
||||
uint32_t last_state_save_ms_ = 0;
|
||||
|
||||
@@ -78,6 +78,7 @@ CANBUS_SCHEMA = cv.Schema(
|
||||
min=0, max=0x1FFFFFFF
|
||||
),
|
||||
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
|
||||
},
|
||||
validate_id,
|
||||
),
|
||||
@@ -100,10 +101,20 @@ async def setup_canbus_core_(var, config):
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
|
||||
)
|
||||
if CONF_REMOTE_TRANSMISSION_REQUEST in conf:
|
||||
cg.add(
|
||||
trigger.set_remote_transmission_request(
|
||||
conf[CONF_REMOTE_TRANSMISSION_REQUEST]
|
||||
)
|
||||
)
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[(cg.std_vector.template(cg.uint8), "x"), (cg.uint32, "can_id")],
|
||||
[
|
||||
(cg.std_vector.template(cg.uint8), "x"),
|
||||
(cg.uint32, "can_id"),
|
||||
(cg.bool_, "remote_transmission_request"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
|
||||
@@ -81,8 +81,10 @@ void Canbus::loop() {
|
||||
// fire all triggers
|
||||
for (auto *trigger : this->triggers_) {
|
||||
if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
|
||||
(trigger->use_extended_id_ == can_message.use_extended_id)) {
|
||||
trigger->trigger(data, can_message.can_id);
|
||||
(trigger->use_extended_id_ == can_message.use_extended_id) &&
|
||||
(!trigger->remote_transmission_request_.has_value() ||
|
||||
trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) {
|
||||
trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,13 +126,18 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
|
||||
std::vector<uint8_t> data_static_{};
|
||||
};
|
||||
|
||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Component {
|
||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
|
||||
friend class Canbus;
|
||||
|
||||
public:
|
||||
explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
|
||||
const bool use_extended_id)
|
||||
: parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){};
|
||||
|
||||
void set_remote_transmission_request(bool remote_transmission_request) {
|
||||
this->remote_transmission_request_ = remote_transmission_request;
|
||||
}
|
||||
|
||||
void setup() override { this->parent_->add_trigger(this); }
|
||||
|
||||
protected:
|
||||
@@ -140,6 +145,7 @@ class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t>, public Com
|
||||
uint32_t can_id_;
|
||||
uint32_t can_id_mask_;
|
||||
bool use_extended_id_;
|
||||
optional<bool> remote_transmission_request_{};
|
||||
};
|
||||
|
||||
} // namespace canbus
|
||||
|
||||
@@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||
if (request->method() == HTTP_GET) {
|
||||
if (request->url() == "/")
|
||||
return true;
|
||||
if (request->url() == "/stylesheet.css")
|
||||
return true;
|
||||
if (request->url() == "/wifi-strength-1.svg")
|
||||
return true;
|
||||
if (request->url() == "/wifi-strength-2.svg")
|
||||
return true;
|
||||
if (request->url() == "/wifi-strength-3.svg")
|
||||
return true;
|
||||
if (request->url() == "/wifi-strength-4.svg")
|
||||
return true;
|
||||
if (request->url() == "/lock.svg")
|
||||
if (request->url() == "/config.json")
|
||||
return true;
|
||||
if (request->url() == "/wifisave")
|
||||
return true;
|
||||
|
||||
@@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
|
||||
validate_climate_fan_mode
|
||||
),
|
||||
cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict,
|
||||
cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable(
|
||||
cv.string_strict
|
||||
),
|
||||
cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset),
|
||||
cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict,
|
||||
cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict),
|
||||
cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode),
|
||||
}
|
||||
)
|
||||
@@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
|
||||
cg.add(var.set_fan_mode(template_))
|
||||
if CONF_CUSTOM_FAN_MODE in config:
|
||||
template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str)
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_CUSTOM_FAN_MODE], args, cg.std_string
|
||||
)
|
||||
cg.add(var.set_custom_fan_mode(template_))
|
||||
if CONF_PRESET in config:
|
||||
template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
|
||||
cg.add(var.set_preset(template_))
|
||||
if CONF_CUSTOM_PRESET in config:
|
||||
template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str)
|
||||
template_ = await cg.templatable(
|
||||
config[CONF_CUSTOM_PRESET], args, cg.std_string
|
||||
)
|
||||
cg.add(var.set_custom_preset(template_))
|
||||
if CONF_SWING_MODE in config:
|
||||
template_ = await cg.templatable(
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace copy {
|
||||
static const char *const TAG = "copy.select";
|
||||
|
||||
void CopySelect::setup() {
|
||||
source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
|
||||
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
|
||||
|
||||
traits.set_options(source_->traits.get_options());
|
||||
|
||||
|
||||
@@ -64,7 +64,10 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N
|
||||
config = {
|
||||
"substitutions": {"name": name},
|
||||
"packages": {project_name: import_url},
|
||||
"esphome": {"name_add_mac_suffix": False},
|
||||
"esphome": {
|
||||
"name": "${name}",
|
||||
"name_add_mac_suffix": False,
|
||||
},
|
||||
}
|
||||
p.write_text(
|
||||
dump(config) + WIFI_CONFIG,
|
||||
|
||||
@@ -93,7 +93,14 @@ deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
|
||||
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
|
||||
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
|
||||
PreventDeepSleepAction = deep_sleep_ns.class_(
|
||||
"PreventDeepSleepAction", automation.Action
|
||||
"PreventDeepSleepAction",
|
||||
automation.Action,
|
||||
cg.Parented.template(DeepSleepComponent),
|
||||
)
|
||||
AllowDeepSleepAction = deep_sleep_ns.class_(
|
||||
"AllowDeepSleepAction",
|
||||
automation.Action,
|
||||
cg.Parented.template(DeepSleepComponent),
|
||||
)
|
||||
|
||||
WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode")
|
||||
@@ -208,28 +215,32 @@ async def to_code(config):
|
||||
cg.add_define("USE_DEEP_SLEEP")
|
||||
|
||||
|
||||
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DeepSleepComponent),
|
||||
cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
|
||||
cv.Exclusive(CONF_UNTIL, "time"): cv.All(cv.only_on_esp32, cv.time_of_day),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
}
|
||||
),
|
||||
cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
|
||||
)
|
||||
|
||||
|
||||
DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id(
|
||||
DEEP_SLEEP_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DeepSleepComponent),
|
||||
}
|
||||
)
|
||||
|
||||
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
|
||||
automation.maybe_simple_id(
|
||||
DEEP_SLEEP_ACTION_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
|
||||
cv.Exclusive(CONF_UNTIL, "time"): cv.All(
|
||||
cv.only_on_esp32, cv.time_of_day
|
||||
),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
}
|
||||
)
|
||||
)
|
||||
),
|
||||
cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA
|
||||
@@ -252,8 +263,16 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA
|
||||
"deep_sleep.prevent",
|
||||
PreventDeepSleepAction,
|
||||
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
|
||||
)
|
||||
async def deep_sleep_prevent_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
@automation.register_action(
|
||||
"deep_sleep.allow",
|
||||
AllowDeepSleepAction,
|
||||
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
|
||||
)
|
||||
async def deep_sleep_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
@@ -21,6 +21,7 @@ optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
|
||||
switch (wakeup_cause) {
|
||||
case ESP_SLEEP_WAKEUP_EXT0:
|
||||
case ESP_SLEEP_WAKEUP_EXT1:
|
||||
case ESP_SLEEP_WAKEUP_GPIO:
|
||||
return this->wakeup_cause_to_run_duration_->gpio_cause;
|
||||
case ESP_SLEEP_WAKEUP_TOUCHPAD:
|
||||
return this->wakeup_cause_to_run_duration_->touch_cause;
|
||||
@@ -72,16 +73,27 @@ float DeepSleepComponent::get_loop_priority() const {
|
||||
return -100.0f; // run after everything else is ready
|
||||
}
|
||||
void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32)
|
||||
void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
|
||||
this->wakeup_pin_mode_ = wakeup_pin_mode;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
|
||||
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
|
||||
|
||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
|
||||
#endif
|
||||
|
||||
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
|
||||
wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
|
||||
void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->prevent_ && !manual) {
|
||||
@@ -107,7 +119,8 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
|
||||
App.run_safe_shutdown_hooks();
|
||||
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
#if defined(USE_ESP32)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
if (this->sleep_duration_.has_value())
|
||||
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
@@ -125,10 +138,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
esp_sleep_enable_touchpad_wakeup();
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
}
|
||||
|
||||
esp_deep_sleep_start();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_VARIANT_ESP32C3
|
||||
if (this->sleep_duration_.has_value())
|
||||
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
|
||||
@@ -137,9 +147,12 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||
level = !level;
|
||||
}
|
||||
esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level);
|
||||
esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()),
|
||||
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
|
||||
}
|
||||
#endif
|
||||
esp_deep_sleep_start();
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance)
|
||||
@@ -147,6 +160,7 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
}
|
||||
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
|
||||
void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
|
||||
void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; }
|
||||
|
||||
} // namespace deep_sleep
|
||||
} // namespace esphome
|
||||
|
||||
@@ -70,17 +70,19 @@ class DeepSleepComponent : public Component {
|
||||
void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
#if defined(USE_ESP32)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
|
||||
void set_ext1_wakeup(Ext1Wakeup ext1_wakeup);
|
||||
|
||||
void set_touch_wakeup(bool touch_wakeup);
|
||||
|
||||
#endif
|
||||
// Set the duration in ms for how long the code should run before entering
|
||||
// deep sleep mode, according to the cause the ESP32 has woken.
|
||||
void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration);
|
||||
|
||||
#endif
|
||||
|
||||
/// Set a duration in ms for how long the code should run before entering deep sleep mode.
|
||||
void set_run_duration(uint32_t time_ms);
|
||||
|
||||
@@ -94,6 +96,7 @@ class DeepSleepComponent : public Component {
|
||||
void begin_sleep(bool manual = false);
|
||||
|
||||
void prevent_deep_sleep();
|
||||
void allow_deep_sleep();
|
||||
|
||||
protected:
|
||||
// Returns nullopt if no run duration is set. Otherwise, returns the run
|
||||
@@ -187,14 +190,14 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
|
||||
#endif
|
||||
};
|
||||
|
||||
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...> {
|
||||
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
|
||||
public:
|
||||
PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {}
|
||||
void play(Ts... x) override { this->parent_->prevent_deep_sleep(); }
|
||||
};
|
||||
|
||||
void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); }
|
||||
|
||||
protected:
|
||||
DeepSleepComponent *deep_sleep_;
|
||||
template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->allow_deep_sleep(); }
|
||||
};
|
||||
|
||||
} // namespace deep_sleep
|
||||
|
||||
1
esphome/components/delonghi/__init__.py
Normal file
1
esphome/components/delonghi/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@grob6000"]
|
||||
20
esphome/components/delonghi/climate.py
Normal file
20
esphome/components/delonghi/climate.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate_ir
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
|
||||
delonghi_ns = cg.esphome_ns.namespace("delonghi")
|
||||
DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DelonghiClimate),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await climate_ir.register_climate_ir(var, config)
|
||||
186
esphome/components/delonghi/delonghi.cpp
Normal file
186
esphome/components/delonghi/delonghi.cpp
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "delonghi.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace delonghi {
|
||||
|
||||
static const char *const TAG = "delonghi.climate";
|
||||
|
||||
void DelonghiClimate::transmit_state() {
|
||||
uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0};
|
||||
remote_state[0] = DELONGHI_ADDRESS;
|
||||
remote_state[1] = this->temperature_();
|
||||
remote_state[1] |= (this->fan_speed_()) << 5;
|
||||
remote_state[2] = this->operation_mode_();
|
||||
// Calculate checksum
|
||||
for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) {
|
||||
remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i];
|
||||
}
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
data->set_carrier_frequency(DELONGHI_IR_FREQUENCY);
|
||||
|
||||
data->mark(DELONGHI_HEADER_MARK);
|
||||
data->space(DELONGHI_HEADER_SPACE);
|
||||
for (unsigned char b : remote_state) {
|
||||
for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
|
||||
data->mark(DELONGHI_BIT_MARK);
|
||||
bool bit = b & mask;
|
||||
data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE);
|
||||
}
|
||||
}
|
||||
data->mark(DELONGHI_BIT_MARK);
|
||||
data->space(0);
|
||||
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
uint8_t DelonghiClimate::operation_mode_() {
|
||||
uint8_t operating_mode = DELONGHI_MODE_ON;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
operating_mode |= DELONGHI_MODE_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
operating_mode |= DELONGHI_MODE_DRY;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
operating_mode |= DELONGHI_MODE_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
operating_mode |= DELONGHI_MODE_AUTO;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
operating_mode |= DELONGHI_MODE_FAN;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
operating_mode = DELONGHI_MODE_OFF;
|
||||
break;
|
||||
}
|
||||
return operating_mode;
|
||||
}
|
||||
|
||||
uint16_t DelonghiClimate::fan_speed_() {
|
||||
uint16_t fan_speed;
|
||||
switch (this->fan_mode.value()) {
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
fan_speed = DELONGHI_FAN_LOW;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
fan_speed = DELONGHI_FAN_MEDIUM;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
fan_speed = DELONGHI_FAN_HIGH;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
fan_speed = DELONGHI_FAN_AUTO;
|
||||
}
|
||||
return fan_speed;
|
||||
}
|
||||
|
||||
uint8_t DelonghiClimate::temperature_() {
|
||||
// Force special temperatures depending on the mode
|
||||
uint8_t temperature = 0b0001;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL;
|
||||
}
|
||||
if (temperature > 0x0F) {
|
||||
temperature = 0x0F; // clamp maximum
|
||||
}
|
||||
return temperature;
|
||||
}
|
||||
|
||||
bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
uint8_t checksum = 0;
|
||||
for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) {
|
||||
checksum += frame[i];
|
||||
}
|
||||
if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) {
|
||||
return false;
|
||||
}
|
||||
uint8_t mode = frame[2] & 0x0F;
|
||||
if (mode & DELONGHI_MODE_ON) {
|
||||
switch (mode & 0x0E) {
|
||||
case DELONGHI_MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
case DELONGHI_MODE_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case DELONGHI_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
case DELONGHI_MODE_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
case DELONGHI_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
uint8_t temperature = frame[1] & 0x0F;
|
||||
if (this->mode == climate::CLIMATE_MODE_HEAT) {
|
||||
this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT;
|
||||
} else {
|
||||
this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL;
|
||||
}
|
||||
uint8_t fan_mode = frame[1] >> 5;
|
||||
switch (fan_mode) {
|
||||
case DELONGHI_FAN_LOW:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case DELONGHI_FAN_MEDIUM:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
case DELONGHI_FAN_HIGH:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
case DELONGHI_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {};
|
||||
if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) {
|
||||
uint8_t byte = 0;
|
||||
for (int8_t bit = 0; bit < 8; bit++) {
|
||||
if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) {
|
||||
byte |= 1 << bit;
|
||||
} else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state_frame[pos] = byte;
|
||||
if (pos == 0) {
|
||||
// frame header
|
||||
if (byte != DELONGHI_ADDRESS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this->parse_state_frame_(state_frame);
|
||||
}
|
||||
|
||||
} // namespace delonghi
|
||||
} // namespace esphome
|
||||
64
esphome/components/delonghi/delonghi.h
Normal file
64
esphome/components/delonghi/delonghi.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace delonghi {
|
||||
|
||||
// Values for DELONGHI ARC43XXX IR Controllers
|
||||
const uint8_t DELONGHI_ADDRESS = 83;
|
||||
|
||||
// Temperature
|
||||
const uint8_t DELONGHI_TEMP_MIN = 13; // Celsius
|
||||
const uint8_t DELONGHI_TEMP_MAX = 32; // Celsius
|
||||
const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17; // Celsius
|
||||
const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12; // Celsius
|
||||
|
||||
// Modes
|
||||
const uint8_t DELONGHI_MODE_AUTO = 0b1000;
|
||||
const uint8_t DELONGHI_MODE_COOL = 0b0000;
|
||||
const uint8_t DELONGHI_MODE_HEAT = 0b0110;
|
||||
const uint8_t DELONGHI_MODE_DRY = 0b0010;
|
||||
const uint8_t DELONGHI_MODE_FAN = 0b0100;
|
||||
const uint8_t DELONGHI_MODE_OFF = 0b0000;
|
||||
const uint8_t DELONGHI_MODE_ON = 0b0001;
|
||||
|
||||
// Fan Speed
|
||||
const uint8_t DELONGHI_FAN_AUTO = 0b00;
|
||||
const uint8_t DELONGHI_FAN_HIGH = 0b01;
|
||||
const uint8_t DELONGHI_FAN_MEDIUM = 0b10;
|
||||
const uint8_t DELONGHI_FAN_LOW = 0b11;
|
||||
|
||||
// IR Transmission - similar to NEC1
|
||||
const uint32_t DELONGHI_IR_FREQUENCY = 38000;
|
||||
const uint32_t DELONGHI_HEADER_MARK = 9000;
|
||||
const uint32_t DELONGHI_HEADER_SPACE = 4500;
|
||||
const uint32_t DELONGHI_BIT_MARK = 465;
|
||||
const uint32_t DELONGHI_ONE_SPACE = 1750;
|
||||
const uint32_t DELONGHI_ZERO_SPACE = 670;
|
||||
|
||||
// State Frame size
|
||||
const uint8_t DELONGHI_STATE_FRAME_SIZE = 8;
|
||||
|
||||
class DelonghiClimate : public climate_ir::ClimateIR {
|
||||
public:
|
||||
DelonghiClimate()
|
||||
: climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_TEMP_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
|
||||
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
|
||||
protected:
|
||||
// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
uint8_t operation_mode_();
|
||||
uint16_t fan_speed_();
|
||||
uint8_t temperature_();
|
||||
// Handle received IR Buffer
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
bool parse_state_frame_(const uint8_t frame[]);
|
||||
};
|
||||
|
||||
} // namespace delonghi
|
||||
} // namespace esphome
|
||||
@@ -242,6 +242,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IMAGE_TYPE_RGB565:
|
||||
for (int img_x = 0; img_x < image->get_width(); img_x++) {
|
||||
for (int img_y = 0; img_y < image->get_height(); img_y++) {
|
||||
this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,6 +504,17 @@ Color Image::get_color_pixel(int x, int y) const {
|
||||
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
|
||||
return Color(color32);
|
||||
}
|
||||
Color Image::get_rgb565_pixel(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return Color::BLACK;
|
||||
const uint32_t pos = (x + y * this->width_) * 2;
|
||||
uint16_t rgb565 =
|
||||
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
|
||||
}
|
||||
Color Image::get_grayscale_pixel(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return Color::BLACK;
|
||||
@@ -532,6 +550,20 @@ Color Animation::get_color_pixel(int x, int y) const {
|
||||
(progmem_read_byte(this->data_start_ + pos + 0) << 16);
|
||||
return Color(color32);
|
||||
}
|
||||
Color Animation::get_rgb565_pixel(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return Color::BLACK;
|
||||
const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_;
|
||||
if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_))
|
||||
return Color::BLACK;
|
||||
const uint32_t pos = (x + y * this->width_ + frame_index) * 2;
|
||||
uint16_t rgb565 =
|
||||
progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1);
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2));
|
||||
}
|
||||
Color Animation::get_grayscale_pixel(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return Color::BLACK;
|
||||
@@ -552,6 +584,12 @@ void Animation::next_frame() {
|
||||
this->current_frame_ = 0;
|
||||
}
|
||||
}
|
||||
void Animation::prev_frame() {
|
||||
this->current_frame_--;
|
||||
if (this->current_frame_ < 0) {
|
||||
this->current_frame_ = this->animation_frame_count_ - 1;
|
||||
}
|
||||
}
|
||||
|
||||
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
|
||||
void DisplayPage::show() { this->parent_->show_page(this); }
|
||||
|
||||
@@ -82,6 +82,7 @@ enum ImageType {
|
||||
IMAGE_TYPE_GRAYSCALE = 1,
|
||||
IMAGE_TYPE_RGB24 = 2,
|
||||
IMAGE_TYPE_TRANSPARENT_BINARY = 3,
|
||||
IMAGE_TYPE_RGB565 = 4,
|
||||
};
|
||||
|
||||
enum DisplayRotation {
|
||||
@@ -453,6 +454,7 @@ class Image {
|
||||
Image(const uint8_t *data_start, int width, int height, ImageType type);
|
||||
virtual bool get_pixel(int x, int y) const;
|
||||
virtual Color get_color_pixel(int x, int y) const;
|
||||
virtual Color get_rgb565_pixel(int x, int y) const;
|
||||
virtual Color get_grayscale_pixel(int x, int y) const;
|
||||
int get_width() const;
|
||||
int get_height() const;
|
||||
@@ -470,11 +472,13 @@ class Animation : public Image {
|
||||
Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
|
||||
bool get_pixel(int x, int y) const override;
|
||||
Color get_color_pixel(int x, int y) const override;
|
||||
Color get_rgb565_pixel(int x, int y) const override;
|
||||
Color get_grayscale_pixel(int x, int y) const override;
|
||||
|
||||
int get_animation_frame_count() const;
|
||||
int get_current_frame() const;
|
||||
void next_frame();
|
||||
void prev_frame();
|
||||
|
||||
protected:
|
||||
int current_frame_;
|
||||
|
||||
@@ -66,6 +66,9 @@ class ColorUtil {
|
||||
}
|
||||
return color_return;
|
||||
}
|
||||
static inline Color rgb332_to_color(uint8_t rgb332_color) {
|
||||
return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332);
|
||||
}
|
||||
static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) {
|
||||
uint16_t red_color, green_color, blue_color;
|
||||
|
||||
@@ -100,11 +103,57 @@ class ColorUtil {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint32_t color_to_grayscale4(Color color) {
|
||||
uint32_t gs4 = esp_scale8(color.white, 15);
|
||||
return gs4;
|
||||
}
|
||||
/***
|
||||
* Converts a Color value to an 8bit index using a 24bit 888 palette.
|
||||
* Uses euclidiean distance to calculate the linear distance between
|
||||
* two points in an RGB cube, then iterates through the full palette
|
||||
* returning the closest match.
|
||||
* @param[in] color The target color.
|
||||
* @param[in] palette The 256*3 byte RGB palette.
|
||||
* @return The 8 bit index of the closest color (e.g. for display buffer).
|
||||
*/
|
||||
// static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) {
|
||||
static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) {
|
||||
uint8_t closest_index = 0;
|
||||
uint32_t minimum_dist2 = UINT32_MAX; // Smallest distance^2 to the target
|
||||
// so far
|
||||
// int8_t(*plt)[][3] = palette;
|
||||
int16_t tgt_r = color.r;
|
||||
int16_t tgt_g = color.g;
|
||||
int16_t tgt_b = color.b;
|
||||
uint16_t x, y, z;
|
||||
// Loop through each row of the palette
|
||||
for (uint16_t i = 0; i < 256; i++) {
|
||||
// Get the pallet rgb color
|
||||
int16_t plt_r = (int16_t) palette[i * 3 + 0];
|
||||
int16_t plt_g = (int16_t) palette[i * 3 + 1];
|
||||
int16_t plt_b = (int16_t) palette[i * 3 + 2];
|
||||
// Calculate euclidian distance (linear distance in rgb cube).
|
||||
x = (uint32_t) std::abs(tgt_r - plt_r);
|
||||
y = (uint32_t) std::abs(tgt_g - plt_g);
|
||||
z = (uint32_t) std::abs(tgt_b - plt_b);
|
||||
uint32_t dist2 = x * x + y * y + z * z;
|
||||
if (dist2 < minimum_dist2) {
|
||||
minimum_dist2 = dist2;
|
||||
closest_index = (uint8_t) i;
|
||||
}
|
||||
}
|
||||
return closest_index;
|
||||
}
|
||||
/***
|
||||
* Converts an 8bit palette index (e.g. from a display buffer) to a color.
|
||||
* @param[in] index The index to look up.
|
||||
* @param[in] palette The 256*3 byte RGB palette.
|
||||
* @return The RGBW Color object looked up by the palette.
|
||||
*/
|
||||
static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) {
|
||||
Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0);
|
||||
return color;
|
||||
}
|
||||
};
|
||||
} // namespace display
|
||||
} // namespace esphome
|
||||
|
||||
0
esphome/components/ens210/__init__.py
Normal file
0
esphome/components/ens210/__init__.py
Normal file
230
esphome/components/ens210/ens210.cpp
Normal file
230
esphome/components/ens210/ens210.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
// ENS210 relative humidity and temperature sensor with I2C interface from ScioSense
|
||||
//
|
||||
// Datasheet: https://www.sciosense.com/wp-content/uploads/2021/01/ENS210.pdf
|
||||
//
|
||||
// Implementation based on:
|
||||
// https://github.com/maarten-pennings/ENS210
|
||||
// https://github.com/sciosense/ENS210_driver
|
||||
|
||||
#include "ens210.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ens210 {
|
||||
|
||||
static const char *const TAG = "ens210";
|
||||
|
||||
// ENS210 chip constants
|
||||
static const uint8_t ENS210_BOOTING_MS = 2; // Booting time in ms (also after reset, or going to high power)
|
||||
static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS =
|
||||
130; // Conversion time in ms for single shot T/H measurement
|
||||
static const uint16_t ENS210_PART_ID = 0x0210; // The expected part id of the ENS210
|
||||
|
||||
// Addresses of the ENS210 registers
|
||||
static const uint8_t ENS210_REGISTER_PART_ID = 0x00;
|
||||
static const uint8_t ENS210_REGISTER_UID = 0x04;
|
||||
static const uint8_t ENS210_REGISTER_SYS_CTRL = 0x10;
|
||||
static const uint8_t ENS210_REGISTER_SYS_STAT = 0x11;
|
||||
static const uint8_t ENS210_REGISTER_SENS_RUN = 0x21;
|
||||
static const uint8_t ENS210_REGISTER_SENS_START = 0x22;
|
||||
static const uint8_t ENS210_REGISTER_SENS_STOP = 0x23;
|
||||
static const uint8_t ENS210_REGISTER_SENS_STAT = 0x24;
|
||||
static const uint8_t ENS210_REGISTER_T_VAL = 0x30;
|
||||
static const uint8_t ENS210_REGISTER_H_VAL = 0x33;
|
||||
|
||||
// CRC-7 constants
|
||||
static const uint8_t CRC7_WIDTH = 7; // A 7 bits CRC has polynomial of 7th order, which has 8 terms
|
||||
static const uint8_t CRC7_POLY = 0x89; // The 8 coefficients of the polynomial
|
||||
static const uint8_t CRC7_IVEC = 0x7F; // Initial vector has all 7 bits high
|
||||
|
||||
// Payload data constants
|
||||
static const uint8_t DATA7_WIDTH = 17;
|
||||
static const uint32_t DATA7_MASK = ((1UL << DATA7_WIDTH) - 1); // 0b 0 1111 1111 1111 1111
|
||||
static const uint32_t DATA7_MSB = (1UL << (DATA7_WIDTH - 1)); // 0b 1 0000 0000 0000 0000
|
||||
|
||||
// Converts a status to a human readable string
|
||||
static const LogString *ens210_status_to_human(int status) {
|
||||
switch (status) {
|
||||
case ENS210Component::ENS210_STATUS_I2C_ERROR:
|
||||
return LOG_STR("I2C error - communication with ENS210 failed!");
|
||||
case ENS210Component::ENS210_STATUS_CRC_ERROR:
|
||||
return LOG_STR("CRC error");
|
||||
case ENS210Component::ENS210_STATUS_INVALID:
|
||||
return LOG_STR("Invalid data");
|
||||
case ENS210Component::ENS210_STATUS_OK:
|
||||
return LOG_STR("Status OK");
|
||||
case ENS210Component::ENS210_WRONG_CHIP_ID:
|
||||
return LOG_STR("ENS210 has wrong chip ID! Is it a ENS210?");
|
||||
default:
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the CRC-7 of 'value' (should only have 17 bits)
|
||||
// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation
|
||||
static uint32_t crc7(uint32_t value) {
|
||||
// Setup polynomial
|
||||
uint32_t polynomial = CRC7_POLY;
|
||||
// Align polynomial with data
|
||||
polynomial = polynomial << (DATA7_WIDTH - CRC7_WIDTH - 1);
|
||||
// Loop variable (indicates which bit to test, start with highest)
|
||||
uint32_t bit = DATA7_MSB;
|
||||
// Make room for CRC value
|
||||
value = value << CRC7_WIDTH;
|
||||
bit = bit << CRC7_WIDTH;
|
||||
polynomial = polynomial << CRC7_WIDTH;
|
||||
// Insert initial vector
|
||||
value |= CRC7_IVEC;
|
||||
// Apply division until all bits done
|
||||
while (bit & (DATA7_MASK << CRC7_WIDTH)) {
|
||||
if (bit & value)
|
||||
value ^= polynomial;
|
||||
bit >>= 1;
|
||||
polynomial >>= 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void ENS210Component::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up ENS210...");
|
||||
uint8_t data[2];
|
||||
uint16_t part_id = 0;
|
||||
// Reset
|
||||
if (!this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80)) {
|
||||
this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80);
|
||||
this->error_code_ = ENS210_STATUS_I2C_ERROR;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Wait to boot after reset
|
||||
delay(ENS210_BOOTING_MS);
|
||||
// Must disable low power to read PART_ID
|
||||
if (!set_low_power_(false)) {
|
||||
// Try to go back to default mode (low power enabled)
|
||||
set_low_power_(true);
|
||||
this->error_code_ = ENS210_STATUS_I2C_ERROR;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Read the PART_ID
|
||||
if (!this->read_bytes(ENS210_REGISTER_PART_ID, data, 2)) {
|
||||
// Try to go back to default mode (low power enabled)
|
||||
set_low_power_(true);
|
||||
this->error_code_ = ENS210_STATUS_I2C_ERROR;
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Pack bytes into partid
|
||||
part_id = data[1] * 256U + data[0] * 1U;
|
||||
// Check expected part id of the ENS210
|
||||
if (part_id != ENS210_PART_ID) {
|
||||
this->error_code_ = ENS210_WRONG_CHIP_ID;
|
||||
this->mark_failed();
|
||||
}
|
||||
// Set default power mode (low power enabled)
|
||||
set_low_power_(true);
|
||||
}
|
||||
|
||||
void ENS210Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ENS210:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "%s", LOG_STR_ARG(ens210_status_to_human(this->error_code_)));
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
float ENS210Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void ENS210Component::update() {
|
||||
// Execute a single measurement
|
||||
if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) {
|
||||
ESP_LOGE(TAG, "Starting single measurement failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Trigger measurement
|
||||
if (!this->write_byte(ENS210_REGISTER_SENS_START, 0x03)) {
|
||||
ESP_LOGE(TAG, "Trigger of measurement failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Wait for measurement to complete
|
||||
this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() {
|
||||
int temperature_data, temperature_status, humidity_data, humidity_status;
|
||||
uint8_t data[6];
|
||||
uint32_t h_val_data, t_val_data;
|
||||
// Set default status for early bail out
|
||||
temperature_status = ENS210_STATUS_I2C_ERROR;
|
||||
humidity_status = ENS210_STATUS_I2C_ERROR;
|
||||
|
||||
// Read T_VAL and H_VAL
|
||||
if (!this->read_bytes(ENS210_REGISTER_T_VAL, data, 6)) {
|
||||
ESP_LOGE(TAG, "Communication with ENS210 failed!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Pack bytes for humidity
|
||||
h_val_data = (uint32_t)((uint32_t) data[5] << 16 | (uint32_t) data[4] << 8 | (uint32_t) data[3]);
|
||||
// Extract humidity data and update the status
|
||||
extract_measurement_(h_val_data, &humidity_data, &humidity_status);
|
||||
|
||||
if (humidity_status == ENS210_STATUS_OK) {
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
float humidity = (humidity_data & 0xFFFF) / 512.0;
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Humidity status failure: %s", LOG_STR_ARG(ens210_status_to_human(humidity_status)));
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
// Pack bytes for temperature
|
||||
t_val_data = (uint32_t)((uint32_t) data[2] << 16 | (uint32_t) data[1] << 8 | (uint32_t) data[0]);
|
||||
// Extract temperature data and update the status
|
||||
extract_measurement_(t_val_data, &temperature_data, &temperature_status);
|
||||
|
||||
if (temperature_status == ENS210_STATUS_OK) {
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
// Temperature in Celsius
|
||||
float temperature = (temperature_data & 0xFFFF) / 64.0 - 27315L / 100.0;
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Temperature status failure: %s", LOG_STR_ARG(ens210_status_to_human(temperature_status)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment.
|
||||
void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) {
|
||||
*data = (val >> 0) & 0xffff;
|
||||
int valid = (val >> 16) & 0x1;
|
||||
uint32_t crc = (val >> 17) & 0x7f;
|
||||
uint32_t payload = (val >> 0) & 0x1ffff;
|
||||
// Check CRC
|
||||
uint8_t crc_ok = crc7(payload) == crc;
|
||||
|
||||
if (!crc_ok) {
|
||||
*status = ENS210_STATUS_CRC_ERROR;
|
||||
} else if (!valid) {
|
||||
*status = ENS210_STATUS_INVALID;
|
||||
} else {
|
||||
*status = ENS210_STATUS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
|
||||
bool ENS210Component::set_low_power_(bool enable) {
|
||||
uint8_t low_power_cmd = enable ? 0x01 : 0x00;
|
||||
ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
|
||||
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
|
||||
delay(ENS210_BOOTING_MS);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ens210
|
||||
} // namespace esphome
|
||||
39
esphome/components/ens210/ens210.h
Normal file
39
esphome/components/ens210/ens210.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ens210 {
|
||||
|
||||
/// This class implements support for the ENS210 relative humidity and temperature i2c sensor.
|
||||
class ENS210Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
float get_setup_priority() const override;
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
void update() override;
|
||||
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
|
||||
enum ErrorCode {
|
||||
ENS210_STATUS_OK = 0, // The value was read, the CRC matches, and data is valid
|
||||
ENS210_STATUS_INVALID, // The value was read, the CRC matches, but the data is invalid (e.g. the measurement was
|
||||
// not yet finished)
|
||||
ENS210_STATUS_CRC_ERROR, // The value was read, but the CRC over the payload (valid and data) does not match
|
||||
ENS210_STATUS_I2C_ERROR, // There was an I2C communication error
|
||||
ENS210_WRONG_CHIP_ID // The read PART_ID is not the expected part id of the ENS210
|
||||
} error_code_{ENS210_STATUS_OK};
|
||||
|
||||
protected:
|
||||
bool set_low_power_(bool enable);
|
||||
void extract_measurement_(uint32_t val, int *data, int *status);
|
||||
|
||||
sensor::Sensor *temperature_sensor_;
|
||||
sensor::Sensor *humidity_sensor_;
|
||||
};
|
||||
|
||||
} // namespace ens210
|
||||
} // namespace esphome
|
||||
58
esphome/components/ens210/sensor.py
Normal file
58
esphome/components/ens210/sensor.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@itn3rd77"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
ens210_ns = cg.esphome_ns.namespace("ens210")
|
||||
|
||||
ENS210Component = ens210_ns.class_(
|
||||
"ENS210Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ENS210Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x43))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if CONF_TEMPERATURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
|
||||
if CONF_HUMIDITY in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
|
||||
cg.add(var.set_humidity_sensor(sens))
|
||||
@@ -107,7 +107,7 @@ def validate_gpio_pin(value):
|
||||
value = _translate_pin(value)
|
||||
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
|
||||
if variant not in _esp32_validations:
|
||||
raise cv.Invalid("Unsupported ESP32 variant {variant}")
|
||||
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
|
||||
|
||||
return _esp32_validations[variant].pin_validation(value)
|
||||
|
||||
@@ -121,7 +121,7 @@ def validate_supports(value):
|
||||
is_pulldown = mode[CONF_PULLDOWN]
|
||||
variant = CORE.data[KEY_ESP32][KEY_VARIANT]
|
||||
if variant not in _esp32_validations:
|
||||
raise cv.Invalid("Unsupported ESP32 variant {variant}")
|
||||
raise cv.Invalid(f"Unsupported ESP32 variant {variant}")
|
||||
|
||||
if is_open_drain and not is_output:
|
||||
raise cv.Invalid(
|
||||
|
||||
@@ -118,12 +118,17 @@ class ESP32Preferences : public ESPPreferences {
|
||||
// go through vector from back to front (makes erase easier/more efficient)
|
||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
||||
const auto &save = s_pending_save[i];
|
||||
esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
|
||||
esp_err_to_name(err));
|
||||
any_failed = true;
|
||||
continue;
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str());
|
||||
if (is_changed(nvs_handle, save)) {
|
||||
esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
|
||||
esp_err_to_name(err));
|
||||
any_failed = true;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size());
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
}
|
||||
@@ -137,6 +142,22 @@ class ESP32Preferences : public ESPPreferences {
|
||||
|
||||
return !any_failed;
|
||||
}
|
||||
bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
|
||||
NVSData stored_data{};
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
stored_data.data.reserve(actual_len);
|
||||
err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return to_save.data != stored_data.data;
|
||||
}
|
||||
};
|
||||
|
||||
void setup_preferences() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome import pins
|
||||
from esphome.const import (
|
||||
CONF_FREQUENCY,
|
||||
@@ -12,6 +13,7 @@ from esphome.const import (
|
||||
CONF_RESOLUTION,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_CONTRAST,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
@@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"]
|
||||
|
||||
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
|
||||
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
|
||||
|
||||
ESP32CameraStreamStartTrigger = esp32_camera_ns.class_(
|
||||
"ESP32CameraStreamStartTrigger",
|
||||
automation.Trigger.template(),
|
||||
)
|
||||
ESP32CameraStreamStopTrigger = esp32_camera_ns.class_(
|
||||
"ESP32CameraStreamStopTrigger",
|
||||
automation.Trigger.template(),
|
||||
)
|
||||
ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize")
|
||||
FRAME_SIZES = {
|
||||
"160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
|
||||
@@ -111,6 +120,10 @@ CONF_TEST_PATTERN = "test_pattern"
|
||||
CONF_MAX_FRAMERATE = "max_framerate"
|
||||
CONF_IDLE_FRAMERATE = "idle_framerate"
|
||||
|
||||
# stream trigger
|
||||
CONF_ON_STREAM_START = "on_stream_start"
|
||||
CONF_ON_STREAM_STOP = "on_stream_stop"
|
||||
|
||||
camera_range_param = cv.int_range(min=-2, max=2)
|
||||
|
||||
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
@@ -178,6 +191,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
|
||||
cv.framerate, cv.Range(min=0, max=1)
|
||||
),
|
||||
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32CameraStreamStartTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
ESP32CameraStreamStopTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -238,3 +265,11 @@ async def to_code(config):
|
||||
if CORE.using_esp_idf:
|
||||
cg.add_library("espressif/esp32-camera", "1.0.0")
|
||||
add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True)
|
||||
|
||||
for conf in config.get(CONF_ON_STREAM_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_STREAM_STOP, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -282,8 +282,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) {
|
||||
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) {
|
||||
this->new_image_callback_.add(std::move(f));
|
||||
}
|
||||
void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); }
|
||||
void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); }
|
||||
void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) {
|
||||
this->stream_start_callback_.add(std::move(callback));
|
||||
}
|
||||
void ESP32Camera::add_stream_stop_callback(std::function<void()> &&callback) {
|
||||
this->stream_stop_callback_.add(std::move(callback));
|
||||
}
|
||||
void ESP32Camera::start_stream(CameraRequester requester) {
|
||||
this->stream_start_callback_.call();
|
||||
this->stream_requesters_ |= (1U << requester);
|
||||
}
|
||||
void ESP32Camera::stop_stream(CameraRequester requester) {
|
||||
this->stream_stop_callback_.call();
|
||||
this->stream_requesters_ &= ~(1U << requester);
|
||||
}
|
||||
void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); }
|
||||
void ESP32Camera::update_camera_parameters() {
|
||||
sensor_t *s = esp_camera_sensor_get();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -145,6 +146,9 @@ class ESP32Camera : public Component, public EntityBase {
|
||||
void request_image(CameraRequester requester);
|
||||
void update_camera_parameters();
|
||||
|
||||
void add_stream_start_callback(std::function<void()> &&callback);
|
||||
void add_stream_stop_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
/* internal methods */
|
||||
uint32_t hash_base() override;
|
||||
@@ -187,6 +191,8 @@ class ESP32Camera : public Component, public EntityBase {
|
||||
QueueHandle_t framebuffer_get_queue_;
|
||||
QueueHandle_t framebuffer_return_queue_;
|
||||
CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_;
|
||||
CallbackManager<void()> stream_start_callback_{};
|
||||
CallbackManager<void()> stream_stop_callback_{};
|
||||
|
||||
uint32_t last_idle_request_{0};
|
||||
uint32_t last_update_{0};
|
||||
@@ -195,6 +201,23 @@ class ESP32Camera : public Component, public EntityBase {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern ESP32Camera *global_esp32_camera;
|
||||
|
||||
class ESP32CameraStreamStartTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) {
|
||||
parent->add_stream_start_callback([this]() { this->trigger(); });
|
||||
}
|
||||
|
||||
protected:
|
||||
};
|
||||
class ESP32CameraStreamStopTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) {
|
||||
parent->add_stream_stop_callback([this]() { this->trigger(); });
|
||||
}
|
||||
|
||||
protected:
|
||||
};
|
||||
|
||||
} // namespace esp32_camera
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from esphome.helpers import copy_file_if_changed
|
||||
|
||||
from .const import (
|
||||
CONF_RESTORE_FROM_FLASH,
|
||||
CONF_EARLY_PIN_INIT,
|
||||
KEY_BOARD,
|
||||
KEY_ESP8266,
|
||||
KEY_PIN_INITIAL_STATES,
|
||||
@@ -148,6 +149,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_BOARD): cv.string_strict,
|
||||
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
|
||||
cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean,
|
||||
cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of(
|
||||
*BUILD_FLASH_MODES, lower=True
|
||||
),
|
||||
@@ -197,6 +199,9 @@ async def to_code(config):
|
||||
if config[CONF_RESTORE_FROM_FLASH]:
|
||||
cg.add_define("USE_ESP8266_PREFERENCES_FLASH")
|
||||
|
||||
if config[CONF_EARLY_PIN_INIT]:
|
||||
cg.add_define("USE_ESP8266_EARLY_PIN_INIT")
|
||||
|
||||
# Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when
|
||||
# out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make
|
||||
# new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of
|
||||
|
||||
@@ -4,6 +4,7 @@ KEY_ESP8266 = "esp8266"
|
||||
KEY_BOARD = "board"
|
||||
KEY_PIN_INITIAL_STATES = "pin_initial_states"
|
||||
CONF_RESTORE_FROM_FLASH = "restore_from_flash"
|
||||
CONF_EARLY_PIN_INIT = "early_pin_init"
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
@@ -55,6 +56,7 @@ extern "C" void resetPins() { // NOLINT
|
||||
// ourselves and this causes pins to toggle during reboot.
|
||||
force_link_symbols();
|
||||
|
||||
#ifdef USE_ESP8266_EARLY_PIN_INIT
|
||||
for (int i = 0; i < 16; i++) {
|
||||
uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i];
|
||||
uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i];
|
||||
@@ -63,6 +65,7 @@ extern "C" void resetPins() { // NOLINT
|
||||
if (level != 255)
|
||||
digitalWrite(i, level); // NOLINT
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "fan.h"
|
||||
#include "fan_helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -61,22 +60,6 @@ void FanCall::validate_() {
|
||||
}
|
||||
}
|
||||
|
||||
// This whole method is deprecated, don't warn about usage of deprecated methods inside of it.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
FanCall &FanCall::set_speed(const char *legacy_speed) {
|
||||
const auto supported_speed_count = this->parent_.get_traits().supported_speed_count();
|
||||
if (strcasecmp(legacy_speed, "low") == 0) {
|
||||
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count));
|
||||
} else if (strcasecmp(legacy_speed, "medium") == 0) {
|
||||
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count));
|
||||
} else if (strcasecmp(legacy_speed, "high") == 0) {
|
||||
this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
FanCall FanRestoreState::to_call(Fan &fan) {
|
||||
auto call = fan.make_call();
|
||||
call.set_state(this->state);
|
||||
|
||||
@@ -16,13 +16,6 @@ namespace fan {
|
||||
(obj)->dump_traits_(TAG, prefix); \
|
||||
}
|
||||
|
||||
/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon
|
||||
enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed {
|
||||
FAN_SPEED_LOW = 0, ///< The fan is running on low speed.
|
||||
FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed.
|
||||
FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed.
|
||||
};
|
||||
|
||||
/// Simple enum to represent the direction of a fan.
|
||||
enum class FanDirection { FORWARD = 0, REVERSE = 1 };
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#include <cassert>
|
||||
#include "fan_helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
// This whole file is deprecated, don't warn about usage of deprecated types in here.
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) {
|
||||
const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1);
|
||||
const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3);
|
||||
return static_cast<FanSpeed>(legacy_level - 1);
|
||||
}
|
||||
|
||||
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) {
|
||||
const auto enum_level = static_cast<int>(speed) + 1;
|
||||
const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels);
|
||||
return static_cast<int>(speed_level);
|
||||
}
|
||||
|
||||
} // namespace fan
|
||||
} // namespace esphome
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "fan.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace fan {
|
||||
|
||||
// Shut-up about usage of deprecated FanSpeed for a bit.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9")
|
||||
FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels);
|
||||
ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9")
|
||||
int speed_enum_to_level(FanSpeed speed, int supported_speed_levels);
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
} // namespace fan
|
||||
} // namespace esphome
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "hbridge_fan.h"
|
||||
#include "esphome/components/fan/fan_helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -3,13 +3,16 @@ import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import display, spi
|
||||
from esphome.const import (
|
||||
CONF_COLOR_PALETTE,
|
||||
CONF_DC_PIN,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_MODEL,
|
||||
CONF_PAGES,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_RESET_PIN,
|
||||
)
|
||||
from esphome.core import HexInt
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
@@ -23,6 +26,7 @@ ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341)
|
||||
ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341)
|
||||
|
||||
ILI9341Model = ili9341_ns.enum("ILI9341Model")
|
||||
ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode")
|
||||
|
||||
MODELS = {
|
||||
"M5STACK": ILI9341Model.M5STACK,
|
||||
@@ -31,6 +35,8 @@ MODELS = {
|
||||
|
||||
ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_")
|
||||
|
||||
COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE")
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
@@ -39,6 +45,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE,
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("1s"))
|
||||
@@ -73,3 +81,13 @@ async def to_code(config):
|
||||
if CONF_LED_PIN in config:
|
||||
led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN])
|
||||
cg.add(var.set_led_pin(led_pin))
|
||||
|
||||
if config[CONF_COLOR_PALETTE] == "GRAYSCALE":
|
||||
cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED))
|
||||
rhs = []
|
||||
for x in range(256):
|
||||
rhs.extend([HexInt(x), HexInt(x), HexInt(x)])
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
cg.add(var.set_palette(prog_arr))
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -112,29 +112,9 @@ void ILI9341Display::display_() {
|
||||
this->y_high_ = 0;
|
||||
}
|
||||
|
||||
uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) {
|
||||
int r = color_8bit >> 5;
|
||||
int g = (color_8bit >> 2) & 0x07;
|
||||
int b = color_8bit & 0x03;
|
||||
uint16_t color = (r * 0x04) << 11;
|
||||
color |= (g * 0x09) << 5;
|
||||
color |= (b * 0x0A);
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) {
|
||||
// convert 16bit color to 8 bit buffer
|
||||
uint8_t r = color_16bit >> 11;
|
||||
uint8_t g = (color_16bit >> 5) & 0x3F;
|
||||
uint8_t b = color_16bit & 0x1F;
|
||||
|
||||
return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5));
|
||||
}
|
||||
|
||||
void ILI9341Display::fill(Color color) {
|
||||
auto color565 = display::ColorUtil::color_to_565(color);
|
||||
memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_());
|
||||
uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
|
||||
memset(this->buffer_, color332, this->get_buffer_length_());
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->get_width_internal() - 1;
|
||||
@@ -181,8 +161,13 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color)
|
||||
this->y_high_ = (y > this->y_high_) ? y : this->y_high_;
|
||||
|
||||
uint32_t pos = (y * width_) + x;
|
||||
auto color565 = display::ColorUtil::color_to_565(color);
|
||||
buffer_[pos] = convert_to_8bit_color_(color565);
|
||||
if (this->buffer_color_mode_ == BITS_8) {
|
||||
uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB);
|
||||
buffer_[pos] = color332;
|
||||
} else { // if (this->buffer_color_mode_ == BITS_8_INDEXED) {
|
||||
uint8_t index = display::ColorUtil::color_to_index8_palette888(color, this->palette_);
|
||||
buffer_[pos] = index;
|
||||
}
|
||||
}
|
||||
|
||||
// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color
|
||||
@@ -247,7 +232,13 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) {
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < sz; ++i) {
|
||||
uint16_t color = convert_to_16bit_color_(*src++);
|
||||
uint16_t color;
|
||||
if (this->buffer_color_mode_ == BITS_8) {
|
||||
color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++));
|
||||
} else { // if (this->buffer_color_mode == BITS_8_INDEXED) {
|
||||
Color col = display::ColorUtil::index8_to_color_palette888(*src++, this->palette_);
|
||||
color = display::ColorUtil::color_to_565(col);
|
||||
}
|
||||
*dst++ = (uint8_t)(color >> 8);
|
||||
*dst++ = (uint8_t) color;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ enum ILI9341Model {
|
||||
TFT_24,
|
||||
};
|
||||
|
||||
enum ILI9341ColorMode {
|
||||
BITS_8,
|
||||
BITS_8_INDEXED,
|
||||
};
|
||||
|
||||
class ILI9341Display : public PollingComponent,
|
||||
public display::DisplayBuffer,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
@@ -24,6 +29,8 @@ class ILI9341Display : public PollingComponent,
|
||||
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
||||
void set_led_pin(GPIOPin *led) { this->led_pin_ = led; }
|
||||
void set_model(ILI9341Model model) { this->model_ = model; }
|
||||
void set_palette(const uint8_t *palette) { this->palette_ = palette; }
|
||||
void set_buffer_color_mode(ILI9341ColorMode color_mode) { this->buffer_color_mode_ = color_mode; }
|
||||
|
||||
void command(uint8_t value);
|
||||
void data(uint8_t value);
|
||||
@@ -51,8 +58,6 @@ class ILI9341Display : public PollingComponent,
|
||||
void reset_();
|
||||
void fill_internal_(Color color);
|
||||
void display_();
|
||||
uint16_t convert_to_16bit_color_(uint8_t color_8bit);
|
||||
uint8_t convert_to_8bit_color_(uint16_t color_16bit);
|
||||
|
||||
ILI9341Model model_;
|
||||
int16_t width_{320}; ///< Display width as modified by current rotation
|
||||
@@ -61,6 +66,9 @@ class ILI9341Display : public PollingComponent,
|
||||
uint16_t y_low_{0};
|
||||
uint16_t x_high_{0};
|
||||
uint16_t y_high_{0};
|
||||
const uint8_t *palette_;
|
||||
|
||||
ILI9341ColorMode buffer_color_mode_{BITS_8};
|
||||
|
||||
uint32_t get_buffer_length_();
|
||||
int get_width_internal() override;
|
||||
|
||||
@@ -25,6 +25,7 @@ IMAGE_TYPE = {
|
||||
"GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE,
|
||||
"RGB24": ImageType.IMAGE_TYPE_RGB24,
|
||||
"TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY,
|
||||
"RGB565": ImageType.IMAGE_TYPE_RGB565,
|
||||
}
|
||||
|
||||
Image_ = display.display_ns.class_("Image")
|
||||
@@ -89,6 +90,21 @@ async def to_code(config):
|
||||
data[pos] = pix[2]
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "RGB565":
|
||||
image = image.convert("RGB")
|
||||
pixels = list(image.getdata())
|
||||
data = [0 for _ in range(height * width * 3)]
|
||||
pos = 0
|
||||
for pix in pixels:
|
||||
R = pix[0] >> 3
|
||||
G = pix[1] >> 2
|
||||
B = pix[2] >> 3
|
||||
rgb = (R << 11) | (G << 5) | B
|
||||
data[pos] = rgb >> 8
|
||||
pos += 1
|
||||
data[pos] = rgb & 255
|
||||
pos += 1
|
||||
|
||||
elif config[CONF_TYPE] == "BINARY":
|
||||
image = image.convert("1", dither=dither)
|
||||
width8 = ((width + 7) // 8) * 8
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER
|
||||
from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG
|
||||
from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -17,14 +19,19 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def validate_logger_baud_rate(config):
|
||||
def validate_logger(config):
|
||||
logger_conf = fv.full_config.get()[CONF_LOGGER]
|
||||
if logger_conf[CONF_BAUD_RATE] == 0:
|
||||
raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0")
|
||||
if CORE.using_esp_idf:
|
||||
if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]:
|
||||
raise cv.Invalid(
|
||||
"improv_serial does not support the selected logger hardware_uart"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate
|
||||
FINAL_VALIDATE_SCHEMA = validate_logger
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -51,7 +51,7 @@ class ImprovSerialComponent : public Component {
|
||||
void write_data_(std::vector<uint8_t> &data);
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
HardwareSerial *hw_serial_{nullptr};
|
||||
Stream *hw_serial_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
uart_port_t uart_num_;
|
||||
|
||||
@@ -26,21 +26,33 @@ std::string build_json(const json_build_t &f) {
|
||||
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
|
||||
#endif
|
||||
|
||||
const size_t request_size = std::min(free_heap, (size_t) 512);
|
||||
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
|
||||
request_size, free_heap);
|
||||
return "{}";
|
||||
size_t request_size = std::min(free_heap, (size_t) 512);
|
||||
while (true) {
|
||||
ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size);
|
||||
DynamicJsonDocument json_document(request_size);
|
||||
if (json_document.capacity() == 0) {
|
||||
ESP_LOGE(TAG,
|
||||
"Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes",
|
||||
request_size, free_heap);
|
||||
return "{}";
|
||||
}
|
||||
JsonObject root = json_document.to<JsonObject>();
|
||||
f(root);
|
||||
if (json_document.overflowed()) {
|
||||
if (request_size == free_heap) {
|
||||
ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes",
|
||||
free_heap);
|
||||
return "{}";
|
||||
}
|
||||
request_size = std::min(request_size * 2, free_heap);
|
||||
continue;
|
||||
}
|
||||
json_document.shrinkToFit();
|
||||
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
}
|
||||
JsonObject root = json_document.to<JsonObject>();
|
||||
f(root);
|
||||
json_document.shrinkToFit();
|
||||
ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity());
|
||||
std::string output;
|
||||
serializeJson(json_document, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
void parse_json(const std::string &data, const json_parse_t &f) {
|
||||
|
||||
@@ -19,8 +19,13 @@ from esphome.const import (
|
||||
CONF_TX_BUFFER_SIZE,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
logger_ns = cg.esphome_ns.namespace("logger")
|
||||
@@ -54,36 +59,51 @@ LOG_LEVEL_SEVERITY = [
|
||||
"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 = {
|
||||
"UART0": logger_ns.UART_SELECTION_UART0,
|
||||
"UART0_SWAP": logger_ns.UART_SELECTION_UART0_SWAP,
|
||||
"UART1": logger_ns.UART_SELECTION_UART1,
|
||||
"UART2": logger_ns.UART_SELECTION_UART2,
|
||||
UART0: logger_ns.UART_SELECTION_UART0,
|
||||
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
|
||||
UART1: logger_ns.UART_SELECTION_UART1,
|
||||
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 = {
|
||||
"UART0": cg.global_ns.Serial,
|
||||
"UART0_SWAP": cg.global_ns.Serial,
|
||||
"UART1": cg.global_ns.Serial1,
|
||||
"UART2": cg.global_ns.Serial2,
|
||||
UART0: cg.global_ns.Serial,
|
||||
UART0_SWAP: cg.global_ns.Serial,
|
||||
UART1: cg.global_ns.Serial1,
|
||||
UART2: cg.global_ns.Serial2,
|
||||
}
|
||||
|
||||
is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
|
||||
|
||||
|
||||
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 get_esp32_variant() in ESP32_REDUCED_VARIANTS:
|
||||
return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value)
|
||||
return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value)
|
||||
variant = get_esp32_variant()
|
||||
if variant in UART_SELECTION_ESP32:
|
||||
return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value)
|
||||
if CORE.is_esp8266:
|
||||
return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value)
|
||||
raise NotImplementedError
|
||||
@@ -113,7 +133,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int,
|
||||
cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes,
|
||||
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_LOGS, default={}): cv.Schema(
|
||||
{
|
||||
@@ -185,6 +205,12 @@ async def to_code(config):
|
||||
if config.get(CONF_ESP8266_STORE_LOG_STRINGS_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
|
||||
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);
|
||||
#endif // USE_ARDUINO
|
||||
#ifdef USE_ESP_IDF
|
||||
uart_write_bytes(uart_num_, msg, strlen(msg));
|
||||
uart_write_bytes(uart_num_, "\n", 1);
|
||||
if (
|
||||
#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
|
||||
}
|
||||
|
||||
@@ -149,13 +163,25 @@ void Logger::pre_setup() {
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
#endif
|
||||
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;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
#ifdef USE_ESP8266
|
||||
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
#endif
|
||||
break;
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
@@ -169,39 +195,41 @@ void Logger::pre_setup() {
|
||||
case UART_SELECTION_UART1:
|
||||
uart_num_ = UART_NUM_1;
|
||||
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:
|
||||
uart_num_ = UART_NUM_2;
|
||||
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{};
|
||||
uart_config.baud_rate = (int) baud_rate_;
|
||||
uart_config.data_bits = UART_DATA_8_BITS;
|
||||
uart_config.parity = UART_PARITY_DISABLE;
|
||||
uart_config.stop_bits = UART_STOP_BITS_1;
|
||||
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||
uart_param_config(uart_num_, &uart_config);
|
||||
const int uart_buffer_size = tx_buffer_size_;
|
||||
// Install UART driver using an event queue here
|
||||
uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
this->hw_serial_->begin(this->baud_rate_);
|
||||
#ifdef USE_ESP8266
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
this->hw_serial_->swap();
|
||||
if (uart_num_ >= 0) {
|
||||
uart_config_t uart_config{};
|
||||
uart_config.baud_rate = (int) baud_rate_;
|
||||
uart_config.data_bits = UART_DATA_8_BITS;
|
||||
uart_config.parity = UART_PARITY_DISABLE;
|
||||
uart_config.stop_bits = UART_STOP_BITS_1;
|
||||
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||
uart_param_config(uart_num_, &uart_config);
|
||||
const int uart_buffer_size = tx_buffer_size_;
|
||||
// Install UART driver using an event queue here
|
||||
uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0);
|
||||
}
|
||||
this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
#endif
|
||||
#endif // USE_ARDUINO
|
||||
#endif // USE_ESP_IDF
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
else {
|
||||
uart_set_debug(UART_NO);
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ESP8266
|
||||
|
||||
global_logger = this;
|
||||
#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) {
|
||||
esp_log_level_set("*", ESP_LOG_VERBOSE);
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO
|
||||
|
||||
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; }
|
||||
const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"};
|
||||
#ifdef USE_ESP32
|
||||
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART2"};
|
||||
#endif
|
||||
const char *const UART_SELECTIONS[] = {
|
||||
"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
|
||||
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
|
||||
#endif
|
||||
#endif // USE_ESP8266
|
||||
void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Logger:");
|
||||
ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
|
||||
|
||||
@@ -24,9 +24,19 @@ namespace logger {
|
||||
enum UARTSelection {
|
||||
UART_SELECTION_UART0 = 0,
|
||||
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,
|
||||
#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
|
||||
UART_SELECTION_UART0_SWAP,
|
||||
#endif
|
||||
@@ -40,7 +50,7 @@ class Logger : public Component {
|
||||
void set_baud_rate(uint32_t baud_rate);
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
#ifdef USE_ARDUINO
|
||||
HardwareSerial *get_hw_serial() const { return hw_serial_; }
|
||||
Stream *get_hw_serial() const { return hw_serial_; }
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
uart_port_t get_uart_num() const { return uart_num_; }
|
||||
@@ -119,7 +129,7 @@ class Logger : public Component {
|
||||
int tx_buffer_size_{0};
|
||||
UARTSelection uart_{UART_SELECTION_UART0};
|
||||
#ifdef USE_ARDUINO
|
||||
HardwareSerial *hw_serial_{nullptr};
|
||||
Stream *hw_serial_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
uart_port_t uart_num_;
|
||||
|
||||
@@ -56,6 +56,11 @@ template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
|
||||
void play(Ts... x) override { this->parent_->do_power_off(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PowerToggleAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_power_toggle(); }
|
||||
};
|
||||
|
||||
} // namespace ac
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
||||
|
||||
@@ -39,6 +39,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
|
||||
void do_beeper_off() { this->set_beeper_feedback(false); }
|
||||
void do_power_on() { this->base_.setPowerState(true); }
|
||||
void do_power_off() { this->base_.setPowerState(false); }
|
||||
void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); }
|
||||
void set_supported_modes(const std::set<ClimateMode> &modes) { this->supported_modes_ = modes; }
|
||||
void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->supported_swing_modes_ = modes; }
|
||||
void set_supported_presets(const std::set<ClimatePreset> &presets) { this->supported_presets_ = presets; }
|
||||
|
||||
@@ -113,7 +113,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
|
||||
cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
|
||||
cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
|
||||
cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
|
||||
cv.OnlyWith(CONF_TRANSMITTER_ID, "remote_transmitter"): cv.use_id(
|
||||
remote_transmitter.RemoteTransmitterComponent
|
||||
),
|
||||
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
|
||||
@@ -163,6 +163,7 @@ BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action)
|
||||
BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action)
|
||||
PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action)
|
||||
PowerOffAction = midea_ac_ns.class_("PowerOffAction", automation.Action)
|
||||
PowerToggleAction = midea_ac_ns.class_("PowerToggleAction", automation.Action)
|
||||
|
||||
MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -249,6 +250,16 @@ async def power_off_to_code(var, config, args):
|
||||
pass
|
||||
|
||||
|
||||
# Power Toggle action
|
||||
@register_action(
|
||||
"power_toggle",
|
||||
PowerToggleAction,
|
||||
cv.Schema({}),
|
||||
)
|
||||
async def power_inv_to_code(var, config, args):
|
||||
pass
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -68,33 +68,54 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
|
||||
|
||||
uint8_t data_len = raw[2];
|
||||
uint8_t data_offset = 3;
|
||||
// the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
|
||||
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
|
||||
data_offset = 2;
|
||||
data_len = 4;
|
||||
}
|
||||
|
||||
// Error ( msb indicates error )
|
||||
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
|
||||
if ((function_code & 0x80) == 0x80) {
|
||||
data_offset = 2;
|
||||
data_len = 1;
|
||||
}
|
||||
// Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes
|
||||
if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) {
|
||||
// Handle user-defined function, since we don't know how big this ought to be,
|
||||
// ideally we should delegate the entire length detection to whatever handler is
|
||||
// installed, but wait, there is the CRC, and if we get a hit there is a good
|
||||
// chance that this is a complete message ... admittedly there is a small chance is
|
||||
// isn't but that is quite small given the purpose of the CRC in the first place
|
||||
data_len = at;
|
||||
data_offset = 1;
|
||||
|
||||
// Byte data_offset..data_offset+data_len-1: Data
|
||||
if (at < data_offset + data_len)
|
||||
return true;
|
||||
uint16_t computed_crc = crc16(raw, data_offset + data_len);
|
||||
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
|
||||
|
||||
// Byte 3+data_len: CRC_LO (over all bytes)
|
||||
if (at == data_offset + data_len)
|
||||
return true;
|
||||
if (computed_crc != remote_crc)
|
||||
return true;
|
||||
|
||||
// Byte data_offset+len+1: CRC_HI (over all bytes)
|
||||
uint16_t computed_crc = crc16(raw, data_offset + data_len);
|
||||
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
|
||||
if (computed_crc != remote_crc) {
|
||||
ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
|
||||
return false;
|
||||
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
|
||||
|
||||
} else {
|
||||
// the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands
|
||||
if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
|
||||
data_offset = 2;
|
||||
data_len = 4;
|
||||
}
|
||||
|
||||
// Error ( msb indicates error )
|
||||
// response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc
|
||||
if ((function_code & 0x80) == 0x80) {
|
||||
data_offset = 2;
|
||||
data_len = 1;
|
||||
}
|
||||
|
||||
// Byte data_offset..data_offset+data_len-1: Data
|
||||
if (at < data_offset + data_len)
|
||||
return true;
|
||||
|
||||
// Byte 3+data_len: CRC_LO (over all bytes)
|
||||
if (at == data_offset + data_len)
|
||||
return true;
|
||||
|
||||
// Byte data_offset+len+1: CRC_HI (over all bytes)
|
||||
uint16_t computed_crc = crc16(raw, data_offset + data_len);
|
||||
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
|
||||
if (computed_crc != remote_crc) {
|
||||
ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
std::vector<uint8_t> data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len);
|
||||
bool found = false;
|
||||
|
||||
@@ -52,7 +52,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
||||
|
||||
// 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]);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace mopeka_pro_check {
|
||||
enum SensorType {
|
||||
STANDARD_BOTTOM_UP = 0x03,
|
||||
TOP_DOWN_AIR_ABOVE = 0x04,
|
||||
BOTTOM_UP_WATER = 0x05
|
||||
BOTTOM_UP_WATER = 0x05,
|
||||
PLUS_BOTTOM_UP = 0x08
|
||||
// all other values are reserved
|
||||
};
|
||||
|
||||
|
||||
@@ -556,7 +556,12 @@ void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; }
|
||||
|
||||
void MQTTClientComponent::disable_discovery() {
|
||||
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() {
|
||||
if (!this->shutdown_message_.topic.empty()) {
|
||||
|
||||
@@ -277,6 +277,7 @@ class MQTTClientComponent : public Component {
|
||||
.retain = true,
|
||||
.clean = false,
|
||||
.unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR,
|
||||
.object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR,
|
||||
};
|
||||
std::string topic_prefix_{};
|
||||
MQTTMessage log_message_;
|
||||
|
||||
@@ -51,10 +51,9 @@ void MQTTCoverComponent::setup() {
|
||||
void MQTTCoverComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
|
||||
auto traits = this->cover_->get_traits();
|
||||
// no state topic for position
|
||||
bool state_topic = !traits.get_supports_position();
|
||||
LOG_MQTT_COMPONENT(state_topic, true)
|
||||
if (!state_topic) {
|
||||
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic)
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic().c_str());
|
||||
}
|
||||
@@ -72,7 +71,6 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
|
||||
root[MQTT_OPTIMISTIC] = true;
|
||||
}
|
||||
if (traits.get_supports_position()) {
|
||||
config.state_topic = false;
|
||||
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
|
||||
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
|
||||
}
|
||||
@@ -92,17 +90,7 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
|
||||
bool MQTTCoverComponent::publish_state() {
|
||||
auto traits = this->cover_->get_traits();
|
||||
bool success = true;
|
||||
if (!traits.get_supports_position()) {
|
||||
const char *state_s = "unknown";
|
||||
if (this->cover_->position == COVER_OPEN) {
|
||||
state_s = "open";
|
||||
} else if (this->cover_->position == COVER_CLOSED) {
|
||||
state_s = "closed";
|
||||
}
|
||||
|
||||
if (!this->publish(this->get_state_topic_(), state_s))
|
||||
success = false;
|
||||
} else {
|
||||
if (traits.get_supports_position()) {
|
||||
std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0);
|
||||
if (!this->publish(this->get_position_state_topic(), pos))
|
||||
success = false;
|
||||
@@ -112,6 +100,14 @@ bool MQTTCoverComponent::publish_state() {
|
||||
if (!this->publish(this->get_tilt_state_topic(), pos))
|
||||
success = false;
|
||||
}
|
||||
const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening"
|
||||
: this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing"
|
||||
: this->cover_->position == COVER_CLOSED ? "closed"
|
||||
: this->cover_->position == COVER_OPEN ? "open"
|
||||
: traits.get_supports_position() ? "open"
|
||||
: "unknown";
|
||||
if (!this->publish(this->get_state_topic_(), state_s))
|
||||
success = false;
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#ifdef USE_MQTT
|
||||
#ifdef USE_FAN
|
||||
#include "esphome/components/fan/fan_helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mqtt {
|
||||
@@ -88,17 +87,6 @@ void MQTTFanComponent::setup() {
|
||||
});
|
||||
}
|
||||
|
||||
if (this->state_->get_traits().supports_speed()) {
|
||||
this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
this->state_->make_call()
|
||||
.set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations)
|
||||
.perform();
|
||||
#pragma GCC diagnostic pop
|
||||
});
|
||||
}
|
||||
|
||||
auto f = std::bind(&MQTTFanComponent::publish_state, this);
|
||||
this->state_->add_on_state_callback([this, f]() { this->defer("send", f); });
|
||||
}
|
||||
@@ -113,8 +101,6 @@ void MQTTFanComponent::dump_config() {
|
||||
if (this->state_->get_traits().supports_speed()) {
|
||||
ESP_LOGCONFIG(TAG, " Speed Level State Topic: '%s'", this->get_speed_level_state_topic().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Speed Level Command Topic: '%s'", this->get_speed_level_command_topic().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Speed State Topic: '%s'", this->get_speed_state_topic().c_str());
|
||||
ESP_LOGCONFIG(TAG, " Speed Command Topic: '%s'", this->get_speed_command_topic().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,10 +112,8 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
|
||||
root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic();
|
||||
}
|
||||
if (this->state_->get_traits().supports_speed()) {
|
||||
root["speed_level_command_topic"] = this->get_speed_level_command_topic();
|
||||
root["speed_level_state_topic"] = this->get_speed_level_state_topic();
|
||||
root[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic();
|
||||
root[MQTT_SPEED_STATE_TOPIC] = this->get_speed_state_topic();
|
||||
root[MQTT_PERCENTAGE_COMMAND_TOPIC] = this->get_speed_level_command_topic();
|
||||
root[MQTT_PERCENTAGE_STATE_TOPIC] = this->get_speed_level_state_topic();
|
||||
}
|
||||
}
|
||||
bool MQTTFanComponent::publish_state() {
|
||||
@@ -148,31 +132,6 @@ bool MQTTFanComponent::publish_state() {
|
||||
bool success = this->publish(this->get_speed_level_state_topic(), payload);
|
||||
failed = failed || !success;
|
||||
}
|
||||
if (traits.supports_speed()) {
|
||||
const char *payload;
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
// NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations)
|
||||
switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) {
|
||||
case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations)
|
||||
payload = "low";
|
||||
break;
|
||||
}
|
||||
case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations)
|
||||
payload = "medium";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations)
|
||||
payload = "high";
|
||||
break;
|
||||
}
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
bool success = this->publish(this->get_speed_state_topic(), payload);
|
||||
failed = failed || !success;
|
||||
}
|
||||
|
||||
return !failed;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ void MQTTSelectComponent::setup() {
|
||||
call.set_option(state);
|
||||
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() {
|
||||
|
||||
@@ -14,6 +14,8 @@ from esphome.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_MQTT_ID,
|
||||
CONF_VALUE,
|
||||
CONF_OPERATION,
|
||||
CONF_CYCLE,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
@@ -35,6 +37,7 @@ ValueRangeTrigger = number_ns.class_(
|
||||
|
||||
# Actions
|
||||
NumberSetAction = number_ns.class_("NumberSetAction", automation.Action)
|
||||
NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action)
|
||||
|
||||
# Conditions
|
||||
NumberInRangeCondition = number_ns.class_(
|
||||
@@ -49,6 +52,15 @@ NUMBER_MODES = {
|
||||
"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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
OPERATION_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Number),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"number.set",
|
||||
NumberSetAction,
|
||||
cv.Schema(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Number),
|
||||
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)
|
||||
cg.add(var.set_value(template_))
|
||||
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_;
|
||||
};
|
||||
|
||||
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 {
|
||||
public:
|
||||
explicit ValueRangeTrigger(Number *parent) : parent_(parent) {}
|
||||
|
||||
@@ -6,30 +6,6 @@ namespace 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) {
|
||||
this->has_state_ = true;
|
||||
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));
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
} // namespace number
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "number_call.h"
|
||||
#include "number_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace number {
|
||||
@@ -20,54 +22,6 @@ namespace 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.
|
||||
*
|
||||
* 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);
|
||||
|
||||
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);
|
||||
|
||||
|
||||
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() {
|
||||
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) {
|
||||
// last transmission too long ago. Reset RX index.
|
||||
this->data_index_ = 0;
|
||||
@@ -65,6 +106,7 @@ void PMSX003Component::loop() {
|
||||
// finished
|
||||
this->parse_data_();
|
||||
this->data_index_ = 0;
|
||||
this->last_update_ = now;
|
||||
} else if (!*check) {
|
||||
// wrong data
|
||||
this->data_index_ = 0;
|
||||
@@ -131,6 +173,25 @@ optional<bool> PMSX003Component::check_byte_() {
|
||||
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_() {
|
||||
switch (this->type_) {
|
||||
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();
|
||||
}
|
||||
uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) {
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
namespace esphome {
|
||||
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 {
|
||||
PMSX003_TYPE_X003 = 0,
|
||||
PMSX003_TYPE_5003T,
|
||||
@@ -14,6 +21,12 @@ enum PMSX003Type {
|
||||
PMSX003_TYPE_5003S,
|
||||
};
|
||||
|
||||
enum PMSX003State {
|
||||
PMSX003_STATE_IDLE = 0,
|
||||
PMSX003_STATE_STABILISING,
|
||||
PMSX003_STATE_WAITING,
|
||||
};
|
||||
|
||||
class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
PMSX003Component() = default;
|
||||
@@ -23,6 +36,8 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
|
||||
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_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);
|
||||
@@ -45,11 +60,17 @@ class PMSX003Component : public uart::UARTDevice, public Component {
|
||||
protected:
|
||||
optional<bool> check_byte_();
|
||||
void parse_data_();
|
||||
void send_command_(uint8_t cmd, uint16_t data);
|
||||
uint16_t get_16_bit_uint_(uint8_t start_index);
|
||||
|
||||
uint8_t data_[64];
|
||||
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 update_interval_{0};
|
||||
PMSX003State state_{PMSX003_STATE_IDLE};
|
||||
PMSX003Type type_;
|
||||
|
||||
// "Standard Particle"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import sensor, uart
|
||||
|
||||
from esphome.const import (
|
||||
CONF_FORMALDEHYDE,
|
||||
CONF_HUMIDITY,
|
||||
@@ -17,6 +18,7 @@ from esphome.const import (
|
||||
CONF_PM_2_5UM,
|
||||
CONF_PM_5_0UM,
|
||||
CONF_PM_10_0UM,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TYPE,
|
||||
DEVICE_CLASS_PM1,
|
||||
@@ -44,6 +46,7 @@ TYPE_PMS5003ST = "PMS5003ST"
|
||||
TYPE_PMS5003S = "PMS5003S"
|
||||
|
||||
PMSX003Type = pmsx003_ns.enum("PMSX003Type")
|
||||
|
||||
PMSX003_TYPES = {
|
||||
TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003,
|
||||
TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T,
|
||||
@@ -68,6 +71,17 @@ def validate_pmsx003_sensors(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 = (
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -157,6 +171,7 @@ CONFIG_SCHEMA = (
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval,
|
||||
}
|
||||
)
|
||||
.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):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -230,3 +256,5 @@ async def to_code(config):
|
||||
if CONF_FORMALDEHYDE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE])
|
||||
cg.add(var.set_formaldehyde_sensor(sens))
|
||||
|
||||
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
|
||||
|
||||
@@ -345,6 +345,8 @@ float PN532::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void PN532::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PN532:");
|
||||
if (this->is_failed())
|
||||
ESP_LOGE(TAG, "Component marked as failed. Check setup logs.");
|
||||
switch (this->error_code_) {
|
||||
case NONE:
|
||||
break;
|
||||
|
||||
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(this->value_.value(x...));
|
||||
}
|
||||
}
|
||||
|
||||
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_AUTOMATIC_SELF_CALIBRATION = 0x2416;
|
||||
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_READ_MEASUREMENT = 0xec05;
|
||||
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_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 uint16_t SCD41_ID = 0x1408;
|
||||
static const uint16_t SCD40_ID = 0x440;
|
||||
|
||||
void SCD4XComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up scd4x...");
|
||||
|
||||
// the sensor needs 1000 ms to enter the idle state
|
||||
this->set_timeout(1000, [this]() {
|
||||
uint16_t raw_read_status;
|
||||
if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) {
|
||||
ESP_LOGE(TAG, "Failed to read data ready status");
|
||||
this->status_clear_error();
|
||||
if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) {
|
||||
ESP_LOGE(TAG, "Failed to stop measurements");
|
||||
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(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]() {
|
||||
// According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after
|
||||
// issuing the stop_periodic_measurement command
|
||||
this->set_timeout(500, [this]() {
|
||||
uint16_t raw_serial_number[3];
|
||||
if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) {
|
||||
ESP_LOGE(TAG, "Failed to read serial number");
|
||||
@@ -89,15 +82,9 @@ void SCD4XComponent::setup() {
|
||||
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;
|
||||
// Finally start sensor measurements
|
||||
this->start_measurement_();
|
||||
ESP_LOGD(TAG, "Sensor initialized");
|
||||
});
|
||||
});
|
||||
@@ -123,12 +110,31 @@ void SCD4XComponent::dump_config() {
|
||||
}
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_));
|
||||
if (this->ambient_pressure_compensation_) {
|
||||
ESP_LOGCONFIG(TAG, " Altitude compensation disabled");
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_);
|
||||
if (this->ambient_pressure_source_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using sensor '%s'",
|
||||
this->ambient_pressure_source_->get_name().c_str());
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled");
|
||||
ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_);
|
||||
if (this->ambient_pressure_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_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
@@ -149,47 +155,105 @@ void SCD4XComponent::update() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if data is ready
|
||||
if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
uint32_t wait_time = 0;
|
||||
if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) {
|
||||
start_measurement_();
|
||||
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;
|
||||
if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Data not ready yet!");
|
||||
return;
|
||||
}
|
||||
uint16_t raw_read_status;
|
||||
|
||||
if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
|
||||
ESP_LOGW(TAG, "Error reading measurement!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
if (!this->read_data(raw_read_status) || raw_read_status == 0x00) {
|
||||
this->status_set_warning();
|
||||
ESP_LOGW(TAG, "Data not ready yet!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read off sensor data
|
||||
uint16_t raw_data[3];
|
||||
if (!this->read_data(raw_data, 3)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) {
|
||||
ESP_LOGW(TAG, "Error reading measurement!");
|
||||
this->status_set_warning();
|
||||
return; // NO RETRY
|
||||
}
|
||||
// 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)
|
||||
this->co2_sensor_->publish_state(raw_data[0]);
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
const float humidity = (100.0f * raw_data[2]) / (1 << 16);
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
|
||||
this->status_clear_warning();
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16);
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
}
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
const float humidity = (100.0f * raw_data[2]) / (1 << 16);
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}); // set_timeout
|
||||
}
|
||||
|
||||
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
|
||||
void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) {
|
||||
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 esphome
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/sensirion_common/i2c_sensirion.h"
|
||||
@@ -7,7 +8,14 @@
|
||||
namespace esphome {
|
||||
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 {
|
||||
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_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; };
|
||||
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:
|
||||
bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa);
|
||||
|
||||
bool start_measurement_();
|
||||
ERRORCODE error_code_;
|
||||
|
||||
bool initialized_{false};
|
||||
@@ -38,7 +49,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri
|
||||
bool ambient_pressure_compensation_;
|
||||
uint16_t ambient_pressure_;
|
||||
bool enable_asc_;
|
||||
|
||||
MeasurementMode measurement_mode_{PERIODIC};
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
|
||||
@@ -2,11 +2,15 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components import sensirion_common
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_CO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_VALUE,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -19,7 +23,7 @@ from esphome.const import (
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@sjtrny"]
|
||||
CODEOWNERS = ["@sjtrny", "@martgras"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensirion_common"]
|
||||
|
||||
@@ -27,12 +31,29 @@ scd4x_ns = cg.esphome_ns.namespace("scd4x")
|
||||
SCD4XComponent = scd4x_ns.class_(
|
||||
"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_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation"
|
||||
CONF_TEMPERATURE_OFFSET = "temperature_offset"
|
||||
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 = (
|
||||
cv.Schema(
|
||||
@@ -69,6 +90,9 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
|
||||
sensor.Sensor
|
||||
),
|
||||
cv.Optional(CONF_MEASUREMENT_MODE, default="periodic"): cv.enum(
|
||||
MEASUREMENT_MODE_OPTIONS, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
@@ -106,3 +130,42 @@ async def to_code(config):
|
||||
if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config:
|
||||
sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE])
|
||||
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_TRIGGER_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_CYCLE,
|
||||
CONF_MODE,
|
||||
CONF_OPERATION,
|
||||
CONF_INDEX,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
@@ -22,14 +26,27 @@ SelectPtr = Select.operator("ptr")
|
||||
|
||||
# Triggers
|
||||
SelectStateTrigger = select_ns.class_(
|
||||
"SelectStateTrigger", automation.Trigger.template(cg.float_)
|
||||
"SelectStateTrigger",
|
||||
automation.Trigger.template(cg.std_string, cg.size_t),
|
||||
)
|
||||
|
||||
# Actions
|
||||
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
|
||||
|
||||
|
||||
SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
|
||||
{
|
||||
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, []):
|
||||
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:
|
||||
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
|
||||
@@ -76,12 +95,18 @@ async def to_code(config):
|
||||
cg.add_global(select_ns.using)
|
||||
|
||||
|
||||
OPERATION_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Select),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"select.set",
|
||||
SelectSetAction,
|
||||
cv.Schema(
|
||||
OPERATION_BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Select),
|
||||
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)
|
||||
cg.add(var.set_option(template_))
|
||||
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 select {
|
||||
|
||||
class SelectStateTrigger : public Trigger<std::string> {
|
||||
class SelectStateTrigger : public Trigger<std::string, size_t> {
|
||||
public:
|
||||
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...> {
|
||||
public:
|
||||
SelectSetAction(Select *select) : select_(select) {}
|
||||
explicit SelectSetAction(Select *select) : select_(select) {}
|
||||
TEMPLATABLE_VALUE(std::string, option)
|
||||
|
||||
void play(Ts... x) override {
|
||||
@@ -29,5 +29,39 @@ template<typename... Ts> class SelectSetAction : public Action<Ts...> {
|
||||
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 esphome
|
||||
|
||||
@@ -6,37 +6,58 @@ namespace 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) {
|
||||
this->has_state_ = true;
|
||||
this->state = state;
|
||||
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str());
|
||||
this->state_callback_.call(state);
|
||||
auto index = this->index_of(state);
|
||||
const auto *name = this->get_name().c_str();
|
||||
if (index.has_value()) {
|
||||
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));
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
} // namespace select
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "select_call.h"
|
||||
#include "select_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
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.
|
||||
*
|
||||
* A select can use publish_state to send out a new value.
|
||||
@@ -51,19 +24,36 @@ class SelectTraits {
|
||||
class Select : public EntityBase {
|
||||
public:
|
||||
std::string state;
|
||||
SelectTraits traits;
|
||||
|
||||
void publish_state(const std::string &state);
|
||||
|
||||
SelectCall make_call() { return SelectCall(this); }
|
||||
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.
|
||||
/// Return whether this select component has gotten a full state yet.
|
||||
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:
|
||||
friend class SelectCall;
|
||||
|
||||
@@ -77,7 +67,7 @@ class Select : public EntityBase {
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void(std::string)> state_callback_;
|
||||
CallbackManager<void(std::string, size_t)> state_callback_;
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user