diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 433e5d2792..ab4f8cc960 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,56 +1,64 @@
{
"name": "ESPHome Dev",
- "image": "esphome/esphome-lint:dev",
+ "image": "ghcr.io/esphome/esphome-lint:dev",
"postCreateCommand": [
"script/devcontainer-post-create"
],
+ "containerEnv": {
+ "DEVCONTAINER": "1"
+ },
"runArgs": [
"--privileged",
"-e",
"ESPHOME_DASHBOARD_USE_PING=1"
],
"appPort": 6052,
- "extensions": [
- // python
- "ms-python.python",
- "visualstudioexptteam.vscodeintellicode",
- // yaml
- "redhat.vscode-yaml",
- // cpp
- "ms-vscode.cpptools",
- // editorconfig
- "editorconfig.editorconfig",
- ],
- "settings": {
- "python.languageServer": "Pylance",
- "python.pythonPath": "/usr/bin/python3",
- "python.linting.pylintEnabled": true,
- "python.linting.enabled": true,
- "python.formatting.provider": "black",
- "editor.formatOnPaste": false,
- "editor.formatOnSave": true,
- "editor.formatOnType": true,
- "files.trimTrailingWhitespace": true,
- "terminal.integrated.defaultProfile.linux": "bash",
- "yaml.customTags": [
- "!secret scalar",
- "!lambda scalar",
- "!include_dir_named scalar",
- "!include_dir_list scalar",
- "!include_dir_merge_list scalar",
- "!include_dir_merge_named scalar"
- ],
- "files.exclude": {
- "**/.git": true,
- "**/.DS_Store": true,
- "**/*.pyc": {
- "when": "$(basename).py"
- },
- "**/__pycache__": true
- },
- "files.associations": {
- "**/.vscode/*.json": "jsonc"
- },
- "C_Cpp.clang_format_path": "/usr/bin/clang-format-11",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ // python
+ "ms-python.python",
+ "visualstudioexptteam.vscodeintellicode",
+ // yaml
+ "redhat.vscode-yaml",
+ // cpp
+ "ms-vscode.cpptools",
+ // editorconfig
+ "editorconfig.editorconfig",
+ ],
+ "settings": {
+ "python.languageServer": "Pylance",
+ "python.pythonPath": "/usr/bin/python3",
+ "python.linting.pylintEnabled": true,
+ "python.linting.enabled": true,
+ "python.formatting.provider": "black",
+ "editor.formatOnPaste": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "files.trimTrailingWhitespace": true,
+ "terminal.integrated.defaultProfile.linux": "bash",
+ "yaml.customTags": [
+ "!secret scalar",
+ "!lambda scalar",
+ "!extend scalar",
+ "!include_dir_named scalar",
+ "!include_dir_list scalar",
+ "!include_dir_merge_list scalar",
+ "!include_dir_merge_named scalar"
+ ],
+ "files.exclude": {
+ "**/.git": true,
+ "**/.DS_Store": true,
+ "**/*.pyc": {
+ "when": "$(basename).py"
+ },
+ "**/__pycache__": true
+ },
+ "files.associations": {
+ "**/.vscode/*.json": "jsonc"
+ },
+ "C_Cpp.clang_format_path": "/usr/bin/clang-format-13"
+ }
+ }
}
}
diff --git a/.editorconfig b/.editorconfig
index 8ccf1eeebc..9e203f60e4 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -25,10 +25,9 @@ indent_size = 2
[*.{yaml,yml}]
indent_style = space
indent_size = 2
-quote_type = single
+quote_type = double
# JSON
[*.json]
indent_style = space
indent_size = 2
-
diff --git a/.gitattributes b/.gitattributes
index dad0966222..1b3fd332b4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,3 @@
# Normalize line endings to LF in the repository
* text eol=lf
+*.png binary
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 864586fe6b..a8ca63d158 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,3 +1,4 @@
+---
# These are supported funding model platforms
custom: https://www.nabucasa.com
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 4add58dfbe..804dad47c7 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,3 +1,4 @@
+---
blank_issues_enabled: false
contact_links:
- name: Issue Tracker
@@ -5,8 +6,10 @@ contact_links:
about: Please create bug reports in the dedicated issue tracker.
- name: Feature Request Tracker
url: https://github.com/esphome/feature-requests
- about: Please create feature requests in the dedicated feature request tracker.
+ about: |
+ Please create feature requests in the dedicated feature request tracker.
- name: Frequently Asked Question
url: https://esphome.io/guides/faq.html
- about: Please view the FAQ for common questions and what to include in a bug report.
-
+ about: |
+ Please view the FAQ for common questions and what
+ to include in a bug report.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 25411c19f5..3221b8ac5c 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,6 +1,6 @@
-# What does this implement/fix?
+# What does this implement/fix?
-Quick description and explanation of changes
+
## Types of changes
@@ -18,6 +18,7 @@ Quick description and explanation of changes
- [ ] ESP32
- [ ] ESP32 IDF
- [ ] ESP8266
+- [ ] RP2040
## Example entry for `config.yaml`:
-
-
-
-
-
- {{ App.get_name() }}
-
-
-
-
-
-
WiFi Networks
-
- The ESP will now try to connect to the network...
- Please give it some time to connect.
- Note: Copy the changed network to your YAML file - the next OTA update will overwrite these settings.
-
-
-
-
-
WiFi Settings
-
-
-
-
-
OTA Update
-
-
-
-
diff --git a/esphome/components/captive_portal/lock.svg b/esphome/components/captive_portal/lock.svg
deleted file mode 100644
index 743a1cc55a..0000000000
--- a/esphome/components/captive_portal/lock.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/esphome/components/captive_portal/stylesheet.css b/esphome/components/captive_portal/stylesheet.css
deleted file mode 100644
index 73f82f05f1..0000000000
--- a/esphome/components/captive_portal/stylesheet.css
+++ /dev/null
@@ -1,58 +0,0 @@
-* {
- box-sizing: inherit;
-}
-
-div, input {
- padding: 5px;
- font-size: 1em;
-}
-
-input {
- width: 95%;
-}
-
-body {
- text-align: center;
- font-family: sans-serif;
-}
-
-button {
- border: 0;
- border-radius: 0.3rem;
- background-color: #1fa3ec;
- color: #fff;
- line-height: 2.4rem;
- font-size: 1.2rem;
- width: 100%;
- padding: 0;
-}
-
-.main {
- text-align: left;
- display: inline-block;
- min-width: 260px;
-}
-
-.network {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.network-left {
- display: flex;
- align-items: center;
-}
-
-.network-ssid {
- margin-bottom: -7px;
- margin-left: 10px;
-}
-
-.info {
- border: 1px solid;
- margin: 10px 0px;
- padding: 15px 10px;
- color: #4f8a10;
- background-color: #dff2bf;
-}
diff --git a/esphome/components/captive_portal/wifi-strength-1.svg b/esphome/components/captive_portal/wifi-strength-1.svg
deleted file mode 100644
index 189a38193c..0000000000
--- a/esphome/components/captive_portal/wifi-strength-1.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/esphome/components/captive_portal/wifi-strength-2.svg b/esphome/components/captive_portal/wifi-strength-2.svg
deleted file mode 100644
index 9b4b2d2396..0000000000
--- a/esphome/components/captive_portal/wifi-strength-2.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/esphome/components/captive_portal/wifi-strength-3.svg b/esphome/components/captive_portal/wifi-strength-3.svg
deleted file mode 100644
index 44b7532bb7..0000000000
--- a/esphome/components/captive_portal/wifi-strength-3.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/esphome/components/captive_portal/wifi-strength-4.svg b/esphome/components/captive_portal/wifi-strength-4.svg
deleted file mode 100644
index a22b0b8281..0000000000
--- a/esphome/components/captive_portal/wifi-strength-4.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp
index 5c60989afa..f1dadf673a 100644
--- a/esphome/components/ccs811/ccs811.cpp
+++ b/esphome/components/ccs811/ccs811.cpp
@@ -145,8 +145,8 @@ void CCS811Component::send_env_data_() {
// https://github.com/adafruit/Adafruit_CCS811/blob/0990f5c620354d8bc087c4706bec091d8e6e5dfd/Adafruit_CCS811.cpp#L135-L142
uint16_t hum_conv = static_cast(lroundf(humidity * 512.0f + 0.5f));
uint16_t temp_conv = static_cast(lroundf(temperature * 512.0f + 0.5f));
- this->write_bytes(0x05, {(uint8_t)((hum_conv >> 8) & 0xff), (uint8_t)((hum_conv & 0xff)),
- (uint8_t)((temp_conv >> 8) & 0xff), (uint8_t)((temp_conv & 0xff))});
+ this->write_bytes(0x05, {(uint8_t) ((hum_conv >> 8) & 0xff), (uint8_t) ((hum_conv & 0xff)),
+ (uint8_t) ((temp_conv >> 8) & 0xff), (uint8_t) ((temp_conv & 0xff))});
}
void CCS811Component::dump_config() {
ESP_LOGCONFIG(TAG, "CCS811");
diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py
index bb8200273d..af3e6574ab 100644
--- a/esphome/components/ccs811/sensor.py
+++ b/esphome/components/ccs811/sensor.py
@@ -2,12 +2,11 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor, text_sensor
from esphome.const import (
- CONF_ICON,
CONF_ID,
ICON_RADIATOR,
ICON_RESTART,
DEVICE_CLASS_CARBON_DIOXIDE,
- DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
+ DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
UNIT_PARTS_PER_BILLION,
@@ -44,14 +43,11 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
- device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
+ device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
- cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon,
- }
+ cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
+ icon=ICON_RESTART
),
cv.Optional(CONF_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor),
@@ -74,8 +70,7 @@ async def to_code(config):
cg.add(var.set_tvoc(sens))
if CONF_VERSION in config:
- sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID])
- await text_sensor.register_text_sensor(sens, config[CONF_VERSION])
+ sens = await text_sensor.new_text_sensor(config[CONF_VERSION])
cg.add(var.set_version(sens))
if CONF_BASELINE in config:
diff --git a/esphome/components/cd74hc4067/__init__.py b/esphome/components/cd74hc4067/__init__.py
index 4fb15d1bf3..d57061b710 100644
--- a/esphome/components/cd74hc4067/__init__.py
+++ b/esphome/components/cd74hc4067/__init__.py
@@ -27,10 +27,10 @@ DEFAULT_DELAY = "2ms"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CD74HC4067Component),
- cv.Required(CONF_PIN_S0): pins.internal_gpio_output_pin_schema,
- cv.Required(CONF_PIN_S1): pins.internal_gpio_output_pin_schema,
- cv.Required(CONF_PIN_S2): pins.internal_gpio_output_pin_schema,
- cv.Required(CONF_PIN_S3): pins.internal_gpio_output_pin_schema,
+ cv.Required(CONF_PIN_S0): pins.gpio_output_pin_schema,
+ cv.Required(CONF_PIN_S1): pins.gpio_output_pin_schema,
+ cv.Required(CONF_PIN_S2): pins.gpio_output_pin_schema,
+ cv.Required(CONF_PIN_S3): pins.gpio_output_pin_schema,
cv.Optional(
CONF_DELAY, default=DEFAULT_DELAY
): cv.positive_time_period_milliseconds,
diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h
index 4a5c2e4e35..6193513575 100644
--- a/esphome/components/cd74hc4067/cd74hc4067.h
+++ b/esphome/components/cd74hc4067/cd74hc4067.h
@@ -19,22 +19,22 @@ class CD74HC4067Component : public Component {
void activate_pin(uint8_t pin);
/// set the pin connected to multiplexer control pin 0
- void set_pin_s0(InternalGPIOPin *pin) { this->pin_s0_ = pin; }
+ void set_pin_s0(GPIOPin *pin) { this->pin_s0_ = pin; }
/// set the pin connected to multiplexer control pin 1
- void set_pin_s1(InternalGPIOPin *pin) { this->pin_s1_ = pin; }
+ void set_pin_s1(GPIOPin *pin) { this->pin_s1_ = pin; }
/// set the pin connected to multiplexer control pin 2
- void set_pin_s2(InternalGPIOPin *pin) { this->pin_s2_ = pin; }
+ void set_pin_s2(GPIOPin *pin) { this->pin_s2_ = pin; }
/// set the pin connected to multiplexer control pin 3
- void set_pin_s3(InternalGPIOPin *pin) { this->pin_s3_ = pin; }
+ void set_pin_s3(GPIOPin *pin) { this->pin_s3_ = pin; }
/// set the delay needed after an input switch
void set_switch_delay(uint32_t switch_delay) { this->switch_delay_ = switch_delay; }
private:
- InternalGPIOPin *pin_s0_;
- InternalGPIOPin *pin_s1_;
- InternalGPIOPin *pin_s2_;
- InternalGPIOPin *pin_s3_;
+ GPIOPin *pin_s0_;
+ GPIOPin *pin_s1_;
+ GPIOPin *pin_s2_;
+ GPIOPin *pin_s3_;
/// the currently active pin
uint8_t active_pin_;
uint32_t switch_delay_;
diff --git a/esphome/components/cd74hc4067/sensor.py b/esphome/components/cd74hc4067/sensor.py
index 7c7cf9ccb7..3eee34b85e 100644
--- a/esphome/components/cd74hc4067/sensor.py
+++ b/esphome/components/cd74hc4067/sensor.py
@@ -25,6 +25,7 @@ CONF_CD74HC4067_ID = "cd74hc4067_id"
CONFIG_SCHEMA = (
sensor.sensor_schema(
+ CD74HC4067Sensor,
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_VOLTAGE,
@@ -33,7 +34,6 @@ CONFIG_SCHEMA = (
)
.extend(
{
- cv.GenerateID(): cv.declare_id(CD74HC4067Sensor),
cv.GenerateID(CONF_CD74HC4067_ID): cv.use_id(CD74HC4067Component),
cv.Required(CONF_NUMBER): cv.int_range(0, 15),
cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler),
@@ -47,8 +47,8 @@ async def to_code(config):
parent = await cg.get_variable(config[CONF_CD74HC4067_ID])
var = cg.new_Pvariable(config[CONF_ID], parent)
- await cg.register_component(var, config)
await sensor.register_sensor(var, config)
+ await cg.register_component(var, config)
cg.add(var.set_pin(config[CONF_NUMBER]))
sens = await cg.get_variable(config[CONF_SENSOR])
diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py
index 87b9a4b3e2..bf167fe837 100644
--- a/esphome/components/climate/__init__.py
+++ b/esphome/components/climate/__init__.py
@@ -20,8 +20,11 @@ from esphome.const import (
CONF_MODE,
CONF_MODE_COMMAND_TOPIC,
CONF_MODE_STATE_TOPIC,
+ CONF_ON_CONTROL,
CONF_ON_STATE,
CONF_PRESET,
+ CONF_PRESET_COMMAND_TOPIC,
+ CONF_PRESET_STATE_TOPIC,
CONF_SWING_MODE,
CONF_SWING_MODE_COMMAND_TOPIC,
CONF_SWING_MODE_STATE_TOPIC,
@@ -73,6 +76,7 @@ CLIMATE_FAN_MODES = {
"MIDDLE": ClimateFanMode.CLIMATE_FAN_MIDDLE,
"FOCUS": ClimateFanMode.CLIMATE_FAN_FOCUS,
"DIFFUSE": ClimateFanMode.CLIMATE_FAN_DIFFUSE,
+ "QUIET": ClimateFanMode.CLIMATE_FAN_QUIET,
}
validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True)
@@ -101,9 +105,40 @@ CLIMATE_SWING_MODES = {
validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True)
+CONF_CURRENT_TEMPERATURE = "current_temperature"
+
+visual_temperature = cv.float_with_unit(
+ "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?"
+)
+
+
+def single_visual_temperature(value):
+ if isinstance(value, dict):
+ return value
+
+ value = visual_temperature(value)
+ return VISUAL_TEMPERATURE_STEP_SCHEMA(
+ {
+ CONF_TARGET_TEMPERATURE: value,
+ CONF_CURRENT_TEMPERATURE: value,
+ }
+ )
+
+
# Actions
ControlAction = climate_ns.class_("ControlAction", automation.Action)
StateTrigger = climate_ns.class_("StateTrigger", automation.Trigger.template())
+ControlTrigger = climate_ns.class_("ControlTrigger", automation.Trigger.template())
+
+VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any(
+ single_visual_temperature,
+ cv.Schema(
+ {
+ cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature,
+ cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature,
+ }
+ ),
+)
CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
{
@@ -113,7 +148,7 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
{
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
- cv.Optional(CONF_TEMPERATURE_STEP): cv.temperature,
+ cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA,
}
),
cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All(
@@ -140,6 +175,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_MODE_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
+ cv.Optional(CONF_PRESET_COMMAND_TOPIC): cv.All(
+ cv.requires_component("mqtt"), cv.publish_topic
+ ),
+ cv.Optional(CONF_PRESET_STATE_TOPIC): cv.All(
+ cv.requires_component("mqtt"), cv.publish_topic
+ ),
cv.Optional(CONF_SWING_MODE_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
@@ -164,6 +205,11 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
+ cv.Optional(CONF_ON_CONTROL): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger),
+ }
+ ),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
@@ -182,7 +228,12 @@ async def setup_climate_core_(var, config):
if CONF_MAX_TEMPERATURE in visual:
cg.add(var.set_visual_max_temperature_override(visual[CONF_MAX_TEMPERATURE]))
if CONF_TEMPERATURE_STEP in visual:
- cg.add(var.set_visual_temperature_step_override(visual[CONF_TEMPERATURE_STEP]))
+ cg.add(
+ var.set_visual_temperature_step_override(
+ visual[CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE],
+ visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE],
+ )
+ )
if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
@@ -214,7 +265,12 @@ async def setup_climate_core_(var, config):
cg.add(mqtt_.set_custom_mode_command_topic(config[CONF_MODE_COMMAND_TOPIC]))
if CONF_MODE_STATE_TOPIC in config:
cg.add(mqtt_.set_custom_mode_state_topic(config[CONF_MODE_STATE_TOPIC]))
-
+ if CONF_PRESET_COMMAND_TOPIC in config:
+ cg.add(
+ mqtt_.set_custom_preset_command_topic(config[CONF_PRESET_COMMAND_TOPIC])
+ )
+ if CONF_PRESET_STATE_TOPIC in config:
+ cg.add(mqtt_.set_custom_preset_state_topic(config[CONF_PRESET_STATE_TOPIC]))
if CONF_SWING_MODE_COMMAND_TOPIC in config:
cg.add(
mqtt_.set_custom_swing_mode_command_topic(
@@ -268,6 +324,10 @@ async def setup_climate_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
+ for conf in config.get(CONF_ON_CONTROL, []):
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
+ await automation.build_automation(trigger, [], conf)
+
async def register_climate(var, config):
if not CORE.has_id(config[CONF_ID]):
@@ -283,13 +343,15 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
- cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
+ cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"),
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),
}
)
@@ -317,20 +379,21 @@ async def climate_control_to_code(config, action_id, template_arg, args):
config[CONF_TARGET_TEMPERATURE_HIGH], args, float
)
cg.add(var.set_target_temperature_high(template_))
- if CONF_AWAY in config:
- template_ = await cg.templatable(config[CONF_AWAY], args, bool)
- cg.add(var.set_away(template_))
if CONF_FAN_MODE in config:
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(
diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h
index 3145358dab..9b06563eb4 100644
--- a/esphome/components/climate/automation.h
+++ b/esphome/components/climate/automation.h
@@ -42,6 +42,13 @@ template class ControlAction : public Action {
Climate *climate_;
};
+class ControlTrigger : public Trigger<> {
+ public:
+ ControlTrigger(Climate *climate) {
+ climate->add_on_control_callback([this]() { this->trigger(); });
+ }
+};
+
class StateTrigger : public Trigger<> {
public:
StateTrigger(Climate *climate) {
diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp
index ebea20ed1f..a032596eb3 100644
--- a/esphome/components/climate/climate.cpp
+++ b/esphome/components/climate/climate.cpp
@@ -1,4 +1,5 @@
#include "climate.h"
+#include "esphome/core/macros.h"
namespace esphome {
namespace climate {
@@ -43,6 +44,7 @@ void ClimateCall::perform() {
if (this->target_temperature_high_.has_value()) {
ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
}
+ this->parent_->control_callback_.call();
this->parent_->control(*this);
}
void ClimateCall::validate_() {
@@ -173,6 +175,8 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
this->set_fan_mode(CLIMATE_FAN_FOCUS);
} else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
+ } else if (str_equals_case_insensitive(fan_mode, "QUIET")) {
+ this->set_fan_mode(CLIMATE_FAN_QUIET);
} else {
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
this->custom_fan_mode_ = fan_mode;
@@ -260,25 +264,11 @@ const optional &ClimateCall::get_mode() const { return this->mode_;
const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
-optional ClimateCall::get_away() const {
- if (!this->preset_.has_value())
- return {};
- return *this->preset_ == ClimatePreset::CLIMATE_PRESET_AWAY;
-}
const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional &ClimateCall::get_preset() const { return this->preset_; }
const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
-ClimateCall &ClimateCall::set_away(bool away) {
- this->preset_ = away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME;
- return *this;
-}
-ClimateCall &ClimateCall::set_away(optional away) {
- if (away.has_value())
- this->preset_ = *away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME;
- return *this;
-}
ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) {
this->target_temperature_high_ = target_temperature_high;
return *this;
@@ -314,6 +304,10 @@ void Climate::add_on_state_callback(std::function &&callback) {
this->state_callback_.add(std::move(callback));
}
+void Climate::add_on_control_callback(std::function &&callback) {
+ this->control_callback_.add(std::move(callback));
+}
+
// Random 32bit value; If this changes existing restore preferences are invalidated
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
@@ -326,14 +320,17 @@ optional Climate::restore_state_() {
return recovered;
}
void Climate::save_state_() {
-#if defined(USE_ESP_IDF) && !defined(CLANG_TIDY)
+#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
+ !defined(CLANG_TIDY)
#pragma GCC diagnostic ignored "-Wclass-memaccess"
+#define TEMP_IGNORE_MEMACCESS
#endif
ClimateDeviceRestoreState state{};
// initialize as zero to prevent random data on stack triggering erase
memset(&state, 0, sizeof(ClimateDeviceRestoreState));
-#if USE_ESP_IDF && !defined(CLANG_TIDY)
+#ifdef TEMP_IGNORE_MEMACCESS
#pragma GCC diagnostic pop
+#undef TEMP_IGNORE_MEMACCESS
#endif
state.mode = this->mode;
@@ -415,7 +412,6 @@ void Climate::publish_state() {
// Save state
this->save_state_();
}
-uint32_t Climate::hash_base() { return 3104134496UL; }
ClimateTraits Climate::get_traits() {
auto traits = this->traits();
@@ -425,9 +421,11 @@ ClimateTraits Climate::get_traits() {
if (this->visual_max_temperature_override_.has_value()) {
traits.set_visual_max_temperature(*this->visual_max_temperature_override_);
}
- if (this->visual_temperature_step_override_.has_value()) {
- traits.set_visual_temperature_step(*this->visual_temperature_step_override_);
+ if (this->visual_target_temperature_step_override_.has_value()) {
+ traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_);
+ traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_);
}
+
return traits;
}
@@ -437,15 +435,11 @@ void Climate::set_visual_min_temperature_override(float visual_min_temperature_o
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
this->visual_max_temperature_override_ = visual_max_temperature_override;
}
-void Climate::set_visual_temperature_step_override(float visual_temperature_step_override) {
- this->visual_temperature_step_override_ = visual_temperature_step_override;
+void Climate::set_visual_temperature_step_override(float target, float current) {
+ this->visual_target_temperature_step_override_ = target;
+ this->visual_current_temperature_step_override_ = current;
}
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
-Climate::Climate(const std::string &name) : EntityBase(name) {}
-#pragma GCC diagnostic pop
-Climate::Climate() : Climate("") {}
ClimateCall Climate::make_call() { return ClimateCall(this); }
ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
@@ -536,13 +530,18 @@ void Climate::dump_traits_(const char *tag) {
ESP_LOGCONFIG(tag, " [x] Visual settings:");
ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature());
ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature());
- ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step());
- if (traits.get_supports_current_temperature())
+ ESP_LOGCONFIG(tag, " - Step:");
+ ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step());
+ ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
+ if (traits.get_supports_current_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
- if (traits.get_supports_two_point_target_temperature())
+ }
+ if (traits.get_supports_two_point_target_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
- if (traits.get_supports_action())
+ }
+ if (traits.get_supports_action()) {
ESP_LOGCONFIG(tag, " [x] Supports action");
+ }
if (!traits.get_supported_modes().empty()) {
ESP_LOGCONFIG(tag, " [x] Supported modes:");
for (ClimateMode m : traits.get_supported_modes())
diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h
index 852b76686c..656e1c4852 100644
--- a/esphome/components/climate/climate.h
+++ b/esphome/components/climate/climate.h
@@ -64,10 +64,6 @@ class ClimateCall {
* For climate devices with two point target temperature control
*/
ClimateCall &set_target_temperature_high(optional target_temperature_high);
- ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20")
- ClimateCall &set_away(bool away);
- ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20")
- ClimateCall &set_away(optional away);
/// Set the fan mode of the climate device.
ClimateCall &set_fan_mode(ClimateFanMode fan_mode);
/// Set the fan mode of the climate device.
@@ -97,8 +93,6 @@ class ClimateCall {
const optional &get_target_temperature() const;
const optional &get_target_temperature_low() const;
const optional &get_target_temperature_high() const;
- ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead", "v1.20")
- optional get_away() const;
const optional &get_fan_mode() const;
const optional &get_swing_mode() const;
const optional &get_custom_fan_mode() const;
@@ -166,11 +160,6 @@ struct ClimateDeviceRestoreState {
*/
class Climate : public EntityBase {
public:
- /// Construct a climate device with empty name (will be set later).
- Climate();
- /// Construct a climate device with a name.
- Climate(const std::string &name);
-
/// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF};
/// The active state of the climate device.
@@ -189,14 +178,6 @@ class Climate : public EntityBase {
};
};
- /** Whether the climate device is in away mode.
- *
- * Away allows climate devices to have two different target temperature configs:
- * one for normal mode and one for away mode.
- */
- ESPDEPRECATED("away is deprecated, use preset instead", "v1.20")
- bool away{false};
-
/// The active fan mode of the climate device.
optional fan_mode;
@@ -219,6 +200,14 @@ class Climate : public EntityBase {
*/
void add_on_state_callback(std::function &&callback);
+ /**
+ * Add a callback for the climate device configuration; each time the configuration parameters of a climate device
+ * is updated (using perform() of a ClimateCall), this callback will be called, before any on_state callback.
+ *
+ * @param callback The callback to call.
+ */
+ void add_on_control_callback(std::function &&callback);
+
/** Make a climate device control call, this is used to control the climate device, see the ClimateCall description
* for more info.
* @return A new ClimateCall instance targeting this climate device.
@@ -241,7 +230,7 @@ class Climate : public EntityBase {
void set_visual_min_temperature_override(float visual_min_temperature_override);
void set_visual_max_temperature_override(float visual_max_temperature_override);
- void set_visual_temperature_step_override(float visual_temperature_step_override);
+ void set_visual_temperature_step_override(float target, float current);
protected:
friend ClimateCall;
@@ -282,14 +271,15 @@ class Climate : public EntityBase {
*/
void save_state_();
- uint32_t hash_base() override;
void dump_traits_(const char *tag);
CallbackManager state_callback_{};
+ CallbackManager control_callback_{};
ESPPreferenceObject rtc_;
optional visual_min_temperature_override_{};
optional visual_max_temperature_override_{};
- optional visual_temperature_step_override_{};
+ optional visual_target_temperature_step_override_{};
+ optional visual_current_temperature_step_override_{};
};
} // namespace climate
diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp
index e46159a750..794f45ccd6 100644
--- a/esphome/components/climate/climate_mode.cpp
+++ b/esphome/components/climate/climate_mode.cpp
@@ -62,6 +62,8 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) {
return LOG_STR("FOCUS");
case climate::CLIMATE_FAN_DIFFUSE:
return LOG_STR("DIFFUSE");
+ case climate::CLIMATE_FAN_QUIET:
+ return LOG_STR("QUIET");
default:
return LOG_STR("UNKNOWN");
}
diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h
index 3e5626919c..c5245812c7 100644
--- a/esphome/components/climate/climate_mode.h
+++ b/esphome/components/climate/climate_mode.h
@@ -62,6 +62,8 @@ enum ClimateFanMode : uint8_t {
CLIMATE_FAN_FOCUS = 7,
/// The fan mode is set to Diffuse
CLIMATE_FAN_DIFFUSE = 8,
+ /// The fan mode is set to Quiet
+ CLIMATE_FAN_QUIET = 9,
};
/// Enum for all modes a climate swing can be in
@@ -76,7 +78,7 @@ enum ClimateSwingMode : uint8_t {
CLIMATE_SWING_HORIZONTAL = 3,
};
-/// Enum for all modes a climate swing can be in
+/// Enum for all preset modes
enum ClimatePreset : uint8_t {
/// No preset is active
CLIMATE_PRESET_NONE = 0,
@@ -108,7 +110,7 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode mode);
/// Convert the given ClimateSwingMode to a human-readable string.
const LogString *climate_swing_mode_to_string(ClimateSwingMode mode);
-/// Convert the given ClimateSwingMode to a human-readable string.
+/// Convert the given PresetMode to a human-readable string.
const LogString *climate_preset_to_string(ClimatePreset preset);
} // namespace climate
diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp
index 16c9cd05be..342dffaad6 100644
--- a/esphome/components/climate/climate_traits.cpp
+++ b/esphome/components/climate/climate_traits.cpp
@@ -1,19 +1,14 @@
#include "climate_traits.h"
-#include
namespace esphome {
namespace climate {
-int8_t ClimateTraits::get_temperature_accuracy_decimals() const {
- // use printf %g to find number of digits based on temperature step
- char buf[32];
- sprintf(buf, "%.5g", this->visual_temperature_step_);
- std::string str{buf};
- size_t dot_pos = str.find('.');
- if (dot_pos == std::string::npos)
- return 0;
+int8_t ClimateTraits::get_target_temperature_accuracy_decimals() const {
+ return step_to_accuracy_decimals(this->visual_target_temperature_step_);
+}
- return str.length() - dot_pos - 1;
+int8_t ClimateTraits::get_current_temperature_accuracy_decimals() const {
+ return step_to_accuracy_decimals(this->visual_current_temperature_step_);
}
} // namespace climate
diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h
index d113510eeb..e8c2db6c06 100644
--- a/esphome/components/climate/climate_traits.h
+++ b/esphome/components/climate/climate_traits.h
@@ -28,7 +28,7 @@ namespace climate {
* - supports action - if the climate device supports reporting the active
* current action of the device with the action property.
* - supports fan modes - optionally, if it has a fan which can be configured in different ways:
- * - on, off, auto, high, medium, low, middle, focus, diffuse
+ * - on, off, auto, high, medium, low, middle, focus, diffuse, quiet
* - supports swing modes - optionally, if it has a swing which can be configured in different ways:
* - off, both, vertical, horizontal
*
@@ -117,15 +117,6 @@ class ClimateTraits {
bool supports_custom_preset(const std::string &custom_preset) const {
return supported_custom_presets_.count(custom_preset);
}
- ESPDEPRECATED("This method is deprecated, use set_supported_presets() instead", "v1.20")
- void set_supports_away(bool supports) {
- if (supports) {
- supported_presets_.insert(CLIMATE_PRESET_AWAY);
- supported_presets_.insert(CLIMATE_PRESET_HOME);
- }
- }
- ESPDEPRECATED("This method is deprecated, use supports_preset() instead", "v1.20")
- bool get_supports_away() const { return supports_preset(CLIMATE_PRESET_AWAY); }
void set_supported_swing_modes(std::set modes) { supported_swing_modes_ = std::move(modes); }
void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); }
@@ -141,15 +132,26 @@ class ClimateTraits {
}
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); }
- std::set get_supported_swing_modes() { return supported_swing_modes_; }
+ std::set get_supported_swing_modes() const { return supported_swing_modes_; }
float get_visual_min_temperature() const { return visual_min_temperature_; }
void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; }
float get_visual_max_temperature() const { return visual_max_temperature_; }
void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; }
- float get_visual_temperature_step() const { return visual_temperature_step_; }
- int8_t get_temperature_accuracy_decimals() const;
- void set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; }
+ float get_visual_target_temperature_step() const { return visual_target_temperature_step_; }
+ float get_visual_current_temperature_step() const { return visual_current_temperature_step_; }
+ void set_visual_target_temperature_step(float temperature_step) {
+ visual_target_temperature_step_ = temperature_step;
+ }
+ void set_visual_current_temperature_step(float temperature_step) {
+ visual_current_temperature_step_ = temperature_step;
+ }
+ void set_visual_temperature_step(float temperature_step) {
+ visual_target_temperature_step_ = temperature_step;
+ visual_current_temperature_step_ = temperature_step;
+ }
+ int8_t get_target_temperature_accuracy_decimals() const;
+ int8_t get_current_temperature_accuracy_decimals() const;
protected:
void set_mode_support_(climate::ClimateMode mode, bool supported) {
@@ -186,7 +188,8 @@ class ClimateTraits {
float visual_min_temperature_{10};
float visual_max_temperature_{30};
- float visual_temperature_step_{0.1};
+ float visual_target_temperature_step_{0.1};
+ float visual_current_temperature_step_{0.1};
};
} // namespace climate
diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py
index 47679fcc68..9a85eace75 100644
--- a/esphome/components/color/__init__.py
+++ b/esphome/components/color/__init__.py
@@ -10,23 +10,42 @@ CONF_RED_INT = "red_int"
CONF_GREEN_INT = "green_int"
CONF_BLUE_INT = "blue_int"
CONF_WHITE_INT = "white_int"
-
-CONFIG_SCHEMA = cv.Schema(
- {
- cv.Required(CONF_ID): cv.declare_id(ColorStruct),
- cv.Exclusive(CONF_RED, "red"): cv.percentage,
- cv.Exclusive(CONF_RED_INT, "red"): cv.uint8_t,
- cv.Exclusive(CONF_GREEN, "green"): cv.percentage,
- cv.Exclusive(CONF_GREEN_INT, "green"): cv.uint8_t,
- cv.Exclusive(CONF_BLUE, "blue"): cv.percentage,
- cv.Exclusive(CONF_BLUE_INT, "blue"): cv.uint8_t,
- cv.Exclusive(CONF_WHITE, "white"): cv.percentage,
- cv.Exclusive(CONF_WHITE_INT, "white"): cv.uint8_t,
- }
-).extend(cv.COMPONENT_SCHEMA)
+CONF_HEX = "hex"
-async def to_code(config):
+def hex_color(value):
+ if len(value) != 6:
+ raise cv.Invalid("Color must have six digits")
+ try:
+ return (int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16))
+ except ValueError as exc:
+ raise cv.Invalid("Color must be hexadecimal") from exc
+
+
+CONFIG_SCHEMA = cv.Any(
+ cv.Schema(
+ {
+ cv.Required(CONF_ID): cv.declare_id(ColorStruct),
+ cv.Exclusive(CONF_RED, "red"): cv.percentage,
+ cv.Exclusive(CONF_RED_INT, "red"): cv.uint8_t,
+ cv.Exclusive(CONF_GREEN, "green"): cv.percentage,
+ cv.Exclusive(CONF_GREEN_INT, "green"): cv.uint8_t,
+ cv.Exclusive(CONF_BLUE, "blue"): cv.percentage,
+ cv.Exclusive(CONF_BLUE_INT, "blue"): cv.uint8_t,
+ cv.Exclusive(CONF_WHITE, "white"): cv.percentage,
+ cv.Exclusive(CONF_WHITE_INT, "white"): cv.uint8_t,
+ }
+ ).extend(cv.COMPONENT_SCHEMA),
+ cv.Schema(
+ {
+ cv.Required(CONF_ID): cv.declare_id(ColorStruct),
+ cv.Required(CONF_HEX): hex_color,
+ }
+ ).extend(cv.COMPONENT_SCHEMA),
+)
+
+
+def from_rgbw(config):
r = 0
if CONF_RED in config:
r = int(config[CONF_RED] * 255)
@@ -51,6 +70,16 @@ async def to_code(config):
elif CONF_WHITE_INT in config:
w = config[CONF_WHITE_INT]
+ return (r, g, b, w)
+
+
+async def to_code(config):
+ if CONF_HEX in config:
+ r, g, b = config[CONF_HEX]
+ w = 0
+ else:
+ r, g, b, w = from_rgbw(config)
+
cg.new_variable(
config[CONF_ID],
cg.StructInitializer(ColorStruct, ("r", r), ("g", g), ("b", b), ("w", w)),
diff --git a/esphome/components/copy/__init__.py b/esphome/components/copy/__init__.py
new file mode 100644
index 0000000000..7594894650
--- /dev/null
+++ b/esphome/components/copy/__init__.py
@@ -0,0 +1,5 @@
+import esphome.codegen as cg
+
+CODEOWNERS = ["@OttoWinter"]
+
+copy_ns = cg.esphome_ns.namespace("copy")
diff --git a/esphome/components/copy/binary_sensor/__init__.py b/esphome/components/copy/binary_sensor/__init__.py
new file mode 100644
index 0000000000..1b6836fae7
--- /dev/null
+++ b/esphome/components/copy/binary_sensor/__init__.py
@@ -0,0 +1,41 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from esphome.const import (
+ CONF_DEVICE_CLASS,
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_SOURCE_ID,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopyBinarySensor = copy_ns.class_(
+ "CopyBinarySensor", binary_sensor.BinarySensor, cg.Component
+)
+
+
+CONFIG_SCHEMA = (
+ binary_sensor.binary_sensor_schema(CopyBinarySensor)
+ .extend(
+ {
+ cv.Required(CONF_SOURCE_ID): cv.use_id(binary_sensor.BinarySensor),
+ }
+ )
+ .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+ inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = await binary_sensor.new_binary_sensor(config)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp b/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp
new file mode 100644
index 0000000000..0d96f58750
--- /dev/null
+++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp
@@ -0,0 +1,18 @@
+#include "copy_binary_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.binary_sensor";
+
+void CopyBinarySensor::setup() {
+ source_->add_on_state_callback([this](bool value) { this->publish_state(value); });
+ if (source_->has_state())
+ this->publish_state(source_->state);
+}
+
+void CopyBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Copy Binary Sensor", this); }
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.h b/esphome/components/copy/binary_sensor/copy_binary_sensor.h
new file mode 100644
index 0000000000..d62ed13c76
--- /dev/null
+++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace copy {
+
+class CopyBinarySensor : public binary_sensor::BinarySensor, public Component {
+ public:
+ void set_source(binary_sensor::BinarySensor *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+ binary_sensor::BinarySensor *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/button/__init__.py b/esphome/components/copy/button/__init__.py
new file mode 100644
index 0000000000..626a5a8db1
--- /dev/null
+++ b/esphome/components/copy/button/__init__.py
@@ -0,0 +1,41 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import button
+from esphome.const import (
+ CONF_DEVICE_CLASS,
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_ID,
+ CONF_SOURCE_ID,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopyButton = copy_ns.class_("CopyButton", button.Button, cg.Component)
+
+
+CONFIG_SCHEMA = (
+ button.button_schema(CopyButton)
+ .extend(
+ {
+ cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button),
+ }
+ )
+ .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+ inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = cg.new_Pvariable(config[CONF_ID])
+ await button.register_button(var, config)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/button/copy_button.cpp b/esphome/components/copy/button/copy_button.cpp
new file mode 100644
index 0000000000..595388775c
--- /dev/null
+++ b/esphome/components/copy/button/copy_button.cpp
@@ -0,0 +1,14 @@
+#include "copy_button.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.button";
+
+void CopyButton::dump_config() { LOG_BUTTON("", "Copy Button", this); }
+
+void CopyButton::press_action() { source_->press(); }
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/button/copy_button.h b/esphome/components/copy/button/copy_button.h
new file mode 100644
index 0000000000..9996ca0c65
--- /dev/null
+++ b/esphome/components/copy/button/copy_button.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/button/button.h"
+
+namespace esphome {
+namespace copy {
+
+class CopyButton : public button::Button, public Component {
+ public:
+ void set_source(button::Button *source) { source_ = source; }
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+ void press_action() override;
+
+ button::Button *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/cover/__init__.py b/esphome/components/copy/cover/__init__.py
new file mode 100644
index 0000000000..155e22883b
--- /dev/null
+++ b/esphome/components/copy/cover/__init__.py
@@ -0,0 +1,38 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import cover
+from esphome.const import (
+ CONF_DEVICE_CLASS,
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_ID,
+ CONF_SOURCE_ID,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component)
+
+
+CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
+ {
+ cv.GenerateID(): cv.declare_id(CopyCover),
+ cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover),
+ }
+).extend(cv.COMPONENT_SCHEMA)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+ inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = cg.new_Pvariable(config[CONF_ID])
+ await cover.register_cover(var, config)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/cover/copy_cover.cpp b/esphome/components/copy/cover/copy_cover.cpp
new file mode 100644
index 0000000000..28f8c9877c
--- /dev/null
+++ b/esphome/components/copy/cover/copy_cover.cpp
@@ -0,0 +1,51 @@
+#include "copy_cover.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.cover";
+
+void CopyCover::setup() {
+ source_->add_on_state_callback([this]() {
+ this->current_operation = this->source_->current_operation;
+ this->position = this->source_->position;
+ this->tilt = this->source_->tilt;
+ this->publish_state();
+ });
+
+ this->current_operation = this->source_->current_operation;
+ this->position = this->source_->position;
+ this->tilt = this->source_->tilt;
+ this->publish_state();
+}
+
+void CopyCover::dump_config() { LOG_COVER("", "Copy Cover", this); }
+
+cover::CoverTraits CopyCover::get_traits() {
+ auto base = source_->get_traits();
+ cover::CoverTraits traits{};
+ // copy traits manually so it doesn't break when new options are added
+ // but the control() method hasn't implemented them yet.
+ traits.set_is_assumed_state(base.get_is_assumed_state());
+ traits.set_supports_stop(base.get_supports_stop());
+ traits.set_supports_position(base.get_supports_position());
+ traits.set_supports_tilt(base.get_supports_tilt());
+ traits.set_supports_toggle(base.get_supports_toggle());
+ return traits;
+}
+
+void CopyCover::control(const cover::CoverCall &call) {
+ auto call2 = source_->make_call();
+ call2.set_stop(call.get_stop());
+ if (call.get_tilt().has_value())
+ call2.set_tilt(*call.get_tilt());
+ if (call.get_position().has_value())
+ call2.set_position(*call.get_position());
+ if (call.get_tilt().has_value())
+ call2.set_tilt(*call.get_tilt());
+ call2.perform();
+}
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/cover/copy_cover.h b/esphome/components/copy/cover/copy_cover.h
new file mode 100644
index 0000000000..fb278523ff
--- /dev/null
+++ b/esphome/components/copy/cover/copy_cover.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/cover/cover.h"
+
+namespace esphome {
+namespace copy {
+
+class CopyCover : public cover::Cover, public Component {
+ public:
+ void set_source(cover::Cover *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ cover::CoverTraits get_traits() override;
+
+ protected:
+ void control(const cover::CoverCall &call) override;
+
+ cover::Cover *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/fan/__init__.py b/esphome/components/copy/fan/__init__.py
new file mode 100644
index 0000000000..22672c02d8
--- /dev/null
+++ b/esphome/components/copy/fan/__init__.py
@@ -0,0 +1,36 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import fan
+from esphome.const import (
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_ID,
+ CONF_SOURCE_ID,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component)
+
+
+CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
+ {
+ cv.GenerateID(): cv.declare_id(CopyFan),
+ cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan),
+ }
+).extend(cv.COMPONENT_SCHEMA)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = cg.new_Pvariable(config[CONF_ID])
+ await fan.register_fan(var, config)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp
new file mode 100644
index 0000000000..74d9da279f
--- /dev/null
+++ b/esphome/components/copy/fan/copy_fan.cpp
@@ -0,0 +1,53 @@
+#include "copy_fan.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.fan";
+
+void CopyFan::setup() {
+ source_->add_on_state_callback([this]() {
+ this->state = source_->state;
+ this->oscillating = source_->oscillating;
+ this->speed = source_->speed;
+ this->direction = source_->direction;
+ this->publish_state();
+ });
+
+ this->state = source_->state;
+ this->oscillating = source_->oscillating;
+ this->speed = source_->speed;
+ this->direction = source_->direction;
+ this->publish_state();
+}
+
+void CopyFan::dump_config() { LOG_FAN("", "Copy Fan", this); }
+
+fan::FanTraits CopyFan::get_traits() {
+ fan::FanTraits traits;
+ auto base = source_->get_traits();
+ // copy traits manually so it doesn't break when new options are added
+ // but the control() method hasn't implemented them yet.
+ traits.set_oscillation(base.supports_oscillation());
+ traits.set_speed(base.supports_speed());
+ traits.set_supported_speed_count(base.supported_speed_count());
+ traits.set_direction(base.supports_direction());
+ return traits;
+}
+
+void CopyFan::control(const fan::FanCall &call) {
+ auto call2 = source_->make_call();
+ if (call.get_state().has_value())
+ call2.set_state(*call.get_state());
+ if (call.get_oscillating().has_value())
+ call2.set_oscillating(*call.get_oscillating());
+ if (call.get_speed().has_value())
+ call2.set_speed(*call.get_speed());
+ if (call.get_direction().has_value())
+ call2.set_direction(*call.get_direction());
+ call2.perform();
+}
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h
new file mode 100644
index 0000000000..1a69810510
--- /dev/null
+++ b/esphome/components/copy/fan/copy_fan.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/fan/fan.h"
+
+namespace esphome {
+namespace copy {
+
+class CopyFan : public fan::Fan, public Component {
+ public:
+ void set_source(fan::Fan *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ fan::FanTraits get_traits() override;
+
+ protected:
+ void control(const fan::FanCall &call) override;
+ ;
+
+ fan::Fan *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/lock/__init__.py b/esphome/components/copy/lock/__init__.py
new file mode 100644
index 0000000000..d19e4a5807
--- /dev/null
+++ b/esphome/components/copy/lock/__init__.py
@@ -0,0 +1,36 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import lock
+from esphome.const import (
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_ID,
+ CONF_SOURCE_ID,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component)
+
+
+CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend(
+ {
+ cv.GenerateID(): cv.declare_id(CopyLock),
+ cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock),
+ }
+).extend(cv.COMPONENT_SCHEMA)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = cg.new_Pvariable(config[CONF_ID])
+ await lock.register_lock(var, config)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp
new file mode 100644
index 0000000000..67a8acffec
--- /dev/null
+++ b/esphome/components/copy/lock/copy_lock.cpp
@@ -0,0 +1,29 @@
+#include "copy_lock.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.lock";
+
+void CopyLock::setup() {
+ source_->add_on_state_callback([this]() { this->publish_state(source_->state); });
+
+ traits.set_assumed_state(source_->traits.get_assumed_state());
+ traits.set_requires_code(source_->traits.get_requires_code());
+ traits.set_supported_states(source_->traits.get_supported_states());
+ traits.set_supports_open(source_->traits.get_supports_open());
+
+ this->publish_state(source_->state);
+}
+
+void CopyLock::dump_config() { LOG_LOCK("", "Copy Lock", this); }
+
+void CopyLock::control(const lock::LockCall &call) {
+ auto call2 = source_->make_call();
+ call2.set_state(call.get_state());
+ call2.perform();
+}
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/lock/copy_lock.h b/esphome/components/copy/lock/copy_lock.h
new file mode 100644
index 0000000000..0554013674
--- /dev/null
+++ b/esphome/components/copy/lock/copy_lock.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/lock/lock.h"
+
+namespace esphome {
+namespace copy {
+
+class CopyLock : public lock::Lock, public Component {
+ public:
+ void set_source(lock::Lock *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+ void control(const lock::LockCall &call) override;
+
+ lock::Lock *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/number/__init__.py b/esphome/components/copy/number/__init__.py
new file mode 100644
index 0000000000..204518da39
--- /dev/null
+++ b/esphome/components/copy/number/__init__.py
@@ -0,0 +1,41 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import number
+from esphome.const import (
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_MODE,
+ CONF_SOURCE_ID,
+ CONF_UNIT_OF_MEASUREMENT,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopyNumber = copy_ns.class_("CopyNumber", number.Number, cg.Component)
+
+
+CONFIG_SCHEMA = (
+ number.number_schema(CopyNumber)
+ .extend(
+ {
+ cv.Required(CONF_SOURCE_ID): cv.use_id(number.Number),
+ }
+ )
+ .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+ inherit_property_from(CONF_UNIT_OF_MEASUREMENT, CONF_SOURCE_ID),
+ inherit_property_from(CONF_MODE, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = await number.new_number(config, min_value=0, max_value=0, step=0)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/number/copy_number.cpp b/esphome/components/copy/number/copy_number.cpp
new file mode 100644
index 0000000000..46dc200b73
--- /dev/null
+++ b/esphome/components/copy/number/copy_number.cpp
@@ -0,0 +1,29 @@
+#include "copy_number.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.number";
+
+void CopyNumber::setup() {
+ source_->add_on_state_callback([this](float value) { this->publish_state(value); });
+
+ traits.set_min_value(source_->traits.get_min_value());
+ traits.set_max_value(source_->traits.get_max_value());
+ traits.set_step(source_->traits.get_step());
+
+ if (source_->has_state())
+ this->publish_state(source_->state);
+}
+
+void CopyNumber::dump_config() { LOG_NUMBER("", "Copy Number", this); }
+
+void CopyNumber::control(float value) {
+ auto call2 = source_->make_call();
+ call2.set_value(value);
+ call2.perform();
+}
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/number/copy_number.h b/esphome/components/copy/number/copy_number.h
new file mode 100644
index 0000000000..1ad956fec4
--- /dev/null
+++ b/esphome/components/copy/number/copy_number.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/number/number.h"
+
+namespace esphome {
+namespace copy {
+
+class CopyNumber : public number::Number, public Component {
+ public:
+ void set_source(number::Number *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+ void control(float value) override;
+
+ number::Number *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/select/__init__.py b/esphome/components/copy/select/__init__.py
new file mode 100644
index 0000000000..6254ed03e1
--- /dev/null
+++ b/esphome/components/copy/select/__init__.py
@@ -0,0 +1,39 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import select
+from esphome.const import (
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_ID,
+ CONF_SOURCE_ID,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopySelect = copy_ns.class_("CopySelect", select.Select, cg.Component)
+
+
+CONFIG_SCHEMA = (
+ select.select_schema(CopySelect)
+ .extend(
+ {
+ cv.Required(CONF_SOURCE_ID): cv.use_id(select.Select),
+ }
+ )
+ .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = cg.new_Pvariable(config[CONF_ID])
+ await select.register_select(var, config, options=[])
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp
new file mode 100644
index 0000000000..bdcbd0b42c
--- /dev/null
+++ b/esphome/components/copy/select/copy_select.cpp
@@ -0,0 +1,27 @@
+#include "copy_select.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.select";
+
+void CopySelect::setup() {
+ source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
+
+ traits.set_options(source_->traits.get_options());
+
+ if (source_->has_state())
+ this->publish_state(source_->state);
+}
+
+void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); }
+
+void CopySelect::control(const std::string &value) {
+ auto call = source_->make_call();
+ call.set_option(value);
+ call.perform();
+}
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h
new file mode 100644
index 0000000000..c8666cd394
--- /dev/null
+++ b/esphome/components/copy/select/copy_select.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/select/select.h"
+
+namespace esphome {
+namespace copy {
+
+class CopySelect : public select::Select, public Component {
+ public:
+ void set_source(select::Select *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+ void control(const std::string &value) override;
+
+ select::Select *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/sensor/__init__.py b/esphome/components/copy/sensor/__init__.py
new file mode 100644
index 0000000000..8e78cda7c7
--- /dev/null
+++ b/esphome/components/copy/sensor/__init__.py
@@ -0,0 +1,45 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor
+from esphome.const import (
+ CONF_DEVICE_CLASS,
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_SOURCE_ID,
+ CONF_STATE_CLASS,
+ CONF_UNIT_OF_MEASUREMENT,
+ CONF_ACCURACY_DECIMALS,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopySensor = copy_ns.class_("CopySensor", sensor.Sensor, cg.Component)
+
+
+CONFIG_SCHEMA = (
+ sensor.sensor_schema(CopySensor)
+ .extend(
+ {
+ cv.Required(CONF_SOURCE_ID): cv.use_id(sensor.Sensor),
+ }
+ )
+ .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_UNIT_OF_MEASUREMENT, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ACCURACY_DECIMALS, CONF_SOURCE_ID),
+ inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID),
+ inherit_property_from(CONF_STATE_CLASS, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = await sensor.new_sensor(config)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/sensor/copy_sensor.cpp b/esphome/components/copy/sensor/copy_sensor.cpp
new file mode 100644
index 0000000000..a47a0cf22b
--- /dev/null
+++ b/esphome/components/copy/sensor/copy_sensor.cpp
@@ -0,0 +1,18 @@
+#include "copy_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.sensor";
+
+void CopySensor::setup() {
+ source_->add_on_state_callback([this](float value) { this->publish_state(value); });
+ if (source_->has_state())
+ this->publish_state(source_->state);
+}
+
+void CopySensor::dump_config() { LOG_SENSOR("", "Copy Sensor", this); }
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/sensor/copy_sensor.h b/esphome/components/copy/sensor/copy_sensor.h
new file mode 100644
index 0000000000..1ae790ada3
--- /dev/null
+++ b/esphome/components/copy/sensor/copy_sensor.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+
+namespace esphome {
+namespace copy {
+
+class CopySensor : public sensor::Sensor, public Component {
+ public:
+ void set_source(sensor::Sensor *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+ sensor::Sensor *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/switch/__init__.py b/esphome/components/copy/switch/__init__.py
new file mode 100644
index 0000000000..beffbe7fbb
--- /dev/null
+++ b/esphome/components/copy/switch/__init__.py
@@ -0,0 +1,39 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import switch
+from esphome.const import (
+ CONF_DEVICE_CLASS,
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_SOURCE_ID,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopySwitch = copy_ns.class_("CopySwitch", switch.Switch, cg.Component)
+
+
+CONFIG_SCHEMA = (
+ switch.switch_schema(CopySwitch)
+ .extend(
+ {
+ cv.Required(CONF_SOURCE_ID): cv.use_id(switch.Switch),
+ }
+ )
+ .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+ inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = await switch.new_switch(config)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/switch/copy_switch.cpp b/esphome/components/copy/switch/copy_switch.cpp
new file mode 100644
index 0000000000..8a9fbb03dd
--- /dev/null
+++ b/esphome/components/copy/switch/copy_switch.cpp
@@ -0,0 +1,26 @@
+#include "copy_switch.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.switch";
+
+void CopySwitch::setup() {
+ source_->add_on_state_callback([this](float value) { this->publish_state(value); });
+
+ this->publish_state(source_->state);
+}
+
+void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); }
+
+void CopySwitch::write_state(bool state) {
+ if (state) {
+ source_->turn_on();
+ } else {
+ source_->turn_off();
+ }
+}
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/switch/copy_switch.h b/esphome/components/copy/switch/copy_switch.h
new file mode 100644
index 0000000000..26cb254ab3
--- /dev/null
+++ b/esphome/components/copy/switch/copy_switch.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/switch/switch.h"
+
+namespace esphome {
+namespace copy {
+
+class CopySwitch : public switch_::Switch, public Component {
+ public:
+ void set_source(switch_::Switch *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+ void write_state(bool state) override;
+
+ switch_::Switch *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/text_sensor/__init__.py b/esphome/components/copy/text_sensor/__init__.py
new file mode 100644
index 0000000000..5b59f21319
--- /dev/null
+++ b/esphome/components/copy/text_sensor/__init__.py
@@ -0,0 +1,37 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import text_sensor
+from esphome.const import (
+ CONF_ENTITY_CATEGORY,
+ CONF_ICON,
+ CONF_SOURCE_ID,
+)
+from esphome.core.entity_helpers import inherit_property_from
+
+from .. import copy_ns
+
+CopyTextSensor = copy_ns.class_("CopyTextSensor", text_sensor.TextSensor, cg.Component)
+
+
+CONFIG_SCHEMA = (
+ text_sensor.text_sensor_schema(CopyTextSensor)
+ .extend(
+ {
+ cv.Required(CONF_SOURCE_ID): cv.use_id(text_sensor.TextSensor),
+ }
+ )
+ .extend(cv.COMPONENT_SCHEMA)
+)
+
+FINAL_VALIDATE_SCHEMA = cv.All(
+ inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
+ inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
+)
+
+
+async def to_code(config):
+ var = await text_sensor.new_text_sensor(config)
+ await cg.register_component(var, config)
+
+ source = await cg.get_variable(config[CONF_SOURCE_ID])
+ cg.add(var.set_source(source))
diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.cpp b/esphome/components/copy/text_sensor/copy_text_sensor.cpp
new file mode 100644
index 0000000000..95fa6d7a1b
--- /dev/null
+++ b/esphome/components/copy/text_sensor/copy_text_sensor.cpp
@@ -0,0 +1,18 @@
+#include "copy_text_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace copy {
+
+static const char *const TAG = "copy.text_sensor";
+
+void CopyTextSensor::setup() {
+ source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
+ if (source_->has_state())
+ this->publish_state(source_->state);
+}
+
+void CopyTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Copy Sensor", this); }
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.h b/esphome/components/copy/text_sensor/copy_text_sensor.h
new file mode 100644
index 0000000000..fe91fe948b
--- /dev/null
+++ b/esphome/components/copy/text_sensor/copy_text_sensor.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/text_sensor/text_sensor.h"
+
+namespace esphome {
+namespace copy {
+
+class CopyTextSensor : public text_sensor::TextSensor, public Component {
+ public:
+ void set_source(text_sensor::TextSensor *source) { source_ = source; }
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+ text_sensor::TextSensor *source_;
+};
+
+} // namespace copy
+} // namespace esphome
diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py
index 0fd27f3f27..90e5ee1f03 100644
--- a/esphome/components/cover/__init__.py
+++ b/esphome/components/cover/__init__.py
@@ -7,6 +7,7 @@ from esphome.const import (
CONF_ID,
CONF_DEVICE_CLASS,
CONF_STATE,
+ CONF_ON_OPEN,
CONF_POSITION,
CONF_POSITION_COMMAND_TOPIC,
CONF_POSITION_STATE_TOPIC,
@@ -16,6 +17,17 @@ from esphome.const import (
CONF_STOP,
CONF_MQTT_ID,
CONF_TRIGGER_ID,
+ DEVICE_CLASS_AWNING,
+ DEVICE_CLASS_BLIND,
+ DEVICE_CLASS_CURTAIN,
+ DEVICE_CLASS_DAMPER,
+ DEVICE_CLASS_DOOR,
+ DEVICE_CLASS_EMPTY,
+ DEVICE_CLASS_GARAGE,
+ DEVICE_CLASS_GATE,
+ DEVICE_CLASS_SHADE,
+ DEVICE_CLASS_SHUTTER,
+ DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
@@ -24,17 +36,17 @@ IS_PLATFORM_COMPONENT = True
CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [
- "",
- "awning",
- "blind",
- "curtain",
- "damper",
- "door",
- "garage",
- "gate",
- "shade",
- "shutter",
- "window",
+ DEVICE_CLASS_AWNING,
+ DEVICE_CLASS_BLIND,
+ DEVICE_CLASS_CURTAIN,
+ DEVICE_CLASS_DAMPER,
+ DEVICE_CLASS_DOOR,
+ DEVICE_CLASS_EMPTY,
+ DEVICE_CLASS_GARAGE,
+ DEVICE_CLASS_GATE,
+ DEVICE_CLASS_SHADE,
+ DEVICE_CLASS_SHUTTER,
+ DEVICE_CLASS_WINDOW,
]
cover_ns = cg.esphome_ns.namespace("cover")
@@ -74,7 +86,6 @@ CoverClosedTrigger = cover_ns.class_(
"CoverClosedTrigger", automation.Trigger.template()
)
-CONF_ON_OPEN = "on_open"
CONF_ON_CLOSED = "on_closed"
COVER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend(
diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp
index 21f35f14de..d139bab8ee 100644
--- a/esphome/components/cover/cover.cpp
+++ b/esphome/components/cover/cover.cpp
@@ -31,9 +31,7 @@ const char *cover_operation_to_str(CoverOperation op) {
}
}
-Cover::Cover(const std::string &name) : EntityBase(name), position{COVER_OPEN} {}
-
-uint32_t Cover::hash_base() { return 1727367479UL; }
+Cover::Cover() : position{COVER_OPEN} {}
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
CoverCall &CoverCall::set_command(const char *command) {
@@ -147,7 +145,7 @@ CoverCall &CoverCall::set_stop(bool stop) {
return *this;
}
bool CoverCall::get_stop() const { return this->stop_; }
-void Cover::set_device_class(const std::string &device_class) { this->device_class_override_ = device_class; }
+
CoverCall Cover::make_call() { return {this}; }
void Cover::open() {
auto call = this->make_call();
@@ -206,18 +204,9 @@ optional Cover::restore_state_() {
return {};
return recovered;
}
-Cover::Cover() : Cover("") {}
-std::string Cover::get_device_class() {
- if (this->device_class_override_.has_value())
- return *this->device_class_override_;
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
- return this->device_class();
-#pragma GCC diagnostic pop
-}
+
bool Cover::is_fully_open() const { return this->position == COVER_OPEN; }
bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; }
-std::string Cover::device_class() { return ""; }
CoverCall CoverRestoreState::to_call(Cover *cover) {
auto call = cover->make_call();
diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h
index 1b5d3a8fa1..89598a9636 100644
--- a/esphome/components/cover/cover.h
+++ b/esphome/components/cover/cover.h
@@ -108,10 +108,9 @@ const char *cover_operation_to_str(CoverOperation op);
* to control all values of the cover. Also implement get_traits() to return what operations
* the cover supports.
*/
-class Cover : public EntityBase {
+class Cover : public EntityBase, public EntityBase_DeviceClass {
public:
explicit Cover();
- explicit Cover(const std::string &name);
/// The current operation of the cover (idle, opening, closing).
CoverOperation current_operation{COVER_OPERATION_IDLE};
@@ -141,8 +140,9 @@ class Cover : public EntityBase {
/** Stop the cover.
*
* This is a legacy method and may be removed later, please use `.make_call()` instead.
+ * As per solution from issue #2885 the call should include perform()
*/
- ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop() instead.", "2021.9")
+ ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
void stop();
void add_on_state_callback(std::function &&f);
@@ -157,8 +157,6 @@ class Cover : public EntityBase {
void publish_state(bool save = true);
virtual CoverTraits get_traits() = 0;
- void set_device_class(const std::string &device_class);
- std::string get_device_class();
/// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0
bool is_fully_open() const;
@@ -170,17 +168,9 @@ class Cover : public EntityBase {
virtual void control(const CoverCall &call) = 0;
- /** Override this to set the default device class.
- *
- * @deprecated This method is deprecated, set the property during config validation instead. (2022.1)
- */
- virtual std::string device_class();
-
optional restore_state_();
- uint32_t hash_base() override;
CallbackManager state_callback_{};
- optional device_class_override_{};
ESPPreferenceObject rtc_;
};
diff --git a/esphome/components/cover/cover_traits.h b/esphome/components/cover/cover_traits.h
index fb30883f77..79001c3b03 100644
--- a/esphome/components/cover/cover_traits.h
+++ b/esphome/components/cover/cover_traits.h
@@ -15,12 +15,15 @@ class CoverTraits {
void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; }
bool get_supports_toggle() const { return this->supports_toggle_; }
void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; }
+ bool get_supports_stop() const { return this->supports_stop_; }
+ void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; }
protected:
bool is_assumed_state_{false};
bool supports_position_{false};
bool supports_tilt_{false};
bool supports_toggle_{false};
+ bool supports_stop_{false};
};
} // namespace cover
diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp
index b0c0531936..fb8e50b87a 100644
--- a/esphome/components/cs5460a/cs5460a.cpp
+++ b/esphome/components/cs5460a/cs5460a.cpp
@@ -305,7 +305,7 @@ bool CS5460AComponent::check_status_() {
voltage_sensor_->publish_state(raw_voltage * voltage_multiplier_);
if (power_sensor_ != nullptr && raw_energy != prev_raw_energy_) {
- int32_t raw = (int32_t)(raw_energy << 8) >> 8; /* Sign-extend */
+ int32_t raw = (int32_t) (raw_energy << 8) >> 8; /* Sign-extend */
power_sensor_->publish_state(raw * power_multiplier_);
prev_raw_energy_ = raw_energy;
}
diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp
index 25d75da3e6..f232f35ea6 100644
--- a/esphome/components/cse7766/cse7766.cpp
+++ b/esphome/components/cse7766/cse7766.cpp
@@ -13,8 +13,9 @@ void CSE7766Component::loop() {
this->raw_data_index_ = 0;
}
- if (this->available() == 0)
+ if (this->available() == 0) {
return;
+ }
this->last_transmission_ = now;
while (this->available() != 0) {
@@ -22,6 +23,7 @@ void CSE7766Component::loop() {
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
+ continue;
}
if (this->raw_data_index_ == 23) {
@@ -51,8 +53,9 @@ bool CSE7766Component::check_byte_() {
if (index == 23) {
uint8_t checksum = 0;
- for (uint8_t i = 2; i < 23; i++)
+ for (uint8_t i = 2; i < 23; i++) {
checksum += this->raw_data_[i];
+ }
if (checksum != this->raw_data_[23]) {
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, this->raw_data_[23]);
@@ -66,20 +69,34 @@ bool CSE7766Component::check_byte_() {
void CSE7766Component::parse_data_() {
ESP_LOGVV(TAG, "CSE7766 Data: ");
for (uint8_t i = 0; i < 23; i++) {
- ESP_LOGVV(TAG, " i=%u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i, BYTE_TO_BINARY(this->raw_data_[i]),
+ ESP_LOGVV(TAG, " %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]),
this->raw_data_[i]);
}
uint8_t header1 = this->raw_data_[0];
if (header1 == 0xAA) {
- ESP_LOGW(TAG, "CSE7766 not calibrated!");
+ ESP_LOGE(TAG, "CSE7766 not calibrated!");
return;
}
- if ((header1 & 0xF0) == 0xF0 && ((header1 >> 0) & 1) == 1) {
- ESP_LOGW(TAG, "CSE7766 reports abnormal hardware: (0x%02X)", header1);
- ESP_LOGW(TAG, " Coefficient storage area is abnormal.");
- return;
+ bool power_cycle_exceeds_range = false;
+
+ if ((header1 & 0xF0) == 0xF0) {
+ if (header1 & 0xD) {
+ ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1);
+ if (header1 & (1 << 3)) {
+ ESP_LOGE(TAG, " Voltage cycle exceeds range.");
+ }
+ if (header1 & (1 << 2)) {
+ ESP_LOGE(TAG, " Current cycle exceeds range.");
+ }
+ if (header1 & (1 << 0)) {
+ ESP_LOGE(TAG, " Coefficient storage area is abnormal.");
+ }
+ return;
+ }
+
+ power_cycle_exceeds_range = header1 & (1 << 1);
}
uint32_t voltage_calib = this->get_24_bit_uint_(2);
@@ -92,46 +109,29 @@ void CSE7766Component::parse_data_() {
uint8_t adj = this->raw_data_[20];
uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
- bool power_ok = true;
- bool voltage_ok = true;
- bool current_ok = true;
-
- if (header1 > 0xF0) {
- // ESP_LOGV(TAG, "CSE7766 reports abnormal hardware: (0x%02X)", byte);
- if ((header1 >> 3) & 1) {
- ESP_LOGV(TAG, " Voltage cycle exceeds range.");
- voltage_ok = false;
- }
- if ((header1 >> 2) & 1) {
- ESP_LOGV(TAG, " Current cycle exceeds range.");
- current_ok = false;
- }
- if ((header1 >> 1) & 1) {
- ESP_LOGV(TAG, " Power cycle exceeds range.");
- power_ok = false;
- }
- if ((header1 >> 0) & 1) {
- ESP_LOGV(TAG, " Coefficient storage area is abnormal.");
- return;
- }
- }
-
- if ((adj & 0x40) == 0x40 && voltage_ok && current_ok) {
+ bool have_voltage = adj & 0x40;
+ if (have_voltage) {
// voltage cycle of serial port outputted is a complete cycle;
this->voltage_acc_ += voltage_calib / float(voltage_cycle);
this->voltage_counts_ += 1;
}
- float power = 0;
- if ((adj & 0x10) == 0x10 && voltage_ok && current_ok && power_ok) {
+ bool have_power = adj & 0x10;
+ float power = 0.0f;
+
+ if (have_power) {
// power cycle of serial port outputted is a complete cycle;
- power = power_calib / float(power_cycle);
+ // According to the user manual, power cycle exceeding range means the measured power is 0
+ if (!power_cycle_exceeds_range) {
+ power = power_calib / float(power_cycle);
+ }
this->power_acc_ += power;
this->power_counts_ += 1;
uint32_t difference;
- if (this->cf_pulses_last_ == 0)
+ if (this->cf_pulses_last_ == 0) {
this->cf_pulses_last_ = cf_pulses;
+ }
if (cf_pulses < this->cf_pulses_last_) {
difference = cf_pulses + (0x10000 - this->cf_pulses_last_);
@@ -139,41 +139,52 @@ void CSE7766Component::parse_data_() {
difference = cf_pulses - this->cf_pulses_last_;
}
this->cf_pulses_last_ = cf_pulses;
- this->energy_total_ += difference * float(power_calib) / 1000000.0 / 3600.0;
+ this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f;
+ this->energy_total_counts_ += 1;
}
- if ((adj & 0x20) == 0x20 && current_ok && voltage_ok && power != 0.0) {
+ if (adj & 0x20) {
// indicates current cycle of serial port outputted is a complete cycle;
- this->current_acc_ += current_calib / float(current_cycle);
+ float current = 0.0f;
+ if (have_voltage && !have_power) {
+ // Testing has shown that when we have voltage and current but not power, that means the power is 0.
+ // We report a power of 0, which in turn means we should report a current of 0.
+ this->power_counts_ += 1;
+ } else if (power != 0.0f) {
+ current = current_calib / float(current_cycle);
+ }
+ this->current_acc_ += current;
this->current_counts_ += 1;
}
}
void CSE7766Component::update() {
- float voltage = this->voltage_counts_ > 0 ? this->voltage_acc_ / this->voltage_counts_ : 0.0f;
- float current = this->current_counts_ > 0 ? this->current_acc_ / this->current_counts_ : 0.0f;
- float power = this->power_counts_ > 0 ? this->power_acc_ / this->power_counts_ : 0.0f;
+ const auto publish_state = [](const char *name, sensor::Sensor *sensor, float &acc, uint32_t &counts) {
+ if (counts != 0) {
+ const auto avg = acc / counts;
- ESP_LOGV(TAG, "Got voltage_acc=%.2f current_acc=%.2f power_acc=%.2f", this->voltage_acc_, this->current_acc_,
- this->power_acc_);
- ESP_LOGV(TAG, "Got voltage_counts=%d current_counts=%d power_counts=%d", this->voltage_counts_, this->current_counts_,
- this->power_counts_);
- ESP_LOGD(TAG, "Got voltage=%.1fV current=%.1fA power=%.1fW", voltage, current, power);
+ ESP_LOGV(TAG, "Got %s_acc=%.2f %s_counts=%d %s=%.1f", name, acc, name, counts, name, avg);
- if (this->voltage_sensor_ != nullptr)
- this->voltage_sensor_->publish_state(voltage);
- if (this->current_sensor_ != nullptr)
- this->current_sensor_->publish_state(current);
- if (this->power_sensor_ != nullptr)
- this->power_sensor_->publish_state(power);
- if (this->energy_sensor_ != nullptr)
- this->energy_sensor_->publish_state(this->energy_total_);
+ if (sensor != nullptr) {
+ sensor->publish_state(avg);
+ }
- this->voltage_acc_ = 0.0f;
- this->current_acc_ = 0.0f;
- this->power_acc_ = 0.0f;
- this->voltage_counts_ = 0;
- this->power_counts_ = 0;
- this->current_counts_ = 0;
+ acc = 0.0f;
+ counts = 0;
+ }
+ };
+
+ publish_state("voltage", this->voltage_sensor_, this->voltage_acc_, this->voltage_counts_);
+ publish_state("current", this->current_sensor_, this->current_acc_, this->current_counts_);
+ publish_state("power", this->power_sensor_, this->power_acc_, this->power_counts_);
+
+ if (this->energy_total_counts_ != 0) {
+ ESP_LOGV(TAG, "Got energy_total=%.2f energy_total_counts=%d", this->energy_total_, this->energy_total_counts_);
+
+ if (this->energy_sensor_ != nullptr) {
+ this->energy_sensor_->publish_state(this->energy_total_);
+ }
+ this->energy_total_counts_ = 0;
+ }
}
uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) {
diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h
index d6062c251c..2f30eec09f 100644
--- a/esphome/components/cse7766/cse7766.h
+++ b/esphome/components/cse7766/cse7766.h
@@ -39,6 +39,8 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice {
uint32_t voltage_counts_{0};
uint32_t current_counts_{0};
uint32_t power_counts_{0};
+ // Setting this to 1 means it will always publish 0 once at startup
+ uint32_t energy_total_counts_{1};
};
} // namespace cse7766
diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp
index 51b0f1318c..d555befcde 100644
--- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp
+++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp
@@ -33,7 +33,10 @@ void CTClampSensor::update() {
const float rms_ac_dc_squared = this->sample_squared_sum_ / this->num_samples_;
const float rms_dc = this->sample_sum_ / this->num_samples_;
- const float rms_ac = std::sqrt(rms_ac_dc_squared - rms_dc * rms_dc);
+ const float rms_ac_squared = rms_ac_dc_squared - rms_dc * rms_dc;
+ float rms_ac = 0;
+ if (rms_ac_squared > 0)
+ rms_ac = std::sqrt(rms_ac_squared);
ESP_LOGD(TAG, "'%s' - Raw AC Value: %.3fA after %d different samples (%d SPS)", this->name_.c_str(), rms_ac,
this->num_samples_, 1000 * this->num_samples_ / this->sample_duration_);
this->publish_state(rms_ac);
diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py
index 049905d0a7..18ea5877d2 100644
--- a/esphome/components/ct_clamp/sensor.py
+++ b/esphome/components/ct_clamp/sensor.py
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
from esphome.components import sensor, voltage_sampler
from esphome.const import (
CONF_SENSOR,
- CONF_ID,
DEVICE_CLASS_CURRENT,
STATE_CLASS_MEASUREMENT,
UNIT_AMPERE,
@@ -19,6 +18,7 @@ CTClampSensor = ct_clamp_ns.class_("CTClampSensor", sensor.Sensor, cg.PollingCom
CONFIG_SCHEMA = (
sensor.sensor_schema(
+ CTClampSensor,
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
@@ -26,7 +26,6 @@ CONFIG_SCHEMA = (
)
.extend(
{
- cv.GenerateID(): cv.declare_id(CTClampSensor),
cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler),
cv.Optional(
CONF_SAMPLE_DURATION, default="200ms"
@@ -38,9 +37,8 @@ CONFIG_SCHEMA = (
async def to_code(config):
- var = cg.new_Pvariable(config[CONF_ID])
+ var = await sensor.new_sensor(config)
await cg.register_component(var, config)
- await sensor.register_sensor(var, config)
sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_source(sens))
diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp
index 9f0a59377d..ff5ad43997 100644
--- a/esphome/components/current_based/current_based_cover.cpp
+++ b/esphome/components/current_based/current_based_cover.cpp
@@ -12,7 +12,9 @@ using namespace esphome::cover;
CoverTraits CurrentBasedCover::get_traits() {
auto traits = CoverTraits();
+ traits.set_supports_stop(true);
traits.set_supports_position(true);
+ traits.set_supports_toggle(true);
traits.set_is_assumed_state(false);
return traits;
}
@@ -20,6 +22,20 @@ void CurrentBasedCover::control(const CoverCall &call) {
if (call.get_stop()) {
this->direction_idle_();
}
+ if (call.get_toggle().has_value()) {
+ if (this->current_operation != COVER_OPERATION_IDLE) {
+ this->start_direction_(COVER_OPERATION_IDLE);
+ this->publish_state();
+ } else {
+ if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
+ this->target_position_ = COVER_OPEN;
+ this->start_direction_(COVER_OPERATION_OPENING);
+ } else {
+ this->target_position_ = COVER_CLOSED;
+ this->start_direction_(COVER_OPERATION_CLOSING);
+ }
+ }
+ }
if (call.get_position().has_value()) {
auto pos = *call.get_position();
if (pos == this->position) {
@@ -131,7 +147,7 @@ void CurrentBasedCover::dump_config() {
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100);
if (this->max_duration_ != UINT32_MAX) {
- ESP_LOGCONFIG(TAG, "Maximun duration: %.1fs", this->max_duration_ / 1e3f);
+ ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f);
}
ESP_LOGCONFIG(TAG, "Start sensing delay: %.1fs", this->start_sensing_delay_ / 1e3f);
ESP_LOGCONFIG(TAG, "Malfunction detection: %s", YESNO(this->malfunction_detection_));
@@ -164,7 +180,7 @@ bool CurrentBasedCover::is_closing_blocked_() const {
if (this->close_obstacle_current_threshold_ == FLT_MAX) {
return false;
}
- return this->open_sensor_->get_state() > this->open_obstacle_current_threshold_;
+ return this->close_sensor_->get_state() > this->close_obstacle_current_threshold_;
}
bool CurrentBasedCover::is_initial_delay_finished_() const {
return millis() - this->start_dir_time_ > this->start_sensing_delay_;
@@ -202,9 +218,11 @@ void CurrentBasedCover::start_direction_(CoverOperation dir) {
trig = this->stop_trigger_;
break;
case COVER_OPERATION_OPENING:
+ this->last_operation_ = dir;
trig = this->open_trigger_;
break;
case COVER_OPERATION_CLOSING:
+ this->last_operation_ = dir;
trig = this->close_trigger_;
break;
default:
diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h
index 220b770c05..b172e762b0 100644
--- a/esphome/components/current_based/current_based_cover.h
+++ b/esphome/components/current_based/current_based_cover.h
@@ -89,6 +89,8 @@ class CurrentBasedCover : public cover::Cover, public Component {
uint32_t start_dir_time_{0};
uint32_t last_publish_time_{0};
float target_position_{0};
+
+ cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
};
} // namespace current_based
diff --git a/esphome/components/custom/binary_sensor/__init__.py b/esphome/components/custom/binary_sensor/__init__.py
index 18d613d4c1..8d6d621b3a 100644
--- a/esphome/components/custom/binary_sensor/__init__.py
+++ b/esphome/components/custom/binary_sensor/__init__.py
@@ -11,7 +11,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(): cv.declare_id(CustomBinarySensorConstructor),
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Required(CONF_BINARY_SENSORS): cv.ensure_list(
- binary_sensor.BINARY_SENSOR_SCHEMA
+ binary_sensor.binary_sensor_schema()
),
}
)
diff --git a/esphome/components/custom/binary_sensor/custom_binary_sensor.h b/esphome/components/custom/binary_sensor/custom_binary_sensor.h
index 314b9b0832..b7d5458d9e 100644
--- a/esphome/components/custom/binary_sensor/custom_binary_sensor.h
+++ b/esphome/components/custom/binary_sensor/custom_binary_sensor.h
@@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
+#include
+
namespace esphome {
namespace custom {
diff --git a/esphome/components/custom/climate/custom_climate.h b/esphome/components/custom/climate/custom_climate.h
index 250d83f69f..37876f7115 100644
--- a/esphome/components/custom/climate/custom_climate.h
+++ b/esphome/components/custom/climate/custom_climate.h
@@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
+#include
+
namespace esphome {
namespace custom {
diff --git a/esphome/components/custom/cover/custom_cover.h b/esphome/components/custom/cover/custom_cover.h
index 71f271bc86..58330b9d54 100644
--- a/esphome/components/custom/cover/custom_cover.h
+++ b/esphome/components/custom/cover/custom_cover.h
@@ -2,6 +2,8 @@
#include "esphome/components/cover/cover.h"
+#include
+
namespace esphome {
namespace custom {
diff --git a/esphome/components/custom/light/custom_light_output.h b/esphome/components/custom/light/custom_light_output.h
index 744e99b889..c2c83ebe97 100644
--- a/esphome/components/custom/light/custom_light_output.h
+++ b/esphome/components/custom/light/custom_light_output.h
@@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/components/light/light_output.h"
+#include
+
namespace esphome {
namespace custom {
diff --git a/esphome/components/custom/output/custom_output.h b/esphome/components/custom/output/custom_output.h
index 1b55d51e29..4624642420 100644
--- a/esphome/components/custom/output/custom_output.h
+++ b/esphome/components/custom/output/custom_output.h
@@ -4,6 +4,8 @@
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
+#include
+
namespace esphome {
namespace custom {
diff --git a/esphome/components/custom/sensor/__init__.py b/esphome/components/custom/sensor/__init__.py
index bf9421e43e..be17d9a334 100644
--- a/esphome/components/custom/sensor/__init__.py
+++ b/esphome/components/custom/sensor/__init__.py
@@ -10,7 +10,7 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CustomSensorConstructor),
cv.Required(CONF_LAMBDA): cv.returning_lambda,
- cv.Required(CONF_SENSORS): cv.ensure_list(sensor.SENSOR_SCHEMA),
+ cv.Required(CONF_SENSORS): cv.ensure_list(sensor.sensor_schema()),
}
)
diff --git a/esphome/components/custom/sensor/custom_sensor.h b/esphome/components/custom/sensor/custom_sensor.h
index 5ef3658e5d..d8f4fbc109 100644
--- a/esphome/components/custom/sensor/custom_sensor.h
+++ b/esphome/components/custom/sensor/custom_sensor.h
@@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
+#include
+
namespace esphome {
namespace custom {
diff --git a/esphome/components/custom/switch/__init__.py b/esphome/components/custom/switch/__init__.py
index e0b9d7751a..5538ae6aa0 100644
--- a/esphome/components/custom/switch/__init__.py
+++ b/esphome/components/custom/switch/__init__.py
@@ -10,13 +10,7 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CustomSwitchConstructor),
cv.Required(CONF_LAMBDA): cv.returning_lambda,
- cv.Required(CONF_SWITCHES): cv.ensure_list(
- switch.SWITCH_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(switch.Switch),
- }
- )
- ),
+ cv.Required(CONF_SWITCHES): cv.ensure_list(switch.switch_schema(switch.Switch)),
}
)
diff --git a/esphome/components/custom/switch/custom_switch.h b/esphome/components/custom/switch/custom_switch.h
index 186e7473fe..9657e4b44d 100644
--- a/esphome/components/custom/switch/custom_switch.h
+++ b/esphome/components/custom/switch/custom_switch.h
@@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
+#include
+
namespace esphome {
namespace custom {
diff --git a/esphome/components/custom/text_sensor/__init__.py b/esphome/components/custom/text_sensor/__init__.py
index 5b6d416436..70728af604 100644
--- a/esphome/components/custom/text_sensor/__init__.py
+++ b/esphome/components/custom/text_sensor/__init__.py
@@ -11,11 +11,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(): cv.declare_id(CustomTextSensorConstructor),
cv.Required(CONF_LAMBDA): cv.returning_lambda,
cv.Required(CONF_TEXT_SENSORS): cv.ensure_list(
- text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- )
+ text_sensor.text_sensor_schema()
),
}
)
diff --git a/esphome/components/custom/text_sensor/custom_text_sensor.h b/esphome/components/custom/text_sensor/custom_text_sensor.h
index f1e9c7665e..13732c00b6 100644
--- a/esphome/components/custom/text_sensor/custom_text_sensor.h
+++ b/esphome/components/custom/text_sensor/custom_text_sensor.h
@@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/components/text_sensor/text_sensor.h"
+#include
+
namespace esphome {
namespace custom {
diff --git a/esphome/components/custom_component/custom_component.h b/esphome/components/custom_component/custom_component.h
index 3f5760e4cf..3b34019a7a 100644
--- a/esphome/components/custom_component/custom_component.h
+++ b/esphome/components/custom_component/custom_component.h
@@ -3,6 +3,8 @@
#include "esphome/core/component.h"
#include "esphome/core/application.h"
+#include
+
namespace esphome {
namespace custom_component {
diff --git a/esphome/components/dac7678/__init__.py b/esphome/components/dac7678/__init__.py
new file mode 100644
index 0000000000..b6cd2b384e
--- /dev/null
+++ b/esphome/components/dac7678/__init__.py
@@ -0,0 +1,32 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c
+from esphome.const import CONF_ID
+
+AUTO_LOAD = ["output"]
+CODEOWNERS = ["@NickB1"]
+DEPENDENCIES = ["i2c"]
+MULTI_CONF = True
+
+dac7678_ns = cg.esphome_ns.namespace("dac7678")
+DAC7678Output = dac7678_ns.class_("DAC7678Output", cg.Component, i2c.I2CDevice)
+CONF_INTERNAL_REFERENCE = "internal_reference"
+
+CONFIG_SCHEMA = (
+ cv.Schema(
+ {
+ cv.GenerateID(): cv.declare_id(DAC7678Output),
+ cv.Optional(CONF_INTERNAL_REFERENCE, default=False): cv.boolean,
+ }
+ )
+ .extend(cv.COMPONENT_SCHEMA)
+ .extend(i2c.i2c_device_schema(0x48))
+)
+
+
+async def to_code(config):
+ var = cg.new_Pvariable(config[CONF_ID])
+ await cg.register_component(var, config)
+ cg.add(var.set_internal_reference(config[CONF_INTERNAL_REFERENCE]))
+ await i2c.register_i2c_device(var, config)
+ return var
diff --git a/esphome/components/dac7678/dac7678_output.cpp b/esphome/components/dac7678/dac7678_output.cpp
new file mode 100644
index 0000000000..b6de615b30
--- /dev/null
+++ b/esphome/components/dac7678/dac7678_output.cpp
@@ -0,0 +1,86 @@
+#include "dac7678_output.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+#include "esphome/core/hal.h"
+
+namespace esphome {
+namespace dac7678 {
+
+static const char *const TAG = "dac7678";
+
+static const uint8_t DAC7678_REG_INPUT_N = 0x00;
+static const uint8_t DAC7678_REG_SELECT_UPDATE_N = 0x10;
+static const uint8_t DAC7678_REG_WRITE_N_UPDATE_ALL = 0x20;
+static const uint8_t DAC7678_REG_WRITE_N_UPDATE_N = 0x30;
+static const uint8_t DAC7678_REG_POWER = 0x40;
+static const uint8_t DAC7678_REG_CLEAR_CODE = 0x50;
+static const uint8_t DAC7678_REG_LDAC = 0x60;
+static const uint8_t DAC7678_REG_SOFTWARE_RESET = 0x70;
+static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80;
+static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90;
+
+void DAC7678Output::setup() {
+ ESP_LOGCONFIG(TAG, "Setting up DAC7678OutputComponent...");
+
+ ESP_LOGV(TAG, "Resetting device...");
+
+ // Reset device
+ if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) {
+ ESP_LOGE(TAG, "Reset failed");
+ this->mark_failed();
+ return;
+ } else {
+ ESP_LOGV(TAG, "Reset succeeded");
+ }
+
+ delayMicroseconds(1000);
+
+ // Set internal reference
+ if (this->internal_reference_) {
+ if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) {
+ ESP_LOGE(TAG, "Set internal reference failed");
+ this->mark_failed();
+ return;
+ } else {
+ ESP_LOGV(TAG, "Internal reference enabled");
+ }
+ }
+}
+
+void DAC7678Output::dump_config() {
+ if (this->is_failed()) {
+ ESP_LOGE(TAG, "Setting up DAC7678 failed!");
+ } else {
+ ESP_LOGCONFIG(TAG, "DAC7678 initialised");
+ }
+}
+
+void DAC7678Output::register_channel(DAC7678Channel *channel) {
+ auto c = channel->channel_;
+ this->min_channel_ = std::min(this->min_channel_, c);
+ this->max_channel_ = std::max(this->max_channel_, c);
+ channel->set_parent(this);
+ ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_);
+}
+
+void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) {
+ if (this->dac_input_reg_[channel] != value) {
+ ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value);
+
+ if (!this->write_byte_16(DAC7678_REG_WRITE_N_UPDATE_N | channel, value << 4)) {
+ this->status_set_warning();
+ return;
+ }
+ }
+ this->dac_input_reg_[channel] = value;
+ this->status_clear_warning();
+}
+
+void DAC7678Channel::write_state(float state) {
+ const float input_rounded = roundf(state * this->full_scale_);
+ auto input = static_cast(input_rounded);
+ this->parent_->set_channel_value_(this->channel_, input);
+}
+
+} // namespace dac7678
+} // namespace esphome
diff --git a/esphome/components/dac7678/dac7678_output.h b/esphome/components/dac7678/dac7678_output.h
new file mode 100644
index 0000000000..abd9875e4c
--- /dev/null
+++ b/esphome/components/dac7678/dac7678_output.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/core/helpers.h"
+#include "esphome/components/output/float_output.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace dac7678 {
+
+class DAC7678Output;
+
+class DAC7678Channel : public output::FloatOutput, public Parented {
+ public:
+ void set_channel(uint8_t channel) { channel_ = channel; }
+
+ protected:
+ friend class DAC7678Output;
+
+ const uint16_t full_scale_ = 0xFFF;
+
+ void write_state(float state) override;
+
+ uint8_t channel_;
+};
+
+/// DAC7678 float output component.
+class DAC7678Output : public Component, public i2c::I2CDevice {
+ public:
+ DAC7678Output() {}
+
+ void register_channel(DAC7678Channel *channel);
+
+ void set_internal_reference(const bool value) { this->internal_reference_ = value; }
+
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override { return setup_priority::HARDWARE; }
+
+ protected:
+ friend DAC7678Channel;
+
+ bool internal_reference_;
+
+ void set_channel_value_(uint8_t channel, uint16_t value);
+
+ uint8_t min_channel_{0xFF};
+ uint8_t max_channel_{0x00};
+ uint16_t dac_input_reg_[8] = {
+ 0,
+ };
+};
+
+} // namespace dac7678
+} // namespace esphome
diff --git a/esphome/components/dac7678/output.py b/esphome/components/dac7678/output.py
new file mode 100644
index 0000000000..f41e5c2422
--- /dev/null
+++ b/esphome/components/dac7678/output.py
@@ -0,0 +1,27 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import output
+from esphome.const import CONF_CHANNEL, CONF_ID
+from . import DAC7678Output, dac7678_ns
+
+DEPENDENCIES = ["dac7678"]
+
+DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput)
+CONF_DAC7678_ID = "dac7678_id"
+
+CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
+ {
+ cv.Required(CONF_ID): cv.declare_id(DAC7678Channel),
+ cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output),
+ cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
+ }
+)
+
+
+async def to_code(config):
+ paren = await cg.get_variable(config[CONF_DAC7678_ID])
+ var = cg.new_Pvariable(config[CONF_ID])
+ cg.add(var.set_channel(config[CONF_CHANNEL]))
+ cg.add(paren.register_channel(var))
+ await output.register_output(var, config)
+ return var
diff --git a/esphome/components/daikin_brc/__init__.py b/esphome/components/daikin_brc/__init__.py
new file mode 100644
index 0000000000..69002c015f
--- /dev/null
+++ b/esphome/components/daikin_brc/__init__.py
@@ -0,0 +1 @@
+CODEOWNERS = ["@hagak"]
diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py
new file mode 100644
index 0000000000..3468b6533c
--- /dev/null
+++ b/esphome/components/daikin_brc/climate.py
@@ -0,0 +1,24 @@
+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"]
+
+daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc")
+DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR)
+
+CONF_USE_FAHRENHEIT = "use_fahrenheit"
+
+CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
+ {
+ cv.GenerateID(): cv.declare_id(DaikinBrcClimate),
+ cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
+ }
+)
+
+
+async def to_code(config):
+ var = cg.new_Pvariable(config[CONF_ID])
+ await climate_ir.register_climate_ir(var, config)
+ cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))
diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp
new file mode 100644
index 0000000000..6683d70f80
--- /dev/null
+++ b/esphome/components/daikin_brc/daikin_brc.cpp
@@ -0,0 +1,273 @@
+#include "daikin_brc.h"
+#include "esphome/components/remote_base/remote_base.h"
+
+namespace esphome {
+namespace daikin_brc {
+
+static const char *const TAG = "daikin_brc.climate";
+
+void DaikinBrcClimate::control(const climate::ClimateCall &call) {
+ this->mode_button_ = 0x00;
+ if (call.get_mode().has_value()) {
+ // Need to determine if this is call due to Mode button pressed so that we can set the Mode button byte
+ this->mode_button_ = DAIKIN_BRC_IR_MODE_BUTTON;
+ }
+ ClimateIR::control(call);
+}
+
+void DaikinBrcClimate::transmit_state() {
+ uint8_t remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE] = {0x11, 0xDA, 0x17, 0x18, 0x04, 0x00, 0x1E, 0x11,
+ 0xDA, 0x17, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x20, 0x00};
+
+ remote_state[12] = this->alt_mode_();
+ remote_state[13] = this->mode_button_;
+ remote_state[14] = this->operation_mode_();
+ remote_state[17] = this->temperature_();
+ remote_state[18] = this->fan_speed_swing_();
+
+ // Calculate checksum
+ for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1; i++) {
+ remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1] += remote_state[i];
+ }
+
+ auto transmit = this->transmitter_->transmit();
+ auto *data = transmit.get_data();
+ data->set_carrier_frequency(DAIKIN_BRC_IR_FREQUENCY);
+
+ data->mark(DAIKIN_BRC_HEADER_MARK);
+ data->space(DAIKIN_BRC_HEADER_SPACE);
+ for (int i = 0; i < DAIKIN_BRC_PREAMBLE_SIZE; i++) {
+ for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
+ data->mark(DAIKIN_BRC_BIT_MARK);
+ bool bit = remote_state[i] & mask;
+ data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE);
+ }
+ }
+ data->mark(DAIKIN_BRC_BIT_MARK);
+ data->space(DAIKIN_BRC_MESSAGE_SPACE);
+ data->mark(DAIKIN_BRC_HEADER_MARK);
+ data->space(DAIKIN_BRC_HEADER_SPACE);
+
+ for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE; i++) {
+ for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask
+ data->mark(DAIKIN_BRC_BIT_MARK);
+ bool bit = remote_state[i] & mask;
+ data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE);
+ }
+ }
+
+ data->mark(DAIKIN_BRC_BIT_MARK);
+ data->space(0);
+
+ transmit.perform();
+}
+
+uint8_t DaikinBrcClimate::alt_mode_() {
+ uint8_t alt_mode = 0x00;
+ switch (this->mode) {
+ case climate::CLIMATE_MODE_DRY:
+ alt_mode = 0x23;
+ break;
+ case climate::CLIMATE_MODE_FAN_ONLY:
+ alt_mode = 0x63;
+ break;
+ case climate::CLIMATE_MODE_HEAT_COOL:
+ case climate::CLIMATE_MODE_COOL:
+ case climate::CLIMATE_MODE_HEAT:
+ default:
+ alt_mode = 0x73;
+ break;
+ }
+ return alt_mode;
+}
+
+uint8_t DaikinBrcClimate::operation_mode_() {
+ uint8_t operating_mode = DAIKIN_BRC_MODE_ON;
+ switch (this->mode) {
+ case climate::CLIMATE_MODE_COOL:
+ operating_mode |= DAIKIN_BRC_MODE_COOL;
+ break;
+ case climate::CLIMATE_MODE_DRY:
+ operating_mode |= DAIKIN_BRC_MODE_DRY;
+ break;
+ case climate::CLIMATE_MODE_HEAT:
+ operating_mode |= DAIKIN_BRC_MODE_HEAT;
+ break;
+ case climate::CLIMATE_MODE_HEAT_COOL:
+ operating_mode |= DAIKIN_BRC_MODE_AUTO;
+ break;
+ case climate::CLIMATE_MODE_FAN_ONLY:
+ operating_mode |= DAIKIN_BRC_MODE_FAN;
+ break;
+ case climate::CLIMATE_MODE_OFF:
+ default:
+ operating_mode = DAIKIN_BRC_MODE_OFF;
+ break;
+ }
+
+ return operating_mode;
+}
+
+uint8_t DaikinBrcClimate::fan_speed_swing_() {
+ uint16_t fan_speed;
+ switch (this->fan_mode.value()) {
+ case climate::CLIMATE_FAN_LOW:
+ fan_speed = DAIKIN_BRC_FAN_1;
+ break;
+ case climate::CLIMATE_FAN_MEDIUM:
+ fan_speed = DAIKIN_BRC_FAN_2;
+ break;
+ case climate::CLIMATE_FAN_HIGH:
+ fan_speed = DAIKIN_BRC_FAN_3;
+ break;
+ default:
+ fan_speed = DAIKIN_BRC_FAN_1;
+ }
+
+ // If swing is enabled switch first 4 bits to 1111
+ switch (this->swing_mode) {
+ case climate::CLIMATE_SWING_BOTH:
+ fan_speed |= DAIKIN_BRC_IR_SWING_ON;
+ break;
+ default:
+ fan_speed |= DAIKIN_BRC_IR_SWING_OFF;
+ break;
+ }
+ return fan_speed;
+}
+
+uint8_t DaikinBrcClimate::temperature_() {
+ // Force special temperatures depending on the mode
+ switch (this->mode) {
+ case climate::CLIMATE_MODE_FAN_ONLY:
+ case climate::CLIMATE_MODE_DRY:
+ if (this->fahrenheit_) {
+ return DAIKIN_BRC_IR_DRY_FAN_TEMP_F;
+ }
+ return DAIKIN_BRC_IR_DRY_FAN_TEMP_C;
+ case climate::CLIMATE_MODE_HEAT_COOL:
+ default:
+ uint8_t temperature;
+ // Temperature in remote is in F
+ if (this->fahrenheit_) {
+ temperature = (uint8_t) roundf(
+ clamp(((this->target_temperature * 1.8) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F));
+ } else {
+ temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1;
+ }
+ return temperature;
+ }
+}
+
+bool DaikinBrcClimate::parse_state_frame_(const uint8_t frame[]) {
+ uint8_t checksum = 0;
+ for (int i = 0; i < (DAIKIN_BRC_STATE_FRAME_SIZE - 1); i++) {
+ checksum += frame[i];
+ }
+ if (frame[DAIKIN_BRC_STATE_FRAME_SIZE - 1] != checksum) {
+ ESP_LOGCONFIG(TAG, "Bad CheckSum %x", checksum);
+ return false;
+ }
+
+ uint8_t mode = frame[7];
+ if (mode & DAIKIN_BRC_MODE_ON) {
+ switch (mode & 0xF0) {
+ case DAIKIN_BRC_MODE_COOL:
+ this->mode = climate::CLIMATE_MODE_COOL;
+ break;
+ case DAIKIN_BRC_MODE_DRY:
+ this->mode = climate::CLIMATE_MODE_DRY;
+ break;
+ case DAIKIN_BRC_MODE_HEAT:
+ this->mode = climate::CLIMATE_MODE_HEAT;
+ break;
+ case DAIKIN_BRC_MODE_AUTO:
+ this->mode = climate::CLIMATE_MODE_HEAT_COOL;
+ break;
+ case DAIKIN_BRC_MODE_FAN:
+ this->mode = climate::CLIMATE_MODE_FAN_ONLY;
+ break;
+ }
+ } else {
+ this->mode = climate::CLIMATE_MODE_OFF;
+ }
+
+ uint8_t temperature = frame[10];
+ float temperature_c;
+ if (this->fahrenheit_) {
+ temperature_c = clamp(((temperature - 32) / 1.8), DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C);
+ } else {
+ temperature_c = (temperature >> 1) + 9;
+ }
+
+ this->target_temperature = temperature_c;
+
+ uint8_t fan_mode = frame[11];
+ uint8_t swing_mode = frame[11];
+ switch (swing_mode & 0xF) {
+ case DAIKIN_BRC_IR_SWING_ON:
+ this->swing_mode = climate::CLIMATE_SWING_BOTH;
+ break;
+ case DAIKIN_BRC_IR_SWING_OFF:
+ this->swing_mode = climate::CLIMATE_SWING_OFF;
+ break;
+ }
+
+ switch (fan_mode & 0xF0) {
+ case DAIKIN_BRC_FAN_1:
+ this->fan_mode = climate::CLIMATE_FAN_LOW;
+ break;
+ case DAIKIN_BRC_FAN_2:
+ this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
+ break;
+ case DAIKIN_BRC_FAN_3:
+ this->fan_mode = climate::CLIMATE_FAN_HIGH;
+ break;
+ }
+ this->publish_state();
+ return true;
+}
+
+bool DaikinBrcClimate::on_receive(remote_base::RemoteReceiveData data) {
+ uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE] = {};
+ if (!data.expect_item(DAIKIN_BRC_HEADER_MARK, DAIKIN_BRC_HEADER_SPACE)) {
+ return false;
+ }
+ for (uint8_t pos = 0; pos < DAIKIN_BRC_STATE_FRAME_SIZE; pos++) {
+ uint8_t byte = 0;
+ for (int8_t bit = 0; bit < 8; bit++) {
+ if (data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ONE_SPACE)) {
+ byte |= 1 << bit;
+ } else if (!data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ZERO_SPACE)) {
+ return false;
+ }
+ }
+ state_frame[pos] = byte;
+ if (pos == 0) {
+ // frame header
+ if (byte != 0x11)
+ return false;
+ } else if (pos == 1) {
+ // frame header
+ if (byte != 0xDA)
+ return false;
+ } else if (pos == 2) {
+ // frame header
+ if (byte != 0x17)
+ return false;
+ } else if (pos == 3) {
+ // frame header
+ if (byte != 0x18)
+ return false;
+ } else if (pos == 4) {
+ // frame type
+ if (byte != 0x00)
+ return false;
+ }
+ }
+ return this->parse_state_frame_(state_frame);
+}
+
+} // namespace daikin_brc
+} // namespace esphome
diff --git a/esphome/components/daikin_brc/daikin_brc.h b/esphome/components/daikin_brc/daikin_brc.h
new file mode 100644
index 0000000000..bdc6384809
--- /dev/null
+++ b/esphome/components/daikin_brc/daikin_brc.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include "esphome/components/climate_ir/climate_ir.h"
+
+namespace esphome {
+namespace daikin_brc {
+
+// Values for Daikin BRC4CXXX IR Controllers
+// Temperature
+const uint8_t DAIKIN_BRC_TEMP_MIN_F = 60; // fahrenheit
+const uint8_t DAIKIN_BRC_TEMP_MAX_F = 90; // fahrenheit
+const float DAIKIN_BRC_TEMP_MIN_C = (DAIKIN_BRC_TEMP_MIN_F - 32) / 1.8; // fahrenheit
+const float DAIKIN_BRC_TEMP_MAX_C = (DAIKIN_BRC_TEMP_MAX_F - 32) / 1.8; // fahrenheit
+
+// Modes
+const uint8_t DAIKIN_BRC_MODE_AUTO = 0x30;
+const uint8_t DAIKIN_BRC_MODE_COOL = 0x20;
+const uint8_t DAIKIN_BRC_MODE_HEAT = 0x10;
+const uint8_t DAIKIN_BRC_MODE_DRY = 0x70;
+const uint8_t DAIKIN_BRC_MODE_FAN = 0x00;
+const uint8_t DAIKIN_BRC_MODE_OFF = 0x00;
+const uint8_t DAIKIN_BRC_MODE_ON = 0x01;
+
+// Fan Speed
+const uint8_t DAIKIN_BRC_FAN_1 = 0x10;
+const uint8_t DAIKIN_BRC_FAN_2 = 0x30;
+const uint8_t DAIKIN_BRC_FAN_3 = 0x50;
+const uint8_t DAIKIN_BRC_FAN_AUTO = 0xA0;
+
+// IR Transmission
+const uint32_t DAIKIN_BRC_IR_FREQUENCY = 38000;
+const uint32_t DAIKIN_BRC_HEADER_MARK = 5070;
+const uint32_t DAIKIN_BRC_HEADER_SPACE = 2140;
+const uint32_t DAIKIN_BRC_BIT_MARK = 370;
+const uint32_t DAIKIN_BRC_ONE_SPACE = 1780;
+const uint32_t DAIKIN_BRC_ZERO_SPACE = 710;
+const uint32_t DAIKIN_BRC_MESSAGE_SPACE = 29410;
+
+const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_F = 72; // Dry/Fan mode is always 17 Celsius.
+const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_C = (17 - 9) * 2; // Dry/Fan mode is always 17 Celsius.
+const uint8_t DAIKIN_BRC_IR_SWING_ON = 0x5;
+const uint8_t DAIKIN_BRC_IR_SWING_OFF = 0x6;
+const uint8_t DAIKIN_BRC_IR_MODE_BUTTON = 0x4; // This is set after a mode action
+
+// State Frame size
+const uint8_t DAIKIN_BRC_STATE_FRAME_SIZE = 15;
+// Preamble size
+const uint8_t DAIKIN_BRC_PREAMBLE_SIZE = 7;
+// Transmit Frame size - includes a preamble
+const uint8_t DAIKIN_BRC_TRANSMIT_FRAME_SIZE = DAIKIN_BRC_PREAMBLE_SIZE + DAIKIN_BRC_STATE_FRAME_SIZE;
+
+class DaikinBrcClimate : public climate_ir::ClimateIR {
+ public:
+ DaikinBrcClimate()
+ : climate_ir::ClimateIR(DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C, 0.5f, true, true,
+ {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
+ {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH}) {}
+
+ /// Set use of Fahrenheit units
+ void set_fahrenheit(bool value) {
+ this->fahrenheit_ = value;
+ this->temperature_step_ = value ? 0.5f : 1.0f;
+ }
+
+ protected:
+ uint8_t mode_button_ = 0x00;
+ // Capture if the MODE was changed
+ void control(const climate::ClimateCall &call) override;
+ // Transmit via IR the state of this climate controller.
+ void transmit_state() override;
+ uint8_t alt_mode_();
+ uint8_t operation_mode_();
+ uint8_t fan_speed_swing_();
+ uint8_t temperature_();
+ // Handle received IR Buffer
+ bool on_receive(remote_base::RemoteReceiveData data) override;
+ bool parse_state_frame_(const uint8_t frame[]);
+ bool fahrenheit_{false};
+};
+
+} // namespace daikin_brc
+} // namespace esphome
diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp
index 1eed2ebf78..302422d6c7 100644
--- a/esphome/components/dallas/dallas_component.cpp
+++ b/esphome/components/dallas/dallas_component.cpp
@@ -32,6 +32,11 @@ void DallasComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up DallasComponent...");
pin_->setup();
+
+ // clear bus with 480µs high, otherwise initial reset in search_vec() fails
+ pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
+ delayMicroseconds(480);
+
one_wire_ = new ESPOneWire(pin_); // NOLINT(cppcoreguidelines-owning-memory)
std::vector raw_sensors;
@@ -99,20 +104,25 @@ void DallasComponent::update() {
this->status_clear_warning();
bool result;
- if (!this->one_wire_->reset()) {
- result = false;
- } else {
- result = true;
- this->one_wire_->skip();
- this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION);
+ {
+ InterruptLock lock;
+ result = this->one_wire_->reset();
}
-
if (!result) {
ESP_LOGE(TAG, "Requesting conversion failed");
this->status_set_warning();
+ for (auto *sensor : this->sensors_) {
+ sensor->publish_state(NAN);
+ }
return;
}
+ {
+ InterruptLock lock;
+ this->one_wire_->skip();
+ this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION);
+ }
+
for (auto *sensor : this->sensors_) {
this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] {
bool res = sensor->read_scratch_pad();
@@ -124,7 +134,6 @@ void DallasComponent::update() {
return;
}
if (!sensor->check_scratch_pad()) {
- ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str());
sensor->publish_state(NAN);
this->status_set_warning();
return;
@@ -152,16 +161,26 @@ const std::string &DallasTemperatureSensor::get_address_name() {
}
bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() {
auto *wire = this->parent_->one_wire_;
- if (!wire->reset()) {
- return false;
+
+ {
+ InterruptLock lock;
+
+ if (!wire->reset()) {
+ return false;
+ }
}
- wire->select(this->address_);
- wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD);
+ {
+ InterruptLock lock;
- for (unsigned char &i : this->scratch_pad_) {
- i = wire->read8();
+ wire->select(this->address_);
+ wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD);
+
+ for (unsigned char &i : this->scratch_pad_) {
+ i = wire->read8();
+ }
}
+
return true;
}
bool DallasTemperatureSensor::setup_sensor() {
@@ -200,17 +219,20 @@ bool DallasTemperatureSensor::setup_sensor() {
}
auto *wire = this->parent_->one_wire_;
- if (wire->reset()) {
- wire->select(this->address_);
- wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD);
- wire->write8(this->scratch_pad_[2]); // high alarm temp
- wire->write8(this->scratch_pad_[3]); // low alarm temp
- wire->write8(this->scratch_pad_[4]); // resolution
- wire->reset();
+ {
+ InterruptLock lock;
+ if (wire->reset()) {
+ wire->select(this->address_);
+ wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD);
+ wire->write8(this->scratch_pad_[2]); // high alarm temp
+ wire->write8(this->scratch_pad_[3]); // low alarm temp
+ wire->write8(this->scratch_pad_[4]); // resolution
+ wire->reset();
- // write value to EEPROM
- wire->select(this->address_);
- wire->write8(0x48);
+ // write value to EEPROM
+ wire->select(this->address_);
+ wire->write8(0x48);
+ }
}
delay(20); // allow it to finish operation
@@ -218,13 +240,29 @@ bool DallasTemperatureSensor::setup_sensor() {
return true;
}
bool DallasTemperatureSensor::check_scratch_pad() {
+ bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]);
+ bool config_validity = false;
+
+ switch (this->get_address8()[0]) {
+ case DALLAS_MODEL_DS18B20:
+ config_validity = ((this->scratch_pad_[4] & 0x9F) == 0x1F);
+ break;
+ default:
+ config_validity = ((this->scratch_pad_[4] & 0x10) == 0x10);
+ }
+
#ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0],
this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4],
this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
crc8(this->scratch_pad_, 8));
#endif
- return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8];
+ if (!chksum_validity) {
+ ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str());
+ } else if (!config_validity) {
+ ESP_LOGW(TAG, "'%s' - Scratch pad config register invalid!", this->get_name().c_str());
+ }
+ return chksum_validity && config_validity;
}
float DallasTemperatureSensor::get_temp_c() {
int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3);
diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h
index 37c098283a..b21bc02e54 100644
--- a/esphome/components/dallas/dallas_component.h
+++ b/esphome/components/dallas/dallas_component.h
@@ -4,6 +4,8 @@
#include "esphome/components/sensor/sensor.h"
#include "esp_one_wire.h"
+#include
+
namespace esphome {
namespace dallas {
diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp
index 6dc085a0bf..32ddf07fb6 100644
--- a/esphome/components/dallas/esp_one_wire.cpp
+++ b/esphome/components/dallas/esp_one_wire.cpp
@@ -15,8 +15,6 @@ ESPOneWire::ESPOneWire(InternalGPIOPin *pin) { pin_ = pin->to_isr(); }
bool HOT IRAM_ATTR ESPOneWire::reset() {
// See reset here:
// https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html
- InterruptLock lock;
-
// Wait for communication to clear (delay G)
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
uint8_t retries = 125;
@@ -43,16 +41,18 @@ bool HOT IRAM_ATTR ESPOneWire::reset() {
}
void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) {
- // See write 1/0 bit here:
- // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html
- InterruptLock lock;
-
// drive bus low
pin_.pin_mode(gpio::FLAG_OUTPUT);
pin_.digital_write(false);
- uint32_t delay0 = bit ? 10 : 65;
- uint32_t delay1 = bit ? 55 : 5;
+ // from datasheet:
+ // write 0 low time: t_low0: min=60µs, max=120µs
+ // write 1 low time: t_low1: min=1µs, max=15µs
+ // time slot: t_slot: min=60µs, max=120µs
+ // recovery time: t_rec: min=1µs
+ // ds18b20 appears to read the bus after roughly 14µs
+ uint32_t delay0 = bit ? 6 : 60;
+ uint32_t delay1 = bit ? 54 : 5;
// delay A/C
delayMicroseconds(delay0);
@@ -63,72 +63,97 @@ void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) {
}
bool HOT IRAM_ATTR ESPOneWire::read_bit() {
- // See read bit here:
- // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html
- InterruptLock lock;
-
- // drive bus low, delay A
+ // drive bus low
pin_.pin_mode(gpio::FLAG_OUTPUT);
pin_.digital_write(false);
+
+ // note: for reading we'll need very accurate timing, as the
+ // timing for the digital_read() is tight; according to the datasheet,
+ // we should read at the end of 16µs starting from the bus low
+ // typically, the ds18b20 pulls the line high after 11µs for a logical 1
+ // and 29µs for a logical 0
+
+ uint32_t start = micros();
+ // datasheet says >1µs
delayMicroseconds(3);
// release bus, delay E
pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
- delayMicroseconds(10);
+
+ // Unfortunately some frameworks have different characteristics than others
+ // esp32 arduino appears to pull the bus low only after the digital_write(false),
+ // whereas on esp-idf it already happens during the pin_mode(OUTPUT)
+ // manually correct for this with these constants.
+
+#ifdef USE_ESP32
+ uint32_t timing_constant = 12;
+#else
+ uint32_t timing_constant = 14;
+#endif
+
+ // measure from start value directly, to get best accurate timing no matter
+ // how long pin_mode/delayMicroseconds took
+ while (micros() - start < timing_constant)
+ ;
// sample bus to read bit from peer
bool r = pin_.digital_read();
- // delay F
- delayMicroseconds(53);
+ // read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked
+ uint32_t now = micros();
+ if (now - start < 60)
+ delayMicroseconds(60 - (now - start));
+
return r;
}
-void ESPOneWire::write8(uint8_t val) {
+void IRAM_ATTR ESPOneWire::write8(uint8_t val) {
for (uint8_t i = 0; i < 8; i++) {
this->write_bit(bool((1u << i) & val));
}
}
-void ESPOneWire::write64(uint64_t val) {
+void IRAM_ATTR ESPOneWire::write64(uint64_t val) {
for (uint8_t i = 0; i < 64; i++) {
this->write_bit(bool((1ULL << i) & val));
}
}
-uint8_t ESPOneWire::read8() {
+uint8_t IRAM_ATTR ESPOneWire::read8() {
uint8_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
ret |= (uint8_t(this->read_bit()) << i);
}
return ret;
}
-uint64_t ESPOneWire::read64() {
+uint64_t IRAM_ATTR ESPOneWire::read64() {
uint64_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
ret |= (uint64_t(this->read_bit()) << i);
}
return ret;
}
-void ESPOneWire::select(uint64_t address) {
+void IRAM_ATTR ESPOneWire::select(uint64_t address) {
this->write8(ONE_WIRE_ROM_SELECT);
this->write64(address);
}
-void ESPOneWire::reset_search() {
+void IRAM_ATTR ESPOneWire::reset_search() {
this->last_discrepancy_ = 0;
this->last_device_flag_ = false;
- this->last_family_discrepancy_ = 0;
this->rom_number_ = 0;
}
-uint64_t ESPOneWire::search() {
+uint64_t IRAM_ATTR ESPOneWire::search() {
if (this->last_device_flag_) {
return 0u;
}
- if (!this->reset()) {
- // Reset failed or no devices present
- this->reset_search();
- return 0u;
+ {
+ InterruptLock lock;
+ if (!this->reset()) {
+ // Reset failed or no devices present
+ this->reset_search();
+ return 0u;
+ }
}
uint8_t id_bit_number = 1;
@@ -137,58 +162,58 @@ uint64_t ESPOneWire::search() {
bool search_result = false;
uint8_t rom_byte_mask = 1;
- // Initiate search
- this->write8(ONE_WIRE_ROM_SEARCH);
- do {
- // read bit
- bool id_bit = this->read_bit();
- // read its complement
- bool cmp_id_bit = this->read_bit();
+ {
+ InterruptLock lock;
+ // Initiate search
+ this->write8(ONE_WIRE_ROM_SEARCH);
+ do {
+ // read bit
+ bool id_bit = this->read_bit();
+ // read its complement
+ bool cmp_id_bit = this->read_bit();
- if (id_bit && cmp_id_bit) {
- // No devices participating in search
- break;
- }
-
- bool branch;
-
- if (id_bit != cmp_id_bit) {
- // only chose one branch, the other one doesn't have any devices.
- branch = id_bit;
- } else {
- // there are devices with both 0s and 1s at this bit
- if (id_bit_number < this->last_discrepancy_) {
- branch = (this->rom_number8_()[rom_byte_number] & rom_byte_mask) > 0;
- } else {
- branch = id_bit_number == this->last_discrepancy_;
+ if (id_bit && cmp_id_bit) {
+ // No devices participating in search
+ break;
}
- if (!branch) {
- last_zero = id_bit_number;
- if (last_zero < 9) {
- this->last_discrepancy_ = last_zero;
+ bool branch;
+
+ if (id_bit != cmp_id_bit) {
+ // only chose one branch, the other one doesn't have any devices.
+ branch = id_bit;
+ } else {
+ // there are devices with both 0s and 1s at this bit
+ if (id_bit_number < this->last_discrepancy_) {
+ branch = (this->rom_number8_()[rom_byte_number] & rom_byte_mask) > 0;
+ } else {
+ branch = id_bit_number == this->last_discrepancy_;
+ }
+
+ if (!branch) {
+ last_zero = id_bit_number;
}
}
- }
- if (branch) {
- // set bit
- this->rom_number8_()[rom_byte_number] |= rom_byte_mask;
- } else {
- // clear bit
- this->rom_number8_()[rom_byte_number] &= ~rom_byte_mask;
- }
+ if (branch) {
+ // set bit
+ this->rom_number8_()[rom_byte_number] |= rom_byte_mask;
+ } else {
+ // clear bit
+ this->rom_number8_()[rom_byte_number] &= ~rom_byte_mask;
+ }
- // choose/announce branch
- this->write_bit(branch);
- id_bit_number++;
- rom_byte_mask <<= 1;
- if (rom_byte_mask == 0u) {
- // go to next byte
- rom_byte_number++;
- rom_byte_mask = 1;
- }
- } while (rom_byte_number < 8); // loop through all bytes
+ // choose/announce branch
+ this->write_bit(branch);
+ id_bit_number++;
+ rom_byte_mask <<= 1;
+ if (rom_byte_mask == 0u) {
+ // go to next byte
+ rom_byte_number++;
+ rom_byte_mask = 1;
+ }
+ } while (rom_byte_number < 8); // loop through all bytes
+ }
if (id_bit_number >= 65) {
this->last_discrepancy_ = last_zero;
@@ -217,7 +242,7 @@ std::vector ESPOneWire::search_vec() {
return res;
}
-void ESPOneWire::skip() {
+void IRAM_ATTR ESPOneWire::skip() {
this->write8(0xCC); // skip ROM
}
diff --git a/esphome/components/dallas/esp_one_wire.h b/esphome/components/dallas/esp_one_wire.h
index ef6f079f02..7544a6fe98 100644
--- a/esphome/components/dallas/esp_one_wire.h
+++ b/esphome/components/dallas/esp_one_wire.h
@@ -60,7 +60,6 @@ class ESPOneWire {
ISRInternalGPIOPin pin_;
uint8_t last_discrepancy_{0};
- uint8_t last_family_discrepancy_{0};
bool last_device_flag_{false};
uint64_t rom_number_{0};
};
diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py
index 14ad0efa7b..9288f0a3a6 100644
--- a/esphome/components/dallas/sensor.py
+++ b/esphome/components/dallas/sensor.py
@@ -9,7 +9,6 @@ from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
- CONF_ID,
)
from . import DallasComponent, dallas_ns
@@ -17,13 +16,13 @@ DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sen
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
+ DallasTemperatureSensor,
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
- cv.GenerateID(): cv.declare_id(DallasTemperatureSensor),
cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent),
cv.Optional(CONF_ADDRESS): cv.hex_int,
cv.Optional(CONF_INDEX): cv.positive_int,
@@ -36,7 +35,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
hub = await cg.get_variable(config[CONF_DALLAS_ID])
- var = cg.new_Pvariable(config[CONF_ID])
+ var = await sensor.new_sensor(config)
if CONF_ADDRESS in config:
cg.add(var.set_address(config[CONF_ADDRESS]))
@@ -49,4 +48,3 @@ async def to_code(config):
cg.add(var.set_parent(hub))
cg.add(hub.register_sensor(var))
- await sensor.register_sensor(var, config)
diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py
index 45b8f98f0c..ce0cf5216a 100644
--- a/esphome/components/daly_bms/__init__.py
+++ b/esphome/components/daly_bms/__init__.py
@@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
-from esphome.const import CONF_ID
+from esphome.const import CONF_ID, CONF_ADDRESS
CODEOWNERS = ["@s1lvi0"]
DEPENDENCIES = ["uart"]
@@ -15,7 +15,12 @@ DalyBmsComponent = daly_bms.class_(
)
CONFIG_SCHEMA = (
- cv.Schema({cv.GenerateID(): cv.declare_id(DalyBmsComponent)})
+ cv.Schema(
+ {
+ cv.GenerateID(): cv.declare_id(DalyBmsComponent),
+ cv.Optional(CONF_ADDRESS, default=0x80): cv.positive_int,
+ }
+ )
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.polling_component_schema("30s"))
)
@@ -25,3 +30,4 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
+ cg.add(var.set_address(config[CONF_ADDRESS]))
diff --git a/esphome/components/daly_bms/binary_sensor.py b/esphome/components/daly_bms/binary_sensor.py
index 23330cd945..7b252b5e89 100644
--- a/esphome/components/daly_bms/binary_sensor.py
+++ b/esphome/components/daly_bms/binary_sensor.py
@@ -1,7 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
-from esphome.const import CONF_ID
from . import DalyBmsComponent, CONF_BMS_DALY_ID
CONF_CHARGING_MOS_ENABLED = "charging_mos_enabled"
@@ -18,18 +17,10 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent),
cv.Optional(
CONF_CHARGING_MOS_ENABLED
- ): binary_sensor.BINARY_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(binary_sensor.BinarySensor),
- }
- ),
+ ): binary_sensor.binary_sensor_schema(),
cv.Optional(
CONF_DISCHARGING_MOS_ENABLED
- ): binary_sensor.BINARY_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(binary_sensor.BinarySensor),
- }
- ),
+ ): binary_sensor.binary_sensor_schema(),
}
).extend(cv.COMPONENT_SCHEMA)
)
@@ -38,9 +29,8 @@ CONFIG_SCHEMA = cv.All(
async def setup_conf(config, key, hub):
if key in config:
conf = config[key]
- sens = cg.new_Pvariable(conf[CONF_ID])
- await binary_sensor.register_binary_sensor(sens, conf)
- cg.add(getattr(hub, f"set_{key}_binary_sensor")(sens))
+ var = await binary_sensor.new_binary_sensor(conf)
+ cg.add(getattr(hub, f"set_{key}_binary_sensor")(var))
async def to_code(config):
diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp
index 44c05f0686..3b41723327 100644
--- a/esphome/components/daly_bms/daly_bms.cpp
+++ b/esphome/components/daly_bms/daly_bms.cpp
@@ -16,6 +16,7 @@ static const uint8_t DALY_REQUEST_MIN_MAX_VOLTAGE = 0x91;
static const uint8_t DALY_REQUEST_MIN_MAX_TEMPERATURE = 0x92;
static const uint8_t DALY_REQUEST_MOS = 0x93;
static const uint8_t DALY_REQUEST_STATUS = 0x94;
+static const uint8_t DALY_REQUEST_CELL_VOLTAGE = 0x95;
static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96;
void DalyBmsComponent::setup() {}
@@ -31,6 +32,7 @@ void DalyBmsComponent::update() {
this->request_data_(DALY_REQUEST_MIN_MAX_TEMPERATURE);
this->request_data_(DALY_REQUEST_MOS);
this->request_data_(DALY_REQUEST_STATUS);
+ this->request_data_(DALY_REQUEST_CELL_VOLTAGE);
this->request_data_(DALY_REQUEST_TEMPERATURE);
std::vector get_battery_level_data;
@@ -48,7 +50,7 @@ void DalyBmsComponent::request_data_(uint8_t data_id) {
uint8_t request_message[DALY_FRAME_SIZE];
request_message[0] = 0xA5; // Start Flag
- request_message[1] = 0x80; // Communication Module Address
+ request_message[1] = addr_; // Communication Module Address
request_message[2] = data_id; // Data ID
request_message[3] = 0x08; // Data Length (Fixed)
request_message[4] = 0x00; // Empty Data
@@ -59,8 +61,8 @@ void DalyBmsComponent::request_data_(uint8_t data_id) {
request_message[9] = 0x00; // |
request_message[10] = 0x00; // |
request_message[11] = 0x00; // Empty Data
- request_message[12] = (uint8_t)(request_message[0] + request_message[1] + request_message[2] +
- request_message[3]); // Checksum (Lower byte of the other bytes sum)
+ request_message[12] = (uint8_t) (request_message[0] + request_message[1] + request_message[2] +
+ request_message[3]); // Checksum (Lower byte of the other bytes sum)
this->write_array(request_message, sizeof(request_message));
this->flush();
@@ -166,6 +168,71 @@ void DalyBmsComponent::decode_data_(std::vector data) {
}
break;
+ case DALY_REQUEST_CELL_VOLTAGE:
+ switch (it[4]) {
+ case 1:
+ if (this->cell_1_voltage_) {
+ this->cell_1_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000);
+ }
+ if (this->cell_2_voltage_) {
+ this->cell_2_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000);
+ }
+ if (this->cell_3_voltage_) {
+ this->cell_3_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000);
+ }
+ break;
+ case 2:
+ if (this->cell_4_voltage_) {
+ this->cell_4_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000);
+ }
+ if (this->cell_5_voltage_) {
+ this->cell_5_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000);
+ }
+ if (this->cell_6_voltage_) {
+ this->cell_6_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000);
+ }
+ break;
+ case 3:
+ if (this->cell_7_voltage_) {
+ this->cell_7_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000);
+ }
+ if (this->cell_8_voltage_) {
+ this->cell_8_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000);
+ }
+ if (this->cell_9_voltage_) {
+ this->cell_9_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000);
+ }
+ break;
+ case 4:
+ if (this->cell_10_voltage_) {
+ this->cell_10_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000);
+ }
+ if (this->cell_11_voltage_) {
+ this->cell_11_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000);
+ }
+ if (this->cell_12_voltage_) {
+ this->cell_12_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000);
+ }
+ break;
+ case 5:
+ if (this->cell_13_voltage_) {
+ this->cell_13_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000);
+ }
+ if (this->cell_14_voltage_) {
+ this->cell_14_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000);
+ }
+ if (this->cell_15_voltage_) {
+ this->cell_15_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000);
+ }
+ break;
+ case 6:
+ if (this->cell_16_voltage_) {
+ this->cell_16_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000);
+ }
+ break;
+ }
+ break;
+
default:
break;
}
diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h
index b5d4c8ae39..d4fe84fe46 100644
--- a/esphome/components/daly_bms/daly_bms.h
+++ b/esphome/components/daly_bms/daly_bms.h
@@ -6,6 +6,8 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/uart/uart.h"
+#include
+
namespace esphome {
namespace daly_bms {
@@ -37,6 +39,23 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice {
void set_cells_number_sensor(sensor::Sensor *cells_number) { cells_number_ = cells_number; }
void set_temperature_1_sensor(sensor::Sensor *temperature_1_sensor) { temperature_1_sensor_ = temperature_1_sensor; }
void set_temperature_2_sensor(sensor::Sensor *temperature_2_sensor) { temperature_2_sensor_ = temperature_2_sensor; }
+ void set_cell_1_voltage_sensor(sensor::Sensor *cell_1_voltage) { cell_1_voltage_ = cell_1_voltage; }
+ void set_cell_2_voltage_sensor(sensor::Sensor *cell_2_voltage) { cell_2_voltage_ = cell_2_voltage; }
+ void set_cell_3_voltage_sensor(sensor::Sensor *cell_3_voltage) { cell_3_voltage_ = cell_3_voltage; }
+ void set_cell_4_voltage_sensor(sensor::Sensor *cell_4_voltage) { cell_4_voltage_ = cell_4_voltage; }
+ void set_cell_5_voltage_sensor(sensor::Sensor *cell_5_voltage) { cell_5_voltage_ = cell_5_voltage; }
+ void set_cell_6_voltage_sensor(sensor::Sensor *cell_6_voltage) { cell_6_voltage_ = cell_6_voltage; }
+ void set_cell_7_voltage_sensor(sensor::Sensor *cell_7_voltage) { cell_7_voltage_ = cell_7_voltage; }
+ void set_cell_8_voltage_sensor(sensor::Sensor *cell_8_voltage) { cell_8_voltage_ = cell_8_voltage; }
+ void set_cell_9_voltage_sensor(sensor::Sensor *cell_9_voltage) { cell_9_voltage_ = cell_9_voltage; }
+ void set_cell_10_voltage_sensor(sensor::Sensor *cell_10_voltage) { cell_10_voltage_ = cell_10_voltage; }
+ void set_cell_11_voltage_sensor(sensor::Sensor *cell_11_voltage) { cell_11_voltage_ = cell_11_voltage; }
+ void set_cell_12_voltage_sensor(sensor::Sensor *cell_12_voltage) { cell_12_voltage_ = cell_12_voltage; }
+ void set_cell_13_voltage_sensor(sensor::Sensor *cell_13_voltage) { cell_13_voltage_ = cell_13_voltage; }
+ void set_cell_14_voltage_sensor(sensor::Sensor *cell_14_voltage) { cell_14_voltage_ = cell_14_voltage; }
+ void set_cell_15_voltage_sensor(sensor::Sensor *cell_15_voltage) { cell_15_voltage_ = cell_15_voltage; }
+ void set_cell_16_voltage_sensor(sensor::Sensor *cell_16_voltage) { cell_16_voltage_ = cell_16_voltage; }
+
// TEXT_SENSORS
void set_status_text_sensor(text_sensor::TextSensor *status_text_sensor) { status_text_sensor_ = status_text_sensor; }
// BINARY_SENSORS
@@ -52,11 +71,14 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice {
void update() override;
float get_setup_priority() const override;
+ void set_address(uint8_t address) { this->addr_ = address; }
protected:
void request_data_(uint8_t data_id);
void decode_data_(std::vector data);
+ uint8_t addr_;
+
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
sensor::Sensor *battery_level_sensor_{nullptr};
@@ -72,6 +94,22 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice {
sensor::Sensor *cells_number_{nullptr};
sensor::Sensor *temperature_1_sensor_{nullptr};
sensor::Sensor *temperature_2_sensor_{nullptr};
+ sensor::Sensor *cell_1_voltage_{nullptr};
+ sensor::Sensor *cell_2_voltage_{nullptr};
+ sensor::Sensor *cell_3_voltage_{nullptr};
+ sensor::Sensor *cell_4_voltage_{nullptr};
+ sensor::Sensor *cell_5_voltage_{nullptr};
+ sensor::Sensor *cell_6_voltage_{nullptr};
+ sensor::Sensor *cell_7_voltage_{nullptr};
+ sensor::Sensor *cell_8_voltage_{nullptr};
+ sensor::Sensor *cell_9_voltage_{nullptr};
+ sensor::Sensor *cell_10_voltage_{nullptr};
+ sensor::Sensor *cell_11_voltage_{nullptr};
+ sensor::Sensor *cell_12_voltage_{nullptr};
+ sensor::Sensor *cell_13_voltage_{nullptr};
+ sensor::Sensor *cell_14_voltage_{nullptr};
+ sensor::Sensor *cell_15_voltage_{nullptr};
+ sensor::Sensor *cell_16_voltage_{nullptr};
text_sensor::TextSensor *status_text_sensor_{nullptr};
diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py
index 1d0ee89914..2274a2153a 100644
--- a/esphome/components/daly_bms/sensor.py
+++ b/esphome/components/daly_bms/sensor.py
@@ -11,14 +11,11 @@ from esphome.const import (
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_TEMPERATURE,
- DEVICE_CLASS_EMPTY,
STATE_CLASS_MEASUREMENT,
- STATE_CLASS_NONE,
UNIT_VOLT,
UNIT_AMPERE,
UNIT_PERCENT,
UNIT_CELSIUS,
- UNIT_EMPTY,
ICON_FLASH,
ICON_PERCENT,
ICON_COUNTER,
@@ -39,6 +36,22 @@ CONF_REMAINING_CAPACITY = "remaining_capacity"
CONF_TEMPERATURE_1 = "temperature_1"
CONF_TEMPERATURE_2 = "temperature_2"
+CONF_CELL_1_VOLTAGE = "cell_1_voltage"
+CONF_CELL_2_VOLTAGE = "cell_2_voltage"
+CONF_CELL_3_VOLTAGE = "cell_3_voltage"
+CONF_CELL_4_VOLTAGE = "cell_4_voltage"
+CONF_CELL_5_VOLTAGE = "cell_5_voltage"
+CONF_CELL_6_VOLTAGE = "cell_6_voltage"
+CONF_CELL_7_VOLTAGE = "cell_7_voltage"
+CONF_CELL_8_VOLTAGE = "cell_8_voltage"
+CONF_CELL_9_VOLTAGE = "cell_9_voltage"
+CONF_CELL_10_VOLTAGE = "cell_10_voltage"
+CONF_CELL_11_VOLTAGE = "cell_11_voltage"
+CONF_CELL_12_VOLTAGE = "cell_12_voltage"
+CONF_CELL_13_VOLTAGE = "cell_13_voltage"
+CONF_CELL_14_VOLTAGE = "cell_14_voltage"
+CONF_CELL_15_VOLTAGE = "cell_15_voltage"
+CONF_CELL_16_VOLTAGE = "cell_16_voltage"
ICON_CURRENT_DC = "mdi:current-dc"
ICON_BATTERY_OUTLINE = "mdi:battery-outline"
ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up"
@@ -63,117 +76,142 @@ TYPES = [
CONF_REMAINING_CAPACITY,
CONF_TEMPERATURE_1,
CONF_TEMPERATURE_2,
+ CONF_CELL_1_VOLTAGE,
+ CONF_CELL_2_VOLTAGE,
+ CONF_CELL_3_VOLTAGE,
+ CONF_CELL_4_VOLTAGE,
+ CONF_CELL_5_VOLTAGE,
+ CONF_CELL_6_VOLTAGE,
+ CONF_CELL_7_VOLTAGE,
+ CONF_CELL_8_VOLTAGE,
+ CONF_CELL_9_VOLTAGE,
+ CONF_CELL_10_VOLTAGE,
+ CONF_CELL_11_VOLTAGE,
+ CONF_CELL_12_VOLTAGE,
+ CONF_CELL_13_VOLTAGE,
+ CONF_CELL_14_VOLTAGE,
+ CONF_CELL_15_VOLTAGE,
+ CONF_CELL_16_VOLTAGE,
]
+CELL_VOLTAGE_SCHEMA = sensor.sensor_schema(
+ unit_of_measurement=UNIT_VOLT,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ state_class=STATE_CLASS_MEASUREMENT,
+ icon=ICON_FLASH,
+ accuracy_decimals=3,
+)
+
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
- UNIT_VOLT,
- ICON_FLASH,
- 1,
- DEVICE_CLASS_VOLTAGE,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_VOLT,
+ icon=ICON_FLASH,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
- UNIT_AMPERE,
- ICON_CURRENT_DC,
- 1,
- DEVICE_CLASS_CURRENT,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_AMPERE,
+ icon=ICON_CURRENT_DC,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_CURRENT,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
- UNIT_PERCENT,
- ICON_PERCENT,
- 1,
- DEVICE_CLASS_BATTERY,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_PERCENT,
+ icon=ICON_PERCENT,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_BATTERY,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_MAX_CELL_VOLTAGE): sensor.sensor_schema(
- UNIT_VOLT,
- ICON_FLASH,
- 2,
- DEVICE_CLASS_VOLTAGE,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_VOLT,
+ icon=ICON_FLASH,
+ accuracy_decimals=2,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_MAX_CELL_VOLTAGE_NUMBER): sensor.sensor_schema(
- UNIT_EMPTY,
- ICON_COUNTER,
- 0,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_NONE,
+ icon=ICON_COUNTER,
+ accuracy_decimals=0,
),
cv.Optional(CONF_MIN_CELL_VOLTAGE): sensor.sensor_schema(
- UNIT_VOLT,
- ICON_FLASH,
- 2,
- DEVICE_CLASS_VOLTAGE,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_VOLT,
+ icon=ICON_FLASH,
+ accuracy_decimals=2,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_MIN_CELL_VOLTAGE_NUMBER): sensor.sensor_schema(
- UNIT_EMPTY,
- ICON_COUNTER,
- 0,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_NONE,
+ icon=ICON_COUNTER,
+ accuracy_decimals=0,
),
cv.Optional(CONF_MAX_TEMPERATURE): sensor.sensor_schema(
- UNIT_CELSIUS,
- ICON_THERMOMETER_CHEVRON_UP,
- 0,
- DEVICE_CLASS_TEMPERATURE,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_CELSIUS,
+ icon=ICON_THERMOMETER_CHEVRON_UP,
+ accuracy_decimals=0,
+ device_class=DEVICE_CLASS_TEMPERATURE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_MAX_TEMPERATURE_PROBE_NUMBER): sensor.sensor_schema(
- UNIT_EMPTY,
- ICON_COUNTER,
- 0,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_NONE,
+ icon=ICON_COUNTER,
+ accuracy_decimals=0,
),
cv.Optional(CONF_MIN_TEMPERATURE): sensor.sensor_schema(
- UNIT_CELSIUS,
- ICON_THERMOMETER_CHEVRON_DOWN,
- 0,
- DEVICE_CLASS_TEMPERATURE,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_CELSIUS,
+ icon=ICON_THERMOMETER_CHEVRON_DOWN,
+ accuracy_decimals=0,
+ device_class=DEVICE_CLASS_TEMPERATURE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_MIN_TEMPERATURE_PROBE_NUMBER): sensor.sensor_schema(
- UNIT_EMPTY,
- ICON_COUNTER,
- 0,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_NONE,
+ icon=ICON_COUNTER,
+ accuracy_decimals=0,
),
cv.Optional(CONF_REMAINING_CAPACITY): sensor.sensor_schema(
- UNIT_AMPERE_HOUR,
- ICON_GAUGE,
- 2,
- DEVICE_CLASS_VOLTAGE,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_AMPERE_HOUR,
+ icon=ICON_GAUGE,
+ accuracy_decimals=2,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CELLS_NUMBER): sensor.sensor_schema(
- UNIT_EMPTY,
- ICON_COUNTER,
- 0,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_NONE,
+ icon=ICON_COUNTER,
+ accuracy_decimals=0,
),
cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema(
- UNIT_CELSIUS,
- ICON_THERMOMETER,
- 0,
- DEVICE_CLASS_TEMPERATURE,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_CELSIUS,
+ icon=ICON_THERMOMETER,
+ accuracy_decimals=0,
+ device_class=DEVICE_CLASS_TEMPERATURE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema(
- UNIT_CELSIUS,
- ICON_THERMOMETER,
- 0,
- DEVICE_CLASS_TEMPERATURE,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_CELSIUS,
+ icon=ICON_THERMOMETER,
+ accuracy_decimals=0,
+ device_class=DEVICE_CLASS_TEMPERATURE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
+ cv.Optional(CONF_CELL_1_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_2_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_3_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_4_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_5_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_6_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_7_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_8_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_9_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_10_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_11_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_12_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_13_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_14_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_15_VOLTAGE): CELL_VOLTAGE_SCHEMA,
+ cv.Optional(CONF_CELL_16_VOLTAGE): CELL_VOLTAGE_SCHEMA,
}
).extend(cv.COMPONENT_SCHEMA)
)
diff --git a/esphome/components/daly_bms/text_sensor.py b/esphome/components/daly_bms/text_sensor.py
index de49a0b4b9..9f23e5f373 100644
--- a/esphome/components/daly_bms/text_sensor.py
+++ b/esphome/components/daly_bms/text_sensor.py
@@ -1,7 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
-from esphome.const import CONF_ICON, CONF_ID, CONF_STATUS
+from esphome.const import CONF_STATUS
from . import DalyBmsComponent, CONF_BMS_DALY_ID
ICON_CAR_BATTERY = "mdi:car-battery"
@@ -14,11 +14,8 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent),
- cv.Optional(CONF_STATUS): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- cv.Optional(CONF_ICON, default=ICON_CAR_BATTERY): cv.icon,
- }
+ cv.Optional(CONF_STATUS): text_sensor.text_sensor_schema(
+ icon=ICON_CAR_BATTERY
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -28,8 +25,7 @@ CONFIG_SCHEMA = cv.All(
async def setup_conf(config, key, hub):
if key in config:
conf = config[key]
- sens = cg.new_Pvariable(conf[CONF_ID])
- await text_sensor.register_text_sensor(sens, conf)
+ sens = await text_sensor.new_text_sensor(conf)
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py
index 6194a55205..e0994be6a0 100644
--- a/esphome/components/dashboard_import/__init__.py
+++ b/esphome/components/dashboard_import/__init__.py
@@ -1,12 +1,19 @@
+import base64
+import secrets
from pathlib import Path
+from typing import Optional
+
+import requests
import esphome.codegen as cg
import esphome.config_validation as cv
+import esphome.final_validate as fv
+from esphome import git
from esphome.components.packages import validate_source_shorthand
+from esphome.const import CONF_REF, CONF_WIFI, CONF_ESPHOME, CONF_PROJECT
from esphome.wizard import wizard_file
from esphome.yaml_util import dump
-
dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import")
# payload is in `esphomelib` mdns record, which only exists if api
@@ -18,18 +25,45 @@ CODEOWNERS = ["@esphome/core"]
def validate_import_url(value):
value = cv.string_strict(value)
value = cv.Length(max=255)(value)
- # ignore result, only check if it's a valid shorthand
validate_source_shorthand(value)
return value
+def validate_full_url(config):
+ if not config[CONF_IMPORT_FULL_CONFIG]:
+ return config
+ source = validate_source_shorthand(config[CONF_PACKAGE_IMPORT_URL])
+ if CONF_REF not in source:
+ raise cv.Invalid(
+ "Must specify a ref (branch or tag) to import from when importing full config"
+ )
+ return config
+
+
CONF_PACKAGE_IMPORT_URL = "package_import_url"
-CONFIG_SCHEMA = cv.Schema(
- {
- cv.Required(CONF_PACKAGE_IMPORT_URL): validate_import_url,
- }
+CONF_IMPORT_FULL_CONFIG = "import_full_config"
+
+CONFIG_SCHEMA = cv.All(
+ cv.Schema(
+ {
+ cv.Required(CONF_PACKAGE_IMPORT_URL): validate_import_url,
+ cv.Optional(CONF_IMPORT_FULL_CONFIG, default=False): cv.boolean,
+ }
+ ),
+ validate_full_url,
)
+
+def _final_validate(config):
+ full_config = fv.full_config.get()[CONF_ESPHOME]
+ if CONF_PROJECT not in full_config:
+ raise cv.Invalid(
+ "Dashboard import requires the `esphome` -> `project` information to be provided."
+ )
+
+
+FINAL_VALIDATE_SCHEMA = _final_validate
+
WIFI_CONFIG = """
wifi:
@@ -40,33 +74,95 @@ wifi:
async def to_code(config):
cg.add_define("USE_DASHBOARD_IMPORT")
- cg.add(dashboard_import_ns.set_package_import_url(config[CONF_PACKAGE_IMPORT_URL]))
+ url = config[CONF_PACKAGE_IMPORT_URL]
+ if config[CONF_IMPORT_FULL_CONFIG]:
+ url += "?full_config"
+ cg.add(dashboard_import_ns.set_package_import_url(url))
-def import_config(path: str, name: str, project_name: str, import_url: str) -> None:
+def import_config(
+ path: str,
+ name: str,
+ friendly_name: Optional[str],
+ project_name: str,
+ import_url: str,
+ network: str = CONF_WIFI,
+ encryption: bool = False,
+) -> None:
p = Path(path)
if p.exists():
raise FileExistsError
if project_name == "esphome.web":
+ if "esp32c3" in import_url:
+ board = "esp32-c3-devkitm-1"
+ platform = "ESP32"
+ elif "esp32s2" in import_url:
+ board = "esp32-s2-saola-1"
+ platform = "ESP32"
+ elif "esp32s3" in import_url:
+ board = "esp32-s3-devkitc-1"
+ platform = "ESP32"
+ elif "esp32" in import_url:
+ board = "esp32dev"
+ platform = "ESP32"
+ elif "esp8266" in import_url:
+ board = "esp01_1m"
+ platform = "ESP8266"
+ elif "pico-w" in import_url:
+ board = "pico-w"
+ platform = "RP2040"
+
+ kwargs = {
+ "name": name,
+ "friendly_name": friendly_name,
+ "platform": platform,
+ "board": board,
+ "ssid": "!secret wifi_ssid",
+ "psk": "!secret wifi_password",
+ }
+ if encryption:
+ noise_psk = secrets.token_bytes(32)
+ key = base64.b64encode(noise_psk).decode()
+ kwargs["api_encryption_key"] = key
+
p.write_text(
- wizard_file(
- name=name,
- platform="ESP32" if "esp32" in import_url else "ESP8266",
- board="esp32dev" if "esp32" in import_url else "esp01_1m",
- ssid="!secret wifi_ssid",
- psk="!secret wifi_password",
- ),
+ wizard_file(**kwargs),
encoding="utf8",
)
else:
- config = {
- "substitutions": {"name": name},
- "packages": {project_name: import_url},
- "esphome": {"name_add_mac_suffix": False},
- }
- p.write_text(
- dump(config) + WIFI_CONFIG,
- encoding="utf8",
- )
+ git_file = git.GitFile.from_shorthand(import_url)
+
+ if git_file.query and "full_config" in git_file.query:
+ url = git_file.raw_url
+ try:
+ req = requests.get(url, timeout=30)
+ req.raise_for_status()
+ except requests.exceptions.RequestException as e:
+ raise ValueError(f"Error while fetching {url}: {e}") from e
+
+ p.write_text(req.text, encoding="utf8")
+
+ else:
+ substitutions = {"name": name}
+ esphome_core = {"name": "${name}", "name_add_mac_suffix": False}
+ if friendly_name:
+ substitutions["friendly_name"] = friendly_name
+ esphome_core["friendly_name"] = "${friendly_name}"
+ config = {
+ "substitutions": substitutions,
+ "packages": {project_name: import_url},
+ "esphome": esphome_core,
+ }
+ if encryption:
+ noise_psk = secrets.token_bytes(32)
+ key = base64.b64encode(noise_psk).decode()
+ config["api"] = {"encryption": {"key": key}}
+
+ output = dump(config)
+
+ if network == CONF_WIFI:
+ output += WIFI_CONFIG
+
+ p.write_text(output, encoding="utf8")
diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py
index 98ad9e2b10..9742b3b19e 100644
--- a/esphome/components/debug/__init__.py
+++ b/esphome/components/debug/__init__.py
@@ -1,68 +1,47 @@
-import esphome.config_validation as cv
import esphome.codegen as cg
-from esphome.components import sensor, text_sensor
+import esphome.config_validation as cv
from esphome.const import (
- CONF_ID,
- CONF_DEVICE,
- CONF_FREE,
- CONF_FRAGMENTATION,
CONF_BLOCK,
+ CONF_DEVICE,
+ CONF_FRAGMENTATION,
+ CONF_FREE,
+ CONF_ID,
CONF_LOOP_TIME,
- UNIT_MILLISECOND,
- UNIT_PERCENT,
- UNIT_BYTES,
- ICON_COUNTER,
- ICON_TIMER,
)
CODEOWNERS = ["@OttoWinter"]
DEPENDENCIES = ["logger"]
+CONF_DEBUG_ID = "debug_id"
debug_ns = cg.esphome_ns.namespace("debug")
DebugComponent = debug_ns.class_("DebugComponent", cg.PollingComponent)
-CONFIG_SCHEMA = cv.Schema(
- {
- cv.GenerateID(): cv.declare_id(DebugComponent),
- cv.Optional(CONF_DEVICE): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {cv.GenerateID(): cv.declare_id(text_sensor.TextSensor)}
- ),
- cv.Optional(CONF_FREE): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0),
- cv.Optional(CONF_BLOCK): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0),
- cv.Optional(CONF_FRAGMENTATION): cv.All(
- cv.only_on_esp8266,
- cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
- sensor.sensor_schema(UNIT_PERCENT, ICON_COUNTER, 1),
- ),
- cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(
- UNIT_MILLISECOND, ICON_TIMER, 1
- ),
- }
-).extend(cv.polling_component_schema("60s"))
+CONFIG_SCHEMA = cv.All(
+ cv.Schema(
+ {
+ cv.GenerateID(): cv.declare_id(DebugComponent),
+ cv.Optional(CONF_DEVICE): cv.invalid(
+ "The 'device' option has been moved to the 'debug' text_sensor component"
+ ),
+ cv.Optional(CONF_FREE): cv.invalid(
+ "The 'free' option has been moved to the 'debug' sensor component"
+ ),
+ cv.Optional(CONF_BLOCK): cv.invalid(
+ "The 'block' option has been moved to the 'debug' sensor component"
+ ),
+ cv.Optional(CONF_FRAGMENTATION): cv.invalid(
+ "The 'fragmentation' option has been moved to the 'debug' sensor component"
+ ),
+ cv.Optional(CONF_LOOP_TIME): cv.invalid(
+ "The 'loop_time' option has been moved to the 'debug' sensor component"
+ ),
+ }
+ ).extend(cv.polling_component_schema("60s")),
+ cv.only_on(["esp32", "esp8266"]),
+)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
-
- if CONF_DEVICE in config:
- sens = cg.new_Pvariable(config[CONF_DEVICE][CONF_ID])
- await text_sensor.register_text_sensor(sens, config[CONF_DEVICE])
- cg.add(var.set_device_info_sensor(sens))
-
- if CONF_FREE in config:
- sens = await sensor.new_sensor(config[CONF_FREE])
- cg.add(var.set_free_sensor(sens))
-
- if CONF_BLOCK in config:
- sens = await sensor.new_sensor(config[CONF_BLOCK])
- cg.add(var.set_block_sensor(sens))
-
- if CONF_FRAGMENTATION in config:
- sens = await sensor.new_sensor(config[CONF_FRAGMENTATION])
- cg.add(var.set_fragmentation_sensor(sens))
-
- if CONF_LOOP_TIME in config:
- sens = await sensor.new_sensor(config[CONF_LOOP_TIME])
- cg.add(var.set_loop_time_sensor(sens))
diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp
index 41bf5f50c7..9843fa1c99 100644
--- a/esphome/components/debug/debug_component.cpp
+++ b/esphome/components/debug/debug_component.cpp
@@ -37,22 +37,25 @@ static uint32_t get_free_heap() {
}
void DebugComponent::dump_config() {
+#ifndef ESPHOME_LOG_HAS_DEBUG
+ return; // Can't log below if debug logging is disabled
+#endif
+
std::string device_info;
+ std::string reset_reason;
device_info.reserve(256);
-#ifndef ESPHOME_LOG_HAS_DEBUG
- ESP_LOGE(TAG, "Debug Component requires debug log level!");
- this->status_set_error();
- return;
-#endif
-
ESP_LOGCONFIG(TAG, "Debug component:");
+#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Device info", this->device_info_);
+#endif // USE_TEXT_SENSOR
+#ifdef USE_SENSOR
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
-#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
+#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
-#endif
+#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
+#endif // USE_SENSOR
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
device_info += ESPHOME_VERSION;
@@ -142,7 +145,6 @@ void DebugComponent::dump_config() {
device_info += "|EFuse MAC: ";
device_info += mac;
- const char *reset_reason;
switch (rtc_get_reset_reason(0)) {
case POWERON_RESET:
reset_reason = "Power On Reset";
@@ -192,7 +194,7 @@ void DebugComponent::dump_config() {
default:
reset_reason = "Unknown Reset Reason";
}
- ESP_LOGD(TAG, "Reset Reason: %s", reset_reason);
+ ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
device_info += "|Reset: ";
device_info += reset_reason;
@@ -266,13 +268,20 @@ void DebugComponent::dump_config() {
device_info += ESP.getResetReason().c_str();
device_info += "|";
device_info += ESP.getResetInfo().c_str();
+
+ reset_reason = ESP.getResetReason().c_str();
#endif
+#ifdef USE_TEXT_SENSOR
if (this->device_info_ != nullptr) {
if (device_info.length() > 255)
device_info.resize(255);
this->device_info_->publish_state(device_info);
}
+ if (this->reset_reason_ != nullptr) {
+ this->reset_reason_->publish_state(reset_reason);
+ }
+#endif // USE_TEXT_SENSOR
}
void DebugComponent::loop() {
@@ -284,6 +293,7 @@ void DebugComponent::loop() {
this->status_momentary_warning("heap", 1000);
}
+#ifdef USE_SENSOR
// calculate loop time - from last call to this one
if (this->loop_time_sensor_ != nullptr) {
uint32_t now = millis();
@@ -291,9 +301,11 @@ void DebugComponent::loop() {
this->max_loop_time_ = std::max(this->max_loop_time_, loop_time);
this->last_loop_timetag_ = now;
}
+#endif // USE_SENSOR
}
void DebugComponent::update() {
+#ifdef USE_SENSOR
if (this->free_sensor_ != nullptr) {
this->free_sensor_->publish_state(get_free_heap());
}
@@ -307,7 +319,7 @@ void DebugComponent::update() {
#endif
}
-#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
+#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
if (this->fragmentation_sensor_ != nullptr) {
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
@@ -318,6 +330,7 @@ void DebugComponent::update() {
this->loop_time_sensor_->publish_state(this->max_loop_time_);
this->max_loop_time_ = 0;
}
+#endif // USE_SENSOR
}
float DebugComponent::get_setup_priority() const { return setup_priority::LATE; }
diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h
index a362c52617..b80fda55eb 100644
--- a/esphome/components/debug/debug_component.h
+++ b/esphome/components/debug/debug_component.h
@@ -1,10 +1,16 @@
#pragma once
#include "esphome/core/component.h"
+#include "esphome/core/defines.h"
#include "esphome/core/macros.h"
#include "esphome/core/helpers.h"
-#include "esphome/components/text_sensor/text_sensor.h"
+
+#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
+#endif
+#ifdef USE_TEXT_SENSOR
+#include "esphome/components/text_sensor/text_sensor.h"
+#endif
namespace esphome {
namespace debug {
@@ -16,27 +22,37 @@ class DebugComponent : public PollingComponent {
float get_setup_priority() const override;
void dump_config() override;
+#ifdef USE_TEXT_SENSOR
void set_device_info_sensor(text_sensor::TextSensor *device_info) { device_info_ = device_info; }
+ void set_reset_reason_sensor(text_sensor::TextSensor *reset_reason) { reset_reason_ = reset_reason; }
+#endif // USE_TEXT_SENSOR
+#ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
-#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
+#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
-
+#endif // USE_SENSOR
protected:
uint32_t free_heap_{};
+#ifdef USE_SENSOR
uint32_t last_loop_timetag_{0};
uint32_t max_loop_time_{0};
- text_sensor::TextSensor *device_info_{nullptr};
sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr};
-#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
+#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
sensor::Sensor *fragmentation_sensor_{nullptr};
#endif
sensor::Sensor *loop_time_sensor_{nullptr};
+#endif // USE_SENSOR
+
+#ifdef USE_TEXT_SENSOR
+ text_sensor::TextSensor *device_info_{nullptr};
+ text_sensor::TextSensor *reset_reason_{nullptr};
+#endif // USE_TEXT_SENSOR
};
} // namespace debug
diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py
new file mode 100644
index 0000000000..f7ea07d138
--- /dev/null
+++ b/esphome/components/debug/sensor.py
@@ -0,0 +1,70 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor
+from esphome.const import (
+ CONF_FREE,
+ CONF_FRAGMENTATION,
+ CONF_BLOCK,
+ CONF_LOOP_TIME,
+ ENTITY_CATEGORY_DIAGNOSTIC,
+ UNIT_MILLISECOND,
+ UNIT_PERCENT,
+ UNIT_BYTES,
+ ICON_COUNTER,
+ ICON_TIMER,
+)
+from . import CONF_DEBUG_ID, DebugComponent
+
+DEPENDENCIES = ["debug"]
+
+CONFIG_SCHEMA = {
+ cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent),
+ cv.Optional(CONF_FREE): sensor.sensor_schema(
+ unit_of_measurement=UNIT_BYTES,
+ icon=ICON_COUNTER,
+ accuracy_decimals=0,
+ entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+ ),
+ cv.Optional(CONF_BLOCK): sensor.sensor_schema(
+ unit_of_measurement=UNIT_BYTES,
+ icon=ICON_COUNTER,
+ accuracy_decimals=0,
+ entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+ ),
+ cv.Optional(CONF_FRAGMENTATION): cv.All(
+ cv.only_on_esp8266,
+ cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
+ sensor.sensor_schema(
+ unit_of_measurement=UNIT_PERCENT,
+ icon=ICON_COUNTER,
+ accuracy_decimals=1,
+ entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+ ),
+ ),
+ cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(
+ unit_of_measurement=UNIT_MILLISECOND,
+ icon=ICON_TIMER,
+ accuracy_decimals=0,
+ entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+ ),
+}
+
+
+async def to_code(config):
+ debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
+
+ if CONF_FREE in config:
+ sens = await sensor.new_sensor(config[CONF_FREE])
+ cg.add(debug_component.set_free_sensor(sens))
+
+ if CONF_BLOCK in config:
+ sens = await sensor.new_sensor(config[CONF_BLOCK])
+ cg.add(debug_component.set_block_sensor(sens))
+
+ if CONF_FRAGMENTATION in config:
+ sens = await sensor.new_sensor(config[CONF_FRAGMENTATION])
+ cg.add(debug_component.set_fragmentation_sensor(sens))
+
+ if CONF_LOOP_TIME in config:
+ sens = await sensor.new_sensor(config[CONF_LOOP_TIME])
+ cg.add(debug_component.set_loop_time_sensor(sens))
diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py
new file mode 100644
index 0000000000..24f938a0e2
--- /dev/null
+++ b/esphome/components/debug/text_sensor.py
@@ -0,0 +1,40 @@
+from esphome.components import text_sensor
+import esphome.config_validation as cv
+import esphome.codegen as cg
+from esphome.const import (
+ CONF_DEVICE,
+ ENTITY_CATEGORY_DIAGNOSTIC,
+ ICON_CHIP,
+ ICON_RESTART,
+)
+
+from . import CONF_DEBUG_ID, DebugComponent
+
+DEPENDENCIES = ["debug"]
+
+
+CONF_RESET_REASON = "reset_reason"
+CONFIG_SCHEMA = cv.Schema(
+ {
+ cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent),
+ cv.Optional(CONF_DEVICE): text_sensor.text_sensor_schema(
+ icon=ICON_CHIP,
+ entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+ ),
+ cv.Optional(CONF_RESET_REASON): text_sensor.text_sensor_schema(
+ icon=ICON_RESTART,
+ entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
+ ),
+ }
+)
+
+
+async def to_code(config):
+ debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
+
+ if CONF_DEVICE in config:
+ sens = await text_sensor.new_text_sensor(config[CONF_DEVICE])
+ cg.add(debug_component.set_device_info_sensor(sens))
+ if CONF_RESET_REASON in config:
+ sens = await text_sensor.new_text_sensor(config[CONF_RESET_REASON])
+ cg.add(debug_component.set_reset_reason_sensor(sens))
diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py
index ba4c2c0d7e..bbd10d58c5 100644
--- a/esphome/components/deep_sleep/__init__.py
+++ b/esphome/components/deep_sleep/__init__.py
@@ -1,19 +1,104 @@
import esphome.codegen as cg
+from esphome.components import time
import esphome.config_validation as cv
from esphome import pins, automation
from esphome.const import (
+ CONF_HOUR,
CONF_ID,
+ CONF_MINUTE,
CONF_MODE,
CONF_NUMBER,
CONF_PINS,
CONF_RUN_DURATION,
+ CONF_SECOND,
CONF_SLEEP_DURATION,
+ CONF_TIME_ID,
CONF_WAKEUP_PIN,
)
+from esphome.components.esp32 import get_esp32_variant
+from esphome.components.esp32.const import (
+ VARIANT_ESP32,
+ VARIANT_ESP32C3,
+ VARIANT_ESP32S2,
+ VARIANT_ESP32S3,
+)
+
+WAKEUP_PINS = {
+ VARIANT_ESP32: [
+ 0,
+ 2,
+ 4,
+ 12,
+ 13,
+ 14,
+ 15,
+ 25,
+ 26,
+ 27,
+ 32,
+ 33,
+ 34,
+ 35,
+ 36,
+ 37,
+ 38,
+ 39,
+ ],
+ VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5],
+ VARIANT_ESP32S2: [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ ],
+ VARIANT_ESP32S3: [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ ],
+}
+
def validate_pin_number(value):
- valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39]
+ valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32])
if value[CONF_NUMBER] not in valid_pins:
raise cv.Invalid(
f"Only pins {', '.join(str(x) for x in valid_pins)} support wakeup"
@@ -21,11 +106,26 @@ def validate_pin_number(value):
return value
+def validate_config(config):
+ if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config:
+ raise cv.Invalid("ESP32-C3 does not support wakeup from touch.")
+ if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config:
+ raise cv.Invalid("ESP32-C3 does not support wakeup from ext1")
+ return config
+
+
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")
@@ -49,6 +149,7 @@ CONF_TOUCH_WAKEUP = "touch_wakeup"
CONF_DEFAULT = "default"
CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason"
CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason"
+CONF_UNTIL = "until"
WAKEUP_CAUSES_SCHEMA = cv.Schema(
{
@@ -139,20 +240,30 @@ async def to_code(config):
cg.add_define("USE_DEEP_SLEEP")
-DEEP_SLEEP_ENTER_SCHEMA = automation.maybe_simple_id(
+DEEP_SLEEP_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(DeepSleepComponent),
- cv.Optional(CONF_SLEEP_DURATION): cv.templatable(
- cv.positive_time_period_milliseconds
- ),
}
)
-
-DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id(
- {
- 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),
)
@@ -165,12 +276,28 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
if CONF_SLEEP_DURATION in config:
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32)
cg.add(var.set_sleep_duration(template_))
+
+ if CONF_UNTIL in config:
+ until = config[CONF_UNTIL]
+ cg.add(var.set_until(until[CONF_HOUR], until[CONF_MINUTE], until[CONF_SECOND]))
+
+ time_ = await cg.get_variable(config[CONF_TIME_ID])
+ cg.add(var.set_time(time_))
+
return var
@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
diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp
index 7774014d3d..f6472a123c 100644
--- a/esphome/components/deep_sleep/deep_sleep_component.cpp
+++ b/esphome/components/deep_sleep/deep_sleep_component.cpp
@@ -1,6 +1,7 @@
#include "deep_sleep_component.h"
-#include "esphome/core/log.h"
+#include
#include "esphome/core/application.h"
+#include "esphome/core/log.h"
#ifdef USE_ESP8266
#include
@@ -20,6 +21,7 @@ optional 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;
@@ -71,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) {
@@ -101,10 +114,13 @@ void DeepSleepComponent::begin_sleep(bool manual) {
#endif
ESP_LOGI(TAG, "Beginning Deep Sleep");
-
+ if (this->sleep_duration_.has_value()) {
+ ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_);
+ }
App.run_safe_shutdown_hooks();
-#ifdef USE_ESP32
+#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) {
@@ -122,7 +138,19 @@ void DeepSleepComponent::begin_sleep(bool manual) {
esp_sleep_enable_touchpad_wakeup();
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
}
-
+#endif
+#ifdef USE_ESP32_VARIANT_ESP32C3
+ if (this->sleep_duration_.has_value())
+ esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
+ if (this->wakeup_pin_ != nullptr) {
+ bool level = !this->wakeup_pin_->is_inverted();
+ if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
+ level = !level;
+ }
+ esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(),
+ static_cast(level));
+ }
+#endif
esp_deep_sleep_start();
#endif
@@ -132,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
diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h
index 59df199a9f..2e54e53c56 100644
--- a/esphome/components/deep_sleep/deep_sleep_component.h
+++ b/esphome/components/deep_sleep/deep_sleep_component.h
@@ -1,14 +1,19 @@
#pragma once
-#include "esphome/core/component.h"
-#include "esphome/core/helpers.h"
#include "esphome/core/automation.h"
+#include "esphome/core/component.h"
#include "esphome/core/hal.h"
+#include "esphome/core/helpers.h"
#ifdef USE_ESP32
#include
#endif
+#ifdef USE_TIME
+#include "esphome/components/time/real_time_clock.h"
+#include "esphome/core/time.h"
+#endif
+
namespace esphome {
namespace deep_sleep {
@@ -57,23 +62,28 @@ class DeepSleepComponent : public Component {
public:
/// Set the duration in ms the component should sleep once it's in deep sleep mode.
void set_sleep_duration(uint32_t time_ms);
-#ifdef USE_ESP32
+#if defined(USE_ESP32)
/** Set the pin to wake up to on the ESP32 once it's in deep sleep mode.
* Use the inverted property to set the wakeup level.
*/
void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; }
void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
+#endif
+
+#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);
@@ -87,6 +97,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
@@ -113,25 +124,81 @@ template class EnterDeepSleepAction : public Action {
EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {}
TEMPLATABLE_VALUE(uint32_t, sleep_duration);
+#ifdef USE_TIME
+ void set_until(uint8_t hour, uint8_t minute, uint8_t second) {
+ this->hour_ = hour;
+ this->minute_ = minute;
+ this->second_ = second;
+ }
+
+ void set_time(time::RealTimeClock *time) { this->time_ = time; }
+#endif
+
void play(Ts... x) override {
if (this->sleep_duration_.has_value()) {
this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...));
}
+#ifdef USE_TIME
+
+ if (this->hour_.has_value()) {
+ auto time = this->time_->now();
+ const uint32_t timestamp_now = time.timestamp;
+
+ bool after_time = false;
+ if (time.hour > this->hour_) {
+ after_time = true;
+ } else {
+ if (time.hour == this->hour_) {
+ if (time.minute > this->minute_) {
+ after_time = true;
+ } else {
+ if (time.minute == this->minute_) {
+ if (time.second > this->second_) {
+ after_time = true;
+ }
+ }
+ }
+ }
+ }
+
+ time.hour = *this->hour_;
+ time.minute = *this->minute_;
+ time.second = *this->second_;
+ time.recalc_timestamp_utc();
+
+ time_t timestamp = time.timestamp; // timestamp in local time zone
+
+ if (after_time)
+ timestamp += 60 * 60 * 24;
+
+ int32_t offset = ESPTime::timezone_offset();
+ timestamp -= offset; // Change timestamp to utc
+ const uint32_t ms_left = (timestamp - timestamp_now) * 1000;
+ this->deep_sleep_->set_sleep_duration(ms_left);
+ }
+#endif
this->deep_sleep_->begin_sleep(true);
}
protected:
DeepSleepComponent *deep_sleep_;
+#ifdef USE_TIME
+ optional hour_;
+ optional minute_;
+ optional second_;
+
+ time::RealTimeClock *time_;
+#endif
};
-template class PreventDeepSleepAction : public Action {
+template class PreventDeepSleepAction : public Action, public Parented {
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 class AllowDeepSleepAction : public Action, public Parented {
+ public:
+ void play(Ts... x) override { this->parent_->allow_deep_sleep(); }
};
} // namespace deep_sleep
diff --git a/esphome/components/delonghi/__init__.py b/esphome/components/delonghi/__init__.py
new file mode 100644
index 0000000000..0a81eb2da7
--- /dev/null
+++ b/esphome/components/delonghi/__init__.py
@@ -0,0 +1 @@
+CODEOWNERS = ["@grob6000"]
diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py
new file mode 100644
index 0000000000..614706defe
--- /dev/null
+++ b/esphome/components/delonghi/climate.py
@@ -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)
diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp
new file mode 100644
index 0000000000..9bc0b5753d
--- /dev/null
+++ b/esphome/components/delonghi/delonghi.cpp
@@ -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
diff --git a/esphome/components/delonghi/delonghi.h b/esphome/components/delonghi/delonghi.h
new file mode 100644
index 0000000000..d310a58aee
--- /dev/null
+++ b/esphome/components/delonghi/delonghi.h
@@ -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
diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py
index 6b4a55aac9..05160bf8cb 100644
--- a/esphome/components/demo/__init__.py
+++ b/esphome/components/demo/__init__.py
@@ -37,12 +37,10 @@ from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
ICON_BLUETOOTH,
ICON_BLUR,
- ICON_EMPTY,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_CELSIUS,
- UNIT_EMPTY,
UNIT_PERCENT,
UNIT_WATT_HOURS,
)
@@ -134,12 +132,8 @@ CONFIG_SCHEMA = cv.Schema(
},
],
): [
- binary_sensor.BINARY_SENSOR_SCHEMA.extend(
+ binary_sensor.binary_sensor_schema(DemoBinarySensor).extend(
cv.polling_component_schema("60s")
- ).extend(
- {
- cv.GenerateID(): cv.declare_id(DemoBinarySensor),
- }
)
],
cv.Optional(
@@ -290,9 +284,10 @@ CONFIG_SCHEMA = cv.Schema(
},
],
): [
- number.NUMBER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend(
+ number.number_schema(DemoNumber)
+ .extend(cv.COMPONENT_SCHEMA)
+ .extend(
{
- cv.GenerateID(): cv.declare_id(DemoNumber),
cv.Required(CONF_TYPE): cv.enum(NUMBER_TYPES, int=True),
cv.Required(CONF_MIN_VALUE): cv.float_,
cv.Required(CONF_MAX_VALUE): cv.float_,
@@ -339,12 +334,8 @@ CONFIG_SCHEMA = cv.Schema(
},
],
): [
- sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0)
- .extend(cv.polling_component_schema("60s"))
- .extend(
- {
- cv.GenerateID(): cv.declare_id(DemoSensor),
- }
+ sensor.sensor_schema(DemoSensor, accuracy_decimals=0).extend(
+ cv.polling_component_schema("60s")
)
],
cv.Optional(
@@ -359,13 +350,7 @@ CONFIG_SCHEMA = cv.Schema(
CONF_ICON: ICON_BLUETOOTH,
},
],
- ): [
- switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend(
- {
- cv.GenerateID(): cv.declare_id(DemoSwitch),
- }
- )
- ],
+ ): [switch.switch_schema(DemoSwitch).extend(cv.COMPONENT_SCHEMA)],
cv.Optional(
CONF_TEXT_SENSORS,
default=[
@@ -378,12 +363,8 @@ CONFIG_SCHEMA = cv.Schema(
},
],
): [
- text_sensor.TEXT_SENSOR_SCHEMA.extend(
+ text_sensor.text_sensor_schema(DemoTextSensor).extend(
cv.polling_component_schema("60s")
- ).extend(
- {
- cv.GenerateID(): cv.declare_id(DemoTextSensor),
- }
)
],
}
@@ -392,9 +373,8 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
for conf in config[CONF_BINARY_SENSORS]:
- var = cg.new_Pvariable(conf[CONF_ID])
+ var = await binary_sensor.new_binary_sensor(conf)
await cg.register_component(var, conf)
- await binary_sensor.register_binary_sensor(var, conf)
for conf in config[CONF_CLIMATES]:
var = cg.new_Pvariable(conf[CONF_ID])
@@ -433,16 +413,13 @@ async def to_code(config):
cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_SENSORS]:
- var = cg.new_Pvariable(conf[CONF_ID])
+ var = await sensor.new_sensor(conf)
await cg.register_component(var, conf)
- await sensor.register_sensor(var, conf)
for conf in config[CONF_SWITCHES]:
- var = cg.new_Pvariable(conf[CONF_ID])
+ var = await switch.new_switch(conf)
await cg.register_component(var, conf)
- await switch.register_switch(var, conf)
for conf in config[CONF_TEXT_SENSORS]:
- var = cg.new_Pvariable(conf[CONF_ID])
+ var = await text_sensor.new_text_sensor(conf)
await cg.register_component(var, conf)
- await text_sensor.register_text_sensor(var, conf)
diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h
index 0cf48dd4ee..1ba80aabf5 100644
--- a/esphome/components/demo/demo_climate.h
+++ b/esphome/components/demo/demo_climate.h
@@ -111,6 +111,7 @@ class DemoClimate : public climate::Climate, public Component {
climate::CLIMATE_FAN_MIDDLE,
climate::CLIMATE_FAN_FOCUS,
climate::CLIMATE_FAN_DIFFUSE,
+ climate::CLIMATE_FAN_QUIET,
});
traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"});
traits.set_supported_swing_modes({
diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h
index ab039736fb..ec266d46ab 100644
--- a/esphome/components/demo/demo_cover.h
+++ b/esphome/components/demo/demo_cover.h
@@ -72,6 +72,7 @@ class DemoCover : public cover::Cover, public Component {
traits.set_supports_tilt(true);
break;
case DemoCoverType::TYPE_4:
+ traits.set_supports_stop(true);
traits.set_is_assumed_state(true);
traits.set_supports_tilt(true);
break;
diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py
index 3cdfc8ab85..5ea04b4804 100644
--- a/esphome/components/dfplayer/__init__.py
+++ b/esphome/components/dfplayer/__init__.py
@@ -40,6 +40,7 @@ DEVICE = {
NextAction = dfplayer_ns.class_("NextAction", automation.Action)
PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action)
+PlayMp3Action = dfplayer_ns.class_("PlayMp3Action", automation.Action)
PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action)
PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action)
SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", automation.Action)
@@ -113,6 +114,25 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args):
return var
+@automation.register_action(
+ "dfplayer.play_mp3",
+ PlayMp3Action,
+ cv.maybe_simple_value(
+ {
+ cv.GenerateID(): cv.use_id(DFPlayer),
+ cv.Required(CONF_FILE): cv.templatable(cv.int_),
+ },
+ key=CONF_FILE,
+ ),
+)
+async def dfplayer_play_mp3_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_FILE], args, float)
+ cg.add(var.set_file(template_))
+ return var
+
+
@automation.register_action(
"dfplayer.play",
PlayFileAction,
@@ -348,7 +368,7 @@ async def dfplayer_random_to_code(config, action_id, template_arg, args):
}
),
)
-async def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args):
+async def dfplayer_is_playing_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp
index 7df551f5d2..a6339dc988 100644
--- a/esphome/components/dfplayer/dfplayer.cpp
+++ b/esphome/components/dfplayer/dfplayer.cpp
@@ -7,10 +7,10 @@ namespace dfplayer {
static const char *const TAG = "dfplayer";
void DFPlayer::play_folder(uint16_t folder, uint16_t file) {
- if (folder < 100 && file < 256) {
+ if (folder <= 10 && file <= 1000) {
this->ack_set_is_playing_ = true;
this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file);
- } else if (folder <= 10 && file <= 1000) {
+ } else if (folder < 100 && file < 256) {
this->ack_set_is_playing_ = true;
this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file);
} else {
@@ -19,7 +19,7 @@ void DFPlayer::play_folder(uint16_t folder, uint16_t file) {
}
void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
- uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t)(argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef};
+ uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t) (argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef};
uint16_t checksum = 0;
for (uint8_t i = 1; i < 7; i++)
checksum += buffer[i];
@@ -77,14 +77,16 @@ void DFPlayer::loop() {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
- } else if (argument == 2)
+ } else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
+ }
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
- } else if (argument == 2)
+ } else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
+ }
break;
case 0x3F:
if (argument == 1) {
diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h
index ae47cb33f1..26e90fd410 100644
--- a/esphome/components/dfplayer/dfplayer.h
+++ b/esphome/components/dfplayer/dfplayer.h
@@ -35,6 +35,10 @@ class DFPlayer : public uart::UARTDevice, public Component {
this->ack_set_is_playing_ = true;
this->send_cmd_(0x02);
}
+ void play_mp3(uint16_t file) {
+ this->ack_set_is_playing_ = true;
+ this->send_cmd_(0x12, file);
+ }
void play_file(uint16_t file) {
this->ack_set_is_playing_ = true;
this->send_cmd_(0x03, file);
@@ -113,6 +117,16 @@ class DFPlayer : public uart::UARTDevice, public Component {
DFPLAYER_SIMPLE_ACTION(NextAction, next)
DFPLAYER_SIMPLE_ACTION(PreviousAction, previous)
+template class PlayMp3Action : public Action, public Parented {
+ public:
+ TEMPLATABLE_VALUE(uint16_t, file)
+
+ void play(Ts... x) override {
+ auto file = this->file_.value(x...);
+ this->parent_->play_mp3(file);
+ }
+};
+
template class PlayFileAction : public Action, public Parented {
public:
TEMPLATABLE_VALUE(uint16_t, file)
diff --git a/esphome/components/dht12/dht12.h b/esphome/components/dht12/dht12.h
index ae4d4fd607..2a706039ba 100644
--- a/esphome/components/dht12/dht12.h
+++ b/esphome/components/dht12/dht12.h
@@ -20,8 +20,8 @@ class DHT12Component : public PollingComponent, public i2c::I2CDevice {
protected:
bool read_data_(uint8_t *data);
- sensor::Sensor *temperature_sensor_;
- sensor::Sensor *humidity_sensor_;
+ sensor::Sensor *temperature_sensor_{nullptr};
+ sensor::Sensor *humidity_sensor_{nullptr};
};
} // namespace dht12
diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp
index 4ad353a254..86e8624d33 100644
--- a/esphome/components/display/display_buffer.cpp
+++ b/esphome/components/display/display_buffer.cpp
@@ -3,9 +3,9 @@
#include
#include "esphome/core/application.h"
#include "esphome/core/color.h"
-#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
namespace esphome {
namespace display {
@@ -24,6 +24,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) {
}
this->clear();
}
+
void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); }
void DisplayBuffer::clear() { this->fill(COLOR_OFF); }
int DisplayBuffer::get_width() {
@@ -50,6 +51,9 @@ int DisplayBuffer::get_height() {
}
void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; }
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
+ if (!this->get_clipping().inside(x, y))
+ return; // NOLINT
+
switch (this->rotation_) {
case DISPLAY_ROTATION_0_DEGREES:
break;
@@ -161,88 +165,53 @@ void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color
} while (dx <= 0);
}
-void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) {
+void DisplayBuffer::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
int x_start, y_start;
int width, height;
this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
-
- int i = 0;
- int x_at = x_start;
- while (text[i] != '\0') {
- int match_length;
- int glyph_n = font->match_next_glyph(text + i, &match_length);
- if (glyph_n < 0) {
- // Unknown char, skip
- ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
- if (!font->get_glyphs().empty()) {
- uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width;
- for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) {
- for (int glyph_y = 0; glyph_y < height; glyph_y++)
- this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
- }
- x_at += glyph_width;
- }
-
- i++;
- continue;
- }
-
- const Glyph &glyph = font->get_glyphs()[glyph_n];
- int scan_x1, scan_y1, scan_width, scan_height;
- glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
-
- for (int glyph_x = scan_x1; glyph_x < scan_x1 + scan_width; glyph_x++) {
- for (int glyph_y = scan_y1; glyph_y < scan_y1 + scan_height; glyph_y++) {
- if (glyph.get_pixel(glyph_x, glyph_y)) {
- this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
- }
- }
- }
-
- x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
-
- i += match_length;
- }
+ font->print(x_start, y_start, this, color, text);
}
-void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) {
+void DisplayBuffer::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format,
+ va_list arg) {
char buffer[256];
int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
if (ret > 0)
this->print(x, y, font, color, align, buffer);
}
-void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) {
- switch (image->get_type()) {
- case IMAGE_TYPE_BINARY:
- 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_pixel(img_x, img_y) ? color_on : color_off);
- }
- }
+void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) {
+ this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off);
+}
+
+void DisplayBuffer::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) {
+ auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT)));
+ auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT)));
+
+ switch (x_align) {
+ case ImageAlign::RIGHT:
+ x -= image->get_width();
break;
- case IMAGE_TYPE_GRAYSCALE:
- 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_grayscale_pixel(img_x, img_y));
- }
- }
+ case ImageAlign::CENTER_HORIZONTAL:
+ x -= image->get_width() / 2;
break;
- case IMAGE_TYPE_RGB24:
- 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_color_pixel(img_x, img_y));
- }
- }
- break;
- case IMAGE_TYPE_TRANSPARENT_BINARY:
- for (int img_x = 0; img_x < image->get_width(); img_x++) {
- for (int img_y = 0; img_y < image->get_height(); img_y++) {
- if (image->get_pixel(img_x, img_y))
- this->draw_pixel_at(x + img_x, y + img_y, color_on);
- }
- }
+ case ImageAlign::LEFT:
+ default:
break;
}
+
+ switch (y_align) {
+ case ImageAlign::BOTTOM:
+ y -= image->get_height();
+ break;
+ case ImageAlign::CENTER_VERTICAL:
+ y -= image->get_height() / 2;
+ break;
+ case ImageAlign::TOP:
+ default:
+ break;
+ }
+
+ image->draw(x, y, this, color_on, color_off);
}
#ifdef USE_GRAPH
@@ -258,7 +227,7 @@ void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_
}
#endif // USE_QR_CODE
-void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1,
+void DisplayBuffer::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
int *width, int *height) {
int x_offset, baseline;
font->measure(text, width, &x_offset, &baseline, height);
@@ -296,34 +265,34 @@ void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font,
break;
}
}
-void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) {
+void DisplayBuffer::print(int x, int y, BaseFont *font, Color color, const char *text) {
this->print(x, y, font, color, TextAlign::TOP_LEFT, text);
}
-void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) {
+void DisplayBuffer::print(int x, int y, BaseFont *font, TextAlign align, const char *text) {
this->print(x, y, font, COLOR_ON, align, text);
}
-void DisplayBuffer::print(int x, int y, Font *font, const char *text) {
+void DisplayBuffer::print(int x, int y, BaseFont *font, const char *text) {
this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
}
-void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) {
+void DisplayBuffer::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, align, format, arg);
va_end(arg);
}
-void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) {
+void DisplayBuffer::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg);
va_end(arg);
}
-void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char *format, ...) {
+void DisplayBuffer::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, align, format, arg);
va_end(arg);
}
-void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) {
+void DisplayBuffer::printf(int x, int y, BaseFont *font, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg);
@@ -361,195 +330,65 @@ void DisplayBuffer::do_update_() {
} else if (this->writer_.has_value()) {
(*this->writer_)(*this);
}
+ // remove all not ended clipping regions
+ while (is_clipping()) {
+ end_clipping();
+ }
}
void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
this->trigger(from, to);
}
-#ifdef USE_TIME
-void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format,
- time::ESPTime time) {
+void DisplayBuffer::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format,
+ ESPTime time) {
char buffer[64];
size_t ret = time.strftime(buffer, sizeof(buffer), format);
if (ret > 0)
this->print(x, y, font, color, align, buffer);
}
-void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) {
+void DisplayBuffer::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time);
}
-void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) {
+void DisplayBuffer::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, align, format, time);
}
-void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time::ESPTime time) {
+void DisplayBuffer::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time);
}
-#endif
-bool Glyph::get_pixel(int x, int y) const {
- const int x_data = x - this->glyph_data_->offset_x;
- const int y_data = y - this->glyph_data_->offset_y;
- if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height)
- return false;
- const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u;
- const uint32_t pos = x_data + y_data * width_8;
- return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u));
-}
-const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
-bool Glyph::compare_to(const char *str) const {
- // 1 -> this->char_
- // 2 -> str
- for (uint32_t i = 0;; i++) {
- if (this->glyph_data_->a_char[i] == '\0')
- return true;
- if (str[i] == '\0')
- return false;
- if (this->glyph_data_->a_char[i] > str[i])
- return false;
- if (this->glyph_data_->a_char[i] < str[i])
- return true;
+void DisplayBuffer::start_clipping(Rect rect) {
+ if (!this->clipping_rectangle_.empty()) {
+ Rect r = this->clipping_rectangle_.back();
+ rect.shrink(r);
}
- // this should not happen
- return false;
+ this->clipping_rectangle_.push_back(rect);
}
-int Glyph::match_length(const char *str) const {
- for (uint32_t i = 0;; i++) {
- if (this->glyph_data_->a_char[i] == '\0')
- return i;
- if (str[i] != this->glyph_data_->a_char[i])
- return 0;
+void DisplayBuffer::end_clipping() {
+ if (this->clipping_rectangle_.empty()) {
+ ESP_LOGE(TAG, "clear: Clipping is not set.");
+ } else {
+ this->clipping_rectangle_.pop_back();
}
- // this should not happen
- return 0;
}
-void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
- *x1 = this->glyph_data_->offset_x;
- *y1 = this->glyph_data_->offset_y;
- *width = this->glyph_data_->width;
- *height = this->glyph_data_->height;
-}
-int Font::match_next_glyph(const char *str, int *match_length) {
- int lo = 0;
- int hi = this->glyphs_.size() - 1;
- while (lo != hi) {
- int mid = (lo + hi + 1) / 2;
- if (this->glyphs_[mid].compare_to(str)) {
- lo = mid;
- } else {
- hi = mid - 1;
- }
+void DisplayBuffer::extend_clipping(Rect add_rect) {
+ if (this->clipping_rectangle_.empty()) {
+ ESP_LOGE(TAG, "add: Clipping is not set.");
+ } else {
+ this->clipping_rectangle_.back().extend(add_rect);
}
- *match_length = this->glyphs_[lo].match_length(str);
- if (*match_length <= 0)
- return -1;
- return lo;
}
-void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
- *baseline = this->baseline_;
- *height = this->bottom_;
- int i = 0;
- int min_x = 0;
- bool has_char = false;
- int x = 0;
- while (str[i] != '\0') {
- int match_length;
- int glyph_n = this->match_next_glyph(str + i, &match_length);
- if (glyph_n < 0) {
- // Unknown char, skip
- if (!this->get_glyphs().empty())
- x += this->get_glyphs()[0].glyph_data_->width;
- i++;
- continue;
- }
-
- const Glyph &glyph = this->glyphs_[glyph_n];
- if (!has_char) {
- min_x = glyph.glyph_data_->offset_x;
- } else {
- min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
- }
- x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
-
- i += match_length;
- has_char = true;
+void DisplayBuffer::shrink_clipping(Rect add_rect) {
+ if (this->clipping_rectangle_.empty()) {
+ ESP_LOGE(TAG, "add: Clipping is not set.");
+ } else {
+ this->clipping_rectangle_.back().shrink(add_rect);
}
- *x_offset = min_x;
- *width = x - min_x;
}
-const std::vector &Font::get_glyphs() const { return this->glyphs_; }
-Font::Font(const GlyphData *data, int data_nr, int baseline, int bottom) : baseline_(baseline), bottom_(bottom) {
- for (int i = 0; i < data_nr; ++i)
- glyphs_.emplace_back(data + i);
-}
-
-bool Image::get_pixel(int x, int y) const {
- if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
- return false;
- const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
- const uint32_t pos = x + y * width_8;
- return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
-}
-Color Image::get_color_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_) * 3;
- const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) |
- (progmem_read_byte(this->data_start_ + pos + 1) << 8) |
- (progmem_read_byte(this->data_start_ + pos + 0) << 16);
- return Color(color32);
-}
-Color Image::get_grayscale_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_);
- const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
- return Color(gray | gray << 8 | gray << 16 | gray << 24);
-}
-int Image::get_width() const { return this->width_; }
-int Image::get_height() const { return this->height_; }
-ImageType Image::get_type() const { return this->type_; }
-Image::Image(const uint8_t *data_start, int width, int height, ImageType type)
- : width_(width), height_(height), type_(type), data_start_(data_start) {}
-
-bool Animation::get_pixel(int x, int y) const {
- if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
- return false;
- const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
- const uint32_t frame_index = this->height_ * width_8 * this->current_frame_;
- if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_))
- return false;
- const uint32_t pos = x + y * width_8 + frame_index;
- return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u));
-}
-Color Animation::get_color_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) * 3;
- const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) |
- (progmem_read_byte(this->data_start_ + pos + 1) << 8) |
- (progmem_read_byte(this->data_start_ + pos + 0) << 16);
- return Color(color32);
-}
-Color Animation::get_grayscale_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);
- const uint8_t gray = progmem_read_byte(this->data_start_ + pos);
- return Color(gray | gray << 8 | gray << 16 | gray << 24);
-}
-Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
- : Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {}
-int Animation::get_animation_frame_count() const { return this->animation_frame_count_; }
-int Animation::get_current_frame() const { return this->current_frame_; }
-void Animation::next_frame() {
- this->current_frame_++;
- if (this->current_frame_ >= animation_frame_count_) {
- this->current_frame_ = 0;
+Rect DisplayBuffer::get_clipping() {
+ if (this->clipping_rectangle_.empty()) {
+ return Rect();
+ } else {
+ return this->clipping_rectangle_.back();
}
}
diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h
index 8ee1cd8779..1a62da2323 100644
--- a/esphome/components/display/display_buffer.h
+++ b/esphome/components/display/display_buffer.h
@@ -1,14 +1,13 @@
#pragma once
+#include
+#include
+#include "rect.h"
+#include "display_color_utils.h"
+#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
-#include "esphome/core/automation.h"
-#include "display_color_utils.h"
-#include
-
-#ifdef USE_TIME
-#include "esphome/components/time/real_time_clock.h"
-#endif
+#include "esphome/core/time.h"
#ifdef USE_GRAPH
#include "esphome/components/graph/graph.h"
@@ -72,16 +71,58 @@ enum class TextAlign {
BOTTOM_RIGHT = BOTTOM | RIGHT,
};
-/// Turn the pixel OFF.
-extern const Color COLOR_OFF;
-/// Turn the pixel ON.
-extern const Color COLOR_ON;
+/** ImageAlign is used to tell the display class how to position a image. By default
+ * the coordinates you enter for the image() functions take the upper left corner of the image
+ * as the "anchor" point. You can customize this behavior to, for example, make the coordinates
+ * refer to the *center* of the image.
+ *
+ * All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
+ * these options are allowed:
+ *
+ * - LEFT (x-coordinate of anchor point is on left)
+ * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image)
+ * - RIGHT (x-coordinate of anchor point is on right)
+ *
+ * For the Y-Axis alignment these options are allowed:
+ *
+ * - TOP (y-coordinate of anchor is on the top of the image)
+ * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image)
+ * - BOTTOM (y-coordinate of anchor is on the bottom of the image)
+ *
+ * These options are then combined to create combined TextAlignment options like:
+ * - TOP_LEFT (default)
+ * - CENTER (anchor point is in the middle of the image bounds)
+ * - ...
+ */
+enum class ImageAlign {
+ TOP = 0x00,
+ CENTER_VERTICAL = 0x01,
+ BOTTOM = 0x02,
-enum ImageType {
- IMAGE_TYPE_BINARY = 0,
- IMAGE_TYPE_GRAYSCALE = 1,
- IMAGE_TYPE_RGB24 = 2,
- IMAGE_TYPE_TRANSPARENT_BINARY = 3,
+ LEFT = 0x00,
+ CENTER_HORIZONTAL = 0x04,
+ RIGHT = 0x08,
+
+ TOP_LEFT = TOP | LEFT,
+ TOP_CENTER = TOP | CENTER_HORIZONTAL,
+ TOP_RIGHT = TOP | RIGHT,
+
+ CENTER_LEFT = CENTER_VERTICAL | LEFT,
+ CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
+ CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
+
+ BOTTOM_LEFT = BOTTOM | LEFT,
+ BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
+ BOTTOM_RIGHT = BOTTOM | RIGHT,
+
+ HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT,
+ VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM
+};
+
+enum DisplayType {
+ DISPLAY_TYPE_BINARY = 1,
+ DISPLAY_TYPE_GRAYSCALE = 2,
+ DISPLAY_TYPE_COLOR = 3,
};
enum DisplayRotation {
@@ -91,8 +132,6 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270,
};
-class Font;
-class Image;
class DisplayBuffer;
class DisplayPage;
class DisplayOnPageChangeTrigger;
@@ -106,6 +145,24 @@ using display_writer_t = std::function;
ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \
}
+/// Turn the pixel OFF.
+extern const Color COLOR_OFF;
+/// Turn the pixel ON.
+extern const Color COLOR_ON;
+
+class BaseImage {
+ public:
+ virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0;
+ virtual int get_width() const = 0;
+ virtual int get_height() const = 0;
+};
+
+class BaseFont {
+ public:
+ virtual void print(int x, int y, DisplayBuffer *display, Color color, const char *text) = 0;
+ virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
+};
+
class DisplayBuffer {
public:
/// Fill the entire screen with the given color.
@@ -117,6 +174,7 @@ class DisplayBuffer {
int get_width();
/// Get the height of the image in pixels with rotation applied.
int get_height();
+
/// Set a single pixel at the specified coordinates to the given color.
void draw_pixel_at(int x, int y, Color color = COLOR_ON);
@@ -151,7 +209,7 @@ class DisplayBuffer {
* @param align The alignment of the text.
* @param text The text to draw.
*/
- void print(int x, int y, Font *font, Color color, TextAlign align, const char *text);
+ void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text);
/** Print `text` with the top left at [x,y] with `font`.
*
@@ -161,7 +219,7 @@ class DisplayBuffer {
* @param color The color to draw the text with.
* @param text The text to draw.
*/
- void print(int x, int y, Font *font, Color color, const char *text);
+ void print(int x, int y, BaseFont *font, Color color, const char *text);
/** Print `text` with the anchor point at [x,y] with `font`.
*
@@ -171,7 +229,7 @@ class DisplayBuffer {
* @param align The alignment of the text.
* @param text The text to draw.
*/
- void print(int x, int y, Font *font, TextAlign align, const char *text);
+ void print(int x, int y, BaseFont *font, TextAlign align, const char *text);
/** Print `text` with the top left at [x,y] with `font`.
*
@@ -180,7 +238,7 @@ class DisplayBuffer {
* @param font The font to draw the text with.
* @param text The text to draw.
*/
- void print(int x, int y, Font *font, const char *text);
+ void print(int x, int y, BaseFont *font, const char *text);
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
*
@@ -192,7 +250,7 @@ class DisplayBuffer {
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
- void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...)
+ void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...)
__attribute__((format(printf, 7, 8)));
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
@@ -204,7 +262,7 @@ class DisplayBuffer {
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
- void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
+ void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
*
@@ -215,7 +273,8 @@ class DisplayBuffer {
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
- void printf(int x, int y, Font *font, TextAlign align, const char *format, ...) __attribute__((format(printf, 6, 7)));
+ void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...)
+ __attribute__((format(printf, 6, 7)));
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
*
@@ -225,9 +284,8 @@ class DisplayBuffer {
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
- void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
+ void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
-#ifdef USE_TIME
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
@@ -238,7 +296,7 @@ class DisplayBuffer {
* @param format The strftime format to use.
* @param time The time to format.
*/
- void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time)
+ void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time)
__attribute__((format(strftime, 7, 0)));
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
@@ -250,7 +308,7 @@ class DisplayBuffer {
* @param format The strftime format to use.
* @param time The time to format.
*/
- void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time)
+ void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time)
__attribute__((format(strftime, 6, 0)));
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
@@ -262,7 +320,7 @@ class DisplayBuffer {
* @param format The strftime format to use.
* @param time The time to format.
*/
- void strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time)
+ void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time)
__attribute__((format(strftime, 6, 0)));
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
@@ -273,19 +331,28 @@ class DisplayBuffer {
* @param format The strftime format to use.
* @param time The time to format.
*/
- void strftime(int x, int y, Font *font, const char *format, time::ESPTime time)
- __attribute__((format(strftime, 5, 0)));
-#endif
+ void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0)));
/** Draw the `image` with the top-left corner at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
- * @param image The image to draw
+ * @param image The image to draw.
* @param color_on The color to replace in binary images for the on bits.
* @param color_off The color to replace in binary images for the off bits.
*/
- void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
+ void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
+
+ /** Draw the `image` at [x,y] to the screen.
+ *
+ * @param x The x coordinate of the upper left corner.
+ * @param y The y coordinate of the upper left corner.
+ * @param image The image to draw.
+ * @param align The alignment of the image.
+ * @param color_on The color to replace in binary images for the on bits.
+ * @param color_off The color to replace in binary images for the off bits.
+ */
+ void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
#ifdef USE_GRAPH
/** Draw the `graph` with the top-left corner at [x,y] to the screen.
@@ -334,7 +401,7 @@ class DisplayBuffer {
* @param width A pointer to store the returned text width in.
* @param height A pointer to store the returned text height in.
*/
- void get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width,
+ void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width,
int *height);
/// Internal method to set the display writer lambda.
@@ -360,8 +427,56 @@ class DisplayBuffer {
virtual int get_width_internal() = 0;
DisplayRotation get_rotation() const { return this->rotation_; }
+ /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
+ * returns the type the display is currently configured to.
+ */
+ virtual DisplayType get_display_type() = 0;
+
+ /** Set the clipping rectangle for further drawing
+ *
+ * @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen)
+ *
+ * return true if success, false if error
+ */
+ void start_clipping(Rect rect);
+ void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
+ start_clipping(Rect(left, top, right - left, bottom - top));
+ };
+
+ /** Add a rectangular region to the invalidation region
+ * - This is usually called when an element has been modified
+ *
+ * @param[in] rect: Rectangle to add to the invalidation region
+ */
+ void extend_clipping(Rect rect);
+ void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
+ this->extend_clipping(Rect(left, top, right - left, bottom - top));
+ };
+
+ /** substract a rectangular region to the invalidation region
+ * - This is usually called when an element has been modified
+ *
+ * @param[in] rect: Rectangle to add to the invalidation region
+ */
+ void shrink_clipping(Rect rect);
+ void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
+ this->shrink_clipping(Rect(left, top, right - left, bottom - top));
+ };
+
+ /** Reset the invalidation region
+ */
+ void end_clipping();
+
+ /** Get the current the clipping rectangle
+ *
+ * return rect for active clipping region
+ */
+ Rect get_clipping();
+
+ bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
+
protected:
- void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
+ void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg);
virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0;
@@ -376,6 +491,7 @@ class DisplayBuffer {
DisplayPage *previous_page_{nullptr};
std::vector on_page_change_triggers_;
bool auto_clear_enabled_{true};
+ std::vector clipping_rectangle_;
};
class DisplayPage {
@@ -396,91 +512,6 @@ class DisplayPage {
DisplayPage *next_{nullptr};
};
-struct GlyphData {
- const char *a_char;
- const uint8_t *data;
- int offset_x;
- int offset_y;
- int width;
- int height;
-};
-
-class Glyph {
- public:
- Glyph(const GlyphData *data) : glyph_data_(data) {}
-
- bool get_pixel(int x, int y) const;
-
- const char *get_char() const;
-
- bool compare_to(const char *str) const;
-
- int match_length(const char *str) const;
-
- void scan_area(int *x1, int *y1, int *width, int *height) const;
-
- protected:
- friend Font;
- friend DisplayBuffer;
-
- const GlyphData *glyph_data_;
-};
-
-class Font {
- public:
- /** Construct the font with the given glyphs.
- *
- * @param glyphs A vector of glyphs, must be sorted lexicographically.
- * @param baseline The y-offset from the top of the text to the baseline.
- * @param bottom The y-offset from the top of the text to the bottom (i.e. height).
- */
- Font(const GlyphData *data, int data_nr, int baseline, int bottom);
-
- int match_next_glyph(const char *str, int *match_length);
-
- void measure(const char *str, int *width, int *x_offset, int *baseline, int *height);
-
- const std::vector &get_glyphs() const;
-
- protected:
- std::vector glyphs_;
- int baseline_;
- int bottom_;
-};
-
-class Image {
- public:
- 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_grayscale_pixel(int x, int y) const;
- int get_width() const;
- int get_height() const;
- ImageType get_type() const;
-
- protected:
- int width_;
- int height_;
- ImageType type_;
- const uint8_t *data_start_;
-};
-
-class Animation : public Image {
- public:
- 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_grayscale_pixel(int x, int y) const override;
-
- int get_animation_frame_count() const;
- int get_current_frame() const;
- void next_frame();
-
- protected:
- int current_frame_;
- int animation_frame_count_;
-};
-
template class DisplayPageShowAction : public Action {
public:
TEMPLATABLE_VALUE(DisplayPage *, page)
diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h
index 202de912de..3114dee359 100644
--- a/esphome/components/display/display_color_utils.h
+++ b/esphome/components/display/display_color_utils.h
@@ -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 euclidean 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
diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp
new file mode 100644
index 0000000000..6e91c86c4f
--- /dev/null
+++ b/esphome/components/display/rect.cpp
@@ -0,0 +1,98 @@
+#include "rect.h"
+
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace display {
+
+static const char *const TAG = "display";
+
+void Rect::expand(int16_t horizontal, int16_t vertical) {
+ if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
+ this->x = this->x - horizontal;
+ this->y = this->y - vertical;
+ this->w = this->w + (2 * horizontal);
+ this->h = this->h + (2 * vertical);
+ }
+}
+
+void Rect::extend(Rect rect) {
+ if (!this->is_set()) {
+ this->x = rect.x;
+ this->y = rect.y;
+ this->w = rect.w;
+ this->h = rect.h;
+ } else {
+ if (this->x > rect.x) {
+ this->w = this->w + (this->x - rect.x);
+ this->x = rect.x;
+ }
+ if (this->y > rect.y) {
+ this->h = this->h + (this->y - rect.y);
+ this->y = rect.y;
+ }
+ if (this->x2() < rect.x2()) {
+ this->w = rect.x2() - this->x;
+ }
+ if (this->y2() < rect.y2()) {
+ this->h = rect.y2() - this->y;
+ }
+ }
+}
+void Rect::shrink(Rect rect) {
+ if (!this->inside(rect)) {
+ (*this) = Rect();
+ } else {
+ if (this->x2() > rect.x2()) {
+ this->w = rect.x2() - this->x;
+ }
+ if (this->x < rect.x) {
+ this->w = this->w + (this->x - rect.x);
+ this->x = rect.x;
+ }
+ if (this->y2() > rect.y2()) {
+ this->h = rect.y2() - this->y;
+ }
+ if (this->y < rect.y) {
+ this->h = this->h + (this->y - rect.y);
+ this->y = rect.y;
+ }
+ }
+}
+
+bool Rect::equal(Rect rect) {
+ return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
+}
+
+bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) { // NOLINT
+ if (!this->is_set()) {
+ return true;
+ }
+ if (absolute) {
+ return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
+ } else {
+ return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
+ }
+}
+
+bool Rect::inside(Rect rect, bool absolute) {
+ if (!this->is_set() || !rect.is_set()) {
+ return true;
+ }
+ if (absolute) {
+ return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
+ } else {
+ return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
+ }
+}
+
+void Rect::info(const std::string &prefix) {
+ if (this->is_set()) {
+ ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
+ this->y2());
+ } else
+ ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
+}
+
+} // namespace display
+} // namespace esphome
diff --git a/esphome/components/display/rect.h b/esphome/components/display/rect.h
new file mode 100644
index 0000000000..867a9c67c7
--- /dev/null
+++ b/esphome/components/display/rect.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace display {
+
+static const int16_t VALUE_NO_SET = 32766;
+
+class Rect {
+ public:
+ int16_t x; ///< X coordinate of corner
+ int16_t y; ///< Y coordinate of corner
+ int16_t w; ///< Width of region
+ int16_t h; ///< Height of region
+
+ Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT
+ inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
+ inline int16_t x2() { return this->x + this->w; }; ///< X coordinate of corner
+ inline int16_t y2() { return this->y + this->h; }; ///< Y coordinate of corner
+
+ inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
+
+ void expand(int16_t horizontal, int16_t vertical);
+
+ void extend(Rect rect);
+ void shrink(Rect rect);
+
+ bool inside(Rect rect, bool absolute = true);
+ bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
+ bool equal(Rect rect);
+ void info(const std::string &prefix = "rect info:");
+};
+
+} // namespace display
+} // namespace esphome
diff --git a/esphome/components/display_menu_base/__init__.py b/esphome/components/display_menu_base/__init__.py
new file mode 100644
index 0000000000..d7326cdc65
--- /dev/null
+++ b/esphome/components/display_menu_base/__init__.py
@@ -0,0 +1,430 @@
+import re
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import automation, core
+from esphome.const import (
+ CONF_ID,
+ CONF_TYPE,
+ CONF_TRIGGER_ID,
+ CONF_ON_VALUE,
+ CONF_COMMAND,
+ CONF_CUSTOM,
+ CONF_NUMBER,
+ CONF_FORMAT,
+ CONF_MODE,
+ CONF_ACTIVE,
+)
+from esphome.automation import maybe_simple_id
+from esphome.components.select import Select
+from esphome.components.number import Number
+from esphome.components.switch import Switch
+
+CODEOWNERS = ["@numo68"]
+
+display_menu_base_ns = cg.esphome_ns.namespace("display_menu_base")
+
+CONF_DISPLAY_ID = "display_id"
+
+CONF_ROTARY = "rotary"
+CONF_JOYSTICK = "joystick"
+CONF_LABEL = "label"
+CONF_MENU = "menu"
+CONF_BACK = "back"
+CONF_TEXT = "text"
+CONF_SELECT = "select"
+CONF_SWITCH = "switch"
+CONF_ITEMS = "items"
+CONF_ON_TEXT = "on_text"
+CONF_OFF_TEXT = "off_text"
+CONF_VALUE_LAMBDA = "value_lambda"
+CONF_IMMEDIATE_EDIT = "immediate_edit"
+CONF_ROOT_ITEM_ID = "root_item_id"
+CONF_ON_ENTER = "on_enter"
+CONF_ON_LEAVE = "on_leave"
+CONF_ON_NEXT = "on_next"
+CONF_ON_PREV = "on_prev"
+
+DisplayMenuComponent = display_menu_base_ns.class_("DisplayMenuComponent", cg.Component)
+
+MenuItem = display_menu_base_ns.class_("MenuItem")
+MenuItemConstPtr = MenuItem.operator("ptr").operator("const")
+MenuItemMenu = display_menu_base_ns.class_("MenuItemMenu")
+MenuItemSelect = display_menu_base_ns.class_("MenuItemSelect")
+MenuItemNumber = display_menu_base_ns.class_("MenuItemNumber")
+MenuItemSwitch = display_menu_base_ns.class_("MenuItemSwitch")
+MenuItemCommand = display_menu_base_ns.class_("MenuItemCommand")
+MenuItemCustom = display_menu_base_ns.class_("MenuItemCustom")
+
+UpAction = display_menu_base_ns.class_("UpAction", automation.Action)
+DownAction = display_menu_base_ns.class_("DownAction", automation.Action)
+LeftAction = display_menu_base_ns.class_("LeftAction", automation.Action)
+RightAction = display_menu_base_ns.class_("RightAction", automation.Action)
+EnterAction = display_menu_base_ns.class_("EnterAction", automation.Action)
+ShowAction = display_menu_base_ns.class_("ShowAction", automation.Action)
+HideAction = display_menu_base_ns.class_("HideAction", automation.Action)
+ShowMainAction = display_menu_base_ns.class_("ShowMainAction", automation.Action)
+
+IsActiveCondition = display_menu_base_ns.class_(
+ "IsActiveCondition", automation.Condition
+)
+
+MULTI_CONF = True
+
+MenuItemType = display_menu_base_ns.enum("MenuItemType")
+
+MENU_ITEM_TYPES = {
+ CONF_LABEL: MenuItemType.MENU_ITEM_LABEL,
+ CONF_MENU: MenuItemType.MENU_ITEM_MENU,
+ CONF_BACK: MenuItemType.MENU_ITEM_BACK,
+ CONF_SELECT: MenuItemType.MENU_ITEM_SELECT,
+ CONF_NUMBER: MenuItemType.MENU_ITEM_NUMBER,
+ CONF_SWITCH: MenuItemType.MENU_ITEM_SWITCH,
+ CONF_COMMAND: MenuItemType.MENU_ITEM_COMMAND,
+ CONF_CUSTOM: MenuItemType.MENU_ITEM_CUSTOM,
+}
+
+MENU_ITEMS_WITH_SPECIALIZED_CLASSES = (
+ CONF_MENU,
+ CONF_SELECT,
+ CONF_NUMBER,
+ CONF_SWITCH,
+ CONF_COMMAND,
+ CONF_CUSTOM,
+)
+
+MenuMode = display_menu_base_ns.enum("MenuMode")
+
+MENU_MODES = {
+ CONF_ROTARY: MenuMode.MENU_MODE_ROTARY,
+ CONF_JOYSTICK: MenuMode.MENU_MODE_JOYSTICK,
+}
+
+DisplayMenuOnEnterTrigger = display_menu_base_ns.class_(
+ "DisplayMenuOnEnterTrigger", automation.Trigger
+)
+
+DisplayMenuOnLeaveTrigger = display_menu_base_ns.class_(
+ "DisplayMenuOnLeaveTrigger", automation.Trigger
+)
+
+DisplayMenuOnValueTrigger = display_menu_base_ns.class_(
+ "DisplayMenuOnValueTrigger", automation.Trigger
+)
+
+DisplayMenuOnNextTrigger = display_menu_base_ns.class_(
+ "DisplayMenuOnNextTrigger", automation.Trigger
+)
+
+DisplayMenuOnPrevTrigger = display_menu_base_ns.class_(
+ "DisplayMenuOnPrevTrigger", automation.Trigger
+)
+
+
+def validate_format(format):
+ if re.search(r"^%([+-])*(\d+)*(\.\d+)*[fg]$", format) is None:
+ raise cv.Invalid(
+ f"{CONF_FORMAT}: has to specify a printf-like format string specifying exactly one f or g type conversion, '{format}' provided"
+ )
+
+ return format
+
+
+# Use a simple indirection to circumvent the recursion limitation
+def menu_item_schema(value):
+ return MENU_ITEM_SCHEMA(value)
+
+
+MENU_ITEM_COMMON_SCHEMA = cv.Schema(
+ {
+ cv.Optional(CONF_TEXT): cv.templatable(cv.string),
+ }
+)
+
+MENU_ITEM_ENTER_LEAVE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
+ {
+ cv.Optional(CONF_ON_ENTER): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+ DisplayMenuOnEnterTrigger
+ ),
+ }
+ ),
+ cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+ DisplayMenuOnLeaveTrigger
+ ),
+ }
+ ),
+ }
+)
+
+MENU_ITEM_VALUE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
+ {
+ cv.Optional(CONF_ON_VALUE): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+ DisplayMenuOnValueTrigger
+ ),
+ }
+ ),
+ }
+)
+
+MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA = MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
+ {
+ cv.Optional(CONF_ON_VALUE): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+ DisplayMenuOnValueTrigger
+ ),
+ }
+ ),
+ }
+)
+
+MENU_ITEM_SCHEMA = cv.typed_schema(
+ {
+ CONF_LABEL: MENU_ITEM_COMMON_SCHEMA.extend(
+ {
+ cv.GenerateID(CONF_ID): cv.declare_id(MenuItem),
+ }
+ ),
+ CONF_BACK: MENU_ITEM_COMMON_SCHEMA.extend(
+ {
+ cv.GenerateID(CONF_ID): cv.declare_id(MenuItem),
+ }
+ ),
+ CONF_MENU: MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
+ {
+ cv.GenerateID(CONF_ID): cv.declare_id(MenuItemMenu),
+ cv.Required(CONF_ITEMS): cv.All(
+ cv.ensure_list(menu_item_schema), cv.Length(min=1)
+ ),
+ }
+ ),
+ CONF_SELECT: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
+ {
+ cv.GenerateID(CONF_ID): cv.declare_id(MenuItemSelect),
+ cv.Required(CONF_SELECT): cv.use_id(Select),
+ cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
+ cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
+ }
+ ),
+ CONF_NUMBER: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
+ {
+ cv.GenerateID(CONF_ID): cv.declare_id(MenuItemNumber),
+ cv.Required(CONF_NUMBER): cv.use_id(Number),
+ cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
+ cv.Optional(CONF_FORMAT, default="%.1f"): cv.All(
+ cv.string_strict,
+ validate_format,
+ ),
+ cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
+ }
+ ),
+ CONF_SWITCH: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
+ {
+ cv.GenerateID(CONF_ID): cv.declare_id(MenuItemSwitch),
+ cv.Required(CONF_SWITCH): cv.use_id(Switch),
+ cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
+ cv.Optional(CONF_ON_TEXT, default="On"): cv.string_strict,
+ cv.Optional(CONF_OFF_TEXT, default="Off"): cv.string_strict,
+ cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
+ }
+ ),
+ CONF_COMMAND: MENU_ITEM_VALUE_SCHEMA.extend(
+ {
+ cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCommand),
+ }
+ ),
+ CONF_CUSTOM: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
+ {
+ cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCustom),
+ cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
+ cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
+ cv.Optional(CONF_ON_NEXT): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+ DisplayMenuOnNextTrigger
+ ),
+ }
+ ),
+ cv.Optional(CONF_ON_PREV): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+ DisplayMenuOnPrevTrigger
+ ),
+ }
+ ),
+ }
+ ),
+ },
+ default_type="label",
+ lower=True,
+)
+
+DISPLAY_MENU_BASE_SCHEMA = cv.Schema(
+ {
+ cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
+ cv.GenerateID(CONF_ROOT_ITEM_ID): cv.declare_id(MenuItemMenu),
+ cv.Optional(CONF_MODE, default=CONF_ROTARY): cv.enum(MENU_MODES),
+ cv.Optional(CONF_ON_ENTER): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+ DisplayMenuOnEnterTrigger
+ ),
+ }
+ ),
+ cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
+ {
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
+ DisplayMenuOnLeaveTrigger
+ ),
+ }
+ ),
+ cv.Required(CONF_ITEMS): cv.All(
+ cv.ensure_list(MENU_ITEM_SCHEMA), cv.Length(min=1)
+ ),
+ }
+).extend(cv.COMPONENT_SCHEMA)
+
+MENU_ACTION_SCHEMA = maybe_simple_id(
+ {
+ cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent),
+ }
+)
+
+
+@automation.register_action("display_menu.up", UpAction, MENU_ACTION_SCHEMA)
+async def menu_up_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("display_menu.down", DownAction, MENU_ACTION_SCHEMA)
+async def menu_down_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("display_menu.left", LeftAction, MENU_ACTION_SCHEMA)
+async def menu_left_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("display_menu.right", RightAction, MENU_ACTION_SCHEMA)
+async def menu_right_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("display_menu.enter", EnterAction, MENU_ACTION_SCHEMA)
+async def menu_enter_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("display_menu.show", ShowAction, MENU_ACTION_SCHEMA)
+async def menu_show_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("display_menu.hide", HideAction, MENU_ACTION_SCHEMA)
+async def menu_hide_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(
+ "display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA
+)
+async def menu_show_main_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_condition(
+ "display_menu.is_active",
+ IsActiveCondition,
+ automation.maybe_simple_id(
+ {
+ cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent),
+ }
+ ),
+)
+async def display_menu_is_active_to_code(config, condition_id, template_arg, args):
+ paren = await cg.get_variable(config[CONF_ID])
+ return cg.new_Pvariable(condition_id, template_arg, paren)
+
+
+async def menu_item_to_code(menu, config, parent):
+ if config[CONF_TYPE] in MENU_ITEMS_WITH_SPECIALIZED_CLASSES:
+ item = cg.new_Pvariable(config[CONF_ID])
+ else:
+ item = cg.new_Pvariable(config[CONF_ID], MENU_ITEM_TYPES[config[CONF_TYPE]])
+ cg.add(parent.add_item(item))
+ if CONF_TEXT in config:
+ if isinstance(config[CONF_TEXT], core.Lambda):
+ template_ = await cg.templatable(
+ config[CONF_TEXT], [(MenuItemConstPtr, "it")], cg.std_string
+ )
+ cg.add(item.set_text(template_))
+ else:
+ cg.add(item.set_text(config[CONF_TEXT]))
+ if CONF_VALUE_LAMBDA in config:
+ template_ = await cg.process_lambda(
+ config[CONF_VALUE_LAMBDA],
+ [(MenuItemConstPtr, "it")],
+ return_type=cg.std_string,
+ )
+ cg.add(item.set_value_lambda(template_))
+ if CONF_ITEMS in config:
+ for c in config[CONF_ITEMS]:
+ await menu_item_to_code(menu, c, item)
+ if CONF_IMMEDIATE_EDIT in config:
+ cg.add(item.set_immediate_edit(config[CONF_IMMEDIATE_EDIT]))
+ if config[CONF_TYPE] == CONF_SELECT:
+ var = await cg.get_variable(config[CONF_SELECT])
+ cg.add(item.set_select_variable(var))
+ if config[CONF_TYPE] == CONF_NUMBER:
+ var = await cg.get_variable(config[CONF_NUMBER])
+ cg.add(item.set_number_variable(var))
+ cg.add(item.set_format(config[CONF_FORMAT]))
+ if config[CONF_TYPE] == CONF_SWITCH:
+ var = await cg.get_variable(config[CONF_SWITCH])
+ cg.add(item.set_switch_variable(var))
+ cg.add(item.set_on_text(config[CONF_ON_TEXT]))
+ cg.add(item.set_off_text(config[CONF_OFF_TEXT]))
+ for conf in config.get(CONF_ON_ENTER, []):
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
+ await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
+ for conf in config.get(CONF_ON_LEAVE, []):
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
+ await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
+ for conf in config.get(CONF_ON_VALUE, []):
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
+ await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
+ for conf in config.get(CONF_ON_NEXT, []):
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
+ await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
+ for conf in config.get(CONF_ON_PREV, []):
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
+ await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
+
+
+async def display_menu_to_code(menu, config):
+ cg.add(menu.set_active(config[CONF_ACTIVE]))
+ root_item = cg.new_Pvariable(config[CONF_ROOT_ITEM_ID])
+ cg.add(menu.set_root_item(root_item))
+ cg.add(menu.set_mode(config[CONF_MODE]))
+ for c in config[CONF_ITEMS]:
+ await menu_item_to_code(menu, c, root_item)
+ for conf in config.get(CONF_ON_ENTER, []):
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
+ await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
+ for conf in config.get(CONF_ON_LEAVE, []):
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
+ await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h
new file mode 100644
index 0000000000..d5394a1e0c
--- /dev/null
+++ b/esphome/components/display_menu_base/automation.h
@@ -0,0 +1,133 @@
+#pragma once
+
+#include "esphome/core/automation.h"
+#include "display_menu_base.h"
+
+namespace esphome {
+namespace display_menu_base {
+
+template class UpAction : public Action {
+ public:
+ explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {}
+
+ void play(Ts... x) override { this->menu_->up(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+
+template class DownAction : public Action {
+ public:
+ explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {}
+
+ void play(Ts... x) override { this->menu_->down(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+
+template class LeftAction : public Action {
+ public:
+ explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {}
+
+ void play(Ts... x) override { this->menu_->left(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+
+template class RightAction : public Action {
+ public:
+ explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {}
+
+ void play(Ts... x) override { this->menu_->right(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+
+template class EnterAction : public Action {
+ public:
+ explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {}
+
+ void play(Ts... x) override { this->menu_->enter(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+
+template class ShowAction : public Action {
+ public:
+ explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {}
+
+ void play(Ts... x) override { this->menu_->show(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+
+template class HideAction : public Action {
+ public:
+ explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {}
+
+ void play(Ts... x) override { this->menu_->hide(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+
+template class ShowMainAction : public Action {
+ public:
+ explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {}
+
+ void play(Ts... x) override { this->menu_->show_main(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+template class IsActiveCondition : public Condition {
+ public:
+ explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {}
+ bool check(Ts... x) override { return this->menu_->is_active(); }
+
+ protected:
+ DisplayMenuComponent *menu_;
+};
+
+class DisplayMenuOnEnterTrigger : public Trigger {
+ public:
+ explicit DisplayMenuOnEnterTrigger(MenuItem *parent) {
+ parent->add_on_enter_callback([this, parent]() { this->trigger(parent); });
+ }
+};
+
+class DisplayMenuOnLeaveTrigger : public Trigger {
+ public:
+ explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) {
+ parent->add_on_leave_callback([this, parent]() { this->trigger(parent); });
+ }
+};
+
+class DisplayMenuOnValueTrigger : public Trigger {
+ public:
+ explicit DisplayMenuOnValueTrigger(MenuItem *parent) {
+ parent->add_on_value_callback([this, parent]() { this->trigger(parent); });
+ }
+};
+
+class DisplayMenuOnNextTrigger : public Trigger {
+ public:
+ explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) {
+ parent->add_on_next_callback([this, parent]() { this->trigger(parent); });
+ }
+};
+
+class DisplayMenuOnPrevTrigger : public Trigger {
+ public:
+ explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) {
+ parent->add_on_prev_callback([this, parent]() { this->trigger(parent); });
+ }
+};
+
+} // namespace display_menu_base
+} // namespace esphome
diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp
new file mode 100644
index 0000000000..57da3cec35
--- /dev/null
+++ b/esphome/components/display_menu_base/display_menu_base.cpp
@@ -0,0 +1,315 @@
+#include "display_menu_base.h"
+#include
+
+namespace esphome {
+namespace display_menu_base {
+
+void DisplayMenuComponent::up() {
+ if (this->check_healthy_and_active_()) {
+ bool changed = false;
+
+ if (this->editing_) {
+ switch (this->mode_) {
+ case MENU_MODE_ROTARY:
+ changed = this->get_selected_item_()->select_prev();
+ break;
+ default:
+ break;
+ }
+ } else {
+ changed = this->cursor_up_();
+ }
+
+ if (changed)
+ this->draw_and_update();
+ }
+}
+
+void DisplayMenuComponent::down() {
+ if (this->check_healthy_and_active_()) {
+ bool changed = false;
+
+ if (this->editing_) {
+ switch (this->mode_) {
+ case MENU_MODE_ROTARY:
+ changed = this->get_selected_item_()->select_next();
+ break;
+ default:
+ break;
+ }
+ } else {
+ changed = this->cursor_down_();
+ }
+
+ if (changed)
+ this->draw_and_update();
+ }
+}
+
+void DisplayMenuComponent::left() {
+ if (this->check_healthy_and_active_()) {
+ bool changed = false;
+
+ switch (this->get_selected_item_()->get_type()) {
+ case MENU_ITEM_SELECT:
+ case MENU_ITEM_SWITCH:
+ case MENU_ITEM_NUMBER:
+ case MENU_ITEM_CUSTOM:
+ switch (this->mode_) {
+ case MENU_MODE_ROTARY:
+ if (this->editing_) {
+ this->finish_editing_();
+ changed = true;
+ }
+ break;
+ case MENU_MODE_JOYSTICK:
+ if (this->editing_ || this->get_selected_item_()->get_immediate_edit())
+ changed = this->get_selected_item_()->select_prev();
+ break;
+ default:
+ break;
+ }
+ break;
+ case MENU_ITEM_BACK:
+ changed = this->leave_menu_();
+ break;
+ default:
+ break;
+ }
+
+ if (changed)
+ this->draw_and_update();
+ }
+}
+
+void DisplayMenuComponent::right() {
+ if (this->check_healthy_and_active_()) {
+ bool changed = false;
+
+ switch (this->get_selected_item_()->get_type()) {
+ case MENU_ITEM_SELECT:
+ case MENU_ITEM_SWITCH:
+ case MENU_ITEM_NUMBER:
+ case MENU_ITEM_CUSTOM:
+ switch (this->mode_) {
+ case MENU_MODE_JOYSTICK:
+ if (this->editing_ || this->get_selected_item_()->get_immediate_edit())
+ changed = this->get_selected_item_()->select_next();
+ default:
+ break;
+ }
+ break;
+ case MENU_ITEM_MENU:
+ changed = this->enter_menu_();
+ break;
+ default:
+ break;
+ }
+
+ if (changed)
+ this->draw_and_update();
+ }
+}
+
+void DisplayMenuComponent::enter() {
+ if (this->check_healthy_and_active_()) {
+ bool changed = false;
+ MenuItem *item = this->get_selected_item_();
+
+ if (this->editing_) {
+ this->finish_editing_();
+ changed = true;
+ } else {
+ switch (item->get_type()) {
+ case MENU_ITEM_MENU:
+ changed = this->enter_menu_();
+ break;
+ case MENU_ITEM_BACK:
+ changed = this->leave_menu_();
+ break;
+ case MENU_ITEM_SELECT:
+ case MENU_ITEM_SWITCH:
+ case MENU_ITEM_CUSTOM:
+ if (item->get_immediate_edit()) {
+ changed = item->select_next();
+ } else {
+ this->editing_ = true;
+ item->on_enter();
+ changed = true;
+ }
+ break;
+ case MENU_ITEM_NUMBER:
+ // A number cannot be immediate in the rotary mode
+ if (!item->get_immediate_edit() || this->mode_ == MENU_MODE_ROTARY) {
+ this->editing_ = true;
+ item->on_enter();
+ changed = true;
+ }
+ break;
+ case MENU_ITEM_COMMAND:
+ changed = item->select_next();
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (changed)
+ this->draw_and_update();
+ }
+}
+
+void DisplayMenuComponent::draw() {
+ if (this->check_healthy_and_active_())
+ this->draw_menu();
+}
+
+void DisplayMenuComponent::show_main() {
+ bool disp_changed = false;
+
+ if (this->is_failed())
+ return;
+
+ this->process_initial_();
+
+ if (this->active_ && this->editing_)
+ this->finish_editing_();
+
+ if (this->displayed_item_ != this->root_item_) {
+ this->displayed_item_->on_leave();
+ disp_changed = true;
+ }
+
+ this->reset_();
+ this->active_ = true;
+
+ if (disp_changed) {
+ this->displayed_item_->on_enter();
+ }
+
+ this->draw_and_update();
+}
+
+void DisplayMenuComponent::show() {
+ if (this->is_failed())
+ return;
+
+ this->process_initial_();
+
+ if (!this->active_) {
+ this->active_ = true;
+ this->draw_and_update();
+ }
+}
+
+void DisplayMenuComponent::hide() {
+ if (this->check_healthy_and_active_()) {
+ if (this->editing_)
+ this->finish_editing_();
+ this->active_ = false;
+ this->update();
+ }
+}
+
+void DisplayMenuComponent::reset_() {
+ this->displayed_item_ = this->root_item_;
+ this->cursor_index_ = this->top_index_ = 0;
+ this->selection_stack_.clear();
+}
+
+void DisplayMenuComponent::process_initial_() {
+ if (!this->root_on_enter_called_) {
+ this->root_item_->on_enter();
+ this->root_on_enter_called_ = true;
+ }
+}
+
+bool DisplayMenuComponent::check_healthy_and_active_() {
+ if (this->is_failed())
+ return false;
+
+ this->process_initial_();
+
+ return this->active_;
+}
+
+bool DisplayMenuComponent::cursor_up_() {
+ bool changed = false;
+
+ if (this->cursor_index_ > 0) {
+ changed = true;
+
+ --this->cursor_index_;
+
+ if (this->cursor_index_ < this->top_index_)
+ this->top_index_ = this->cursor_index_;
+ }
+
+ return changed;
+}
+
+bool DisplayMenuComponent::cursor_down_() {
+ bool changed = false;
+
+ if (this->cursor_index_ + 1 < this->displayed_item_->items_size()) {
+ changed = true;
+
+ ++this->cursor_index_;
+
+ if (this->cursor_index_ >= this->top_index_ + this->rows_)
+ this->top_index_ = this->cursor_index_ - this->rows_ + 1;
+ }
+
+ return changed;
+}
+
+bool DisplayMenuComponent::enter_menu_() {
+ this->displayed_item_->on_leave();
+ this->displayed_item_ = static_cast(this->get_selected_item_());
+ this->selection_stack_.push_front({this->top_index_, this->cursor_index_});
+ this->cursor_index_ = this->top_index_ = 0;
+ this->displayed_item_->on_enter();
+
+ return true;
+}
+
+bool DisplayMenuComponent::leave_menu_() {
+ bool changed = false;
+
+ if (this->displayed_item_->get_parent() != nullptr) {
+ this->displayed_item_->on_leave();
+ this->displayed_item_ = this->displayed_item_->get_parent();
+ this->top_index_ = this->selection_stack_.front().first;
+ this->cursor_index_ = this->selection_stack_.front().second;
+ this->selection_stack_.pop_front();
+ this->displayed_item_->on_enter();
+ changed = true;
+ }
+
+ return changed;
+}
+
+void DisplayMenuComponent::finish_editing_() {
+ switch (this->get_selected_item_()->get_type()) {
+ case MENU_ITEM_SELECT:
+ case MENU_ITEM_NUMBER:
+ case MENU_ITEM_SWITCH:
+ case MENU_ITEM_CUSTOM:
+ this->get_selected_item_()->on_leave();
+ break;
+ default:
+ break;
+ }
+
+ this->editing_ = false;
+}
+
+void DisplayMenuComponent::draw_menu() {
+ for (size_t i = 0; i < this->rows_ && this->top_index_ + i < this->displayed_item_->items_size(); ++i) {
+ this->draw_item(this->displayed_item_->get_item(this->top_index_ + i), i,
+ this->top_index_ + i == this->cursor_index_);
+ }
+}
+
+} // namespace display_menu_base
+} // namespace esphome
diff --git a/esphome/components/display_menu_base/display_menu_base.h b/esphome/components/display_menu_base/display_menu_base.h
new file mode 100644
index 0000000000..46bb0a8192
--- /dev/null
+++ b/esphome/components/display_menu_base/display_menu_base.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "esphome/core/component.h"
+
+#include "menu_item.h"
+
+#include
+
+namespace esphome {
+namespace display_menu_base {
+
+enum MenuMode {
+ MENU_MODE_ROTARY,
+ MENU_MODE_JOYSTICK,
+};
+
+class MenuItem;
+
+/** Class to display a hierarchical menu.
+ *
+ */
+class DisplayMenuComponent : public Component {
+ public:
+ void set_root_item(MenuItemMenu *item) { this->displayed_item_ = this->root_item_ = item; }
+ void set_active(bool active) { this->active_ = active; }
+ void set_mode(MenuMode mode) { this->mode_ = mode; }
+ void set_rows(uint8_t rows) { this->rows_ = rows; }
+
+ float get_setup_priority() const override { return setup_priority::PROCESSOR; }
+
+ void up();
+ void down();
+ void left();
+ void right();
+ void enter();
+
+ void show_main();
+ void show();
+ void hide();
+
+ void draw();
+
+ bool is_active() const { return this->active_; }
+
+ protected:
+ void reset_();
+ void process_initial_();
+ bool check_healthy_and_active_();
+ MenuItem *get_selected_item_() { return this->displayed_item_->get_item(this->cursor_index_); }
+ bool cursor_up_();
+ bool cursor_down_();
+ bool enter_menu_();
+ bool leave_menu_();
+ void finish_editing_();
+ virtual void draw_menu();
+ virtual void draw_item(const MenuItem *item, uint8_t row, bool selected) = 0;
+ virtual void update() {}
+ virtual void draw_and_update() {
+ draw_menu();
+ update();
+ }
+
+ uint8_t rows_;
+ bool active_;
+ MenuMode mode_;
+ MenuItemMenu *root_item_{nullptr};
+
+ MenuItemMenu *displayed_item_{nullptr};
+ uint8_t top_index_{0};
+ uint8_t cursor_index_{0};
+ std::forward_list> selection_stack_{};
+ bool editing_{false};
+ bool root_on_enter_called_{false};
+};
+
+} // namespace display_menu_base
+} // namespace esphome
diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp
new file mode 100644
index 0000000000..bbe6ec0e89
--- /dev/null
+++ b/esphome/components/display_menu_base/menu_item.cpp
@@ -0,0 +1,179 @@
+#include "menu_item.h"
+
+#include
+
+namespace esphome {
+namespace display_menu_base {
+
+void MenuItem::on_enter() { this->on_enter_callbacks_.call(); }
+
+void MenuItem::on_leave() { this->on_leave_callbacks_.call(); }
+
+void MenuItem::on_value_() { this->on_value_callbacks_.call(); }
+
+#ifdef USE_SELECT
+std::string MenuItemSelect::get_value_text() const {
+ std::string result;
+
+ if (this->value_getter_.has_value()) {
+ result = this->value_getter_.value()(this);
+ } else {
+ if (this->select_var_ != nullptr) {
+ result = this->select_var_->state;
+ }
+ }
+
+ return result;
+}
+
+bool MenuItemSelect::select_next() {
+ bool changed = false;
+
+ if (this->select_var_ != nullptr) {
+ this->select_var_->make_call().select_next(true).perform();
+ changed = true;
+ }
+
+ return changed;
+}
+
+bool MenuItemSelect::select_prev() {
+ bool changed = false;
+
+ if (this->select_var_ != nullptr) {
+ this->select_var_->make_call().select_previous(true).perform();
+ changed = true;
+ }
+
+ return changed;
+}
+#endif // USE_SELECT
+
+#ifdef USE_NUMBER
+std::string MenuItemNumber::get_value_text() const {
+ std::string result;
+
+ if (this->value_getter_.has_value()) {
+ result = this->value_getter_.value()(this);
+ } else {
+ char data[32];
+ snprintf(data, sizeof(data), this->format_.c_str(), get_number_value_());
+ result = data;
+ }
+
+ return result;
+}
+
+bool MenuItemNumber::select_next() {
+ bool changed = false;
+
+ if (this->number_var_ != nullptr) {
+ float last = this->number_var_->state;
+ this->number_var_->make_call().number_increment(false).perform();
+
+ if (this->number_var_->state != last) {
+ this->on_value_();
+ changed = true;
+ }
+ }
+
+ return changed;
+}
+
+bool MenuItemNumber::select_prev() {
+ bool changed = false;
+
+ if (this->number_var_ != nullptr) {
+ float last = this->number_var_->state;
+ this->number_var_->make_call().number_decrement(false).perform();
+
+ if (this->number_var_->state != last) {
+ this->on_value_();
+ changed = true;
+ }
+ }
+
+ return changed;
+}
+
+float MenuItemNumber::get_number_value_() const {
+ float result = 0.0;
+
+ if (this->number_var_ != nullptr) {
+ if (!this->number_var_->has_state() || this->number_var_->state < this->number_var_->traits.get_min_value()) {
+ result = this->number_var_->traits.get_min_value();
+ } else if (this->number_var_->state > this->number_var_->traits.get_max_value()) {
+ result = this->number_var_->traits.get_max_value();
+ } else {
+ result = this->number_var_->state;
+ }
+ }
+
+ return result;
+}
+#endif // USE_NUMBER
+
+#ifdef USE_SWITCH
+std::string MenuItemSwitch::get_value_text() const {
+ std::string result;
+
+ if (this->value_getter_.has_value()) {
+ result = this->value_getter_.value()(this);
+ } else {
+ result = this->get_switch_state_() ? this->switch_on_text_ : this->switch_off_text_;
+ }
+
+ return result;
+}
+
+bool MenuItemSwitch::select_next() { return this->toggle_switch_(); }
+
+bool MenuItemSwitch::select_prev() { return this->toggle_switch_(); }
+
+bool MenuItemSwitch::get_switch_state_() const { return (this->switch_var_ != nullptr && this->switch_var_->state); }
+
+bool MenuItemSwitch::toggle_switch_() {
+ bool changed = false;
+
+ if (this->switch_var_ != nullptr) {
+ this->switch_var_->toggle();
+ this->on_value_();
+ changed = true;
+ }
+
+ return changed;
+}
+#endif // USE_SWITCH
+
+std::string MenuItemCustom::get_value_text() const {
+ return (this->value_getter_.has_value()) ? this->value_getter_.value()(this) : "";
+}
+
+bool MenuItemCommand::select_next() {
+ this->on_value_();
+ return true;
+}
+
+bool MenuItemCommand::select_prev() {
+ this->on_value_();
+ return true;
+}
+
+bool MenuItemCustom::select_next() {
+ this->on_next_();
+ this->on_value_();
+ return true;
+}
+
+bool MenuItemCustom::select_prev() {
+ this->on_prev_();
+ this->on_value_();
+ return true;
+}
+
+void MenuItemCustom::on_next_() { this->on_next_callbacks_.call(); }
+
+void MenuItemCustom::on_prev_() { this->on_prev_callbacks_.call(); }
+
+} // namespace display_menu_base
+} // namespace esphome
diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h
new file mode 100644
index 0000000000..a30f31e88f
--- /dev/null
+++ b/esphome/components/display_menu_base/menu_item.h
@@ -0,0 +1,187 @@
+#pragma once
+
+#include "esphome/core/defines.h"
+#include "esphome/core/automation.h"
+
+#ifdef USE_NUMBER
+#include "esphome/components/number/number.h"
+#endif
+#ifdef USE_SELECT
+#include "esphome/components/select/select.h"
+#endif
+#ifdef USE_SWITCH
+#include "esphome/components/switch/switch.h"
+#endif
+
+#include
+
+namespace esphome {
+namespace display_menu_base {
+
+enum MenuItemType {
+ MENU_ITEM_LABEL,
+ MENU_ITEM_MENU,
+ MENU_ITEM_BACK,
+ MENU_ITEM_SELECT,
+ MENU_ITEM_NUMBER,
+ MENU_ITEM_SWITCH,
+ MENU_ITEM_COMMAND,
+ MENU_ITEM_CUSTOM,
+};
+
+class MenuItem;
+class MenuItemMenu;
+using value_getter_t = std::function;
+
+class MenuItem {
+ public:
+ explicit MenuItem(MenuItemType t) : item_type_(t) {}
+ void set_parent(MenuItemMenu *parent) { this->parent_ = parent; }
+ MenuItemMenu *get_parent() { return this->parent_; }
+ MenuItemType get_type() const { return this->item_type_; }
+ template void set_text(V val) { this->text_ = val; }
+ void add_on_enter_callback(std::function &&cb) { this->on_enter_callbacks_.add(std::move(cb)); }
+ void add_on_leave_callback(std::function &&cb) { this->on_leave_callbacks_.add(std::move(cb)); }
+ void add_on_value_callback(std::function &&cb) { this->on_value_callbacks_.add(std::move(cb)); }
+
+ std::string get_text() const { return const_cast(this)->text_.value(this); }
+ virtual bool get_immediate_edit() const { return false; }
+ virtual bool has_value() const { return false; }
+ virtual std::string get_value_text() const { return ""; }
+
+ virtual bool select_next() { return false; }
+ virtual bool select_prev() { return false; }
+
+ void on_enter();
+ void on_leave();
+
+ protected:
+ void on_value_();
+
+ MenuItemType item_type_;
+ MenuItemMenu *parent_{nullptr};
+ TemplatableValue text_;
+
+ CallbackManager on_enter_callbacks_{};
+ CallbackManager on_leave_callbacks_{};
+ CallbackManager on_value_callbacks_{};
+};
+
+class MenuItemMenu : public MenuItem {
+ public:
+ explicit MenuItemMenu() : MenuItem(MENU_ITEM_MENU) {}
+ void add_item(MenuItem *item) {
+ item->set_parent(this);
+ this->items_.push_back(item);
+ }
+ size_t items_size() const { return this->items_.size(); }
+ MenuItem *get_item(size_t i) { return this->items_[i]; }
+
+ protected:
+ std::vector items_;
+};
+
+class MenuItemEditable : public MenuItem {
+ public:
+ explicit MenuItemEditable(MenuItemType t) : MenuItem(t) {}
+ void set_immediate_edit(bool val) { this->immediate_edit_ = val; }
+ bool get_immediate_edit() const override { return this->immediate_edit_; }
+ void set_value_lambda(value_getter_t &&getter) { this->value_getter_ = getter; }
+
+ protected:
+ bool immediate_edit_{false};
+ optional value_getter_{};
+};
+
+#ifdef USE_SELECT
+class MenuItemSelect : public MenuItemEditable {
+ public:
+ explicit MenuItemSelect() : MenuItemEditable(MENU_ITEM_SELECT) {}
+ void set_select_variable(select::Select *var) { this->select_var_ = var; }
+
+ bool has_value() const override { return true; }
+ std::string get_value_text() const override;
+
+ bool select_next() override;
+ bool select_prev() override;
+
+ protected:
+ select::Select *select_var_{nullptr};
+};
+#endif
+
+#ifdef USE_NUMBER
+class MenuItemNumber : public MenuItemEditable {
+ public:
+ explicit MenuItemNumber() : MenuItemEditable(MENU_ITEM_NUMBER) {}
+ void set_number_variable(number::Number *var) { this->number_var_ = var; }
+ void set_format(const std::string &fmt) { this->format_ = fmt; }
+
+ bool has_value() const override { return true; }
+ std::string get_value_text() const override;
+
+ bool select_next() override;
+ bool select_prev() override;
+
+ protected:
+ float get_number_value_() const;
+
+ number::Number *number_var_{nullptr};
+ std::string format_;
+};
+#endif
+
+#ifdef USE_SWITCH
+class MenuItemSwitch : public MenuItemEditable {
+ public:
+ explicit MenuItemSwitch() : MenuItemEditable(MENU_ITEM_SWITCH) {}
+ void set_switch_variable(switch_::Switch *var) { this->switch_var_ = var; }
+ void set_on_text(const std::string &t) { this->switch_on_text_ = t; }
+ void set_off_text(const std::string &t) { this->switch_off_text_ = t; }
+
+ bool has_value() const override { return true; }
+ std::string get_value_text() const override;
+
+ bool select_next() override;
+ bool select_prev() override;
+
+ protected:
+ bool get_switch_state_() const;
+ bool toggle_switch_();
+
+ switch_::Switch *switch_var_{nullptr};
+ std::string switch_on_text_;
+ std::string switch_off_text_;
+};
+#endif
+
+class MenuItemCommand : public MenuItem {
+ public:
+ explicit MenuItemCommand() : MenuItem(MENU_ITEM_COMMAND) {}
+
+ bool select_next() override;
+ bool select_prev() override;
+};
+
+class MenuItemCustom : public MenuItemEditable {
+ public:
+ explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {}
+ void add_on_next_callback(std::function &&cb) { this->on_next_callbacks_.add(std::move(cb)); }
+ void add_on_prev_callback(std::function &&cb) { this->on_prev_callbacks_.add(std::move(cb)); }
+
+ bool has_value() const override { return this->value_getter_.has_value(); }
+ std::string get_value_text() const override;
+
+ bool select_next() override;
+ bool select_prev() override;
+
+ protected:
+ void on_next_();
+ void on_prev_();
+
+ CallbackManager on_next_callbacks_{};
+ CallbackManager on_prev_callbacks_{};
+};
+
+} // namespace display_menu_base
+} // namespace esphome
diff --git a/esphome/components/dps310/__init__.py b/esphome/components/dps310/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp
new file mode 100644
index 0000000000..22fb52967f
--- /dev/null
+++ b/esphome/components/dps310/dps310.cpp
@@ -0,0 +1,189 @@
+#include "dps310.h"
+#include "esphome/core/log.h"
+#include "esphome/core/hal.h"
+
+namespace esphome {
+namespace dps310 {
+
+static const char *const TAG = "dps310";
+
+void DPS310Component::setup() {
+ uint8_t coef_data_raw[DPS310_NUM_COEF_REGS];
+ auto timer = DPS310_INIT_TIMEOUT;
+ uint8_t reg = 0;
+
+ ESP_LOGCONFIG(TAG, "Setting up DPS310...");
+ // first, reset the sensor
+ if (!this->write_byte(DPS310_REG_RESET, DPS310_CMD_RESET)) {
+ this->mark_failed();
+ return;
+ }
+ delay(10);
+ // wait for the sensor and its coefficients to be ready
+ while (timer-- && (!(reg & DPS310_BIT_SENSOR_RDY) || !(reg & DPS310_BIT_COEF_RDY))) {
+ reg = this->read_byte(DPS310_REG_MEAS_CFG).value_or(0);
+ delay(5);
+ }
+
+ if (!(reg & DPS310_BIT_SENSOR_RDY) || !(reg & DPS310_BIT_COEF_RDY)) { // the flags were not set in time
+ this->mark_failed();
+ return;
+ }
+ // read device ID
+ if (!this->read_byte(DPS310_REG_PROD_REV_ID, &this->prod_rev_id_)) {
+ this->mark_failed();
+ return;
+ }
+ // read in coefficients used to calculate the compensated pressure and temperature values
+ if (!this->read_bytes(DPS310_REG_COEF, coef_data_raw, DPS310_NUM_COEF_REGS)) {
+ this->mark_failed();
+ return;
+ }
+ // read in coefficients source register, too -- we need this a few lines down
+ if (!this->read_byte(DPS310_REG_TMP_COEF_SRC, ®)) {
+ this->mark_failed();
+ return;
+ }
+ // set up operational stuff
+ if (!this->write_byte(DPS310_REG_PRS_CFG, DPS310_VAL_PRS_CFG)) {
+ this->mark_failed();
+ return;
+ }
+ if (!this->write_byte(DPS310_REG_TMP_CFG, DPS310_VAL_TMP_CFG | (reg & DPS310_BIT_TMP_COEF_SRC))) {
+ this->mark_failed();
+ return;
+ }
+ if (!this->write_byte(DPS310_REG_CFG, DPS310_VAL_REG_CFG)) {
+ this->mark_failed();
+ return;
+ }
+ if (!this->write_byte(DPS310_REG_MEAS_CFG, 0x07)) { // enable background mode
+ this->mark_failed();
+ return;
+ }
+
+ this->c0_ = // we only ever use c0/2, so just divide by 2 here to save time later
+ DPS310Component::twos_complement(
+ int16_t(((uint16_t) coef_data_raw[0] << 4) | (((uint16_t) coef_data_raw[1] >> 4) & 0x0F)), 12) /
+ 2;
+
+ this->c1_ =
+ DPS310Component::twos_complement(int16_t((((uint16_t) coef_data_raw[1] & 0x0F) << 8) | coef_data_raw[2]), 12);
+
+ this->c00_ = ((uint32_t) coef_data_raw[3] << 12) | ((uint32_t) coef_data_raw[4] << 4) |
+ (((uint32_t) coef_data_raw[5] >> 4) & 0x0F);
+ this->c00_ = DPS310Component::twos_complement(c00_, 20);
+
+ this->c10_ =
+ (((uint32_t) coef_data_raw[5] & 0x0F) << 16) | ((uint32_t) coef_data_raw[6] << 8) | (uint32_t) coef_data_raw[7];
+ this->c10_ = DPS310Component::twos_complement(c10_, 20);
+
+ this->c01_ = int16_t(((uint16_t) coef_data_raw[8] << 8) | (uint16_t) coef_data_raw[9]);
+ this->c11_ = int16_t(((uint16_t) coef_data_raw[10] << 8) | (uint16_t) coef_data_raw[11]);
+ this->c20_ = int16_t(((uint16_t) coef_data_raw[12] << 8) | (uint16_t) coef_data_raw[13]);
+ this->c21_ = int16_t(((uint16_t) coef_data_raw[14] << 8) | (uint16_t) coef_data_raw[15]);
+ this->c30_ = int16_t(((uint16_t) coef_data_raw[16] << 8) | (uint16_t) coef_data_raw[17]);
+}
+
+void DPS310Component::dump_config() {
+ ESP_LOGCONFIG(TAG, "DPS310:");
+ ESP_LOGCONFIG(TAG, " Product ID: %u", this->prod_rev_id_ & 0x0F);
+ ESP_LOGCONFIG(TAG, " Revision ID: %u", (this->prod_rev_id_ >> 4) & 0x0F);
+ LOG_I2C_DEVICE(this);
+ if (this->is_failed()) {
+ ESP_LOGE(TAG, "Communication with DPS310 failed!");
+ }
+ LOG_UPDATE_INTERVAL(this);
+ LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
+ LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
+}
+
+float DPS310Component::get_setup_priority() const { return setup_priority::DATA; }
+
+void DPS310Component::update() {
+ if (!this->update_in_progress_) {
+ this->update_in_progress_ = true;
+ this->read_();
+ }
+}
+
+void DPS310Component::read_() {
+ uint8_t reg = 0;
+ if (!this->read_byte(DPS310_REG_MEAS_CFG, ®)) {
+ this->status_set_warning();
+ return;
+ }
+
+ if ((!this->got_pres_) && (reg & DPS310_BIT_PRS_RDY)) {
+ this->read_pressure_();
+ }
+
+ if ((!this->got_temp_) && (reg & DPS310_BIT_TMP_RDY)) {
+ this->read_temperature_();
+ }
+
+ if (this->got_pres_ && this->got_temp_) {
+ this->calculate_values_(this->raw_temperature_, this->raw_pressure_);
+ this->got_pres_ = false;
+ this->got_temp_ = false;
+ this->update_in_progress_ = false;
+ this->status_clear_warning();
+ } else {
+ auto f = std::bind(&DPS310Component::read_, this);
+ this->set_timeout("dps310", 10, f);
+ }
+}
+
+void DPS310Component::read_pressure_() {
+ uint8_t bytes[3];
+ if (!this->read_bytes(DPS310_REG_PRS_B2, bytes, 3)) {
+ this->status_set_warning();
+ return;
+ }
+ this->got_pres_ = true;
+ this->raw_pressure_ = DPS310Component::twos_complement(
+ int32_t((uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | (uint32_t(bytes[2]))), 24);
+}
+
+void DPS310Component::read_temperature_() {
+ uint8_t bytes[3];
+ if (!this->read_bytes(DPS310_REG_TMP_B2, bytes, 3)) {
+ this->status_set_warning();
+ return;
+ }
+ this->got_temp_ = true;
+ this->raw_temperature_ = DPS310Component::twos_complement(
+ int32_t((uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | (uint32_t(bytes[2]))), 24);
+}
+
+// Calculations are taken from the datasheet which can be found here:
+// https://www.infineon.com/dgdl/Infineon-DPS310-DataSheet-v01_02-EN.pdf?fileId=5546d462576f34750157750826c42242
+// Sections "How to Calculate Compensated Pressure Values" and "How to Calculate Compensated Temperature Values"
+// Variable names below match variable names from the datasheet but lowercased
+void DPS310Component::calculate_values_(int32_t raw_temperature, int32_t raw_pressure) {
+ const float t_raw_sc = (float) raw_temperature / DPS310_SCALE_FACTOR;
+ const float p_raw_sc = (float) raw_pressure / DPS310_SCALE_FACTOR;
+
+ const float temperature = t_raw_sc * this->c1_ + this->c0_; // c0/2 done earlier!
+
+ const float pressure = (this->c00_ + p_raw_sc * (this->c10_ + p_raw_sc * (this->c20_ + p_raw_sc * this->c30_)) +
+ t_raw_sc * this->c01_ + t_raw_sc * p_raw_sc * (this->c11_ + p_raw_sc * this->c21_)) /
+ 100; // divide by 100 for hPa
+
+ if (this->temperature_sensor_ != nullptr) {
+ this->temperature_sensor_->publish_state(temperature);
+ }
+ if (this->pressure_sensor_ != nullptr) {
+ this->pressure_sensor_->publish_state(pressure);
+ }
+}
+
+int32_t DPS310Component::twos_complement(int32_t val, uint8_t bits) {
+ if (val & ((uint32_t) 1 << (bits - 1))) {
+ val -= (uint32_t) 1 << bits;
+ }
+ return val;
+}
+
+} // namespace dps310
+} // namespace esphome
diff --git a/esphome/components/dps310/dps310.h b/esphome/components/dps310/dps310.h
new file mode 100644
index 0000000000..50e7d93c8a
--- /dev/null
+++ b/esphome/components/dps310/dps310.h
@@ -0,0 +1,65 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace dps310 {
+
+static const uint8_t DPS310_REG_PRS_B2 = 0x00; // Highest byte of pressure data
+static const uint8_t DPS310_REG_TMP_B2 = 0x03; // Highest byte of temperature data
+static const uint8_t DPS310_REG_PRS_CFG = 0x06; // Pressure configuration
+static const uint8_t DPS310_REG_TMP_CFG = 0x07; // Temperature configuration
+static const uint8_t DPS310_REG_MEAS_CFG = 0x08; // Sensor configuration
+static const uint8_t DPS310_REG_CFG = 0x09; // Interrupt/FIFO configuration
+static const uint8_t DPS310_REG_RESET = 0x0C; // Soft reset
+static const uint8_t DPS310_REG_PROD_REV_ID = 0x0D; // Register that contains the part ID
+static const uint8_t DPS310_REG_COEF = 0x10; // Top of calibration coefficient data space
+static const uint8_t DPS310_REG_TMP_COEF_SRC = 0x28; // Temperature calibration src
+
+static const uint8_t DPS310_BIT_PRS_RDY = 0x10; // Pressure measurement is ready
+static const uint8_t DPS310_BIT_TMP_RDY = 0x20; // Temperature measurement is ready
+static const uint8_t DPS310_BIT_SENSOR_RDY = 0x40; // Sensor initialization complete when bit is set
+static const uint8_t DPS310_BIT_COEF_RDY = 0x80; // Coefficients are available when bit is set
+static const uint8_t DPS310_BIT_TMP_COEF_SRC = 0x80; // Temperature measurement source (0 = ASIC, 1 = MEMS element)
+static const uint8_t DPS310_BIT_REQ_PRES = 0x01; // Set this bit to request pressure reading
+static const uint8_t DPS310_BIT_REQ_TEMP = 0x02; // Set this bit to request temperature reading
+
+static const uint8_t DPS310_CMD_RESET = 0x89; // What to write to reset the device
+
+static const uint8_t DPS310_VAL_PRS_CFG = 0x01; // Value written to DPS310_REG_PRS_CFG at startup
+static const uint8_t DPS310_VAL_TMP_CFG = 0x01; // Value written to DPS310_REG_TMP_CFG at startup
+static const uint8_t DPS310_VAL_REG_CFG = 0x00; // Value written to DPS310_REG_CFG at startup
+
+static const uint8_t DPS310_INIT_TIMEOUT = 20; // How long to wait for DPS310 start-up to complete
+static const uint8_t DPS310_NUM_COEF_REGS = 18; // Number of coefficients we need to read from the device
+static const int32_t DPS310_SCALE_FACTOR = 1572864; // Measurement compensation scale factor
+
+class DPS310Component : public PollingComponent, public i2c::I2CDevice {
+ public:
+ void setup() override;
+ void dump_config() override;
+ float get_setup_priority() const override;
+ void update() override;
+
+ void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
+ void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
+
+ protected:
+ void read_();
+ void read_pressure_();
+ void read_temperature_();
+ void calculate_values_(int32_t raw_temperature, int32_t raw_pressure);
+ static int32_t twos_complement(int32_t val, uint8_t bits);
+
+ sensor::Sensor *temperature_sensor_{nullptr};
+ sensor::Sensor *pressure_sensor_{nullptr};
+ int32_t raw_pressure_, raw_temperature_, c00_, c10_;
+ int16_t c0_, c1_, c01_, c11_, c20_, c21_, c30_;
+ uint8_t prod_rev_id_;
+ bool got_pres_, got_temp_, update_in_progress_;
+};
+
+} // namespace dps310
+} // namespace esphome
diff --git a/esphome/components/dps310/sensor.py b/esphome/components/dps310/sensor.py
new file mode 100644
index 0000000000..742c873d9e
--- /dev/null
+++ b/esphome/components/dps310/sensor.py
@@ -0,0 +1,62 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import (
+ CONF_ID,
+ CONF_PRESSURE,
+ CONF_TEMPERATURE,
+ DEVICE_CLASS_PRESSURE,
+ DEVICE_CLASS_TEMPERATURE,
+ STATE_CLASS_MEASUREMENT,
+ UNIT_CELSIUS,
+ ICON_GAUGE,
+ ICON_THERMOMETER,
+ UNIT_HECTOPASCAL,
+)
+
+CODEOWNERS = ["@kbx81"]
+
+DEPENDENCIES = ["i2c"]
+
+dps310_ns = cg.esphome_ns.namespace("dps310")
+DPS310Component = dps310_ns.class_(
+ "DPS310Component", cg.PollingComponent, i2c.I2CDevice
+)
+
+CONFIG_SCHEMA = (
+ cv.Schema(
+ {
+ cv.GenerateID(): cv.declare_id(DPS310Component),
+ cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
+ unit_of_measurement=UNIT_CELSIUS,
+ icon=ICON_THERMOMETER,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_TEMPERATURE,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ cv.Required(CONF_PRESSURE): sensor.sensor_schema(
+ unit_of_measurement=UNIT_HECTOPASCAL,
+ icon=ICON_GAUGE,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_PRESSURE,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ }
+ )
+ .extend(cv.polling_component_schema("60s"))
+ .extend(i2c.i2c_device_schema(0x77))
+)
+
+
+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_PRESSURE in config:
+ sens = await sensor.new_sensor(config[CONF_PRESSURE])
+ cg.add(var.set_pressure_sensor(sens))
diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp
index d249e9743a..472ccc7a9a 100644
--- a/esphome/components/ds1307/ds1307.cpp
+++ b/esphome/components/ds1307/ds1307.cpp
@@ -37,14 +37,14 @@ void DS1307Component::read_time() {
ESP_LOGW(TAG, "RTC halted, not syncing to system clock.");
return;
}
- time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10),
- .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10),
- .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10),
- .day_of_week = uint8_t(ds1307_.reg.weekday),
- .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10),
- .day_of_year = 1, // ignored by recalc_timestamp_utc(false)
- .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10),
- .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)};
+ ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10),
+ .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10),
+ .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10),
+ .day_of_week = uint8_t(ds1307_.reg.weekday),
+ .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10),
+ .day_of_year = 1, // ignored by recalc_timestamp_utc(false)
+ .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10),
+ .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py
index 7a7681082e..d3d20ca2a7 100644
--- a/esphome/components/dsmr/__init__.py
+++ b/esphome/components/dsmr/__init__.py
@@ -10,6 +10,8 @@ from esphome.const import (
CODEOWNERS = ["@glmnet", "@zuidwijk"]
+MULTI_CONF = True
+
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["sensor", "text_sensor"]
@@ -17,6 +19,7 @@ CONF_CRC_CHECK = "crc_check"
CONF_DECRYPTION_KEY = "decryption_key"
CONF_DSMR_ID = "dsmr_id"
CONF_GAS_MBUS_ID = "gas_mbus_id"
+CONF_WATER_MBUS_ID = "water_mbus_id"
CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
CONF_REQUEST_INTERVAL = "request_interval"
CONF_REQUEST_PIN = "request_pin"
@@ -51,6 +54,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
+ cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_,
cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
cv.Optional(
@@ -79,10 +83,11 @@ async def to_code(config):
cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds))
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds))
- cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID])
+ cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
+ cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
# DSMR Parser
- cg.add_library("glmnet/Dsmr", "0.5")
+ cg.add_library("glmnet/Dsmr", "0.7")
# Crypto
- cg.add_library("rweather/Crypto", "0.2.0")
+ cg.add_library("rweather/Crypto", "0.4.0")
diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp
index 7b339e5fe0..f382730912 100644
--- a/esphome/components/dsmr/dsmr.cpp
+++ b/esphome/components/dsmr/dsmr.cpp
@@ -171,7 +171,7 @@ void Dsmr::receive_telegram_() {
this->telegram_[this->bytes_read_] = c;
this->bytes_read_++;
- // Check for a footer, i.e. exlamation mark, followed by a hex checksum.
+ // Check for a footer, i.e. exclamation mark, followed by a hex checksum.
if (c == '!') {
ESP_LOGV(TAG, "Footer of telegram found");
this->footer_found_ = true;
diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h
index 76f79ee55c..6621d02cae 100644
--- a/esphome/components/dsmr/dsmr.h
+++ b/esphome/components/dsmr/dsmr.h
@@ -13,6 +13,8 @@
#include
#include
+#include
+
namespace esphome {
namespace dsmr {
diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py
index d809d0d105..2e2050ecab 100644
--- a/esphome/components/dsmr/sensor.py
+++ b/esphome/components/dsmr/sensor.py
@@ -2,19 +2,17 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
+ CONF_ID,
DEVICE_CLASS_CURRENT,
- DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
- ICON_EMPTY,
+ DEVICE_CLASS_WATER,
STATE_CLASS_MEASUREMENT,
- STATE_CLASS_NONE,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_CUBIC_METER,
- UNIT_EMPTY,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
@@ -30,202 +28,220 @@ CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr),
cv.Optional("energy_delivered_lux"): sensor.sensor_schema(
- UNIT_KILOWATT_HOURS,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_ENERGY,
- STATE_CLASS_TOTAL_INCREASING,
+ unit_of_measurement=UNIT_KILOWATT_HOURS,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_ENERGY,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema(
- UNIT_KILOWATT_HOURS,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_ENERGY,
- STATE_CLASS_TOTAL_INCREASING,
+ unit_of_measurement=UNIT_KILOWATT_HOURS,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_ENERGY,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema(
- UNIT_KILOWATT_HOURS,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_ENERGY,
- STATE_CLASS_TOTAL_INCREASING,
+ unit_of_measurement=UNIT_KILOWATT_HOURS,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_ENERGY,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_lux"): sensor.sensor_schema(
- UNIT_KILOWATT_HOURS,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_ENERGY,
- STATE_CLASS_TOTAL_INCREASING,
+ unit_of_measurement=UNIT_KILOWATT_HOURS,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_ENERGY,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff1"): sensor.sensor_schema(
- UNIT_KILOWATT_HOURS,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_ENERGY,
- STATE_CLASS_TOTAL_INCREASING,
+ unit_of_measurement=UNIT_KILOWATT_HOURS,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_ENERGY,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff2"): sensor.sensor_schema(
- UNIT_KILOWATT_HOURS,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_ENERGY,
- STATE_CLASS_TOTAL_INCREASING,
+ unit_of_measurement=UNIT_KILOWATT_HOURS,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_ENERGY,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("total_imported_energy"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_NONE,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
+ accuracy_decimals=3,
),
cv.Optional("total_exported_energy"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_NONE,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
+ accuracy_decimals=3,
),
cv.Optional("power_delivered"): sensor.sensor_schema(
- UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_KILOWATT,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_returned"): sensor.sensor_schema(
- UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_KILOWATT,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_delivered"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_POWER,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE,
+ accuracy_decimals=3,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_returned"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_POWER,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE,
+ accuracy_decimals=3,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("electricity_threshold"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=3,
),
cv.Optional("electricity_switch_position"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=3,
),
cv.Optional("electricity_failures"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=0,
),
cv.Optional("electricity_long_failures"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=0,
),
cv.Optional("electricity_sags_l1"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=0,
),
cv.Optional("electricity_sags_l2"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=0,
),
cv.Optional("electricity_sags_l3"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=0,
),
cv.Optional("electricity_swells_l1"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=0,
),
cv.Optional("electricity_swells_l2"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=0,
),
cv.Optional("electricity_swells_l3"): sensor.sensor_schema(
- UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE
+ accuracy_decimals=0,
),
cv.Optional("current_l1"): sensor.sensor_schema(
- UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_AMPERE,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_CURRENT,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l2"): sensor.sensor_schema(
- UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_AMPERE,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_CURRENT,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("current_l3"): sensor.sensor_schema(
- UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_AMPERE,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_CURRENT,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_delivered_l1"): sensor.sensor_schema(
- UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_KILOWATT,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_delivered_l2"): sensor.sensor_schema(
- UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_KILOWATT,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_delivered_l3"): sensor.sensor_schema(
- UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_KILOWATT,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_returned_l1"): sensor.sensor_schema(
- UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_KILOWATT,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_returned_l2"): sensor.sensor_schema(
- UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_KILOWATT,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_returned_l3"): sensor.sensor_schema(
- UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
+ unit_of_measurement=UNIT_KILOWATT,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_POWER,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE,
+ accuracy_decimals=3,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE,
+ accuracy_decimals=3,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE,
+ accuracy_decimals=3,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE,
+ accuracy_decimals=3,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE,
+ accuracy_decimals=3,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema(
- UNIT_KILOVOLT_AMPS_REACTIVE,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_EMPTY,
- STATE_CLASS_MEASUREMENT,
+ unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE,
+ accuracy_decimals=3,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l1"): sensor.sensor_schema(
- UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
+ unit_of_measurement=UNIT_VOLT,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l2"): sensor.sensor_schema(
- UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
+ unit_of_measurement=UNIT_VOLT,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("voltage_l3"): sensor.sensor_schema(
- UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE
+ unit_of_measurement=UNIT_VOLT,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_VOLTAGE,
+ state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("gas_delivered"): sensor.sensor_schema(
- UNIT_CUBIC_METER,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_GAS,
- STATE_CLASS_TOTAL_INCREASING,
+ unit_of_measurement=UNIT_CUBIC_METER,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_GAS,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("gas_delivered_be"): sensor.sensor_schema(
- UNIT_CUBIC_METER,
- ICON_EMPTY,
- 3,
- DEVICE_CLASS_GAS,
- STATE_CLASS_TOTAL_INCREASING,
+ unit_of_measurement=UNIT_CUBIC_METER,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_GAS,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
+ ),
+ cv.Optional("water_delivered"): sensor.sensor_schema(
+ unit_of_measurement=UNIT_CUBIC_METER,
+ accuracy_decimals=3,
+ device_class=DEVICE_CLASS_WATER,
+ state_class=STATE_CLASS_TOTAL_INCREASING,
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -238,10 +254,10 @@ async def to_code(config):
for key, conf in config.items():
if not isinstance(conf, dict):
continue
- id = conf.get("id")
+ id = conf[CONF_ID]
if id and id.type == sensor.Sensor:
- s = await sensor.new_sensor(conf)
- cg.add(getattr(hub, f"set_{key}")(s))
+ sens = await sensor.new_sensor(conf)
+ cg.add(getattr(hub, f"set_{key}")(sens))
sensors.append(f"F({key})")
if sensors:
diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py
index 339eea711f..202cc07020 100644
--- a/esphome/components/dsmr/text_sensor.py
+++ b/esphome/components/dsmr/text_sensor.py
@@ -1,9 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
-from esphome.const import (
- CONF_ID,
-)
+
from . import Dsmr, CONF_DSMR_ID
AUTO_LOAD = ["dsmr"]
@@ -11,71 +9,19 @@ AUTO_LOAD = ["dsmr"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr),
- cv.Optional("identification"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("p1_version"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("p1_version_be"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("timestamp"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("electricity_tariff"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("electricity_failure_log"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("message_short"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("message_long"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("gas_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("thermal_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("water_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("sub_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
- cv.Optional("gas_delivered_text"): text_sensor.TEXT_SENSOR_SCHEMA.extend(
- {
- cv.GenerateID(): cv.declare_id(text_sensor.TextSensor),
- }
- ),
+ cv.Optional("identification"): text_sensor.text_sensor_schema(),
+ cv.Optional("p1_version"): text_sensor.text_sensor_schema(),
+ cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(),
+ cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
+ cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(),
+ cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(),
+ cv.Optional("message_short"): text_sensor.text_sensor_schema(),
+ cv.Optional("message_long"): text_sensor.text_sensor_schema(),
+ cv.Optional("gas_equipment_id"): text_sensor.text_sensor_schema(),
+ cv.Optional("thermal_equipment_id"): text_sensor.text_sensor_schema(),
+ cv.Optional("water_equipment_id"): text_sensor.text_sensor_schema(),
+ cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(),
+ cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -89,8 +35,7 @@ async def to_code(config):
continue
id = conf.get("id")
if id and id.type == text_sensor.TextSensor:
- var = cg.new_Pvariable(conf[CONF_ID])
- await text_sensor.register_text_sensor(var, conf)
+ var = await text_sensor.new_text_sensor(conf)
cg.add(getattr(hub, f"set_{key}")(var))
text_sensors.append(f"F({key})")
diff --git a/esphome/components/duty_cycle/sensor.py b/esphome/components/duty_cycle/sensor.py
index 6a367328e6..3dcdf7a818 100644
--- a/esphome/components/duty_cycle/sensor.py
+++ b/esphome/components/duty_cycle/sensor.py
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
from esphome import pins
from esphome.components import sensor
from esphome.const import (
- CONF_ID,
CONF_PIN,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
@@ -17,25 +16,20 @@ DutyCycleSensor = duty_cycle_ns.class_(
CONFIG_SCHEMA = (
sensor.sensor_schema(
+ DutyCycleSensor,
unit_of_measurement=UNIT_PERCENT,
icon=ICON_PERCENT,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
)
- .extend(
- {
- cv.GenerateID(): cv.declare_id(DutyCycleSensor),
- cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema),
- }
- )
+ .extend({cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema)})
.extend(cv.polling_component_schema("60s"))
)
async def to_code(config):
- var = cg.new_Pvariable(config[CONF_ID])
+ var = await sensor.new_sensor(config)
await cg.register_component(var, config)
- await sensor.register_sensor(var, config)
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py
index bb662e0989..cec0bdf4fa 100644
--- a/esphome/components/e131/__init__.py
+++ b/esphome/components/e131/__init__.py
@@ -4,6 +4,7 @@ from esphome.components.light.types import AddressableLightEffect
from esphome.components.light.effects import register_addressable_effect
from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS
+AUTO_LOAD = ["socket"]
DEPENDENCIES = ["network"]
e131_ns = cg.esphome_ns.namespace("e131")
@@ -23,16 +24,11 @@ CHANNELS = {
CONF_UNIVERSE = "universe"
CONF_E131_ID = "e131_id"
-CONFIG_SCHEMA = cv.All(
- cv.Schema(
- {
- cv.GenerateID(): cv.declare_id(E131Component),
- cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(
- *METHODS, upper=True
- ),
- }
- ),
- cv.only_with_arduino,
+CONFIG_SCHEMA = cv.Schema(
+ {
+ cv.GenerateID(): cv.declare_id(E131Component),
+ cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True),
+ }
)
diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp
index 6d584687ce..818006ced7 100644
--- a/esphome/components/e131/e131.cpp
+++ b/esphome/components/e131/e131.cpp
@@ -1,18 +1,7 @@
-#ifdef USE_ARDUINO
-
#include "e131.h"
#include "e131_addressable_light_effect.h"
#include "esphome/core/log.h"
-#ifdef USE_ESP32
-#include
-#endif
-
-#ifdef USE_ESP8266
-#include
-#include
-#endif
-
namespace esphome {
namespace e131 {
@@ -22,17 +11,41 @@ static const int PORT = 5568;
E131Component::E131Component() {}
E131Component::~E131Component() {
- if (udp_) {
- udp_->stop();
+ if (this->socket_) {
+ this->socket_->close();
}
}
void E131Component::setup() {
- udp_ = make_unique();
+ this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
- if (!udp_->begin(PORT)) {
- ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT);
- mark_failed();
+ int enable = 1;
+ int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
+ if (err != 0) {
+ ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
+ // we can still continue
+ }
+ err = this->socket_->setblocking(false);
+ if (err != 0) {
+ ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
+ this->mark_failed();
+ return;
+ }
+
+ struct sockaddr_storage server;
+
+ socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), PORT);
+ if (sl == 0) {
+ ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno);
+ this->mark_failed();
+ return;
+ }
+ server.ss_family = AF_INET;
+
+ err = this->socket_->bind((struct sockaddr *) &server, sizeof(server));
+ if (err != 0) {
+ ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
+ this->mark_failed();
return;
}
@@ -43,22 +56,22 @@ void E131Component::loop() {
std::vector payload;
E131Packet packet;
int universe = 0;
+ uint8_t buf[1460];
- while (uint16_t packet_size = udp_->parsePacket()) {
- payload.resize(packet_size);
+ ssize_t len = this->socket_->read(buf, sizeof(buf));
+ if (len == -1) {
+ return;
+ }
+ payload.resize(len);
+ memmove(&payload[0], buf, len);
- if (!udp_->read(&payload[0], payload.size())) {
- continue;
- }
+ if (!this->packet_(payload, universe, packet)) {
+ ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
+ return;
+ }
- if (!packet_(payload, universe, packet)) {
- ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
- continue;
- }
-
- if (!process_(universe, packet)) {
- ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
- }
+ if (!this->process_(universe, packet)) {
+ ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
}
}
@@ -106,5 +119,3 @@ bool E131Component::process_(int universe, const E131Packet &packet) {
} // namespace e131
} // namespace esphome
-
-#endif // USE_ARDUINO
diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h
index 648cfb4585..364a05af75 100644
--- a/esphome/components/e131/e131.h
+++ b/esphome/components/e131/e131.h
@@ -1,14 +1,12 @@
#pragma once
-#ifdef USE_ARDUINO
-
+#include "esphome/components/socket/socket.h"
#include "esphome/core/component.h"
+#include
#include
#include
-#include
-
-class UDP;
+#include
namespace esphome {
namespace e131 {
@@ -46,7 +44,7 @@ class E131Component : public esphome::Component {
void leave_(int universe);
E131ListenMethod listen_method_{E131_MULTICAST};
- std::unique_ptr udp_;
+ std::unique_ptr socket_;
std::set light_effects_;
std::map universe_consumers_;
std::map universe_packets_;
@@ -54,5 +52,3 @@ class E131Component : public esphome::Component {
} // namespace e131
} // namespace esphome
-
-#endif // USE_ARDUINO
diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp
index 7a3e71808e..42eb0fc56b 100644
--- a/esphome/components/e131/e131_addressable_light_effect.cpp
+++ b/esphome/components/e131/e131_addressable_light_effect.cpp
@@ -1,7 +1,5 @@
-#ifdef USE_ARDUINO
-
-#include "e131.h"
#include "e131_addressable_light_effect.h"
+#include "e131.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -92,5 +90,3 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
} // namespace e131
} // namespace esphome
-
-#endif // USE_ARDUINO
diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h
index b3e481e43b..56df9cd80f 100644
--- a/esphome/components/e131/e131_addressable_light_effect.h
+++ b/esphome/components/e131/e131_addressable_light_effect.h
@@ -1,7 +1,5 @@
#pragma once
-#ifdef USE_ARDUINO
-
#include "esphome/core/component.h"
#include "esphome/components/light/addressable_light_effect.h"
@@ -44,5 +42,3 @@ class E131AddressableLightEffect : public light::AddressableLightEffect {
} // namespace e131
} // namespace esphome
-
-#endif // USE_ARDUINO
diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp
index f199d3574b..ac8b72f6e7 100644
--- a/esphome/components/e131/e131_packet.cpp
+++ b/esphome/components/e131/e131_packet.cpp
@@ -1,15 +1,13 @@
-#ifdef USE_ARDUINO
-
+#include
#include "e131.h"
+#include "esphome/components/network/ip_address.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
-#include "esphome/components/network/ip_address.h"
-#include
-#include
-#include
-#include
#include
+#include
+#include
+#include
namespace esphome {
namespace e131 {
@@ -62,7 +60,7 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *)
bool E131Component::join_igmp_groups_() {
if (listen_method_ != E131_MULTICAST)
return false;
- if (!udp_)
+ if (this->socket_ == nullptr)
return false;
for (auto universe : universe_consumers_) {
@@ -140,5 +138,3 @@ bool E131Component::packet_(const std::vector &data, int &universe, E13
} // namespace e131
} // namespace esphome
-
-#endif // USE_ARDUINO
diff --git a/esphome/components/ee895/__init__.py b/esphome/components/ee895/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp
new file mode 100644
index 0000000000..a7186ffbbc
--- /dev/null
+++ b/esphome/components/ee895/ee895.cpp
@@ -0,0 +1,115 @@
+#include "ee895.h"
+#include "esphome/core/log.h"
+#include "esphome/core/helpers.h"
+
+namespace esphome {
+namespace ee895 {
+
+static const char *const TAG = "ee895";
+
+static const uint16_t CRC16_ONEWIRE_START = 0xFFFF;
+static const uint8_t FUNCTION_CODE_READ = 0x03;
+static const uint16_t SERIAL_NUMBER = 0x0000;
+static const uint16_t TEMPERATURE_ADDRESS = 0x03EA;
+static const uint16_t CO2_ADDRESS = 0x0424;
+static const uint16_t PRESSURE_ADDRESS = 0x04B0;
+
+void EE895Component::setup() {
+ uint16_t crc16_check = 0;
+ ESP_LOGCONFIG(TAG, "Setting up EE895...");
+ write_command_(SERIAL_NUMBER, 8);
+ uint8_t serial_number[20];
+ this->read(serial_number, 20);
+
+ crc16_check = (serial_number[19] << 8) + serial_number[18];
+ if (crc16_check != calc_crc16_(serial_number, 19)) {
+ this->error_code_ = CRC_CHECK_FAILED;
+ this->mark_failed();
+ return;
+ }
+ ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(serial_number + 2, 16).c_str());
+}
+
+void EE895Component::dump_config() {
+ ESP_LOGCONFIG(TAG, "EE895:");
+ LOG_I2C_DEVICE(this);
+ switch (this->error_code_) {
+ case COMMUNICATION_FAILED:
+ ESP_LOGE(TAG, "Communication with EE895 failed!");
+ break;
+ case CRC_CHECK_FAILED:
+ ESP_LOGE(TAG, "The crc check failed");
+ break;
+ case NONE:
+ default:
+ break;
+ }
+ LOG_UPDATE_INTERVAL(this);
+ LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
+ LOG_SENSOR(" ", "CO2", this->co2_sensor_);
+ LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
+}
+
+float EE895Component::get_setup_priority() const { return setup_priority::DATA; }
+
+void EE895Component::update() {
+ write_command_(TEMPERATURE_ADDRESS, 2);
+ this->set_timeout(50, [this]() {
+ float temperature = read_float_();
+
+ write_command_(CO2_ADDRESS, 2);
+ float co2 = read_float_();
+
+ write_command_(PRESSURE_ADDRESS, 2);
+ float pressure = read_float_();
+ ESP_LOGD(TAG, "Got temperature=%.1f°C co2=%.0fppm pressure=%.1f%mbar", temperature, co2, pressure);
+ if (this->temperature_sensor_ != nullptr)
+ this->temperature_sensor_->publish_state(temperature);
+ if (this->co2_sensor_ != nullptr)
+ this->co2_sensor_->publish_state(co2);
+ if (this->pressure_sensor_ != nullptr)
+ this->pressure_sensor_->publish_state(pressure);
+ this->status_clear_warning();
+ });
+}
+
+void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) {
+ uint8_t address[7];
+ uint16_t crc16 = 0;
+ address[0] = FUNCTION_CODE_READ;
+ address[1] = (addr >> 8) & 0xFF;
+ address[2] = addr & 0xFF;
+ address[3] = (reg_cnt >> 8) & 0xFF;
+ address[4] = reg_cnt & 0xFF;
+ crc16 = calc_crc16_(address, 6);
+ address[5] = crc16 & 0xFF;
+ address[6] = (crc16 >> 8) & 0xFF;
+ this->write(address, 7, true);
+}
+
+float EE895Component::read_float_() {
+ uint16_t crc16_check = 0;
+ uint8_t i2c_response[8];
+ this->read(i2c_response, 8);
+ crc16_check = (i2c_response[7] << 8) + i2c_response[6];
+ if (crc16_check != calc_crc16_(i2c_response, 7)) {
+ this->error_code_ = CRC_CHECK_FAILED;
+ this->status_set_warning();
+ return 0;
+ }
+ uint32_t x = encode_uint32(i2c_response[4], i2c_response[5], i2c_response[2], i2c_response[3]);
+ float value;
+ memcpy(&value, &x, sizeof(value)); // convert uin32_t IEEE-754 format to float
+ return value;
+}
+
+uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) {
+ uint8_t crc_check_buf[22];
+ for (int i = 0; i < len; i++) {
+ crc_check_buf[i + 1] = buf[i];
+ }
+ crc_check_buf[0] = this->address_;
+ return crc16(crc_check_buf, len);
+}
+} // namespace ee895
+} // namespace esphome
diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h
new file mode 100644
index 0000000000..83bd7c6e82
--- /dev/null
+++ b/esphome/components/ee895/ee895.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace ee895 {
+
+/// This class implements support for the ee895 of temperature i2c sensors.
+class EE895Component : public PollingComponent, public i2c::I2CDevice {
+ public:
+ void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
+ void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
+ void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
+
+ float get_setup_priority() const override;
+ void setup() override;
+ void dump_config() override;
+ void update() override;
+
+ protected:
+ void write_command_(uint16_t addr, uint16_t reg_cnt);
+ float read_float_();
+ uint16_t calc_crc16_(const uint8_t buf[], uint8_t len);
+ sensor::Sensor *co2_sensor_;
+ sensor::Sensor *temperature_sensor_;
+ sensor::Sensor *pressure_sensor_;
+
+ enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
+};
+
+} // namespace ee895
+} // namespace esphome
diff --git a/esphome/components/ee895/sensor.py b/esphome/components/ee895/sensor.py
new file mode 100644
index 0000000000..d06f9ca02f
--- /dev/null
+++ b/esphome/components/ee895/sensor.py
@@ -0,0 +1,69 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import (
+ CONF_ID,
+ CONF_PRESSURE,
+ CONF_TEMPERATURE,
+ CONF_CO2,
+ DEVICE_CLASS_TEMPERATURE,
+ DEVICE_CLASS_PRESSURE,
+ STATE_CLASS_MEASUREMENT,
+ UNIT_HECTOPASCAL,
+ UNIT_CELSIUS,
+ ICON_MOLECULE_CO2,
+ UNIT_PARTS_PER_MILLION,
+)
+
+CODEOWNERS = ["@Stock-M"]
+
+DEPENDENCIES = ["i2c"]
+
+ee895_ns = cg.esphome_ns.namespace("ee895")
+EE895Component = ee895_ns.class_("EE895Component", cg.PollingComponent, i2c.I2CDevice)
+
+CONFIG_SCHEMA = (
+ cv.Schema(
+ {
+ cv.GenerateID(): cv.declare_id(EE895Component),
+ cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
+ unit_of_measurement=UNIT_CELSIUS,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_TEMPERATURE,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ cv.Required(CONF_CO2): sensor.sensor_schema(
+ unit_of_measurement=UNIT_PARTS_PER_MILLION,
+ icon=ICON_MOLECULE_CO2,
+ accuracy_decimals=0,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ cv.Required(CONF_PRESSURE): sensor.sensor_schema(
+ unit_of_measurement=UNIT_HECTOPASCAL,
+ accuracy_decimals=1,
+ device_class=DEVICE_CLASS_PRESSURE,
+ state_class=STATE_CLASS_MEASUREMENT,
+ ),
+ }
+ )
+ .extend(cv.polling_component_schema("60s"))
+ .extend(i2c.i2c_device_schema(0x5F))
+)
+
+
+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_CO2 in config:
+ sens = await sensor.new_sensor(config[CONF_CO2])
+ cg.add(var.set_co2_sensor(sens))
+
+ if CONF_PRESSURE in config:
+ sens = await sensor.new_sensor(config[CONF_PRESSURE])
+ cg.add(var.set_pressure_sensor(sens))
diff --git a/esphome/components/ektf2232/ektf2232.cpp b/esphome/components/ektf2232/ektf2232.cpp
index 8df25fce24..80f5f8a8e2 100644
--- a/esphome/components/ektf2232/ektf2232.cpp
+++ b/esphome/components/ektf2232/ektf2232.cpp
@@ -2,6 +2,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
+#include
+
namespace esphome {
namespace ektf2232 {
diff --git a/esphome/components/ektf2232/touchscreen.py b/esphome/components/ektf2232/touchscreen.py
index b3513b2670..d937265e7a 100644
--- a/esphome/components/ektf2232/touchscreen.py
+++ b/esphome/components/ektf2232/touchscreen.py
@@ -3,7 +3,7 @@ import esphome.config_validation as cv
from esphome import pins
from esphome.components import i2c, touchscreen
-from esphome.const import CONF_ID
+from esphome.const import CONF_ID, CONF_INTERRUPT_PIN
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["i2c"]
@@ -17,10 +17,8 @@ EKTF2232Touchscreen = ektf2232_ns.class_(
)
CONF_EKTF2232_ID = "ektf2232_id"
-CONF_INTERRUPT_PIN = "interrupt_pin"
CONF_RTS_PIN = "rts_pin"
-
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
cv.Schema(
{
diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp
index 67c6a4ebd3..1190acc46b 100644
--- a/esphome/components/endstop/endstop_cover.cpp
+++ b/esphome/components/endstop/endstop_cover.cpp
@@ -11,7 +11,9 @@ using namespace esphome::cover;
CoverTraits EndstopCover::get_traits() {
auto traits = CoverTraits();
+ traits.set_supports_stop(true);
traits.set_supports_position(true);
+ traits.set_supports_toggle(true);
traits.set_is_assumed_state(false);
return traits;
}
@@ -20,6 +22,20 @@ void EndstopCover::control(const CoverCall &call) {
this->start_direction_(COVER_OPERATION_IDLE);
this->publish_state();
}
+ if (call.get_toggle().has_value()) {
+ if (this->current_operation != COVER_OPERATION_IDLE) {
+ this->start_direction_(COVER_OPERATION_IDLE);
+ this->publish_state();
+ } else {
+ if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) {
+ this->target_position_ = COVER_OPEN;
+ this->start_direction_(COVER_OPERATION_OPENING);
+ } else {
+ this->target_position_ = COVER_CLOSED;
+ this->start_direction_(COVER_OPERATION_CLOSING);
+ }
+ }
+ }
if (call.get_position().has_value()) {
auto pos = *call.get_position();
if (pos == this->position) {
@@ -125,9 +141,11 @@ void EndstopCover::start_direction_(CoverOperation dir) {
trig = this->stop_trigger_;
break;
case COVER_OPERATION_OPENING:
+ this->last_operation_ = dir;
trig = this->open_trigger_;
break;
case COVER_OPERATION_CLOSING:
+ this->last_operation_ = dir;
trig = this->close_trigger_;
break;
default:
diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h
index f8d2746234..6ae15de8c1 100644
--- a/esphome/components/endstop/endstop_cover.h
+++ b/esphome/components/endstop/endstop_cover.h
@@ -51,6 +51,7 @@ class EndstopCover : public cover::Cover, public Component {
uint32_t start_dir_time_{0};
uint32_t last_publish_time_{0};
float target_position_{0};
+ cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
};
} // namespace endstop
diff --git a/esphome/components/ens210/__init__.py b/esphome/components/ens210/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp
new file mode 100644
index 0000000000..86890c05e8
--- /dev/null
+++ b/esphome/components/ens210/ens210.cpp
@@ -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 measurement.
+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
diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h
new file mode 100644
index 0000000000..0fb6ff634d
--- /dev/null
+++ b/esphome/components/ens210/ens210.h
@@ -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_{nullptr};
+ sensor::Sensor *humidity_sensor_{nullptr};
+};
+
+} // namespace ens210
+} // namespace esphome
diff --git a/esphome/components/ens210/sensor.py b/esphome/components/ens210/sensor.py
new file mode 100644
index 0000000000..3037156e01
--- /dev/null
+++ b/esphome/components/ens210/sensor.py
@@ -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))
diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py
index 1229675ad8..3ca140f0d4 100644
--- a/esphome/components/esp32/__init__.py
+++ b/esphome/components/esp32/__init__.py
@@ -4,36 +4,50 @@ from pathlib import Path
import logging
import os
-from esphome.helpers import copy_file_if_changed, write_file_if_changed
+from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p
from esphome.const import (
CONF_BOARD,
+ CONF_COMPONENTS,
CONF_FRAMEWORK,
+ CONF_NAME,
CONF_SOURCE,
CONF_TYPE,
CONF_VARIANT,
CONF_VERSION,
CONF_ADVANCED,
+ CONF_REFRESH,
+ CONF_PATH,
+ CONF_URL,
+ CONF_REF,
CONF_IGNORE_EFUSE_MAC_CRC,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
+ TYPE_GIT,
+ TYPE_LOCAL,
__version__,
)
-from esphome.core import CORE, HexInt
+from esphome.core import CORE, HexInt, TimePeriod
import esphome.config_validation as cv
import esphome.codegen as cg
+from esphome import git
from .const import ( # noqa
KEY_BOARD,
+ KEY_COMPONENTS,
KEY_ESP32,
+ KEY_PATH,
+ KEY_REF,
+ KEY_REFRESH,
+ KEY_REPO,
KEY_SDKCONFIG_OPTIONS,
KEY_VARIANT,
VARIANT_ESP32C3,
VARIANT_FRIENDLY,
VARIANTS,
)
-from .boards import BOARD_TO_VARIANT
+from .boards import BOARDS
# force import gpio to register pin schema
from .gpio import esp32_pin_to_code # noqa
@@ -51,6 +65,7 @@ def set_core_data(config):
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "esp-idf"
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
+ CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
@@ -61,12 +76,30 @@ def set_core_data(config):
return config
-def get_esp32_variant():
- return CORE.data[KEY_ESP32][KEY_VARIANT]
+def get_esp32_variant(core_obj=None):
+ return (core_obj or CORE).data[KEY_ESP32][KEY_VARIANT]
-def is_esp32c3():
- return get_esp32_variant() == VARIANT_ESP32C3
+def only_on_variant(*, supported=None, unsupported=None):
+ """Config validator for features only available on some ESP32 variants."""
+ if supported is not None and not isinstance(supported, list):
+ supported = [supported]
+ if unsupported is not None and not isinstance(unsupported, list):
+ unsupported = [unsupported]
+
+ def validator_(obj):
+ variant = get_esp32_variant()
+ if supported is not None and variant not in supported:
+ raise cv.Invalid(
+ f"This feature is only available on {', '.join(supported)}"
+ )
+ if unsupported is not None and variant in unsupported:
+ raise cv.Invalid(
+ f"This feature is not available on {', '.join(unsupported)}"
+ )
+ return obj
+
+ return validator_
@dataclass
@@ -86,6 +119,21 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS][name] = value
+def add_idf_component(
+ name: str, repo: str, ref: str = None, path: str = None, refresh: TimePeriod = None
+):
+ """Add an esp-idf component to the project."""
+ if not CORE.using_esp_idf:
+ raise ValueError("Not an esp-idf project")
+ if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]:
+ CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = {
+ KEY_REPO: repo,
+ KEY_REF: ref,
+ KEY_PATH: path,
+ KEY_REFRESH: refresh,
+ }
+
+
def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
# a PIO platformio/framework-arduinoespressif32 value
@@ -111,27 +159,27 @@ def _format_framework_espidf_version(ver: cv.Version) -> str:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32
-RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(1, 0, 6)
+RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5)
# The platformio/espressif32 version to use for arduino frameworks
# - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
-ARDUINO_PLATFORM_VERSION = cv.Version(3, 5, 0)
+ARDUINO_PLATFORM_VERSION = cv.Version(5, 3, 0)
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
-RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 3, 2)
+RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 4)
# The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
-ESP_IDF_PLATFORM_VERSION = cv.Version(3, 5, 0)
+ESP_IDF_PLATFORM_VERSION = cv.Version(5, 3, 0)
def _arduino_check_versions(value):
value = value.copy()
lookups = {
- "dev": (cv.Version(2, 0, 0), "https://github.com/espressif/arduino-esp32.git"),
- "latest": (cv.Version(1, 0, 6), None),
+ "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"),
+ "latest": (cv.Version(2, 0, 7), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
}
@@ -165,8 +213,8 @@ def _arduino_check_versions(value):
def _esp_idf_check_versions(value):
value = value.copy()
lookups = {
- "dev": (cv.Version(5, 0, 0), "https://github.com/espressif/esp-idf.git"),
- "latest": (cv.Version(4, 3, 2), None),
+ "dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"),
+ "latest": (cv.Version(5, 0, 1), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
}
@@ -204,7 +252,7 @@ def _parse_platform_version(value):
try:
# if platform version is a valid version constraint, prefix the default package
cv.platformio_version_constraint(value)
- return f"platformio/espressif32 @ {value}"
+ return f"platformio/espressif32@{value}"
except cv.Invalid:
return value
@@ -212,14 +260,14 @@ def _parse_platform_version(value):
def _detect_variant(value):
if CONF_VARIANT not in value:
board = value[CONF_BOARD]
- if board not in BOARD_TO_VARIANT:
+ if board not in BOARDS:
raise cv.Invalid(
"This board is unknown, please set the variant manually",
path=[CONF_BOARD],
)
value = value.copy()
- value[CONF_VARIANT] = BOARD_TO_VARIANT[board]
+ value[CONF_VARIANT] = BOARDS[board][KEY_VARIANT]
return value
@@ -252,6 +300,18 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
}
),
+ cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
+ cv.Schema(
+ {
+ cv.Required(CONF_NAME): cv.string_strict,
+ cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA,
+ cv.Optional(CONF_PATH): cv.string,
+ cv.Optional(CONF_REFRESH, default="1d"): cv.All(
+ cv.string, cv.source_refresh
+ ),
+ }
+ )
+ ),
}
),
_esp_idf_check_versions,
@@ -307,7 +367,12 @@ async def to_code(config):
cg.add_build_flag("-Wno-nonnull-compare")
cg.add_platformio_option(
"platform_packages",
- [f"platformio/framework-espidf @ {conf[CONF_SOURCE]}"],
+ [f"platformio/framework-espidf@{conf[CONF_SOURCE]}"],
+ )
+ # platformio/toolchain-esp32ulp does not support linux_aarch64 yet and has not been updated for over 2 years
+ # This is espressif's own published version which is more up to date.
+ cg.add_platformio_option(
+ "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"]
)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False)
add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True)
@@ -333,9 +398,14 @@ async def to_code(config):
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_MAC_CRC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_MAC_CRC")
- add_idf_sdkconfig_option(
- "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False
- )
+ if (framework_ver.major, framework_ver.minor) >= (4, 4):
+ add_idf_sdkconfig_option(
+ "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False
+ )
+ else:
+ add_idf_sdkconfig_option(
+ "CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE", False
+ )
cg.add_define(
"USE_ESP_IDF_VERSION_CODE",
@@ -344,13 +414,26 @@ async def to_code(config):
),
)
+ for component in conf[CONF_COMPONENTS]:
+ source = component[CONF_SOURCE]
+ if source[CONF_TYPE] == TYPE_GIT:
+ add_idf_component(
+ name=component[CONF_NAME],
+ repo=source[CONF_URL],
+ ref=source.get(CONF_REF),
+ path=component.get(CONF_PATH),
+ refresh=component[CONF_REFRESH],
+ )
+ elif source[CONF_TYPE] == TYPE_LOCAL:
+ _LOGGER.warning("Local components are not implemented yet.")
+
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
cg.add_platformio_option(
"platform_packages",
- [f"platformio/framework-arduinoespressif32 @ {conf[CONF_SOURCE]}"],
+ [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"],
)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
@@ -375,11 +458,11 @@ spiffs, data, spiffs, 0x391000, 0x00F000
IDF_PARTITIONS_CSV = """\
# Name, Type, SubType, Offset, Size, Flags
-nvs, data, nvs, , 0x4000,
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
app0, app, ota_0, , 0x1C0000,
app1, app, ota_1, , 0x1C0000,
+nvs, data, nvs, , 0x6d000,
"""
@@ -440,6 +523,32 @@ def copy_files():
__version__,
)
+ import shutil
+
+ shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True)
+
+ if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
+ components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
+
+ for name, component in components.items():
+ repo_dir, _ = git.clone_or_update(
+ url=component[KEY_REPO],
+ ref=component[KEY_REF],
+ refresh=component[KEY_REFRESH],
+ domain="idf_components",
+ )
+ mkdir_p(CORE.relative_build_path("components"))
+ component_dir = repo_dir
+ if component[KEY_PATH] is not None:
+ component_dir = component_dir / component[KEY_PATH]
+
+ shutil.copytree(
+ component_dir,
+ CORE.relative_build_path(f"components/{name}"),
+ dirs_exist_ok=True,
+ ignore=shutil.ignore_patterns(".git", ".github"),
+ )
+
dir = os.path.dirname(__file__)
post_build_file = os.path.join(dir, "post_build.py.script")
copy_file_if_changed(
diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py
index 56fd4932b4..30297654bc 100644
--- a/esphome/components/esp32/boards.py
+++ b/esphome/components/esp32/boards.py
@@ -1,4 +1,4 @@
-from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3
+from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3, VARIANT_ESP32S3
ESP32_BASE_PINS = {
"TX": 1,
@@ -42,6 +42,97 @@ ESP32_BASE_PINS = {
}
ESP32_BOARD_PINS = {
+ "adafruit_feather_esp32s2_tft": {
+ "BUTTON": 0,
+ "A0": 18,
+ "A1": 17,
+ "A2": 16,
+ "A3": 15,
+ "A4": 14,
+ "A5": 8,
+ "SCK": 36,
+ "MOSI": 35,
+ "MISO": 37,
+ "RX": 2,
+ "TX": 1,
+ "D13": 13,
+ "D12": 12,
+ "D11": 11,
+ "D10": 10,
+ "D9": 9,
+ "D6": 6,
+ "D5": 5,
+ "NEOPIXEL": 33,
+ "PIN_NEOPIXEL": 33,
+ "NEOPIXEL_POWER": 34,
+ "SCL": 41,
+ "SDA": 42,
+ "TFT_I2C_POWER": 21,
+ "TFT_CS": 7,
+ "TFT_DC": 39,
+ "TFT_RESET": 40,
+ "TFT_BACKLIGHT": 45,
+ "LED": 13,
+ "LED_BUILTIN": 13,
+ },
+ "adafruit_qtpy_esp32c3": {
+ "A0": 4,
+ "A1": 3,
+ "A2": 1,
+ "A3": 0,
+ "SDA": 5,
+ "SCL": 6,
+ "MOSI": 7,
+ "MISO": 8,
+ "SCK": 10,
+ "RX": 20,
+ "TX": 21,
+ "NEOPIXEL": 2,
+ "PIN_NEOPIXEL": 2,
+ "BUTTON": 9,
+ "SWITCH": 9,
+ },
+ "adafruit_qtpy_esp32s2": {
+ "A0": 18,
+ "A1": 17,
+ "A2": 9,
+ "A3": 8,
+ "SDA": 7,
+ "SCL": 6,
+ "MOSI": 35,
+ "MISO": 37,
+ "SCK": 36,
+ "RX": 16,
+ "TX": 5,
+ "SDA1": 41,
+ "SCL1": 40,
+ "NEOPIXEL": 39,
+ "PIN_NEOPIXEL": 39,
+ "NEOPIXEL_POWER": 38,
+ "D0": 0,
+ "BUTTON": 0,
+ "SWITCH": 0,
+ },
+ "adafruit_qtpy_esp32": {
+ "A0": 26,
+ "A1": 25,
+ "A2": 27,
+ "A3": 15,
+ "SDA": 4,
+ "SCL": 33,
+ "MOSI": 13,
+ "MISO": 12,
+ "SCK": 14,
+ "RX": 7,
+ "TX": 32,
+ "SDA1": 22,
+ "SCL1": 19,
+ "NEOPIXEL": 5,
+ "PIN_NEOPIXEL": 5,
+ "NEOPIXEL_POWER": 8,
+ "BUTTON": 0,
+ "SWITCH": 0,
+ },
"alksesp32": {
"A0": 32,
"A1": 33,
@@ -521,8 +612,54 @@ ESP32_BOARD_PINS = {
},
"lolin32": {"LED": 5},
"lolin32_lite": {"LED": 22},
+ "lolin_c3_mini": {
+ "TX": 21,
+ "RX": 20,
+ "SDA": 8,
+ "SCL": 10,
+ "SS": 5,
+ "MOSI": 4,
+ "MISO": 3,
+ "SCK": 2,
+ "A0": 0,
+ "A1": 1,
+ "A2": 2,
+ "A3": 3,
+ "A4": 4,
+ "A5": 5,
+ "D0": 1,
+ "D1": 10,
+ "D2": 8,
+ "D3": 7,
+ "D4": 6,
+ "D5": 2,
+ "D6": 3,
+ "D7": 4,
+ "D8": 5,
+ "LED": 7,
+ "BUTTON": 9,
+ },
"lolin_d32": {"LED": 5, "_VBAT": 35},
"lolin_d32_pro": {"LED": 5, "_VBAT": 35},
+ "lolin_s2_mini": {
+ "TX": 43,
+ "RX": 44,
+ "SPICS1": 29,
+ "SPIHD": 31,
+ "SPIWP": 32,
+ "SPICS0": 33,
+ "SPICLK": 34,
+ "SPIQ": 35,
+ "SPID": 36,
+ "MISO": 9,
+ "MOSI": 11,
+ "SCK": 7,
+ "SCL": 35,
+ "SDA": 33,
+ "DAC1": 17,
+ "DAC2": 18,
+ "LED": 15,
+ },
"lopy": {
"A1": 37,
"A2": 38,
@@ -957,126 +1094,743 @@ ESP32_BOARD_PINS = {
"D13": 2,
},
"xinabox_cw02": {"LED": 27},
+ "upesy_wroom": {"LED": 2},
+ "upesy_wrover": {"LED": 2},
}
"""
-BOARD_TO_VARIANT generated with:
+BOARDS generated with:
git clone https://github.com/platformio/platform-espressif32
for x in platform-espressif32/boards/*.json; do
mcu=$(jq -r .build.mcu <"$x");
+ name=$(jq -r .name <"$x");
fname=$(basename "$x")
board="${fname%.*}"
variant=$(echo "$mcu" | tr '[:lower:]' '[:upper:]')
- echo " \"$board\": VARIANT_${variant},"
+ echo " \"$board\": {\"name\": \"$name\", \"variant\": VARIANT_${variant},},"
done | sort
"""
-BOARD_TO_VARIANT = {
- "alksesp32": VARIANT_ESP32,
- "az-delivery-devkit-v4": VARIANT_ESP32,
- "bpi-bit": VARIANT_ESP32,
- "briki_abc_esp32": VARIANT_ESP32,
- "briki_mbc-wb_esp32": VARIANT_ESP32,
- "d-duino-32": VARIANT_ESP32,
- "esp320": VARIANT_ESP32,
- "esp32-c3-devkitm-1": VARIANT_ESP32C3,
- "esp32cam": VARIANT_ESP32,
- "esp32-devkitlipo": VARIANT_ESP32,
- "esp32dev": VARIANT_ESP32,
- "esp32doit-devkit-v1": VARIANT_ESP32,
- "esp32doit-espduino": VARIANT_ESP32,
- "esp32-evb": VARIANT_ESP32,
- "esp32-gateway": VARIANT_ESP32,
- "esp32-poe-iso": VARIANT_ESP32,
- "esp32-poe": VARIANT_ESP32,
- "esp32-pro": VARIANT_ESP32,
- "esp32-s2-kaluga-1": VARIANT_ESP32S2,
- "esp32-s2-saola-1": VARIANT_ESP32S2,
- "esp32thing_plus": VARIANT_ESP32,
- "esp32thing": VARIANT_ESP32,
- "esp32vn-iot-uno": VARIANT_ESP32,
- "espea32": VARIANT_ESP32,
- "espectro32": VARIANT_ESP32,
- "espino32": VARIANT_ESP32,
- "esp-wrover-kit": VARIANT_ESP32,
- "etboard": VARIANT_ESP32,
- "featheresp32-s2": VARIANT_ESP32S2,
- "featheresp32": VARIANT_ESP32,
- "firebeetle32": VARIANT_ESP32,
- "fm-devkit": VARIANT_ESP32,
- "frogboard": VARIANT_ESP32,
- "healthypi4": VARIANT_ESP32,
- "heltec_wifi_kit_32_v2": VARIANT_ESP32,
- "heltec_wifi_kit_32": VARIANT_ESP32,
- "heltec_wifi_lora_32_V2": VARIANT_ESP32,
- "heltec_wifi_lora_32": VARIANT_ESP32,
- "heltec_wireless_stick_lite": VARIANT_ESP32,
- "heltec_wireless_stick": VARIANT_ESP32,
- "honeylemon": VARIANT_ESP32,
- "hornbill32dev": VARIANT_ESP32,
- "hornbill32minima": VARIANT_ESP32,
- "imbrios-logsens-v1p1": VARIANT_ESP32,
- "inex_openkb": VARIANT_ESP32,
- "intorobot": VARIANT_ESP32,
- "iotaap_magnolia": VARIANT_ESP32,
- "iotbusio": VARIANT_ESP32,
- "iotbusproteus": VARIANT_ESP32,
- "kits-edu": VARIANT_ESP32,
- "labplus_mpython": VARIANT_ESP32,
- "lolin32_lite": VARIANT_ESP32,
- "lolin32": VARIANT_ESP32,
- "lolin_d32_pro": VARIANT_ESP32,
- "lolin_d32": VARIANT_ESP32,
- "lopy4": VARIANT_ESP32,
- "lopy": VARIANT_ESP32,
- "m5stack-atom": VARIANT_ESP32,
- "m5stack-core2": VARIANT_ESP32,
- "m5stack-core-esp32": VARIANT_ESP32,
- "m5stack-coreink": VARIANT_ESP32,
- "m5stack-fire": VARIANT_ESP32,
- "m5stack-grey": VARIANT_ESP32,
- "m5stack-timer-cam": VARIANT_ESP32,
- "m5stick-c": VARIANT_ESP32,
- "magicbit": VARIANT_ESP32,
- "mgbot-iotik32a": VARIANT_ESP32,
- "mgbot-iotik32b": VARIANT_ESP32,
- "mhetesp32devkit": VARIANT_ESP32,
- "mhetesp32minikit": VARIANT_ESP32,
- "microduino-core-esp32": VARIANT_ESP32,
- "nano32": VARIANT_ESP32,
- "nina_w10": VARIANT_ESP32,
- "node32s": VARIANT_ESP32,
- "nodemcu-32s": VARIANT_ESP32,
- "nscreen-32": VARIANT_ESP32,
- "odroid_esp32": VARIANT_ESP32,
- "onehorse32dev": VARIANT_ESP32,
- "oroca_edubot": VARIANT_ESP32,
- "pico32": VARIANT_ESP32,
- "piranha_esp32": VARIANT_ESP32,
- "pocket_32": VARIANT_ESP32,
- "pycom_gpy": VARIANT_ESP32,
- "qchip": VARIANT_ESP32,
- "quantum": VARIANT_ESP32,
- "sensesiot_weizen": VARIANT_ESP32,
- "sg-o_airMon": VARIANT_ESP32,
- "s_odi_ultra": VARIANT_ESP32,
- "sparkfun_lora_gateway_1-channel": VARIANT_ESP32,
- "tinypico": VARIANT_ESP32,
- "ttgo-lora32-v1": VARIANT_ESP32,
- "ttgo-lora32-v21": VARIANT_ESP32,
- "ttgo-lora32-v2": VARIANT_ESP32,
- "ttgo-t1": VARIANT_ESP32,
- "ttgo-t7-v13-mini32": VARIANT_ESP32,
- "ttgo-t7-v14-mini32": VARIANT_ESP32,
- "ttgo-t-beam": VARIANT_ESP32,
- "ttgo-t-watch": VARIANT_ESP32,
- "turta_iot_node": VARIANT_ESP32,
- "vintlabs-devkit-v1": VARIANT_ESP32,
- "wemosbat": VARIANT_ESP32,
- "wemos_d1_mini32": VARIANT_ESP32,
- "wesp32": VARIANT_ESP32,
- "widora-air": VARIANT_ESP32,
- "wifiduino32": VARIANT_ESP32,
- "xinabox_cw02": VARIANT_ESP32,
+BOARDS = {
+ "adafruit_feather_esp32s2_tft": {
+ "name": "Adafruit Feather ESP32-S2 TFT",
+ "variant": VARIANT_ESP32S2,
+ },
+ "adafruit_feather_esp32s3": {
+ "name": "Adafruit Feather ESP32-S3 2MB PSRAM",
+ "variant": VARIANT_ESP32S3,
+ },
+ "adafruit_feather_esp32s3_nopsram": {
+ "name": "Adafruit Feather ESP32-S3 No PSRAM",
+ "variant": VARIANT_ESP32S3,
+ },
+ "adafruit_feather_esp32s3_tft": {
+ "name": "Adafruit Feather ESP32-S3 TFT",
+ "variant": VARIANT_ESP32S3,
+ },
+ "adafruit_feather_esp32_v2": {
+ "name": "Adafruit Feather ESP32 V2",
+ "variant": VARIANT_ESP32,
+ },
+ "adafruit_funhouse_esp32s2": {
+ "name": "Adafruit FunHouse",
+ "variant": VARIANT_ESP32S2,
+ },
+ "adafruit_itsybitsy_esp32": {
+ "name": "Adafruit ItsyBitsy ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "adafruit_magtag29_esp32s2": {
+ "name": "Adafruit MagTag 2.9",
+ "variant": VARIANT_ESP32S2,
+ },
+ "adafruit_metro_esp32s2": {
+ "name": "Adafruit Metro ESP32-S2",
+ "variant": VARIANT_ESP32S2,
+ },
+ "adafruit_qtpy_esp32c3": {
+ "name": "Adafruit QT Py ESP32-C3",
+ "variant": VARIANT_ESP32C3,
+ },
+ "adafruit_qtpy_esp32": {
+ "name": "Adafruit QT Py ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "adafruit_qtpy_esp32s2": {
+ "name": "Adafruit QT Py ESP32-S2",
+ "variant": VARIANT_ESP32S2,
+ },
+ "adafruit_qtpy_esp32s3_nopsram": {
+ "name": "Adafruit QT Py ESP32-S3 No PSRAM",
+ "variant": VARIANT_ESP32S3,
+ },
+ "airm2m_core_esp32c3": {
+ "name": "AirM2M CORE ESP32C3",
+ "variant": VARIANT_ESP32C3,
+ },
+ "alksesp32": {
+ "name": "ALKS ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "atmegazero_esp32s2": {
+ "name": "EspinalLab ATMegaZero ESP32-S2",
+ "variant": VARIANT_ESP32S2,
+ },
+ "az-delivery-devkit-v4": {
+ "name": "AZ-Delivery ESP-32 Dev Kit C V4",
+ "variant": VARIANT_ESP32,
+ },
+ "bee_motion_mini": {
+ "name": "Smart Bee Motion Mini",
+ "variant": VARIANT_ESP32C3,
+ },
+ "bee_motion": {
+ "name": "Smart Bee Motion",
+ "variant": VARIANT_ESP32S2,
+ },
+ "bee_motion_s3": {
+ "name": "Smart Bee Motion S3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "bee_s3": {
+ "name": "Smart Bee S3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "bpi-bit": {
+ "name": "BPI-Bit",
+ "variant": VARIANT_ESP32,
+ },
+ "briki_abc_esp32": {
+ "name": "Briki ABC (MBC-WB) - ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "briki_mbc-wb_esp32": {
+ "name": "Briki MBC-WB - ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "cnrs_aw2eth": {
+ "name": "CNRS AW2ETH",
+ "variant": VARIANT_ESP32,
+ },
+ "connaxio_espoir": {
+ "name": "Connaxio's Espoir",
+ "variant": VARIANT_ESP32,
+ },
+ "d-duino-32": {
+ "name": "D-duino-32",
+ "variant": VARIANT_ESP32,
+ },
+ "deneyapkart1A": {
+ "name": "Deneyap Kart 1A",
+ "variant": VARIANT_ESP32,
+ },
+ "deneyapkartg": {
+ "name": "Deneyap Kart G",
+ "variant": VARIANT_ESP32C3,
+ },
+ "deneyapkart": {
+ "name": "Deneyap Kart",
+ "variant": VARIANT_ESP32,
+ },
+ "deneyapmini": {
+ "name": "Deneyap Mini",
+ "variant": VARIANT_ESP32S2,
+ },
+ "denky32": {
+ "name": "Denky32 (WROOM32)",
+ "variant": VARIANT_ESP32,
+ },
+ "denky_d4": {
+ "name": "Denky D4 (PICO-V3-02)",
+ "variant": VARIANT_ESP32,
+ },
+ "dfrobot_beetle_esp32c3": {
+ "name": "DFRobot Beetle ESP32-C3",
+ "variant": VARIANT_ESP32C3,
+ },
+ "dfrobot_firebeetle2_esp32s3": {
+ "name": "DFRobot Firebeetle 2 ESP32-S3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "dpu_esp32": {
+ "name": "TAMC DPU ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "esp320": {
+ "name": "Electronic SweetPeas ESP320",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32-c3-devkitm-1": {
+ "name": "Espressif ESP32-C3-DevKitM-1",
+ "variant": VARIANT_ESP32C3,
+ },
+ "esp32cam": {
+ "name": "AI Thinker ESP32-CAM",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32-devkitlipo": {
+ "name": "OLIMEX ESP32-DevKit-LiPo",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32dev": {
+ "name": "Espressif ESP32 Dev Module",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32doit-devkit-v1": {
+ "name": "DOIT ESP32 DEVKIT V1",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32doit-espduino": {
+ "name": "DOIT ESPduino32",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32-evb": {
+ "name": "OLIMEX ESP32-EVB",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32-gateway": {
+ "name": "OLIMEX ESP32-GATEWAY",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32-poe-iso": {
+ "name": "OLIMEX ESP32-PoE-ISO",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32-poe": {
+ "name": "OLIMEX ESP32-PoE",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32-pro": {
+ "name": "OLIMEX ESP32-PRO",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32-s2-franzininho": {
+ "name": "Franzininho WiFi Board",
+ "variant": VARIANT_ESP32S2,
+ },
+ "esp32-s2-kaluga-1": {
+ "name": "Espressif ESP32-S2-Kaluga-1 Kit",
+ "variant": VARIANT_ESP32S2,
+ },
+ "esp32-s2-saola-1": {
+ "name": "Espressif ESP32-S2-Saola-1",
+ "variant": VARIANT_ESP32S2,
+ },
+ "esp32s3box": {
+ "name": "Espressif ESP32-S3-Box",
+ "variant": VARIANT_ESP32S3,
+ },
+ "esp32s3camlcd": {
+ "name": "ESP32S3 CAM LCD",
+ "variant": VARIANT_ESP32S3,
+ },
+ "esp32-s3-devkitc-1": {
+ "name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)",
+ "variant": VARIANT_ESP32S3,
+ },
+ "esp32thing": {
+ "name": "SparkFun ESP32 Thing",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32thing_plus": {
+ "name": "SparkFun ESP32 Thing Plus",
+ "variant": VARIANT_ESP32,
+ },
+ "esp32vn-iot-uno": {
+ "name": "ESP32vn IoT Uno",
+ "variant": VARIANT_ESP32,
+ },
+ "espea32": {
+ "name": "April Brother ESPea32",
+ "variant": VARIANT_ESP32,
+ },
+ "espectro32": {
+ "name": "ESPectro32",
+ "variant": VARIANT_ESP32,
+ },
+ "espino32": {
+ "name": "ESPino32",
+ "variant": VARIANT_ESP32,
+ },
+ "esp-wrover-kit": {
+ "name": "Espressif ESP-WROVER-KIT",
+ "variant": VARIANT_ESP32,
+ },
+ "etboard": {
+ "name": "ETBoard",
+ "variant": VARIANT_ESP32,
+ },
+ "featheresp32": {
+ "name": "Adafruit ESP32 Feather",
+ "variant": VARIANT_ESP32,
+ },
+ "featheresp32-s2": {
+ "name": "Adafruit ESP32-S2 Feather Development Board",
+ "variant": VARIANT_ESP32S2,
+ },
+ "firebeetle32": {
+ "name": "FireBeetle-ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "fm-devkit": {
+ "name": "ESP32 FM DevKit",
+ "variant": VARIANT_ESP32,
+ },
+ "franzininho_wifi_esp32s2": {
+ "name": "Franzininho WiFi",
+ "variant": VARIANT_ESP32S2,
+ },
+ "franzininho_wifi_msc_esp32s2": {
+ "name": "Franzininho WiFi MSC",
+ "variant": VARIANT_ESP32S2,
+ },
+ "frogboard": {
+ "name": "Frog Board ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "healthypi4": {
+ "name": "ProtoCentral HealthyPi 4",
+ "variant": VARIANT_ESP32,
+ },
+ "heltec_wifi_kit_32": {
+ "name": "Heltec WiFi Kit 32",
+ "variant": VARIANT_ESP32,
+ },
+ "heltec_wifi_kit_32_v2": {
+ "name": "Heltec WiFi Kit 32 (V2)",
+ "variant": VARIANT_ESP32,
+ },
+ "heltec_wifi_lora_32": {
+ "name": "Heltec WiFi LoRa 32",
+ "variant": VARIANT_ESP32,
+ },
+ "heltec_wifi_lora_32_V2": {
+ "name": "Heltec WiFi LoRa 32 (V2)",
+ "variant": VARIANT_ESP32,
+ },
+ "heltec_wireless_stick_lite": {
+ "name": "Heltec Wireless Stick Lite",
+ "variant": VARIANT_ESP32,
+ },
+ "heltec_wireless_stick": {
+ "name": "Heltec Wireless Stick",
+ "variant": VARIANT_ESP32,
+ },
+ "honeylemon": {
+ "name": "HONEYLemon",
+ "variant": VARIANT_ESP32,
+ },
+ "hornbill32dev": {
+ "name": "Hornbill ESP32 Dev",
+ "variant": VARIANT_ESP32,
+ },
+ "hornbill32minima": {
+ "name": "Hornbill ESP32 Minima",
+ "variant": VARIANT_ESP32,
+ },
+ "imbrios-logsens-v1p1": {
+ "name": "Imbrios LogSens V1P1",
+ "variant": VARIANT_ESP32,
+ },
+ "inex_openkb": {
+ "name": "INEX OpenKB",
+ "variant": VARIANT_ESP32,
+ },
+ "intorobot": {
+ "name": "IntoRobot Fig",
+ "variant": VARIANT_ESP32,
+ },
+ "iotaap_magnolia": {
+ "name": "IoTaaP Magnolia",
+ "variant": VARIANT_ESP32,
+ },
+ "iotbusio": {
+ "name": "oddWires IoT-Bus Io",
+ "variant": VARIANT_ESP32,
+ },
+ "iotbusproteus": {
+ "name": "oddWires IoT-Bus Proteus",
+ "variant": VARIANT_ESP32,
+ },
+ "kb32-ft": {
+ "name": "MakerAsia KB32-FT",
+ "variant": VARIANT_ESP32,
+ },
+ "kits-edu": {
+ "name": "KITS ESP32 EDU",
+ "variant": VARIANT_ESP32,
+ },
+ "labplus_mpython": {
+ "name": "Labplus mPython",
+ "variant": VARIANT_ESP32,
+ },
+ "lionbit": {
+ "name": "Lion:Bit Dev Board",
+ "variant": VARIANT_ESP32,
+ },
+ "lolin32_lite": {
+ "name": "WEMOS LOLIN32 Lite",
+ "variant": VARIANT_ESP32,
+ },
+ "lolin32": {
+ "name": "WEMOS LOLIN32",
+ "variant": VARIANT_ESP32,
+ },
+ "lolin_c3_mini": {
+ "name": "WEMOS LOLIN C3 Mini",
+ "variant": VARIANT_ESP32C3,
+ },
+ "lolin_d32": {
+ "name": "WEMOS LOLIN D32",
+ "variant": VARIANT_ESP32,
+ },
+ "lolin_d32_pro": {
+ "name": "WEMOS LOLIN D32 PRO",
+ "variant": VARIANT_ESP32,
+ },
+ "lolin_s2_mini": {
+ "name": "WEMOS LOLIN S2 Mini",
+ "variant": VARIANT_ESP32S2,
+ },
+ "lolin_s2_pico": {
+ "name": "WEMOS LOLIN S2 PICO",
+ "variant": VARIANT_ESP32S2,
+ },
+ "lolin_s3": {
+ "name": "WEMOS LOLIN S3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "lopy4": {
+ "name": "Pycom LoPy4",
+ "variant": VARIANT_ESP32,
+ },
+ "lopy": {
+ "name": "Pycom LoPy",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stack-atom": {
+ "name": "M5Stack-ATOM",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stack-core2": {
+ "name": "M5Stack Core2",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stack-core-esp32": {
+ "name": "M5Stack Core ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stack-coreink": {
+ "name": "M5Stack-Core Ink",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stack-fire": {
+ "name": "M5Stack FIRE",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stack-grey": {
+ "name": "M5Stack GREY ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stack-station": {
+ "name": "M5Stack Station",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stack-timer-cam": {
+ "name": "M5Stack Timer CAM",
+ "variant": VARIANT_ESP32,
+ },
+ "m5stick-c": {
+ "name": "M5Stick-C",
+ "variant": VARIANT_ESP32,
+ },
+ "magicbit": {
+ "name": "MagicBit",
+ "variant": VARIANT_ESP32,
+ },
+ "mgbot-iotik32a": {
+ "name": "MGBOT IOTIK 32A",
+ "variant": VARIANT_ESP32,
+ },
+ "mgbot-iotik32b": {
+ "name": "MGBOT IOTIK 32B",
+ "variant": VARIANT_ESP32,
+ },
+ "mhetesp32devkit": {
+ "name": "MH ET LIVE ESP32DevKIT",
+ "variant": VARIANT_ESP32,
+ },
+ "mhetesp32minikit": {
+ "name": "MH ET LIVE ESP32MiniKit",
+ "variant": VARIANT_ESP32,
+ },
+ "microduino-core-esp32": {
+ "name": "Microduino Core ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "micros2": {
+ "name": "microS2",
+ "variant": VARIANT_ESP32S2,
+ },
+ "minimain_esp32s2": {
+ "name": "Deparment of Alchemy MiniMain ESP32-S2",
+ "variant": VARIANT_ESP32S2,
+ },
+ "nano32": {
+ "name": "MakerAsia Nano32",
+ "variant": VARIANT_ESP32,
+ },
+ "nina_w10": {
+ "name": "u-blox NINA-W10 series",
+ "variant": VARIANT_ESP32,
+ },
+ "node32s": {
+ "name": "Node32s",
+ "variant": VARIANT_ESP32,
+ },
+ "nodemcu-32s2": {
+ "name": "Ai-Thinker NodeMCU-32S2 (ESP-12K)",
+ "variant": VARIANT_ESP32S2,
+ },
+ "nodemcu-32s": {
+ "name": "NodeMCU-32S",
+ "variant": VARIANT_ESP32,
+ },
+ "nscreen-32": {
+ "name": "YeaCreate NSCREEN-32",
+ "variant": VARIANT_ESP32,
+ },
+ "odroid_esp32": {
+ "name": "ODROID-GO",
+ "variant": VARIANT_ESP32,
+ },
+ "onehorse32dev": {
+ "name": "Onehorse ESP32 Dev Module",
+ "variant": VARIANT_ESP32,
+ },
+ "oroca_edubot": {
+ "name": "OROCA EduBot",
+ "variant": VARIANT_ESP32,
+ },
+ "pico32": {
+ "name": "ESP32 Pico Kit",
+ "variant": VARIANT_ESP32,
+ },
+ "piranha_esp32": {
+ "name": "Fishino Piranha ESP-32",
+ "variant": VARIANT_ESP32,
+ },
+ "pocket_32": {
+ "name": "Dongsen Tech Pocket 32",
+ "variant": VARIANT_ESP32,
+ },
+ "pycom_gpy": {
+ "name": "Pycom GPy",
+ "variant": VARIANT_ESP32,
+ },
+ "qchip": {
+ "name": "Qchip",
+ "variant": VARIANT_ESP32,
+ },
+ "quantum": {
+ "name": "Noduino Quantum",
+ "variant": VARIANT_ESP32,
+ },
+ "seeed_xiao_esp32c3": {
+ "name": "Seeed Studio XIAO ESP32C3",
+ "variant": VARIANT_ESP32C3,
+ },
+ "sensesiot_weizen": {
+ "name": "LOGISENSES Senses Weizen",
+ "variant": VARIANT_ESP32,
+ },
+ "sg-o_airMon": {
+ "name": "SG-O AirMon",
+ "variant": VARIANT_ESP32,
+ },
+ "s_odi_ultra": {
+ "name": "S.ODI Ultra v1",
+ "variant": VARIANT_ESP32,
+ },
+ "sparkfun_esp32_iot_redboard": {
+ "name": "SparkFun ESP32 IoT RedBoard",
+ "variant": VARIANT_ESP32,
+ },
+ "sparkfun_esp32micromod": {
+ "name": "SparkFun ESP32 MicroMod",
+ "variant": VARIANT_ESP32,
+ },
+ "sparkfun_esp32s2_thing_plus_c": {
+ "name": "SparkFun ESP32 Thing Plus C",
+ "variant": VARIANT_ESP32,
+ },
+ "sparkfun_esp32s2_thing_plus": {
+ "name": "SparkFun ESP32-S2 Thing Plus",
+ "variant": VARIANT_ESP32S2,
+ },
+ "sparkfun_lora_gateway_1-channel": {
+ "name": "SparkFun LoRa Gateway 1-Channel",
+ "variant": VARIANT_ESP32,
+ },
+ "tamc_termod_s3": {
+ "name": "TAMC Termod S3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "tinypico": {
+ "name": "Unexpected Maker TinyPICO",
+ "variant": VARIANT_ESP32,
+ },
+ "trueverit-iot-driver-mk2": {
+ "name": "Trueverit ESP32 Universal IoT Driver MK II",
+ "variant": VARIANT_ESP32,
+ },
+ "trueverit-iot-driver-mk3": {
+ "name": "Trueverit ESP32 Universal IoT Driver MK III",
+ "variant": VARIANT_ESP32,
+ },
+ "trueverit-iot-driver": {
+ "name": "Trueverit ESP32 Universal IoT Driver",
+ "variant": VARIANT_ESP32,
+ },
+ "ttgo-lora32-v1": {
+ "name": "TTGO LoRa32-OLED V1",
+ "variant": VARIANT_ESP32,
+ },
+ "ttgo-lora32-v21": {
+ "name": "TTGO LoRa32-OLED v2.1.6",
+ "variant": VARIANT_ESP32,
+ },
+ "ttgo-lora32-v2": {
+ "name": "TTGO LoRa32-OLED V2",
+ "variant": VARIANT_ESP32,
+ },
+ "ttgo-t1": {
+ "name": "TTGO T1",
+ "variant": VARIANT_ESP32,
+ },
+ "ttgo-t7-v13-mini32": {
+ "name": "TTGO T7 V1.3 Mini32",
+ "variant": VARIANT_ESP32,
+ },
+ "ttgo-t7-v14-mini32": {
+ "name": "TTGO T7 V1.4 Mini32",
+ "variant": VARIANT_ESP32,
+ },
+ "ttgo-t-beam": {
+ "name": "TTGO T-Beam",
+ "variant": VARIANT_ESP32,
+ },
+ "ttgo-t-oi-plus": {
+ "name": "TTGO T-OI PLUS RISC-V ESP32-C3",
+ "variant": VARIANT_ESP32C3,
+ },
+ "ttgo-t-watch": {
+ "name": "TTGO T-Watch",
+ "variant": VARIANT_ESP32,
+ },
+ "turta_iot_node": {
+ "name": "Turta IoT Node",
+ "variant": VARIANT_ESP32,
+ },
+ "um_feathers2": {
+ "name": "Unexpected Maker FeatherS2",
+ "variant": VARIANT_ESP32S2,
+ },
+ "um_feathers2_neo": {
+ "name": "Unexpected Maker FeatherS2 Neo",
+ "variant": VARIANT_ESP32S2,
+ },
+ "um_feathers3": {
+ "name": "Unexpected Maker FeatherS3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "um_pros3": {
+ "name": "Unexpected Maker PROS3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "um_rmp": {
+ "name": "Unexpected Maker RMP",
+ "variant": VARIANT_ESP32S2,
+ },
+ "um_tinys2": {
+ "name": "Unexpected Maker TinyS2",
+ "variant": VARIANT_ESP32S2,
+ },
+ "um_tinys3": {
+ "name": "Unexpected Maker TinyS3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "unphone7": {
+ "name": "unPhone 7",
+ "variant": VARIANT_ESP32,
+ },
+ "unphone8": {
+ "name": "unPhone 8",
+ "variant": VARIANT_ESP32S3,
+ },
+ "unphone9": {
+ "name": "unPhone 9",
+ "variant": VARIANT_ESP32S3,
+ },
+ "upesy_wroom": {
+ "name": "uPesy ESP32 Wroom DevKit",
+ "variant": VARIANT_ESP32,
+ },
+ "upesy_wrover": {
+ "name": "uPesy ESP32 Wrover DevKit",
+ "variant": VARIANT_ESP32,
+ },
+ "vintlabs-devkit-v1": {
+ "name": "VintLabs ESP32 Devkit",
+ "variant": VARIANT_ESP32,
+ },
+ "watchy": {
+ "name": "SQFMI Watchy v2.0",
+ "variant": VARIANT_ESP32,
+ },
+ "wemosbat": {
+ "name": "WeMos WiFi and Bluetooth Battery",
+ "variant": VARIANT_ESP32,
+ },
+ "wemos_d1_mini32": {
+ "name": "WEMOS D1 MINI ESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "wemos_d1_uno32": {
+ "name": "WEMOS D1 R32",
+ "variant": VARIANT_ESP32,
+ },
+ "wesp32": {
+ "name": "Silicognition wESP32",
+ "variant": VARIANT_ESP32,
+ },
+ "widora-air": {
+ "name": "Widora AIR",
+ "variant": VARIANT_ESP32,
+ },
+ "wifiduino32c3": {
+ "name": "Blinker WiFiduinoV2 (ESP32-C3)",
+ "variant": VARIANT_ESP32C3,
+ },
+ "wifiduino32": {
+ "name": "Blinker WiFiduino32",
+ "variant": VARIANT_ESP32,
+ },
+ "wifiduino32s3": {
+ "name": "Blinker WiFiduino32S3",
+ "variant": VARIANT_ESP32S3,
+ },
+ "wipy3": {
+ "name": "Pycom WiPy3",
+ "variant": VARIANT_ESP32,
+ },
+ "wt32-eth01": {
+ "name": "Wireless-Tag WT32-ETH01 Ethernet Module",
+ "variant": VARIANT_ESP32,
+ },
+ "xinabox_cw02": {
+ "name": "XinaBox CW02",
+ "variant": VARIANT_ESP32,
+ },
}
diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py
index d92b449ee9..d13df01d3a 100644
--- a/esphome/components/esp32/const.py
+++ b/esphome/components/esp32/const.py
@@ -4,6 +4,11 @@ KEY_ESP32 = "esp32"
KEY_BOARD = "board"
KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
+KEY_COMPONENTS = "components"
+KEY_REPO = "repo"
+KEY_REF = "ref"
+KEY_REFRESH = "refresh"
+KEY_PATH = "path"
VARIANT_ESP32 = "ESP32"
VARIANT_ESP32S2 = "ESP32S2"
diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp
index 6123d83a34..512a8857b6 100644
--- a/esphome/components/esp32/core.cpp
+++ b/esphome/components/esp32/core.cpp
@@ -7,6 +7,7 @@
#include
#include
#include
+#include
#include
#if ESP_IDF_VERSION_MAJOR >= 4
@@ -23,7 +24,7 @@ void loop();
namespace esphome {
void IRAM_ATTR HOT yield() { vPortYield(); }
-uint32_t IRAM_ATTR HOT millis() { return (uint32_t)(esp_timer_get_time() / 1000ULL); }
+uint32_t IRAM_ATTR HOT millis() { return (uint32_t) (esp_timer_get_time() / 1000ULL); }
void IRAM_ATTR HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
diff --git a/esphome/components/esp32/gpio_idf.cpp b/esphome/components/esp32/gpio.cpp
similarity index 76%
rename from esphome/components/esp32/gpio_idf.cpp
rename to esphome/components/esp32/gpio.cpp
index 498843ebff..7896597d3e 100644
--- a/esphome/components/esp32/gpio_idf.cpp
+++ b/esphome/components/esp32/gpio.cpp
@@ -1,20 +1,19 @@
-#ifdef USE_ESP32_FRAMEWORK_ESP_IDF
+#ifdef USE_ESP32
-#include "gpio_idf.h"
+#include "gpio.h"
#include "esphome/core/log.h"
+#include
namespace esphome {
namespace esp32 {
static const char *const TAG = "esp32";
-bool IDFInternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) {
flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN));
- if (flags == gpio::FLAG_NONE) {
- return GPIO_MODE_DISABLE;
- } else if (flags == gpio::FLAG_INPUT) {
+ if (flags == gpio::FLAG_INPUT) {
return GPIO_MODE_INPUT;
} else if (flags == gpio::FLAG_OUTPUT) {
return GPIO_MODE_OUTPUT;
@@ -25,7 +24,7 @@ static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) {
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) {
return GPIO_MODE_INPUT_OUTPUT;
} else {
- // unsupported
+ // unsupported or gpio::FLAG_NONE
return GPIO_MODE_DISABLE;
}
}
@@ -35,14 +34,14 @@ struct ISRPinArg {
bool inverted;
};
-ISRInternalGPIOPin IDFInternalGPIOPin::to_isr() const {
+ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_;
arg->inverted = inverted_;
return ISRInternalGPIOPin((void *) arg);
}
-void IDFInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
+void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE;
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
@@ -74,13 +73,13 @@ void IDFInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio:
gpio_isr_handler_add(pin_, func, arg);
}
-std::string IDFInternalGPIOPin::dump_summary() const {
+std::string ESP32InternalGPIOPin::dump_summary() const {
char buffer[32];
- snprintf(buffer, sizeof(buffer), "GPIO%u", static_cast(pin_));
+ snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(pin_));
return buffer;
}
-void IDFInternalGPIOPin::setup() {
+void ESP32InternalGPIOPin::setup() {
gpio_config_t conf{};
conf.pin_bit_mask = 1ULL << static_cast(pin_);
conf.mode = flags_to_mode(flags_);
@@ -88,14 +87,16 @@ void IDFInternalGPIOPin::setup() {
conf.pull_down_en = flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE;
conf.intr_type = GPIO_INTR_DISABLE;
gpio_config(&conf);
- gpio_set_drive_capability(pin_, drive_strength_);
+ if (flags_ & gpio::FLAG_OUTPUT) {
+ gpio_set_drive_capability(pin_, drive_strength_);
+ }
}
-void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) {
+void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
// can't call gpio_config here because that logs in esp-idf which may cause issues
gpio_set_direction(pin_, flags_to_mode(flags));
gpio_pull_mode_t pull_mode = GPIO_FLOATING;
- if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) {
+ if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) {
pull_mode = GPIO_PULLUP_PULLDOWN;
} else if (flags & gpio::FLAG_PULLUP) {
pull_mode = GPIO_PULLUP_ONLY;
@@ -105,9 +106,9 @@ void IDFInternalGPIOPin::pin_mode(gpio::Flags flags) {
gpio_set_pull_mode(pin_, pull_mode);
}
-bool IDFInternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; }
-void IDFInternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); }
-void IDFInternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); }
+bool ESP32InternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; }
+void ESP32InternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); }
+void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); }
} // namespace esp32
@@ -128,7 +129,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
auto *arg = reinterpret_cast(arg_);
gpio_set_direction(arg->pin, flags_to_mode(flags));
gpio_pull_mode_t pull_mode = GPIO_FLOATING;
- if (flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)) {
+ if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) {
pull_mode = GPIO_PULLUP_PULLDOWN;
} else if (flags & gpio::FLAG_PULLUP) {
pull_mode = GPIO_PULLUP_ONLY;
@@ -140,4 +141,4 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
} // namespace esphome
-#endif // USE_ESP32_FRAMEWORK_ESP_IDF
+#endif // USE_ESP32
diff --git a/esphome/components/esp32/gpio_idf.h b/esphome/components/esp32/gpio.h
similarity index 90%
rename from esphome/components/esp32/gpio_idf.h
rename to esphome/components/esp32/gpio.h
index a07d11378a..23b723e0b4 100644
--- a/esphome/components/esp32/gpio_idf.h
+++ b/esphome/components/esp32/gpio.h
@@ -1,13 +1,13 @@
#pragma once
-#ifdef USE_ESP32_FRAMEWORK_ESP_IDF
+#ifdef USE_ESP32
#include "esphome/core/hal.h"
#include
namespace esphome {
namespace esp32 {
-class IDFInternalGPIOPin : public InternalGPIOPin {
+class ESP32InternalGPIOPin : public InternalGPIOPin {
public:
void set_pin(gpio_num_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
@@ -37,4 +37,4 @@ class IDFInternalGPIOPin : public InternalGPIOPin {
} // namespace esp32
} // namespace esphome
-#endif // USE_ESP32_FRAMEWORK_ESP_IDF
+#endif // USE_ESP32
diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py
index 5819943f37..7848d1d552 100644
--- a/esphome/components/esp32/gpio.py
+++ b/esphome/components/esp32/gpio.py
@@ -38,8 +38,7 @@ from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_support
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
-IDFInternalGPIOPin = esp32_ns.class_("IDFInternalGPIOPin", cg.InternalGPIOPin)
-ArduinoInternalGPIOPin = esp32_ns.class_("ArduinoInternalGPIOPin", cg.InternalGPIOPin)
+ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
def _lookup_pin(value):
@@ -105,23 +104,30 @@ _esp32_validations = {
def validate_gpio_pin(value):
value = _translate_pin(value)
+ board = CORE.data[KEY_ESP32][KEY_BOARD]
+ board_pins = boards.ESP32_BOARD_PINS.get(board, {})
+
+ # Resolved aliased board pins (shorthand when two boards have the same pin configuration)
+ while isinstance(board_pins, str):
+ board_pins = boards.ESP32_BOARD_PINS[board_pins]
+
+ if value in board_pins.values():
+ return 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)
def validate_supports(value):
mode = value[CONF_MODE]
- is_input = mode[CONF_INPUT]
is_output = mode[CONF_OUTPUT]
is_open_drain = mode[CONF_OPEN_DRAIN]
- is_pullup = mode[CONF_PULLUP]
- 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(
@@ -129,26 +135,6 @@ def validate_supports(value):
)
value = _esp32_validations[variant].usage_validation(value)
- if CORE.using_arduino:
- # (input, output, open_drain, pullup, pulldown)
- supported_modes = {
- # INPUT
- (True, False, False, False, False),
- # OUTPUT
- (False, True, False, False, False),
- # INPUT_PULLUP
- (True, False, False, True, False),
- # INPUT_PULLDOWN
- (True, False, False, False, True),
- # OUTPUT_OPEN_DRAIN
- (False, True, True, False, False),
- }
- key = (is_input, is_output, is_open_drain, is_pullup, is_pulldown)
- if key not in supported_modes:
- raise cv.Invalid(
- "This pin mode is not supported on ESP32 for arduino frameworks",
- [CONF_MODE],
- )
return value
@@ -163,18 +149,10 @@ DRIVE_STRENGTHS = {
gpio_num_t = cg.global_ns.enum("gpio_num_t")
-def _choose_pin_declaration(value):
- if CORE.using_esp_idf:
- return cv.declare_id(IDFInternalGPIOPin)(value)
- if CORE.using_arduino:
- return cv.declare_id(ArduinoInternalGPIOPin)(value)
- raise NotImplementedError
-
-
CONF_DRIVE_STRENGTH = "drive_strength"
ESP32_PIN_SCHEMA = cv.All(
{
- cv.GenerateID(): _choose_pin_declaration,
+ cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
@@ -186,8 +164,7 @@ ESP32_PIN_SCHEMA = cv.All(
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
- cv.SplitDefault(CONF_DRIVE_STRENGTH, esp32_idf="20mA"): cv.All(
- cv.only_with_esp_idf,
+ cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All(
cv.float_with_unit("current", "mA", optional_unit=True),
cv.enum(DRIVE_STRENGTHS),
),
@@ -200,10 +177,7 @@ ESP32_PIN_SCHEMA = cv.All(
async def esp32_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
- if CORE.using_esp_idf:
- cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
- else:
- cg.add(var.set_pin(num))
+ cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
cg.add(var.set_inverted(config[CONF_INVERTED]))
if CONF_DRIVE_STRENGTH in config:
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
diff --git a/esphome/components/esp32/gpio_esp32.py b/esphome/components/esp32/gpio_esp32.py
index dbafb73dba..66ba2ffa62 100644
--- a/esphome/components/esp32/gpio_esp32.py
+++ b/esphome/components/esp32/gpio_esp32.py
@@ -42,7 +42,7 @@ def esp32_validate_gpio_pin(value):
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
value,
)
- if value in (20, 24, 28, 29, 30, 31):
+ if value in (24, 28, 29, 30, 31):
# These pins are not exposed in GPIO mux (reason unknown)
# but they're missing from IO_MUX list in datasheet
raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.")
diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script
index 7feaf9e8e5..c941bdb386 100644
--- a/esphome/components/esp32/post_build.py.script
+++ b/esphome/components/esp32/post_build.py.script
@@ -1,13 +1,30 @@
# Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664
-import esptool
-
# pylint: disable=E0602
Import("env") # noqa
+import os
+import shutil
+
+if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
+ try:
+ import esptool
+ except ImportError:
+ env.Execute("$PYTHONEXE -m pip install esptool")
+else:
+ import subprocess
+from SCons.Script import ARGUMENTS
+
+# Copy over the default sdkconfig.
+from os import path
+if path.exists("./sdkconfig.defaults"):
+ os.makedirs(".temp", exist_ok=True)
+ shutil.copy("./sdkconfig.defaults", "./.temp/sdkconfig-esp32-idf")
def esp32_create_combined_bin(source, target, env):
- print("Generating combined binary for serial flashing")
+ verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0")))
+ if verbose:
+ print("Generating combined binary for serial flashing")
app_offset = 0x10000
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin")
@@ -24,20 +41,26 @@ def esp32_create_combined_bin(source, target, env):
"--flash_size",
flash_size,
]
- print(" Offset | File")
+ if verbose:
+ print(" Offset | File")
for section in sections:
sect_adr, sect_file = section.split(" ", 1)
- print(f" - {sect_adr} | {sect_file}")
+ if verbose:
+ print(f" - {sect_adr} | {sect_file}")
cmd += [sect_adr, sect_file]
- print(f" - {hex(app_offset)} | {firmware_name}")
cmd += [hex(app_offset), firmware_name]
- print()
- print(f"Using esptool.py arguments: {' '.join(cmd)}")
- print()
- esptool.main(cmd)
+ if verbose:
+ print(f" - {hex(app_offset)} | {firmware_name}")
+ print()
+ print(f"Using esptool.py arguments: {' '.join(cmd)}")
+ print()
+ if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
+ esptool.main(cmd)
+ else:
+ subprocess.run(["esptool.py", *cmd])
# pylint: disable=E0602
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa
diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp
index 8c2b67a942..f90b8a4603 100644
--- a/esphome/components/esp32/preferences.cpp
+++ b/esphome/components/esp32/preferences.cpp
@@ -5,6 +5,7 @@
#include "esphome/core/log.h"
#include
#include
+#include
#include
#include
@@ -36,6 +37,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
save.key = key;
save.data.assign(data, data + len);
s_pending_save.emplace_back(save);
+ ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len);
return true;
}
bool load(uint8_t *data, size_t len) override {
@@ -65,6 +67,8 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err));
return false;
+ } else {
+ ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %d", key.c_str(), len);
}
return true;
}
@@ -73,7 +77,6 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
class ESP32Preferences : public ESPPreferences {
public:
uint32_t nvs_handle;
- uint32_t current_offset = 0;
void open() {
nvs_flash_init();
@@ -97,12 +100,9 @@ class ESP32Preferences : public ESPPreferences {
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = nvs_handle;
- current_offset += length;
- uint32_t keyval = current_offset ^ type;
- char keybuf[16];
- snprintf(keybuf, sizeof(keybuf), "%d", keyval);
- pref->key = keybuf; // copied to std::string
+ uint32_t keyval = type;
+ pref->key = str_sprintf("%" PRIu32, keyval);
return ESPPreferenceObject(pref);
}
@@ -111,22 +111,40 @@ class ESP32Preferences : public ESPPreferences {
if (s_pending_save.empty())
return true;
- ESP_LOGD(TAG, "Saving preferences to flash...");
+ ESP_LOGD(TAG, "Saving %d preferences to flash...", s_pending_save.size());
// goal try write all pending saves even if one fails
- bool any_failed = false;
+ int cached = 0, written = 0, failed = 0;
+ esp_err_t last_err = ESP_OK;
+ std::string last_key{};
// 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());
+ ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), 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));
+ failed++;
+ last_err = err;
+ last_key = save.key;
+ continue;
+ }
+ written++;
+ } else {
+ ESP_LOGV(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size());
+ cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
}
+ ESP_LOGD(TAG, "Saving %d preferences to flash: %d cached, %d written, %d failed", cached + written + failed, cached,
+ written, failed);
+ if (failed > 0) {
+ ESP_LOGE(TAG, "Error saving %d preferences to flash. Last error=%s for key=%s", failed, esp_err_to_name(last_err),
+ last_key.c_str());
+ }
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
esp_err_t err = nvs_commit(nvs_handle);
@@ -135,7 +153,34 @@ class ESP32Preferences : public ESPPreferences {
return false;
}
- return !any_failed;
+ return failed == 0;
+ }
+ 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.resize(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;
+ }
+
+ bool reset() override {
+ ESP_LOGD(TAG, "Cleaning up preferences in flash...");
+ s_pending_save.clear();
+
+ nvs_flash_deinit();
+ nvs_flash_erase();
+ // Make the handle invalid to prevent any saves until restart
+ nvs_handle = 0;
+ return true;
}
};
diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py
index 4b5c741ad9..f508cecb87 100644
--- a/esphome/components/esp32_ble/__init__.py
+++ b/esphome/components/esp32_ble/__init__.py
@@ -2,26 +2,56 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE
-from esphome.components.esp32 import add_idf_sdkconfig_option
+from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant, const
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz"]
-CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"]
+CONFLICTS_WITH = ["esp32_ble_beacon"]
+
+CONF_BLE_ID = "ble_id"
+CONF_IO_CAPABILITY = "io_capability"
+
+NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component)
+GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler")
+GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler")
+GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler")
+
+IoCapability = esp32_ble_ns.enum("IoCapability")
+IO_CAPABILITY = {
+ "none": IoCapability.IO_CAP_NONE,
+ "keyboard_only": IoCapability.IO_CAP_IN,
+ "keyboard_display": IoCapability.IO_CAP_KBDISP,
+ "display_only": IoCapability.IO_CAP_OUT,
+ "display_yes_no": IoCapability.IO_CAP_IO,
+}
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32BLE),
+ cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum(
+ IO_CAPABILITY, lower=True
+ ),
}
).extend(cv.COMPONENT_SCHEMA)
+def validate_variant(_):
+ variant = get_esp32_variant()
+ if variant in NO_BLUETOOTH_VARIANTS:
+ raise cv.Invalid(f"{variant} does not support Bluetooth")
+
+
+FINAL_VALIDATE_SCHEMA = validate_variant
+
+
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+ cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY]))
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp
index ecd591d169..6c9124447a 100644
--- a/esphome/components/esp32_ble/ble.cpp
+++ b/esphome/components/esp32_ble/ble.cpp
@@ -4,13 +4,14 @@
#include "esphome/core/application.h"
#include "esphome/core/log.h"
-#include
-#include
-#include
#include
-#include
-#include
+#include
+#include
#include
+#include
+#include
+#include
+#include
#ifdef USE_ARDUINO
#include
@@ -31,24 +32,17 @@ void ESP32BLE::setup() {
return;
}
+#ifdef USE_ESP32_BLE_SERVER
this->advertising_ = new BLEAdvertising(); // NOLINT(cppcoreguidelines-owning-memory)
this->advertising_->set_scan_response(true);
this->advertising_->set_min_preferred_interval(0x06);
this->advertising_->start();
+#endif // USE_ESP32_BLE_SERVER
ESP_LOGD(TAG, "BLE setup complete");
}
-void ESP32BLE::mark_failed() {
- Component::mark_failed();
-#ifdef USE_ESP32_BLE_SERVER
- if (this->server_ != nullptr) {
- this->server_->mark_failed();
- }
-#endif
-}
-
bool ESP32BLE::ble_setup_() {
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
@@ -100,13 +94,16 @@ bool ESP32BLE::ble_setup_() {
ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err);
return false;
}
- err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler);
- if (err != ESP_OK) {
- ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
- return false;
+
+ if (!this->gap_event_handlers_.empty()) {
+ err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler);
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
+ return false;
+ }
}
- if (this->has_server()) {
+ if (!this->gatts_event_handlers_.empty()) {
err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err);
@@ -114,7 +111,7 @@ bool ESP32BLE::ble_setup_() {
}
}
- if (this->has_client()) {
+ if (!this->gattc_event_handlers_.empty()) {
err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err);
@@ -137,8 +134,7 @@ bool ESP32BLE::ble_setup_() {
return false;
}
- esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE;
- err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t));
+ err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err);
return false;
@@ -158,6 +154,10 @@ void ESP32BLE::loop() {
this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if,
&ble_event->event_.gatts.gatts_param);
break;
+ case BLEEvent::GATTC:
+ this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if,
+ &ble_event->event_.gattc.gattc_param);
+ break;
case BLEEvent::GAP:
this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param);
break;
@@ -176,9 +176,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event);
- switch (event) {
- default:
- break;
+ for (auto *gap_handler : this->gap_event_handlers_) {
+ gap_handler->gap_event_handler(event, param);
}
}
@@ -191,19 +190,70 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) {
ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
-#ifdef USE_ESP32_BLE_SERVER
- this->server_->gatts_event_handler(event, gatts_if, param);
-#endif
+ for (auto *gatts_handler : this->gatts_event_handlers_) {
+ gatts_handler->gatts_event_handler(event, gatts_if, param);
+ }
}
+void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
+ esp_ble_gattc_cb_param_t *param) {
+ BLEEvent *new_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory)
+ global_ble->ble_events_.push(new_event);
+} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
+
void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
- // this->client_->gattc_event_handler(event, gattc_if, param);
+ ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
+ for (auto *gattc_handler : this->gattc_event_handlers_) {
+ gattc_handler->gattc_event_handler(event, gattc_if, param);
+ }
}
float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }
-void ESP32BLE::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE:"); }
+void ESP32BLE::dump_config() {
+ const uint8_t *mac_address = esp_bt_dev_get_address();
+ if (mac_address) {
+ const char *io_capability_s;
+ switch (this->io_cap_) {
+ case ESP_IO_CAP_OUT:
+ io_capability_s = "display_only";
+ break;
+ case ESP_IO_CAP_IO:
+ io_capability_s = "display_yes_no";
+ break;
+ case ESP_IO_CAP_IN:
+ io_capability_s = "keyboard_only";
+ break;
+ case ESP_IO_CAP_NONE:
+ io_capability_s = "none";
+ break;
+ case ESP_IO_CAP_KBDISP:
+ io_capability_s = "keyboard_display";
+ break;
+ default:
+ io_capability_s = "invalid";
+ break;
+ }
+ ESP_LOGCONFIG(TAG, "ESP32 BLE:");
+ ESP_LOGCONFIG(TAG, " MAC address: %02X:%02X:%02X:%02X:%02X:%02X", mac_address[0], mac_address[1], mac_address[2],
+ mac_address[3], mac_address[4], mac_address[5]);
+ ESP_LOGCONFIG(TAG, " IO Capability: %s", io_capability_s);
+ } else {
+ ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled");
+ }
+}
+
+uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
+ uint64_t u = 0;
+ u |= uint64_t(address[0] & 0xFF) << 40;
+ u |= uint64_t(address[1] & 0xFF) << 32;
+ u |= uint64_t(address[2] & 0xFF) << 24;
+ u |= uint64_t(address[3] & 0xFF) << 16;
+ u |= uint64_t(address[4] & 0xFF) << 8;
+ u |= uint64_t(address[5] & 0xFF) << 0;
+ return u;
+}
ESP32BLE *global_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h
index 0477dee070..cde17da425 100644
--- a/esphome/components/esp32_ble/ble.h
+++ b/esphome/components/esp32_ble/ble.h
@@ -5,20 +5,21 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
-#include "queue.h"
-#ifdef USE_ESP32_BLE_SERVER
-#include "esphome/components/esp32_ble_server/ble_server.h"
-#endif
+#include "queue.h"
+#include "ble_event.h"
#ifdef USE_ESP32
#include
#include
#include
+
namespace esphome {
namespace esp32_ble {
+uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
+
// NOLINTNEXTLINE(modernize-use-using)
typedef struct {
void *peer_device;
@@ -26,28 +27,46 @@ typedef struct {
uint16_t mtu;
} conn_status_t;
+enum IoCapability {
+ IO_CAP_OUT = ESP_IO_CAP_OUT,
+ IO_CAP_IO = ESP_IO_CAP_IO,
+ IO_CAP_IN = ESP_IO_CAP_IN,
+ IO_CAP_NONE = ESP_IO_CAP_NONE,
+ IO_CAP_KBDISP = ESP_IO_CAP_KBDISP,
+};
+
+class GAPEventHandler {
+ public:
+ virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
+};
+
+class GATTcEventHandler {
+ public:
+ virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
+ esp_ble_gattc_cb_param_t *param) = 0;
+};
+
+class GATTsEventHandler {
+ public:
+ virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
+ esp_ble_gatts_cb_param_t *param) = 0;
+};
+
class ESP32BLE : public Component {
public:
+ void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; }
+
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override;
- void mark_failed() override;
-
- bool has_server() {
-#ifdef USE_ESP32_BLE_SERVER
- return this->server_ != nullptr;
-#else
- return false;
-#endif
- }
- bool has_client() { return false; }
BLEAdvertising *get_advertising() { return this->advertising_; }
-#ifdef USE_ESP32_BLE_SERVER
- void set_server(esp32_ble_server::BLEServer *server) { this->server_ = server; }
-#endif
+ void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
+ void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
+ void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
+
protected:
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
@@ -59,11 +78,13 @@ class ESP32BLE : public Component {
bool ble_setup_();
-#ifdef USE_ESP32_BLE_SERVER
- esp32_ble_server::BLEServer *server_{nullptr};
-#endif
+ std::vector gap_event_handlers_;
+ std::vector gattc_event_handlers_;
+ std::vector gatts_event_handlers_;
+
Queue ble_events_;
BLEAdvertising *advertising_;
+ esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp
index 31b1f4c383..2083bf5f08 100644
--- a/esphome/components/esp32_ble/ble_advertising.cpp
+++ b/esphome/components/esp32_ble/ble_advertising.cpp
@@ -42,6 +42,11 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) {
this->advertising_uuids_.end());
}
+void BLEAdvertising::set_manufacturer_data(uint8_t *data, uint16_t size) {
+ this->advertising_data_.p_manufacturer_data = data;
+ this->advertising_data_.manufacturer_len = size;
+}
+
void BLEAdvertising::start() {
int num_services = this->advertising_uuids_.size();
if (num_services == 0) {
diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h
index 01e2ba1295..079bd6c14c 100644
--- a/esphome/components/esp32_ble/ble_advertising.h
+++ b/esphome/components/esp32_ble/ble_advertising.h
@@ -20,6 +20,7 @@ class BLEAdvertising {
void remove_service_uuid(ESPBTUUID uuid);
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
+ void set_manufacturer_data(uint8_t *data, uint16_t size);
void start();
void stop();
diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h
new file mode 100644
index 0000000000..1cf63b2fab
--- /dev/null
+++ b/esphome/components/esp32_ble/ble_event.h
@@ -0,0 +1,94 @@
+#pragma once
+
+#ifdef USE_ESP32
+
+#include
+
+#include
+#include
+#include
+
+namespace esphome {
+namespace esp32_ble {
+// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
+// This class stores each event in a single type.
+class BLEEvent {
+ public:
+ BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
+ this->event_.gap.gap_event = e;
+ memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t));
+ this->type_ = GAP;
+ };
+
+ BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
+ this->event_.gattc.gattc_event = e;
+ this->event_.gattc.gattc_if = i;
+ memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t));
+ // Need to also make a copy of relevant event data.
+ switch (e) {
+ case ESP_GATTC_NOTIFY_EVT:
+ this->data.assign(p->notify.value, p->notify.value + p->notify.value_len);
+ this->event_.gattc.gattc_param.notify.value = this->data.data();
+ break;
+ case ESP_GATTC_READ_CHAR_EVT:
+ case ESP_GATTC_READ_DESCR_EVT:
+ this->data.assign(p->read.value, p->read.value + p->read.value_len);
+ this->event_.gattc.gattc_param.read.value = this->data.data();
+ break;
+ default:
+ break;
+ }
+ this->type_ = GATTC;
+ };
+
+ BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
+ this->event_.gatts.gatts_event = e;
+ this->event_.gatts.gatts_if = i;
+ memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t));
+ // Need to also make a copy of relevant event data.
+ switch (e) {
+ case ESP_GATTS_WRITE_EVT:
+ this->data.assign(p->write.value, p->write.value + p->write.len);
+ this->event_.gatts.gatts_param.write.value = this->data.data();
+ break;
+ default:
+ break;
+ }
+ this->type_ = GATTS;
+ };
+
+ union {
+ // NOLINTNEXTLINE(readability-identifier-naming)
+ struct gap_event {
+ esp_gap_ble_cb_event_t gap_event;
+ esp_ble_gap_cb_param_t gap_param;
+ } gap;
+
+ // NOLINTNEXTLINE(readability-identifier-naming)
+ struct gattc_event {
+ esp_gattc_cb_event_t gattc_event;
+ esp_gatt_if_t gattc_if;
+ esp_ble_gattc_cb_param_t gattc_param;
+ } gattc;
+
+ // NOLINTNEXTLINE(readability-identifier-naming)
+ struct gatts_event {
+ esp_gatts_cb_event_t gatts_event;
+ esp_gatt_if_t gatts_if;
+ esp_ble_gatts_cb_param_t gatts_param;
+ } gatts;
+ } event_;
+
+ std::vector data{};
+ // NOLINTNEXTLINE(readability-identifier-naming)
+ enum ble_event_t : uint8_t {
+ GAP,
+ GATTC,
+ GATTS,
+ } type_;
+};
+
+} // namespace esp32_ble
+} // namespace esphome
+
+#endif
diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp
index 8556aa87df..a50d3dbd42 100644
--- a/esphome/components/esp32_ble/ble_uuid.cpp
+++ b/esphome/components/esp32_ble/ble_uuid.cpp
@@ -27,8 +27,7 @@ ESPBTUUID ESPBTUUID::from_uint32(uint32_t uuid) {
ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) {
ESPBTUUID ret;
ret.uuid_.len = ESP_UUID_LEN_128;
- for (size_t i = 0; i < ESP_UUID_LEN_128; i++)
- ret.uuid_.uuid.uuid128[i] = data[i];
+ memcpy(ret.uuid_.uuid.uuid128, data, ESP_UUID_LEN_128);
return ret;
}
ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
@@ -91,10 +90,13 @@ ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) {
ESPBTUUID ret;
ret.uuid_.len = uuid.len;
- ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16;
- ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32;
- for (size_t i = 0; i < ESP_UUID_LEN_128; i++)
- ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i];
+ if (uuid.len == ESP_UUID_LEN_16) {
+ ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16;
+ } else if (uuid.len == ESP_UUID_LEN_32) {
+ ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32;
+ } else if (uuid.len == ESP_UUID_LEN_128) {
+ memcpy(ret.uuid_.uuid.uuid128, uuid.uuid.uuid128, ESP_UUID_LEN_128);
+ }
return ret;
}
ESPBTUUID ESPBTUUID::as_128bit() const {
@@ -158,30 +160,26 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
}
return false;
}
-esp_bt_uuid_t ESPBTUUID::get_uuid() { return this->uuid_; }
-std::string ESPBTUUID::to_string() {
- char sbuf[64];
+esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
+std::string ESPBTUUID::to_string() const {
switch (this->uuid_.len) {
case ESP_UUID_LEN_16:
- sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff);
- break;
+ return str_snprintf("0x%02X%02X", 6, this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff);
case ESP_UUID_LEN_32:
- sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff),
- (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff);
- break;
+ return str_snprintf("0x%02X%02X%02X%02X", 10, this->uuid_.uuid.uuid32 >> 24,
+ (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff),
+ this->uuid_.uuid.uuid32 & 0xff);
default:
case ESP_UUID_LEN_128:
- char *bpos = sbuf;
+ std::string buf;
for (int8_t i = 15; i >= 0; i--) {
- sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]);
- bpos += 2;
+ buf += str_snprintf("%02X", 2, this->uuid_.uuid.uuid128[i]);
if (i == 6 || i == 8 || i == 10 || i == 12)
- sprintf(bpos++, "-");
+ buf += "-";
}
- sbuf[47] = '\0';
- break;
+ return buf;
}
- return sbuf;
+ return "";
}
} // namespace esp32_ble
diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h
index f953f9fede..790a57c59d 100644
--- a/esphome/components/esp32_ble/ble_uuid.h
+++ b/esphome/components/esp32_ble/ble_uuid.h
@@ -32,9 +32,9 @@ class ESPBTUUID {
bool operator==(const ESPBTUUID &uuid) const;
bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); }
- esp_bt_uuid_t get_uuid();
+ esp_bt_uuid_t get_uuid() const;
- std::string to_string();
+ std::string to_string() const;
protected:
esp_bt_uuid_t uuid_;
diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h
index 8d05eca058..5b31b97ae2 100644
--- a/esphome/components/esp32_ble/queue.h
+++ b/esphome/components/esp32_ble/queue.h
@@ -2,16 +2,9 @@
#ifdef USE_ESP32
-#include "esphome/core/component.h"
-#include "esphome/core/helpers.h"
-
-#include
#include
-#include
+#include
-#include
-#include
-#include
#include
#include
@@ -57,84 +50,6 @@ template class Queue {
SemaphoreHandle_t m_;
};
-// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
-// This class stores each event in a single type.
-class BLEEvent {
- public:
- BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
- this->event_.gap.gap_event = e;
- memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t));
- this->type_ = GAP;
- };
-
- BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
- this->event_.gattc.gattc_event = e;
- this->event_.gattc.gattc_if = i;
- memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t));
- // Need to also make a copy of notify event data.
- switch (e) {
- case ESP_GATTC_NOTIFY_EVT:
- memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len);
- this->event_.gattc.gattc_param.notify.value = this->event_.gattc.data;
- break;
- case ESP_GATTC_READ_CHAR_EVT:
- case ESP_GATTC_READ_DESCR_EVT:
- memcpy(this->event_.gattc.data, p->read.value, p->read.value_len);
- this->event_.gattc.gattc_param.read.value = this->event_.gattc.data;
- break;
- default:
- break;
- }
- this->type_ = GATTC;
- };
-
- BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
- this->event_.gatts.gatts_event = e;
- this->event_.gatts.gatts_if = i;
- memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t));
- // Need to also make a copy of write data.
- switch (e) {
- case ESP_GATTS_WRITE_EVT:
- memcpy(this->event_.gatts.data, p->write.value, p->write.len);
- this->event_.gatts.gatts_param.write.value = this->event_.gatts.data;
- break;
- default:
- break;
- }
- this->type_ = GATTS;
- };
-
- union {
- // NOLINTNEXTLINE(readability-identifier-naming)
- struct gap_event {
- esp_gap_ble_cb_event_t gap_event;
- esp_ble_gap_cb_param_t gap_param;
- } gap;
-
- // NOLINTNEXTLINE(readability-identifier-naming)
- struct gattc_event {
- esp_gattc_cb_event_t gattc_event;
- esp_gatt_if_t gattc_if;
- esp_ble_gattc_cb_param_t gattc_param;
- uint8_t data[64];
- } gattc;
-
- // NOLINTNEXTLINE(readability-identifier-naming)
- struct gatts_event {
- esp_gatts_cb_event_t gatts_event;
- esp_gatt_if_t gatts_if;
- esp_ble_gatts_cb_param_t gatts_param;
- uint8_t data[64];
- } gatts;
- } event_;
- // NOLINTNEXTLINE(readability-identifier-naming)
- enum ble_event_t : uint8_t {
- GAP,
- GATTC,
- GATTS,
- } type_;
-};
-
} // namespace esp32_ble
} // namespace esphome
diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py
index d6cbb15dd2..311919dcd4 100644
--- a/esphome/components/esp32_ble_beacon/__init__.py
+++ b/esphome/components/esp32_ble_beacon/__init__.py
@@ -1,8 +1,9 @@
import esphome.codegen as cg
import esphome.config_validation as cv
-from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID
-from esphome.core import CORE
+from esphome.const import CONF_ID, CONF_TYPE, CONF_UUID, CONF_TX_POWER
+from esphome.core import CORE, TimePeriod
from esphome.components.esp32 import add_idf_sdkconfig_option
+from esphome.components import esp32_ble
DEPENDENCIES = ["esp32"]
CONFLICTS_WITH = ["esp32_ble_tracker"]
@@ -12,16 +13,49 @@ ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component)
CONF_MAJOR = "major"
CONF_MINOR = "minor"
+CONF_MIN_INTERVAL = "min_interval"
+CONF_MAX_INTERVAL = "max_interval"
+CONF_MEASURED_POWER = "measured_power"
-CONFIG_SCHEMA = cv.Schema(
- {
- cv.GenerateID(): cv.declare_id(ESP32BLEBeacon),
- cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True),
- cv.Required(CONF_UUID): cv.uuid,
- cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t,
- cv.Optional(CONF_MINOR, default=61958): cv.uint16_t,
- }
-).extend(cv.COMPONENT_SCHEMA)
+
+def validate_config(config):
+ if config[CONF_MIN_INTERVAL] > config.get(CONF_MAX_INTERVAL):
+ raise cv.Invalid("min_interval must be <= max_interval")
+ return config
+
+
+CONFIG_SCHEMA = cv.All(
+ cv.Schema(
+ {
+ cv.GenerateID(): cv.declare_id(ESP32BLEBeacon),
+ cv.Required(CONF_TYPE): cv.one_of("IBEACON", upper=True),
+ cv.Required(CONF_UUID): cv.uuid,
+ cv.Optional(CONF_MAJOR, default=10167): cv.uint16_t,
+ cv.Optional(CONF_MINOR, default=61958): cv.uint16_t,
+ cv.Optional(CONF_MIN_INTERVAL, default="100ms"): cv.All(
+ cv.positive_time_period_milliseconds,
+ cv.Range(
+ min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240)
+ ),
+ ),
+ cv.Optional(CONF_MAX_INTERVAL, default="100ms"): cv.All(
+ cv.positive_time_period_milliseconds,
+ cv.Range(
+ min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240)
+ ),
+ ),
+ cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range(
+ min=-128, max=0
+ ),
+ cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All(
+ cv.decibel, cv.one_of(-12, -9, -6, -3, 0, 3, 6, 9, int=True)
+ ),
+ }
+ ).extend(cv.COMPONENT_SCHEMA),
+ validate_config,
+)
+
+FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
async def to_code(config):
@@ -31,6 +65,10 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add(var.set_major(config[CONF_MAJOR]))
cg.add(var.set_minor(config[CONF_MINOR]))
+ cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL]))
+ cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL]))
+ cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
+ cg.add(var.set_tx_power(config[CONF_TX_POWER]))
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp
index 955bc8595f..589fcc1e82 100644
--- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp
+++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp
@@ -36,11 +36,24 @@ static esp_ble_adv_params_t ble_adv_params = {
#define ENDIAN_CHANGE_U16(x) ((((x) &0xFF00) >> 8) + (((x) &0xFF) << 8))
static const esp_ble_ibeacon_head_t IBEACON_COMMON_HEAD = {
- .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502};
+ .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = {0x4C, 0x00}, .beacon_type = {0x02, 0x15}};
void ESP32BLEBeacon::dump_config() {
ESP_LOGCONFIG(TAG, "ESP32 BLE Beacon:");
- ESP_LOGCONFIG(TAG, " Major: %u, Minor: %u", this->major_, this->minor_);
+ char uuid[37];
+ char *bpos = uuid;
+ for (int8_t ii = 0; ii < 16; ++ii) {
+ bpos += sprintf(bpos, "%02X", this->uuid_[ii]);
+ if (ii == 3 || ii == 5 || ii == 7 || ii == 9) {
+ bpos += sprintf(bpos, "-");
+ }
+ }
+ uuid[36] = '\0';
+ ESP_LOGCONFIG(TAG,
+ " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d"
+ ", TX Power: %ddBm",
+ uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_,
+ this->tx_power_);
}
void ESP32BLEBeacon::setup() {
@@ -67,6 +80,9 @@ void ESP32BLEBeacon::ble_core_task(void *params) {
}
void ESP32BLEBeacon::ble_setup() {
+ ble_adv_params.adv_int_min = static_cast(global_esp32_ble_beacon->min_interval_ / 0.625f);
+ ble_adv_params.adv_int_max = static_cast(global_esp32_ble_beacon->max_interval_ / 0.625f);
+
// Initialize non-volatile storage for the bluetooth controller
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
@@ -118,6 +134,12 @@ void ESP32BLEBeacon::ble_setup() {
ESP_LOGE(TAG, "esp_bluedroid_enable failed: %d", err);
return;
}
+ err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV,
+ static_cast((global_esp32_ble_beacon->tx_power_ + 12) / 3));
+ if (err != ESP_OK) {
+ ESP_LOGE(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
+ return;
+ }
err = esp_ble_gap_register_callback(ESP32BLEBeacon::gap_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
@@ -130,7 +152,7 @@ void ESP32BLEBeacon::ble_setup() {
sizeof(ibeacon_adv_data.ibeacon_vendor.proximity_uuid));
ibeacon_adv_data.ibeacon_vendor.minor = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->minor_);
ibeacon_adv_data.ibeacon_vendor.major = ENDIAN_CHANGE_U16(global_esp32_ble_beacon->major_);
- ibeacon_adv_data.ibeacon_vendor.measured_power = 0xC5;
+ ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast(global_esp32_ble_beacon->measured_power_);
esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data));
}
@@ -153,7 +175,7 @@ void ESP32BLEBeacon::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap
break;
}
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: {
- err = param->adv_start_cmpl.status;
+ err = param->adv_stop_cmpl.status;
if (err != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(TAG, "BLE adv stop failed: %s", esp_err_to_name(err));
} else {
diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h
index 80ad2041f2..5208b67ea3 100644
--- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h
+++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h
@@ -5,6 +5,7 @@
#ifdef USE_ESP32
#include
+#include
namespace esphome {
namespace esp32_ble_beacon {
@@ -14,8 +15,8 @@ typedef struct {
uint8_t flags[3];
uint8_t length;
uint8_t type;
- uint16_t company_id;
- uint16_t beacon_type;
+ uint8_t company_id[2];
+ uint8_t beacon_type[2];
} __attribute__((packed)) esp_ble_ibeacon_head_t;
// NOLINTNEXTLINE(modernize-use-using)
@@ -42,6 +43,10 @@ class ESP32BLEBeacon : public Component {
void set_major(uint16_t major) { this->major_ = major; }
void set_minor(uint16_t minor) { this->minor_ = minor; }
+ void set_min_interval(uint16_t val) { this->min_interval_ = val; }
+ void set_max_interval(uint16_t val) { this->max_interval_ = val; }
+ void set_measured_power(int8_t val) { this->measured_power_ = val; }
+ void set_tx_power(int8_t val) { this->tx_power_ = val; }
protected:
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
@@ -51,6 +56,10 @@ class ESP32BLEBeacon : public Component {
std::array uuid_;
uint16_t major_{};
uint16_t minor_{};
+ uint16_t min_interval_{};
+ uint16_t max_interval_{};
+ int8_t measured_power_{};
+ int8_t tx_power_{};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
diff --git a/esphome/components/esp32_ble_client/__init__.py b/esphome/components/esp32_ble_client/__init__.py
new file mode 100644
index 0000000000..94a5576d0b
--- /dev/null
+++ b/esphome/components/esp32_ble_client/__init__.py
@@ -0,0 +1,12 @@
+import esphome.codegen as cg
+
+from esphome.components import esp32_ble_tracker
+
+AUTO_LOAD = ["esp32_ble_tracker"]
+CODEOWNERS = ["@jesserockz"]
+DEPENDENCIES = ["esp32"]
+
+esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client")
+BLEClientBase = esp32_ble_client_ns.class_(
+ "BLEClientBase", esp32_ble_tracker.ESPBTClient, cg.Component
+)
diff --git a/esphome/components/esp32_ble_client/ble_characteristic.cpp b/esphome/components/esp32_ble_client/ble_characteristic.cpp
new file mode 100644
index 0000000000..2fd7fe9871
--- /dev/null
+++ b/esphome/components/esp32_ble_client/ble_characteristic.cpp
@@ -0,0 +1,99 @@
+#include "ble_characteristic.h"
+#include "ble_client_base.h"
+#include "ble_service.h"
+
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace esp32_ble_client {
+
+static const char *const TAG = "esp32_ble_client";
+
+BLECharacteristic::~BLECharacteristic() {
+ for (auto &desc : this->descriptors)
+ delete desc; // NOLINT(cppcoreguidelines-owning-memory)
+}
+
+void BLECharacteristic::release_descriptors() {
+ this->parsed = false;
+ for (auto &desc : this->descriptors)
+ delete desc; // NOLINT(cppcoreguidelines-owning-memory)
+ this->descriptors.clear();
+}
+
+void BLECharacteristic::parse_descriptors() {
+ this->parsed = true;
+ uint16_t offset = 0;
+ esp_gattc_descr_elem_t result;
+
+ while (true) {
+ uint16_t count = 1;
+ esp_gatt_status_t status =
+ esp_ble_gattc_get_all_descr(this->service->client->get_gattc_if(), this->service->client->get_conn_id(),
+ this->handle, &result, &count, offset);
+ if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {
+ break;
+ }
+ if (status != ESP_GATT_OK) {
+ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d",
+ this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status);
+ break;
+ }
+ if (count == 0) {
+ break;
+ }
+
+ BLEDescriptor *desc = new BLEDescriptor(); // NOLINT(cppcoreguidelines-owning-memory)
+ desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid);
+ desc->handle = result.handle;
+ desc->characteristic = this;
+ this->descriptors.push_back(desc);
+ ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(),
+ this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle);
+ offset++;
+ }
+}
+
+BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) {
+ if (!this->parsed)
+ this->parse_descriptors();
+ for (auto &desc : this->descriptors) {
+ if (desc->uuid == uuid)
+ return desc;
+ }
+ return nullptr;
+}
+BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) {
+ return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid));
+}
+BLEDescriptor *BLECharacteristic::get_descriptor_by_handle(uint16_t handle) {
+ if (!this->parsed)
+ this->parse_descriptors();
+ for (auto &desc : this->descriptors) {
+ if (desc->handle == handle)
+ return desc;
+ }
+ return nullptr;
+}
+
+esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type) {
+ auto *client = this->service->client;
+ auto status = esp_ble_gattc_write_char(client->get_gattc_if(), client->get_conn_id(), this->handle, new_val_size,
+ new_val, write_type, ESP_GATT_AUTH_REQ_NONE);
+ if (status) {
+ ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d",
+ this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status);
+ }
+ return status;
+}
+
+esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) {
+ return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP);
+}
+
+} // namespace esp32_ble_client
+} // namespace esphome
+
+#endif // USE_ESP32
diff --git a/esphome/components/esp32_ble_client/ble_characteristic.h b/esphome/components/esp32_ble_client/ble_characteristic.h
new file mode 100644
index 0000000000..a014788e65
--- /dev/null
+++ b/esphome/components/esp32_ble_client/ble_characteristic.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#ifdef USE_ESP32
+
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+
+#include "ble_descriptor.h"
+
+#include
+
+namespace esphome {
+namespace esp32_ble_client {
+
+namespace espbt = esphome::esp32_ble_tracker;
+
+class BLEService;
+
+class BLECharacteristic {
+ public:
+ ~BLECharacteristic();
+ bool parsed = false;
+ espbt::ESPBTUUID uuid;
+ uint16_t handle;
+ esp_gatt_char_prop_t properties;
+ std::vector descriptors;
+ void parse_descriptors();
+ void release_descriptors();
+ BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid);
+ BLEDescriptor *get_descriptor(uint16_t uuid);
+ BLEDescriptor *get_descriptor_by_handle(uint16_t handle);
+ esp_err_t write_value(uint8_t *new_val, int16_t new_val_size);
+ esp_err_t write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type);
+ BLEService *service;
+};
+
+} // namespace esp32_ble_client
+} // namespace esphome
+
+#endif // USE_ESP32
diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp
new file mode 100644
index 0000000000..40eff49266
--- /dev/null
+++ b/esphome/components/esp32_ble_client/ble_client_base.cpp
@@ -0,0 +1,418 @@
+#include "ble_client_base.h"
+
+#include "esphome/core/helpers.h"
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace esp32_ble_client {
+
+static const char *const TAG = "esp32_ble_client";
+static const esp_bt_uuid_t NOTIFY_DESC_UUID = {
+ .len = ESP_UUID_LEN_16,
+ .uuid =
+ {
+ .uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG,
+ },
+};
+
+void BLEClientBase::setup() {
+ static uint8_t connection_index = 0;
+ this->connection_index_ = connection_index++;
+
+ auto ret = esp_ble_gattc_app_register(this->app_id);
+ if (ret) {
+ ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
+ this->mark_failed();
+ }
+ this->set_state(espbt::ClientState::IDLE);
+}
+
+void BLEClientBase::loop() {
+ // READY_TO_CONNECT means we have discovered the device
+ // and the scanner has been stopped by the tracker.
+ if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
+ this->connect();
+ }
+}
+
+float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
+
+bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
+ if (this->address_ == 0 || device.address_uint64() != this->address_)
+ return false;
+ if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING)
+ return false;
+
+ ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str());
+ this->set_state(espbt::ClientState::DISCOVERED);
+
+ auto addr = device.address_uint64();
+ this->remote_bda_[0] = (addr >> 40) & 0xFF;
+ this->remote_bda_[1] = (addr >> 32) & 0xFF;
+ this->remote_bda_[2] = (addr >> 24) & 0xFF;
+ this->remote_bda_[3] = (addr >> 16) & 0xFF;
+ this->remote_bda_[4] = (addr >> 8) & 0xFF;
+ this->remote_bda_[5] = (addr >> 0) & 0xFF;
+ this->remote_addr_type_ = device.get_address_type();
+ return true;
+}
+
+void BLEClientBase::connect() {
+ ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(),
+ this->remote_addr_type_);
+ this->paired_ = false;
+ auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
+ if (ret) {
+ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
+ ret);
+ this->set_state(espbt::ClientState::IDLE);
+ } else {
+ this->set_state(espbt::ClientState::CONNECTING);
+ }
+}
+
+esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
+
+void BLEClientBase::disconnect() {
+ if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING)
+ return;
+ ESP_LOGI(TAG, "[%d] [%s] Disconnecting.", this->connection_index_, this->address_str_.c_str());
+ auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
+ if (err != ESP_OK) {
+ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(),
+ err);
+ }
+
+ if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT ||
+ this->state_ == espbt::ClientState::DISCOVERED) {
+ this->set_address(0);
+ this->set_state(espbt::ClientState::IDLE);
+ } else {
+ this->set_state(espbt::ClientState::DISCONNECTING);
+ }
+}
+
+void BLEClientBase::release_services() {
+ for (auto &svc : this->services_)
+ delete svc; // NOLINT(cppcoreguidelines-owning-memory)
+ this->services_.clear();
+#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
+ esp_ble_gattc_cache_clean(this->remote_bda_);
+#endif
+}
+
+bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
+ esp_ble_gattc_cb_param_t *param) {
+ if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
+ return false;
+ if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_)
+ return false;
+
+ ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_,
+ this->address_str_.c_str(), event, esp_gattc_if);
+
+ switch (event) {
+ case ESP_GATTC_REG_EVT: {
+ if (param->reg.status == ESP_GATT_OK) {
+ ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(),
+ this->app_id);
+ this->gattc_if_ = esp_gattc_if;
+ } else {
+ ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_,
+ this->address_str_.c_str(), param->reg.app_id, param->reg.status);
+ }
+ break;
+ }
+ case ESP_GATTC_OPEN_EVT: {
+ ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str());
+ this->conn_id_ = param->open.conn_id;
+ this->service_count_ = 0;
+ if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
+ ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(),
+ param->open.status);
+ this->set_state(espbt::ClientState::IDLE);
+ break;
+ }
+ auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
+ if (ret) {
+ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
+ this->address_str_.c_str(), ret);
+ }
+ if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
+ ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
+ this->set_state(espbt::ClientState::CONNECTED);
+ this->state_ = espbt::ClientState::ESTABLISHED;
+ break;
+ }
+ esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
+ break;
+ }
+ case ESP_GATTC_CFG_MTU_EVT: {
+ if (param->cfg_mtu.status != ESP_GATT_OK) {
+ ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_,
+ this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status);
+ this->set_state(espbt::ClientState::IDLE);
+ break;
+ }
+ ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
+ param->cfg_mtu.status, param->cfg_mtu.mtu);
+ this->mtu_ = param->cfg_mtu.mtu;
+ break;
+ }
+ case ESP_GATTC_DISCONNECT_EVT: {
+ if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0)
+ return false;
+ ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_,
+ this->address_str_.c_str(), param->disconnect.reason);
+ this->release_services();
+ this->set_state(espbt::ClientState::IDLE);
+ break;
+ }
+ case ESP_GATTC_SEARCH_RES_EVT: {
+ this->service_count_++;
+ if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
+ // V3 clients don't need services initialized since
+ // they only request by handle after receiving the services.
+ break;
+ }
+ BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory)
+ ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid);
+ ble_service->start_handle = param->search_res.start_handle;
+ ble_service->end_handle = param->search_res.end_handle;
+ ble_service->client = this;
+ this->services_.push_back(ble_service);
+ break;
+ }
+ case ESP_GATTC_SEARCH_CMPL_EVT: {
+ ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str());
+ for (auto &svc : this->services_) {
+ ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
+ svc->uuid.to_string().c_str());
+ ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_,
+ this->address_str_.c_str(), svc->start_handle, svc->end_handle);
+ }
+ ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
+ this->set_state(espbt::ClientState::CONNECTED);
+ this->state_ = espbt::ClientState::ESTABLISHED;
+ break;
+ }
+ case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
+ if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
+ this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
+ // Client is responsible for flipping the descriptor value
+ // when using the cache
+ break;
+ }
+ esp_gattc_descr_elem_t desc_result;
+ uint16_t count = 1;
+ esp_gatt_status_t descr_status =
+ esp_ble_gattc_get_descr_by_char_handle(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle,
+ NOTIFY_DESC_UUID, &desc_result, &count);
+ if (descr_status != ESP_GATT_OK) {
+ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_,
+ this->address_str_.c_str(), descr_status);
+ break;
+ }
+ esp_gattc_char_elem_t char_result;
+ esp_gatt_status_t char_status =
+ esp_ble_gattc_get_all_char(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle,
+ param->reg_for_notify.handle, &char_result, &count, 0);
+ if (char_status != ESP_GATT_OK) {
+ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
+ this->address_str_.c_str(), char_status);
+ break;
+ }
+
+ /*
+ 1 = notify
+ 2 = indicate
+ */
+ uint16_t notify_en = char_result.properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY ? 1 : 2;
+ esp_err_t status =
+ esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
+ (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
+ if (status) {
+ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_,
+ this->address_str_.c_str(), status);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ return true;
+}
+
+void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
+ switch (event) {
+ // This event is sent by the server when it requests security
+ case ESP_GAP_BLE_SEC_REQ_EVT:
+ if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
+ break;
+ ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event);
+ esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
+ break;
+ // This event is sent once authentication has completed
+ case ESP_GAP_BLE_AUTH_CMPL_EVT:
+ if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
+ break;
+ esp_bd_addr_t bd_addr;
+ memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
+ ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(),
+ format_hex(bd_addr, 6).c_str());
+ if (!param->ble_security.auth_cmpl.success) {
+ ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(),
+ param->ble_security.auth_cmpl.fail_reason);
+ } else {
+ this->paired_ = true;
+ ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
+ this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type,
+ param->ble_security.auth_cmpl.auth_mode);
+ }
+ break;
+ // There are other events we'll want to implement at some point to support things like pass key
+ // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md
+ default:
+ break;
+ }
+}
+
+// Parse GATT values into a float for a sensor.
+// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/
+float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) {
+ // A length of one means a single octet value.
+ if (length == 0)
+ return 0;
+ if (length == 1)
+ return (float) ((uint8_t) value[0]);
+
+ switch (value[0]) {
+ case 0x1: // boolean.
+ case 0x2: // 2bit.
+ case 0x3: // nibble.
+ case 0x4: // uint8.
+ return (float) ((uint8_t) value[1]);
+ case 0x5: // uint12.
+ case 0x6: // uint16.
+ if (length > 2) {
+ return (float) encode_uint16(value[1], value[2]);
+ }
+ // fall through
+ case 0x7: // uint24.
+ if (length > 3) {
+ return (float) encode_uint24(value[1], value[2], value[3]);
+ }
+ // fall through
+ case 0x8: // uint32.
+ if (length > 4) {
+ return (float) encode_uint32(value[1], value[2], value[3], value[4]);
+ }
+ // fall through
+ case 0xC: // int8.
+ return (float) ((int8_t) value[1]);
+ case 0xD: // int12.
+ case 0xE: // int16.
+ if (length > 2) {
+ return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]);
+ }
+ // fall through
+ case 0xF: // int24.
+ if (length > 3) {
+ return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3]));
+ }
+ // fall through
+ case 0x10: // int32.
+ if (length > 4) {
+ return (float) ((int32_t) (value[1] << 24) + (int32_t) (value[2] << 16) + (int32_t) (value[3] << 8) +
+ (int32_t) (value[4]));
+ }
+ }
+ ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_,
+ this->address_str_.c_str(), value[0], length);
+ return NAN;
+}
+
+BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) {
+ for (auto *svc : this->services_) {
+ if (svc->uuid == uuid)
+ return svc;
+ }
+ return nullptr;
+}
+
+BLEService *BLEClientBase::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); }
+
+BLECharacteristic *BLEClientBase::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) {
+ auto *svc = this->get_service(service);
+ if (svc == nullptr)
+ return nullptr;
+ return svc->get_characteristic(chr);
+}
+
+BLECharacteristic *BLEClientBase::get_characteristic(uint16_t service, uint16_t chr) {
+ return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr));
+}
+
+BLECharacteristic *BLEClientBase::get_characteristic(uint16_t handle) {
+ for (auto *svc : this->services_) {
+ if (!svc->parsed)
+ svc->parse_characteristics();
+ for (auto *chr : svc->characteristics) {
+ if (chr->handle == handle)
+ return chr;
+ }
+ }
+ return nullptr;
+}
+
+BLEDescriptor *BLEClientBase::get_config_descriptor(uint16_t handle) {
+ auto *chr = this->get_characteristic(handle);
+ if (chr != nullptr) {
+ if (!chr->parsed)
+ chr->parse_descriptors();
+ for (auto &desc : chr->descriptors) {
+ if (desc->uuid.get_uuid().uuid.uuid16 == ESP_GATT_UUID_CHAR_CLIENT_CONFIG)
+ return desc;
+ }
+ }
+ return nullptr;
+}
+
+BLEDescriptor *BLEClientBase::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) {
+ auto *svc = this->get_service(service);
+ if (svc == nullptr)
+ return nullptr;
+ auto *ch = svc->get_characteristic(chr);
+ if (ch == nullptr)
+ return nullptr;
+ return ch->get_descriptor(descr);
+}
+
+BLEDescriptor *BLEClientBase::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) {
+ return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr),
+ espbt::ESPBTUUID::from_uint16(descr));
+}
+
+BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) {
+ for (auto *svc : this->services_) {
+ if (!svc->parsed)
+ svc->parse_characteristics();
+ for (auto *chr : svc->characteristics) {
+ if (!chr->parsed)
+ chr->parse_descriptors();
+ for (auto *desc : chr->descriptors) {
+ if (desc->handle == handle)
+ return desc;
+ }
+ }
+ }
+ return nullptr;
+}
+
+} // namespace esp32_ble_client
+} // namespace esphome
+
+#endif // USE_ESP32
diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h
new file mode 100644
index 0000000000..97886d0b19
--- /dev/null
+++ b/esphome/components/esp32_ble_client/ble_client_base.h
@@ -0,0 +1,101 @@
+#pragma once
+
+#ifdef USE_ESP32
+
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/core/component.h"
+
+#include "ble_service.h"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace esphome {
+namespace esp32_ble_client {
+
+namespace espbt = esphome::esp32_ble_tracker;
+
+class BLEClientBase : public espbt::ESPBTClient, public Component {
+ public:
+ void setup() override;
+ void loop() override;
+ float get_setup_priority() const override;
+
+ bool parse_device(const espbt::ESPBTDevice &device) override;
+ void on_scan_end() override {}
+ bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
+ esp_ble_gattc_cb_param_t *param) override;
+ void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
+ void connect() override;
+ esp_err_t pair();
+ void disconnect();
+ void release_services();
+
+ bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
+
+ void set_address(uint64_t address) {
+ this->address_ = address;
+ if (address == 0) {
+ memset(this->remote_bda_, 0, sizeof(this->remote_bda_));
+ this->address_str_ = "";
+ } else {
+ this->address_str_ =
+ str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, (uint8_t) (this->address_ >> 40) & 0xff,
+ (uint8_t) (this->address_ >> 32) & 0xff, (uint8_t) (this->address_ >> 24) & 0xff,
+ (uint8_t) (this->address_ >> 16) & 0xff, (uint8_t) (this->address_ >> 8) & 0xff,
+ (uint8_t) (this->address_ >> 0) & 0xff);
+ }
+ }
+ std::string address_str() const { return this->address_str_; }
+
+ BLEService *get_service(espbt::ESPBTUUID uuid);
+ BLEService *get_service(uint16_t uuid);
+ BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr);
+ BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr);
+ BLECharacteristic *get_characteristic(uint16_t handle);
+ BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr);
+ BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr);
+ BLEDescriptor *get_descriptor(uint16_t handle);
+ // Get the configuration descriptor for the given characteristic handle.
+ BLEDescriptor *get_config_descriptor(uint16_t handle);
+
+ float parse_char_value(uint8_t *value, uint16_t length);
+
+ int get_gattc_if() const { return this->gattc_if_; }
+ uint8_t *get_remote_bda() { return this->remote_bda_; }
+ esp_ble_addr_type_t get_remote_addr_type() const { return this->remote_addr_type_; }
+ void set_remote_addr_type(esp_ble_addr_type_t address_type) { this->remote_addr_type_ = address_type; }
+ uint16_t get_conn_id() const { return this->conn_id_; }
+ uint64_t get_address() const { return this->address_; }
+ bool is_paired() const { return this->paired_; }
+
+ uint8_t get_connection_index() const { return this->connection_index_; }
+
+ virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; }
+
+ protected:
+ int gattc_if_;
+ esp_bd_addr_t remote_bda_;
+ esp_ble_addr_type_t remote_addr_type_;
+ uint16_t conn_id_{0xFFFF};
+ uint64_t address_{0};
+ std::string address_str_{};
+ uint8_t connection_index_;
+ int16_t service_count_{0};
+ uint16_t mtu_{23};
+ bool paired_{false};
+ espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
+
+ std::vector services_;
+};
+
+} // namespace esp32_ble_client
+} // namespace esphome
+
+#endif // USE_ESP32
diff --git a/esphome/components/esp32_ble_client/ble_descriptor.h b/esphome/components/esp32_ble_client/ble_descriptor.h
new file mode 100644
index 0000000000..c05430144f
--- /dev/null
+++ b/esphome/components/esp32_ble_client/ble_descriptor.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#ifdef USE_ESP32
+
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+
+namespace esphome {
+namespace esp32_ble_client {
+
+namespace espbt = esphome::esp32_ble_tracker;
+
+class BLECharacteristic;
+
+class BLEDescriptor {
+ public:
+ espbt::ESPBTUUID uuid;
+ uint16_t handle;
+
+ BLECharacteristic *characteristic;
+};
+
+} // namespace esp32_ble_client
+} // namespace esphome
+
+#endif // USE_ESP32
diff --git a/esphome/components/esp32_ble_client/ble_service.cpp b/esphome/components/esp32_ble_client/ble_service.cpp
new file mode 100644
index 0000000000..b22d2a1788
--- /dev/null
+++ b/esphome/components/esp32_ble_client/ble_service.cpp
@@ -0,0 +1,77 @@
+#include "ble_service.h"
+#include "ble_client_base.h"
+
+#include "esphome/core/log.h"
+
+#ifdef USE_ESP32
+
+namespace esphome {
+namespace esp32_ble_client {
+
+static const char *const TAG = "esp32_ble_client";
+
+BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) {
+ if (!this->parsed)
+ this->parse_characteristics();
+ for (auto &chr : this->characteristics) {
+ if (chr->uuid == uuid)
+ return chr;
+ }
+ return nullptr;
+}
+
+BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) {
+ return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid));
+}
+
+BLEService::~BLEService() {
+ for (auto &chr : this->characteristics)
+ delete chr; // NOLINT(cppcoreguidelines-owning-memory)
+}
+
+void BLEService::release_characteristics() {
+ this->parsed = false;
+ for (auto &chr : this->characteristics)
+ delete chr; // NOLINT(cppcoreguidelines-owning-memory)
+ this->characteristics.clear();
+}
+
+void BLEService::parse_characteristics() {
+ this->parsed = true;
+ uint16_t offset = 0;
+ esp_gattc_char_elem_t result;
+
+ while (true) {
+ uint16_t count = 1;
+ esp_gatt_status_t status =
+ esp_ble_gattc_get_all_char(this->client->get_gattc_if(), this->client->get_conn_id(), this->start_handle,
+ this->end_handle, &result, &count, offset);
+ if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) {
+ break;
+ }
+ if (status != ESP_GATT_OK) {
+ ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(),
+ this->client->address_str().c_str(), status);
+ break;
+ }
+ if (count == 0) {
+ break;
+ }
+
+ BLECharacteristic *characteristic = new BLECharacteristic(); // NOLINT(cppcoreguidelines-owning-memory)
+ characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid);
+ characteristic->properties = result.properties;
+ characteristic->handle = result.char_handle;
+ characteristic->service = this;
+ this->characteristics.push_back(characteristic);
+ ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(),
+ this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle,
+ characteristic->properties);
+ offset++;
+ }
+}
+
+} // namespace esp32_ble_client
+} // namespace esphome
+
+#endif // USE_ESP32
diff --git a/esphome/components/esp32_ble_client/ble_service.h b/esphome/components/esp32_ble_client/ble_service.h
new file mode 100644
index 0000000000..41fc3e838b
--- /dev/null
+++ b/esphome/components/esp32_ble_client/ble_service.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#ifdef USE_ESP32
+
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+
+#include "ble_characteristic.h"
+
+#include
+
+namespace esphome {
+namespace esp32_ble_client {
+
+namespace espbt = esphome::esp32_ble_tracker;
+
+class BLEClientBase;
+
+class BLEService {
+ public:
+ ~BLEService();
+ bool parsed = false;
+ espbt::ESPBTUUID uuid;
+ uint16_t start_handle;
+ uint16_t end_handle;
+ std::vector characteristics;
+ BLEClientBase *client;
+ void parse_characteristics();
+ void release_characteristics();
+ BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid);
+ BLECharacteristic *get_characteristic(uint16_t uuid);
+};
+
+} // namespace esp32_ble_client
+} // namespace esphome
+
+#endif // USE_ESP32
diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py
index 2fcc5c7743..0ddfa62c1b 100644
--- a/esphome/components/esp32_ble_server/__init__.py
+++ b/esphome/components/esp32_ble_server/__init__.py
@@ -7,21 +7,25 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
AUTO_LOAD = ["esp32_ble"]
CODEOWNERS = ["@jesserockz"]
-CONFLICTS_WITH = ["esp32_ble_tracker", "esp32_ble_beacon"]
+CONFLICTS_WITH = ["esp32_ble_beacon"]
DEPENDENCIES = ["esp32"]
CONF_MANUFACTURER = "manufacturer"
-CONF_BLE_ID = "ble_id"
esp32_ble_server_ns = cg.esphome_ns.namespace("esp32_ble_server")
-BLEServer = esp32_ble_server_ns.class_("BLEServer", cg.Component)
+BLEServer = esp32_ble_server_ns.class_(
+ "BLEServer",
+ cg.Component,
+ esp32_ble.GATTsEventHandler,
+ cg.Parented.template(esp32_ble.ESP32BLE),
+)
BLEServiceComponent = esp32_ble_server_ns.class_("BLEServiceComponent")
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLEServer),
- cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
+ cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_MANUFACTURER, default="ESPHome"): cv.string,
cv.Optional(CONF_MODEL): cv.string,
}
@@ -29,16 +33,18 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
- parent = await cg.get_variable(config[CONF_BLE_ID])
var = cg.new_Pvariable(config[CONF_ID])
+
await cg.register_component(var, config)
+ parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
+ cg.add(parent.register_gatts_event_handler(var))
+ cg.add(var.set_parent(parent))
+
cg.add(var.set_manufacturer(config[CONF_MANUFACTURER]))
if CONF_MODEL in config:
cg.add(var.set_model(config[CONF_MODEL]))
cg.add_define("USE_ESP32_BLE_SERVER")
- cg.add(parent.set_server(var))
-
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp
index df822ac0b9..15a51f6ede 100644
--- a/esphome/components/esp32_ble_server/ble_characteristic.cpp
+++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp
@@ -148,44 +148,44 @@ bool BLECharacteristic::is_failed() {
void BLECharacteristic::set_broadcast_property(bool value) {
if (value) {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST);
} else {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST);
}
}
void BLECharacteristic::set_indicate_property(bool value) {
if (value) {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE);
} else {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE);
}
}
void BLECharacteristic::set_notify_property(bool value) {
if (value) {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY);
} else {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY);
}
}
void BLECharacteristic::set_read_property(bool value) {
if (value) {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ);
} else {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ);
}
}
void BLECharacteristic::set_write_property(bool value) {
if (value) {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE);
} else {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE);
}
}
void BLECharacteristic::set_write_no_response_property(bool value) {
if (value) {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
} else {
- this->properties_ = (esp_gatt_char_prop_t)(this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
+ this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
}
}
diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp
index 15bea07021..7cbf40c076 100644
--- a/esphome/components/esp32_ble_server/ble_server.cpp
+++ b/esphome/components/esp32_ble_server/ble_server.cpp
@@ -25,7 +25,8 @@ static const uint16_t VERSION_UUID = 0x2A26;
static const uint16_t MANUFACTURER_UUID = 0x2A29;
void BLEServer::setup() {
- if (this->is_failed()) {
+ if (this->parent_->is_failed()) {
+ this->mark_failed();
ESP_LOGE(TAG, "BLE Server was marked failed by ESP32BLE");
return;
}
diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h
index d275eeab01..ac759f2dcd 100644
--- a/esphome/components/esp32_ble_server/ble_server.h
+++ b/esphome/components/esp32_ble_server/ble_server.h
@@ -3,6 +3,7 @@
#include "ble_service.h"
#include "ble_characteristic.h"
+#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble/ble_advertising.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/queue.h"
@@ -12,6 +13,7 @@
#include
#include
+#include
#ifdef USE_ESP32
@@ -31,7 +33,7 @@ class BLEServiceComponent {
virtual void stop();
};
-class BLEServer : public Component {
+class BLEServer : public Component, public GATTsEventHandler, public Parented {
public:
void setup() override;
void loop() override;
@@ -54,7 +56,8 @@ class BLEServer : public Component {
uint32_t get_connected_client_count() { return this->connected_clients_; }
const std::map