Skip to content

[core] All entity platforms declare their EntityType in the constructor - #64

Merged
zkoalexey merged 4 commits into
dev-jethubfrom
JXD-316-entity-type
Aug 25, 2026
Merged

[core] All entity platforms declare their EntityType in the constructor#64
zkoalexey merged 4 commits into
dev-jethubfrom
JXD-316-entity-type

Conversation

@zkoalexey

@zkoalexey zkoalexey commented Aug 12, 2026

Copy link
Copy Markdown

Every entity platform now passes its EntityType to the EntityBase constructor, so entity->type() is meaningful for all 21 entity types instead of just three.

Why

EntityType is the fork's own mechanism: EntityBase(EntityType) in core/entity_base.h, the enum in core/entity_types.h, and App.get_entity_by_key(type, key) in core/application.h:496, which switches on the type and falls through to default: return nullptr. Only Sensor, BinarySensor and Switch ever set their type; every other entity class stayed EntityType::NONE.

That is not just a coverage gap — it silently breaks entity renaming on shipping devices. The user_names component (in jethome-iot/esphome-components) stores the type next to the name and looks the entity back up by it on boot:

this->type = entity->type();                     // user_names_component.h:31   -> "type": "none"
...
EntityBase *entity =
    App.get_entity_by_key(record->type, hash, false);   // user_names_component.cpp:49 -> nullptr

PUT /api/user_names itself works — that request carries the entity type in its body — so the rename lands and the record is persisted, with "type": "none". On the next boot the lookup hits the default: branch, the log says Entity not found for source_name '...' (type: none), and the custom name is gone. Only sensors, binary sensors and switches survive a restart.

This is reachable on current hardware. jxd-cpu-e1eth.yaml — the include that enables user_names on every E1-ETH board — declares number and text_sensor entities, and the feature packages jxd-r6-e1eth-lcd.yaml pulls in add select (firmware-type-selector.yaml), plus button and update (firmware-update.yaml). All five report EntityType::NONE today.

The other consumer is display_menu_base, which picks a renderer by exact type match and skips the entity when there is none (display_menu_base.cpp:64entity->type() != EntityType::NONE). Only SENSOR / SWITCH / BINARY_SENSOR renderers exist, so newly typed entities are still skipped exactly as before — nothing is routed anywhere new by this change.

Scope

19 files, one line each — a constructor initialiser. No signature changes, nothing dispatches on the new values inside the framework itself: alarm_control_panel, button, climate, cover, datetime (date / time / datetime), event, fan, light, lock, media_player, number, select, text, text_sensor, update, valve.

The branch was written in August 2025 against a much older dev-jethub and has been merged up with current dev-jethub (0902dec93c) in 84e0ee55b3. The merge is clean and the diff against the base is unchanged — the same 19 lines — so the CI run on this head is the check that counts.

Verification

Compiled against the earlier base. jxd-r6-e1eth-lcd only instantiates sensor / binary_sensor / switch / text_sensor / number, i.e. 2 of the 19 touched files, so two throwaway configs covered the rest:

  • jxd-r6-e1eth-lcd.yaml (esp32, esp-idf) — successfully compiled.
  • esp-idf config with template platforms + binary light, binary fan, template event, thermostat climate, http_request update — covers light, fan, event, date/time/datetime, valve, alarm_control_panel, lock, cover, button, select, text, climate, update — successfully compiled.
  • arduino config with i2s_audio media_player (that platform is arduino-only) — successfully compiled.

All 19 touched files therefore went through the compiler. clang-format --dry-run -Werror is clean on all of them.

Checked specifically, since turning implicit constructors into user-provided ones is the real hazard here: no codegen path instantiates a base entity class directly — every generated object is a derived type (TemplateDate, TemplateNumber, …) whose own default constructor is still implicit, so value-initialisation still zero-initialises the whole object including members like Number::state and DateEntity::year_. The one site in the fork that does construct a base entity class directly is components/pid/pid_simulator.h:29 (new sensor::Sensor()), and Sensor already had a user-provided constructor before this branch. Every other newly-touched base is abstract, so direct construction is not possible at all; only Event and TextSensor are concrete.

DateEntity's constructor sits in a protected: block, which is legal here — the class is abstract (control() = 0, date_entity.h:63), and both in-tree subclasses (TemplateDate, DemoDate) declare no constructor of their own, so their implicit ones reach the protected base constructor normally.

Notes / possible follow-ups

Not fixed here, listed so they are not lost:

  • Records already written with "type": "none" on a deployed device are not repaired by this change — they stay unresolvable until the entity is renamed again. Re-resolving by object_id across types belongs in user_names; separate change, separate repo.
  • DateTimeBase gains DateTimeBase(EntityType) and therefore loses its implicit default constructor. All three in-tree subclasses are updated in this branch and nothing else in the fork or in esphome-components derives it, but an out-of-tree component deriving datetime::DateTimeBase directly would stop compiling. StatefulEntityBase keeps both forms (core/entity_base.h) — adding DateTimeBase() {} would match that precedent.
  • Camera derives EntityBase but the enum has no CAMERA value, so cameras stay EntityType::NONE.
  • core/entity_types.py still maps only switch and sensor, so the YAML side (type: on a custom menu render, via display_menu_render_base) does not yet expose the new types even though C++ reports them.
  • Constructor placement is not uniform across the touched headers: DateEntity's lands in the leading protected: block while its siblings TimeEntity and DateTimeEntity are public:, and a few others sit after the public data members. Cosmetic — instantiation is unaffected — but moving DateEntity's to public: would match the siblings.
  • The newly user-provided constructors leave members that have no default member initialiser untouched: Event::last_event_type (a raw pointer, and Event is concrete), Number::state, the uint8_t/uint16_t fields of the three datetime entities, DateTimeBase::rtc_. Nothing constructs those bases today, so nothing is indeterminate now — but a future new event::Event(), a std::vector<event::Event>, or a subclass that gains its own user-provided constructor would be. Adding {} / {nullptr} initialisers to those members closes it.
  • EntityBase() {} and StatefulEntityBase() {} (core/entity_base.h:30,216) still exist, so nothing forces a future entity class to declare a type. Removing them would make the invariant compiler-enforced — after Camera gets an enum value.
  • EntityBase(EntityType type) assigns in the constructor body rather than a mem-init list (core/entity_base.h:31), and neither it nor DateTimeBase(EntityType) is explicit. Pre-existing style, harmless (both classes are abstract or never implicitly converted, and google-explicit-constructor is off in .clang-tidy), but this is the constructor the whole branch funnels through.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the C++ side of ESPHome’s EntityType plumbing by ensuring each entity platform sets a meaningful EntityType via the EntityBase constructor. This enables consumers (e.g., display menu renderers and APIs) to reliably distinguish entity kinds without domain-string heuristics.

Changes:

  • Add EntityBase(EntityType::...) (or DateTimeBase(EntityType::...)) base-initializers to entity classes that previously defaulted to EntityType::NONE.
  • Introduce a typed DateTimeBase(EntityType) constructor and update date/time/datetime entities to pass the appropriate EntityType.
  • Update a few .cpp-defined constructors (cover/lock/valve/light state) to include the EntityBase initializer.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
esphome/components/alarm_control_panel/alarm_control_panel.h Initialize EntityBase with EntityType::ALARM_CONTROL_PANEL in constructor.
esphome/components/button/button.h Initialize EntityBase with EntityType::BUTTON in constructor.
esphome/components/climate/climate.h Initialize EntityBase with EntityType::CLIMATE in constructor.
esphome/components/cover/cover.cpp Add EntityBase(EntityType::COVER) to Cover constructor initializer list.
esphome/components/datetime/date_entity.h Pass EntityType::DATETIME_DATE through DateTimeBase construction.
esphome/components/datetime/datetime_base.h Add DateTimeBase(EntityType) constructor that forwards to EntityBase.
esphome/components/datetime/datetime_entity.h Pass EntityType::DATETIME_DATETIME through DateTimeBase construction.
esphome/components/datetime/time_entity.h Pass EntityType::DATETIME_TIME through DateTimeBase construction.
esphome/components/event/event.h Initialize EntityBase with EntityType::EVENT in constructor.
esphome/components/fan/fan.h Initialize EntityBase with EntityType::FAN in constructor.
esphome/components/light/light_state.cpp Add EntityBase(EntityType::LIGHT) to LightState constructor initializer list.
esphome/components/lock/lock.cpp Add EntityBase(EntityType::LOCK) to Lock constructor initializer list.
esphome/components/media_player/media_player.h Initialize EntityBase with EntityType::MEDIA_PLAYER in constructor.
esphome/components/number/number.h Initialize EntityBase with EntityType::NUMBER in constructor.
esphome/components/select/select.h Initialize EntityBase with EntityType::SELECT in constructor.
esphome/components/text/text.h Initialize EntityBase with EntityType::TEXT in constructor.
esphome/components/text_sensor/text_sensor.h Initialize EntityBase with EntityType::TEXT_SENSOR in constructor (replaces = default).
esphome/components/update/update_entity.h Initialize EntityBase with EntityType::UPDATE in constructor.
esphome/components/valve/valve.cpp Add EntityBase(EntityType::VALVE) to Valve constructor initializer list.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +32 to 33
Number() : EntityBase(EntityType::NUMBER) {}
float state;
Comment on lines +26 to 27
Event() : EntityBase(EntityType::EVENT) {}
const std::string *last_event_type;
uint8_t second_;

