migrate to using same area info for top level and sub devices

This commit is contained in:
J. Nick Koston
2025-06-21 16:37:21 +02:00
parent 86fb0e317f
commit 7d84f0e650
9 changed files with 136 additions and 53 deletions

View File

@@ -188,7 +188,7 @@ message DeviceInfoRequest {
// Empty
}
message SubAreaInfo {
message AreaInfo {
uint32 area_id = 1;
string name = 2;
}
@@ -249,7 +249,10 @@ message DeviceInfoResponse {
bool api_encryption_supported = 19;
repeated SubDeviceInfo sub_devices = 20;
repeated SubAreaInfo sub_areas = 21;
repeated AreaInfo areas = 21;
// Top-level area info to phase out suggested_area
AreaInfo area = 22;
}
message ListEntitiesRequest {

View File

@@ -1628,11 +1628,13 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
sub_device_info.area_id = sub_device->get_area_id();
resp.sub_devices.push_back(sub_device_info);
}
#endif
#ifdef USE_AREAS
for (auto const &area : App.get_areas()) {
SubAreaInfo area_info;
AreaInfo area_info;
area_info.area_id = area->get_area_id();
area_info.name = area->get_name();
resp.sub_areas.push_back(area_info);
resp.areas.push_back(area_info);
}
#endif
return resp;

View File

@@ -812,7 +812,7 @@ void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); }
#endif
bool SubAreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) {
bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->area_id = value.as_uint32();
@@ -822,7 +822,7 @@ bool SubAreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) {
return false;
}
}
bool SubAreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->name = value.as_string();
@@ -832,18 +832,18 @@ bool SubAreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
return false;
}
}
void SubAreaInfo::encode(ProtoWriteBuffer buffer) const {
void AreaInfo::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->area_id);
buffer.encode_string(2, this->name);
}
void SubAreaInfo::calculate_size(uint32_t &total_size) const {
void AreaInfo::calculate_size(uint32_t &total_size) const {
ProtoSize::add_uint32_field(total_size, 1, this->area_id, false);
ProtoSize::add_string_field(total_size, 1, this->name, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void SubAreaInfo::dump_to(std::string &out) const {
void AreaInfo::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("SubAreaInfo {\n");
out.append("AreaInfo {\n");
out.append(" area_id: ");
sprintf(buffer, "%" PRIu32, this->area_id);
out.append(buffer);
@@ -998,7 +998,11 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v
return true;
}
case 21: {
this->sub_areas.push_back(value.as_message<SubAreaInfo>());
this->areas.push_back(value.as_message<AreaInfo>());
return true;
}
case 22: {
this->area = value.as_message<AreaInfo>();
return true;
}
default:
@@ -1028,9 +1032,10 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->sub_devices) {
buffer.encode_message<SubDeviceInfo>(20, it, true);
}
for (auto &it : this->sub_areas) {
buffer.encode_message<SubAreaInfo>(21, it, true);
for (auto &it : this->areas) {
buffer.encode_message<AreaInfo>(21, it, true);
}
buffer.encode_message<AreaInfo>(22, this->area);
}
void DeviceInfoResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->uses_password, false);
@@ -1053,7 +1058,8 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address, false);
ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported, false);
ProtoSize::add_repeated_message(total_size, 2, this->sub_devices);
ProtoSize::add_repeated_message(total_size, 2, this->sub_areas);
ProtoSize::add_repeated_message(total_size, 2, this->areas);
ProtoSize::add_message_object(total_size, 2, this->area, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfoResponse::dump_to(std::string &out) const {
@@ -1146,11 +1152,15 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
out.append("\n");
}
for (const auto &it : this->sub_areas) {
out.append(" sub_areas: ");
for (const auto &it : this->areas) {
out.append(" areas: ");
it.dump_to(out);
out.append("\n");
}
out.append(" area: ");
this->area.dump_to(out);
out.append("\n");
out.append("}");
}
#endif

View File

@@ -416,7 +416,7 @@ class DeviceInfoRequest : public ProtoMessage {
protected:
};
class SubAreaInfo : public ProtoMessage {
class AreaInfo : public ProtoMessage {
public:
uint32_t area_id{0};
std::string name{};
@@ -448,7 +448,7 @@ class SubDeviceInfo : public ProtoMessage {
class DeviceInfoResponse : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 201;
static constexpr uint16_t ESTIMATED_SIZE = 219;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "device_info_response"; }
#endif
@@ -472,7 +472,8 @@ class DeviceInfoResponse : public ProtoMessage {
std::string bluetooth_mac_address{};
bool api_encryption_supported{false};
std::vector<SubDeviceInfo> sub_devices{};
std::vector<SubAreaInfo> sub_areas{};
std::vector<AreaInfo> areas{};
AreaInfo area{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -11,7 +11,9 @@
#ifdef USE_SUB_DEVICE
#include "esphome/core/sub_device.h"
#include "esphome/core/sub_area.h"
#endif
#ifdef USE_AREAS
#include "esphome/core/area.h"
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
@@ -92,7 +94,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick
class Application {
public:
void pre_setup(const std::string &name, const std::string &friendly_name, const char *area, const char *comment,
void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment,
const char *compilation_time, bool name_add_mac_suffix) {
arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix;
@@ -107,14 +109,16 @@ class Application {
this->name_ = name;
this->friendly_name_ = friendly_name;
}
this->area_ = area;
// area is now handled through the areas system
this->comment_ = comment;
this->compilation_time_ = compilation_time;
}
#ifdef USE_SUB_DEVICE
void register_sub_device(SubDevice *sub_device) { this->sub_devices_.push_back(sub_device); }
void register_area(SubArea *area) { this->areas_.push_back(area); }
#endif
#ifdef USE_AREAS
void register_area(Area *area) { this->areas_.push_back(area); }
#endif
void set_current_component(Component *component) { this->current_component_ = component; }
@@ -295,7 +299,15 @@ class Application {
const std::string &get_friendly_name() const { return this->friendly_name_; }
/// Get the area of this Application set by pre_setup().
std::string get_area() const { return this->area_ == nullptr ? "" : this->area_; }
std::string get_area() const {
#ifdef USE_AREAS
// If we have areas registered, return the name of the first one (which is the top-level area)
if (!this->areas_.empty() && this->areas_[0] != nullptr) {
return this->areas_[0]->get_name();
}
#endif
return "";
}
/// Get the comment of this Application set by pre_setup().
std::string get_comment() const { return this->comment_; }
@@ -346,7 +358,9 @@ class Application {
#ifdef USE_SUB_DEVICE
const std::vector<SubDevice *> &get_sub_devices() { return this->sub_devices_; }
const std::vector<SubArea *> &get_areas() { return this->areas_; }
#endif
#ifdef USE_AREAS
const std::vector<Area *> &get_areas() { return this->areas_; }
#endif
#ifdef USE_BINARY_SENSOR
const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; }
@@ -626,7 +640,9 @@ class Application {
#ifdef USE_SUB_DEVICE
std::vector<SubDevice *> sub_devices_{};
std::vector<SubArea *> areas_{};
#endif
#ifdef USE_AREAS
std::vector<Area *> areas_{};
#endif
#ifdef USE_BINARY_SENSOR
std::vector<binary_sensor::BinarySensor *> binary_sensors_{};
@@ -694,7 +710,6 @@ class Application {
std::string name_;
std::string friendly_name_;
const char *area_{nullptr};
const char *comment_{nullptr};
const char *compilation_time_{nullptr};
bool name_add_mac_suffix_;

View File

@@ -5,7 +5,7 @@
namespace esphome {
class SubArea {
class Area {
public:
void set_area_id(uint32_t area_id) { area_id_ = area_id; }
uint32_t get_area_id() { return area_id_; }

View File

@@ -40,6 +40,7 @@ from esphome.helpers import (
copy_file_if_changed,
fnv1a_32bit_hash,
get_str_env,
slugify,
walk_files,
)
@@ -58,7 +59,7 @@ ProjectUpdateTrigger = cg.esphome_ns.class_(
"ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string)
)
SubDevice = cg.esphome_ns.class_("SubDevice")
SubArea = cg.esphome_ns.class_("SubArea")
Area = cg.esphome_ns.class_("Area")
VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"}
@@ -127,7 +128,15 @@ CONFIG_SCHEMA = cv.All(
{
cv.Required(CONF_NAME): cv.valid_name,
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string,
cv.Optional(CONF_AREA, ""): cv.string,
cv.Optional(CONF_AREA): cv.Any(
cv.string, # Old way: just a string
cv.Schema( # New way: structured area
{
cv.GenerateID(CONF_ID): cv.declare_id(Area),
cv.Required(CONF_NAME): cv.string,
}
),
),
cv.Optional(CONF_COMMENT): cv.string,
cv.Required(CONF_BUILD_PATH): cv.string,
cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
@@ -180,7 +189,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SUB_AREAS, default=[]): cv.ensure_list(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(SubArea),
cv.GenerateID(CONF_ID): cv.declare_id(Area),
cv.Required(CONF_NAME): cv.string,
}
),
@@ -190,7 +199,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(CONF_ID): cv.declare_id(SubDevice),
cv.Required(CONF_NAME): cv.string,
cv.Optional(CONF_AREA_ID): cv.use_id(SubArea),
cv.Optional(CONF_AREA_ID): cv.use_id(Area),
}
),
),
@@ -374,7 +383,6 @@ async def to_code(config):
cg.App.pre_setup(
config[CONF_NAME],
config[CONF_FRIENDLY_NAME],
config[CONF_AREA],
config.get(CONF_COMMENT, ""),
cg.RawExpression('__DATE__ ", " __TIME__'),
config[CONF_NAME_ADD_MAC_SUFFIX],
@@ -445,6 +453,38 @@ async def to_code(config):
if config[CONF_PLATFORMIO_OPTIONS]:
CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS])
# Handle area configuration
if area_conf := config.get(CONF_AREA):
if isinstance(area_conf, dict):
# New way: structured area configuration
area_var = cg.new_Pvariable(area_conf[CONF_ID])
area_id = fnv1a_32bit_hash(str(area_conf[CONF_ID]))
area_name = area_conf[CONF_NAME]
else:
# Old way: string-based area (deprecated)
area_slug = slugify(area_conf)
_LOGGER.warning(
"Using 'area' as a string is deprecated. Please use the new format:\n"
"area:\n"
" id: %s\n"
' name: "%s"',
area_slug,
area_conf,
)
# Create a synthetic area for backwards compatibility
area_var = cg.new_Pvariable(
cg.ID(f"area_{area_slug}", is_declaration=True, type=Area)
)
area_id = fnv1a_32bit_hash(area_conf)
area_name = area_conf
# Common setup for both ways
cg.add(area_var.set_area_id(area_id))
cg.add(area_var.set_name(area_name))
cg.add(cg.App.register_area(area_var))
# Define USE_AREAS to enable area processing
cg.add_define("USE_AREAS")
# Process sub-devices and areas
if sub_devices := config.get(CONF_SUB_DEVICES):
# Process areas first
@@ -455,6 +495,8 @@ async def to_code(config):
cg.add(area.set_area_id(area_id))
cg.add(area.set_name(area_conf[CONF_NAME]))
cg.add(cg.App.register_area(area))
# Define USE_AREAS since we have areas
cg.add_define("USE_AREAS")
# Process sub-devices
for dev_conf in sub_devices:

View File

@@ -1,25 +1,9 @@
from __future__ import annotations
import unicodedata
from esphome.const import ALLOWED_NAME_CHARS
def strip_accents(value):
return "".join(
c
for c in unicodedata.normalize("NFD", str(value))
if unicodedata.category(c) != "Mn"
)
from esphome.helpers import slugify
def friendly_name_slugify(value):
value = (
strip_accents(value)
.lower()
.replace(" ", "-")
.replace("_", "-")
.replace("--", "-")
.strip("-")
)
return "".join(c for c in value if c in ALLOWED_NAME_CHARS)
"""Convert a friendly name to a slug with dashes instead of underscores."""
# First use the standard slugify, then convert underscores to dashes
return slugify(value).replace("_", "-")

View File

@@ -38,6 +38,32 @@ def fnv1a_32bit_hash(string: str) -> int:
return hash_value
def strip_accents(value: str) -> str:
"""Remove accents from a string."""
import unicodedata
return "".join(
c
for c in unicodedata.normalize("NFD", str(value))
if unicodedata.category(c) != "Mn"
)
def slugify(value: str) -> str:
"""Convert a string to a valid C++ identifier slug."""
from esphome.const import ALLOWED_NAME_CHARS
value = (
strip_accents(value)
.lower()
.replace(" ", "_")
.replace("-", "_")
.replace("__", "_")
.strip("_")
)
return "".join(c for c in value if c in ALLOWED_NAME_CHARS)
def indent_all_but_first_and_last(text, padding=" "):
lines = text.splitlines(True)
if len(lines) <= 2: