[core] All entity platforms declare their EntityType in the constructor - #64
Conversation
There was a problem hiding this comment.
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::...)(orDateTimeBase(EntityType::...)) base-initializers to entity classes that previously defaulted toEntityType::NONE. - Introduce a typed
DateTimeBase(EntityType)constructor and updatedate/time/datetimeentities to pass the appropriateEntityType. - Update a few
.cpp-defined constructors (cover/lock/valve/light state) to include theEntityBaseinitializer.
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.
| Number() : EntityBase(EntityType::NUMBER) {} | ||
| float state; |
| 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) {} |
| 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) {} |
| UpdateEntity() : EntityBase(EntityType::UPDATE) {} | ||
| void publish_state(); |
Triage of the Copilot reviewAll seven comments make the same argument: turning an implicit default constructor into a user-provided one makes 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 e1 = new template_::TemplateEvent();
d1 = new template_::TemplateDate();
t1 = new template_::TemplateTime();
dt1 = new template_::TemplateDateTime();
u1 = new http_request::HttpRequestUpdate();
The one comment that points at a real gap is the Verified by compiling: |
Every entity platform now passes its
EntityTypeto theEntityBaseconstructor, soentity->type()is meaningful for all 21 entity types instead of just three.Why
EntityTypeis the fork's own mechanism:EntityBase(EntityType)incore/entity_base.h, the enum incore/entity_types.h, andApp.get_entity_by_key(type, key)incore/application.h:496, which switches on the type and falls through todefault: return nullptr. OnlySensor,BinarySensorandSwitchever set their type; every other entity class stayedEntityType::NONE.That is not just a coverage gap — it silently breaks entity renaming on shipping devices. The
user_namescomponent (injethome-iot/esphome-components) stores the type next to the name and looks the entity back up by it on boot:PUT /api/user_namesitself 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 thedefault:branch, the log saysEntity 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 enablesuser_nameson every E1-ETH board — declaresnumberandtext_sensorentities, and the feature packagesjxd-r6-e1eth-lcd.yamlpulls in addselect(firmware-type-selector.yaml), plusbuttonandupdate(firmware-update.yaml). All five reportEntityType::NONEtoday.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:64→entity->type() != EntityType::NONE). OnlySENSOR/SWITCH/BINARY_SENSORrenderers 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-jethuband has been merged up with currentdev-jethub(0902dec93c) in84e0ee55b3. 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-lcdonly 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.i2s_audiomedia_player (that platform is arduino-only) — successfully compiled.All 19 touched files therefore went through the compiler.
clang-format --dry-run -Werroris 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 likeNumber::stateandDateEntity::year_. The one site in the fork that does construct a base entity class directly iscomponents/pid/pid_simulator.h:29(new sensor::Sensor()), andSensoralready had a user-provided constructor before this branch. Every other newly-touched base is abstract, so direct construction is not possible at all; onlyEventandTextSensorare concrete.DateEntity's constructor sits in aprotected: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:
"type": "none"on a deployed device are not repaired by this change — they stay unresolvable until the entity is renamed again. Re-resolving byobject_idacross types belongs inuser_names; separate change, separate repo.DateTimeBasegainsDateTimeBase(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 inesphome-componentsderives it, but an out-of-tree component derivingdatetime::DateTimeBasedirectly would stop compiling.StatefulEntityBasekeeps both forms (core/entity_base.h) — addingDateTimeBase() {}would match that precedent.CameraderivesEntityBasebut the enum has noCAMERAvalue, so cameras stayEntityType::NONE.core/entity_types.pystill maps onlyswitchandsensor, so the YAML side (type:on acustommenu render, viadisplay_menu_render_base) does not yet expose the new types even though C++ reports them.DateEntity's lands in the leadingprotected:block while its siblingsTimeEntityandDateTimeEntityarepublic:, and a few others sit after the public data members. Cosmetic — instantiation is unaffected — but movingDateEntity's topublic:would match the siblings.Event::last_event_type(a raw pointer, andEventis concrete),Number::state, theuint8_t/uint16_tfields of the three datetime entities,DateTimeBase::rtc_. Nothing constructs those bases today, so nothing is indeterminate now — but a futurenew event::Event(), astd::vector<event::Event>, or a subclass that gains its own user-provided constructor would be. Adding{}/{nullptr}initialisers to those members closes it.EntityBase() {}andStatefulEntityBase() {}(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 — afterCameragets 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 norDateTimeBase(EntityType)isexplicit. Pre-existing style, harmless (both classes are abstract or never implicitly converted, andgoogle-explicit-constructoris off in.clang-tidy), but this is the constructor the whole branch funnels through.🤖 Generated with Claude Code