public:
TimeEntity() : DateTimeBase(EntityType::DATETIME_TIME) {}
uint8_t second_;

public:
DateTimeEntity() : DateTimeBase(EntityType::DATETIME_DATETIME) {}
Comment on lines +38 to 41
DateEntity() : DateTimeBase(EntityType::DATETIME_DATE) {}
uint16_t year_;
uint8_t month_;
uint8_t day_;

class AlarmControlPanel : public EntityBase {
public:
AlarmControlPanel() : EntityBase(EntityType::ALARM_CONTROL_PANEL) {}
Comment on lines +32 to 33
UpdateEntity() : EntityBase(EntityType::UPDATE) {}
void publish_state();
@zkoalexey

Copy link
Copy Markdown
Author

Triage of the Copilot review

All seven comments make the same argument: turning an implicit default constructor into a user-provided one makes new X() stop zero-initializing members that have no default initializer (Number::state, Event::last_event_type, the DateEntity/TimeEntity/DateTimeEntity fields, UpdateEntity::update_info_, the AlarmControlPanel state members).

The rule is real, but it does not apply here, and no code is being changed in response. Value-initialization is decided by the type actually named in new T(), not by its bases: per [dcl.init], if T's default constructor is neither user-provided nor deleted, the whole object is zero-initialized first — every base subobject and their members included — and only then is the constructor run. Codegen never instantiates a base entity class; it always instantiates the generated platform subclass. From main.cpp of a build off this branch:

e1  = new template_::TemplateEvent();
d1  = new template_::TemplateDate();
t1  = new template_::TemplateTime();
dt1 = new template_::TemplateDateTime();
u1  = new http_request::HttpRequestUpdate();

TemplateEvent, TemplateDate, TemplateTime, TemplateDateTime, TemplateNumber and HttpRequestUpdate declare no default constructor of their own, so theirs stays implicit, the zero-initialization step still happens, and Number::state, Event::last_event_type, year_/month_/day_ and update_info_.progress are zeroed exactly as before this change. Grepping the fork for direct construction of the base classes (new number::Number(), new event::Event(), new datetime::DateEntity(), …) returns nothing.

The one comment that points at a real gap is the alarm_control_panel one — but it is not caused by this PR. TemplateAlarmControlPanel already has a user-provided constructor with an empty body on dev-jethub (template_alarm_control_panel.h:54, template_alarm_control_panel.cpp:16), so new TemplateAlarmControlPanel() skipped zero-initialization before this branch too, and current_state_ / desired_state_ / last_update_ were left to setup() either way. This PR changes nothing there. Worth fixing on its own (NSDMIs on those three members), separately from this change.

Verified by compiling: jxd-r6-e1eth-lcd, an esp-idf config covering the template platforms plus light/fan/event/climate/update, and an arduino config for media_player — all 19 touched files went through the compiler, all green.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

@zkoalexey
zkoalexey merged commit 03eaa06 into dev-jethub Aug 25, 2026
30 checks passed
@zkoalexey
zkoalexey deleted the JXD-316-entity-type branch August 25, 2026 11:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants