Conversation
WalkthroughVersion bump and major firmware rework for ESPHome device. Introduces deep sleep and alarm detection logic with PM number concentration measurement support. Adds new SEN5x custom component for reading particle concentrations. Firmware source selection logic corrected across device configurations. Changes
Sequence Diagram(s)sequenceDiagram
participant Device as ESPHome Device
participant HA as Home Assistant
participant Sensors as SEN5x & PM Sensors
participant Sleep as Deep Sleep Controller
Device->>Device: Boot Sequence
Device->>Sensors: Initialize SEN5x (0x0413)
Sensors-->>Device: Return PM number concentrations
Device->>Device: Calculate Total PM Count
alt OTA Mode Active or Prevent Sleep
Device->>Device: Log Warning
Device->>Sleep: Prevent Deep Sleep
else Check Alarm Conditions
Device->>Device: Evaluate Temperature/Humidity/PM Range
alt Alarm Triggered
Device->>HA: Update Sensor Values
Device->>Device: Publish Alarm State
Device->>Sleep: Prevent Deep Sleep
else No Alarm
Device->>Sleep: Schedule Deep Sleep After Delay
end
end
HA-->>Device: Client Connected (OTA or Manual)
Device->>Sleep: Cancel/Defer Sleep if OTA Active
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@Integrations/ESPHome/Core.yaml`:
- Around line 691-704: The entity configuration for the template sensor "Sleep
Duration" (id deep_sleep_sleep_duration) uses an invalid uppercase
entity_category value; change entity_category: "CONFIG" to the lowercase string
entity_category: "config" so ESPHome validation accepts it.
In `@Integrations/ESPHome/custom_components/sen5x_number/sen5x_number.cpp`:
- Around line 89-148: read_data_() currently always returns true even when
parse_value() fails CRC for every sensor field; add a bool flag (e.g.,
any_published = false) at the start of the parsing block and set it to true
whenever you call publish_state on any of the members (pm_0_5_number_,
pm_1_0_number_, pm_2_5_number_, pm_4_0_number_, pm_10_0_number_,
typical_particle_size_); after processing all fields return any_published
instead of true so the method returns false when no values passed CRC and
nothing was published, leaving parse_value and publish_state calls unchanged.
In `@Integrations/ESPHome/custom_components/sen5x_number/sensor.py`:
- Around line 34-67: Remove the inappropriate device_class from the
number-concentration sensor schemas: in sensor.py update the
sensor.sensor_schema calls for CONF_PM_0_5_NUMBER, CONF_PM_1_0_NUMBER,
CONF_PM_2_5_NUMBER, CONF_PM_4_0_NUMBER and CONF_PM_10_0_NUMBER to omit
device_class=DEVICE_CLASS_PM25 (leave unit_of_measurement, icon,
accuracy_decimals and state_class intact); this ensures the number sensors use
no device_class as required by Home Assistant specs.
🧹 Nitpick comments (1)
Integrations/ESPHome/Core.yaml (1)
815-831: Avoid masking NaNs in total PM count calculation.
std::maxcan turn NaN inputs into 0, so totals may publish even when source values are invalid. Consider guarding and skipping publish when any input is NaN.🔧 Suggested change
@@ // Ranges: 0.3-0.5, 0.5-1, 1-2.5, 2.5-4, 4-10 + if (std::isnan(id(pm_0_5_number).state) || + std::isnan(id(pm_1_0_number).state) || + std::isnan(id(pm_2_5_number).state) || + std::isnan(id(pm_4_0_number).state) || + std::isnan(id(pm_10_0_number).state)) { + ESP_LOGW("Apollo", "Skipping Total PM Count publish due to NaN inputs"); + return; + } float pm_0_3_to_0_5 = id(pm_0_5_number).state;
| - platform: template | ||
| name: "Sleep Duration" | ||
| id: deep_sleep_sleep_duration | ||
| min_value: 0.01 | ||
| max_value: 24 | ||
| step: 0.01 | ||
| mode: box | ||
| update_interval: never | ||
| optimistic: true | ||
| restore_value: true | ||
| initial_value: 1 | ||
| icon: "mdi:arrow-collapse-right" | ||
| entity_category: CONFIG | ||
| unit_of_measurement: "h" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
ESPHome entity_category config validation values
💡 Result:
Valid values for ESPHome's entity_category are the Home Assistant EntityCategory options: config or diagnostic (you can also leave it unset / None). You can also set it to an empty string ("") in ESPHome to remove the default category. [1][2]
Sources:
[1] Home Assistant developer docs — Entity (entity_category = CONFIG or DIAGNOSTIC / None).
[2] ESPHome component docs — entity_category option (examples, and note that set to "" removes default).
🏁 Script executed:
#!/bin/bash
# Search for entity_category usage patterns in common ESPHome configurations
# and check if there's any code that validates this field
rg "entity_category" --type yaml --type python -i -B 2 -A 2 | head -100Repository: ApolloAutomation/TEMP_PROJECT-1
Length of output: 105
🌐 Web query:
ESPHome entity_category lowercase config case sensitive validation
💡 Result:
Short answer: Use lowercase strings (e.g. "diagnostic", "config") for entity_category in ESPHome. The docs and examples show entity_category as a string with lowercase values (or the empty string ""), and conventions/validation expect those exact values. [1][2][3]
Notes:
- Examples in the ESPHome docs use lowercase entity_category values (e.g. "diagnostic"). [3]
- The docs describe entity_category as an optional string and show "" to clear it. [1][2]
- Home Assistant enforces which entity types are valid for certain categories; mismatches can produce errors when entities are added. If you see errors, ensure you use the correct category for the component type. [4]
Sources:
- ESPHome component docs (entity_category entries). [1]
- ESPHome sensor/text_sensor/select docs (entity_category usage). [2]
- ESPHome template example using entity_category: "diagnostic". [3]
- Home Assistant community discussion about entity_category/type mismatches. [4]
Change entity_category to use lowercase config.
ESPHome only accepts lowercase values for entity_category ("config" or "diagnostic"). The uppercase variant "CONFIG" is invalid and will fail config validation.
🔧 Suggested change
- entity_category: CONFIG
+ entity_category: config📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - platform: template | |
| name: "Sleep Duration" | |
| id: deep_sleep_sleep_duration | |
| min_value: 0.01 | |
| max_value: 24 | |
| step: 0.01 | |
| mode: box | |
| update_interval: never | |
| optimistic: true | |
| restore_value: true | |
| initial_value: 1 | |
| icon: "mdi:arrow-collapse-right" | |
| entity_category: CONFIG | |
| unit_of_measurement: "h" | |
| - platform: template | |
| name: "Sleep Duration" | |
| id: deep_sleep_sleep_duration | |
| min_value: 0.01 | |
| max_value: 24 | |
| step: 0.01 | |
| mode: box | |
| update_interval: never | |
| optimistic: true | |
| restore_value: true | |
| initial_value: 1 | |
| icon: "mdi:arrow-collapse-right" | |
| entity_category: config | |
| unit_of_measurement: "h" |
🤖 Prompt for AI Agents
In `@Integrations/ESPHome/Core.yaml` around lines 691 - 704, The entity
configuration for the template sensor "Sleep Duration" (id
deep_sleep_sleep_duration) uses an invalid uppercase entity_category value;
change entity_category: "CONFIG" to the lowercase string entity_category:
"config" so ESPHome validation accepts it.
| auto parse_value = [&](uint8_t offset) -> float { | ||
| uint8_t msb = response[offset]; | ||
| uint8_t lsb = response[offset + 1]; | ||
| uint8_t crc = response[offset + 2]; | ||
|
|
||
| if (crc != this->sht_crc_(msb, lsb)) { | ||
| ESP_LOGW(TAG, "CRC check failed for byte offset %d", offset); | ||
| return NAN; | ||
| } | ||
|
|
||
| uint16_t raw = (static_cast<uint16_t>(msb) << 8) | lsb; | ||
| return static_cast<float>(raw); | ||
| }; | ||
|
|
||
| // Extract number concentrations (converting from #/cm³ to #/m³) | ||
| // Note: 1 #/cm³ = 1,000,000 #/m³ | ||
| if (this->pm_0_5_number_ != nullptr) { | ||
| float value = parse_value(12) / 10.0f; // Scaled by 10 per datasheet | ||
| if (!std::isnan(value)) { | ||
| this->pm_0_5_number_->publish_state(value * 1e6f); // Convert to #/m³ | ||
| } | ||
| } | ||
|
|
||
| if (this->pm_1_0_number_ != nullptr) { | ||
| float value = parse_value(15) / 10.0f; | ||
| if (!std::isnan(value)) { | ||
| this->pm_1_0_number_->publish_state(value * 1e6f); | ||
| } | ||
| } | ||
|
|
||
| if (this->pm_2_5_number_ != nullptr) { | ||
| float value = parse_value(18) / 10.0f; | ||
| if (!std::isnan(value)) { | ||
| this->pm_2_5_number_->publish_state(value * 1e6f); | ||
| } | ||
| } | ||
|
|
||
| if (this->pm_4_0_number_ != nullptr) { | ||
| float value = parse_value(21) / 10.0f; | ||
| if (!std::isnan(value)) { | ||
| this->pm_4_0_number_->publish_state(value * 1e6f); | ||
| } | ||
| } | ||
|
|
||
| if (this->pm_10_0_number_ != nullptr) { | ||
| float value = parse_value(24) / 10.0f; | ||
| if (!std::isnan(value)) { | ||
| this->pm_10_0_number_->publish_state(value * 1e6f); | ||
| } | ||
| } | ||
|
|
||
| // Typical particle size in µm | ||
| if (this->typical_particle_size_ != nullptr) { | ||
| float value = parse_value(27) / 1000.0f; // Scaled by 1000 per datasheet | ||
| if (!std::isnan(value)) { | ||
| this->typical_particle_size_->publish_state(value); | ||
| } | ||
| } | ||
|
|
||
| return true; |
There was a problem hiding this comment.
Return failure when no values pass CRC.
read_data_() currently returns true even if all values fail CRC, so update() clears the warning and silently drops data. Track whether any value was published and return false if none.
🔧 Suggested change
@@
- auto parse_value = [&](uint8_t offset) -> float {
+ bool any_valid = false;
+ auto parse_value = [&](uint8_t offset) -> float {
@@
if (!std::isnan(value)) {
this->pm_0_5_number_->publish_state(value * 1e6f); // Convert to `#/m³`
+ any_valid = true;
}
}
@@
if (!std::isnan(value)) {
this->pm_1_0_number_->publish_state(value * 1e6f);
+ any_valid = true;
}
}
@@
if (!std::isnan(value)) {
this->pm_2_5_number_->publish_state(value * 1e6f);
+ any_valid = true;
}
}
@@
if (!std::isnan(value)) {
this->pm_4_0_number_->publish_state(value * 1e6f);
+ any_valid = true;
}
}
@@
if (!std::isnan(value)) {
this->pm_10_0_number_->publish_state(value * 1e6f);
+ any_valid = true;
}
}
@@
if (!std::isnan(value)) {
this->typical_particle_size_->publish_state(value);
+ any_valid = true;
}
}
- return true;
+ return any_valid;🤖 Prompt for AI Agents
In `@Integrations/ESPHome/custom_components/sen5x_number/sen5x_number.cpp` around
lines 89 - 148, read_data_() currently always returns true even when
parse_value() fails CRC for every sensor field; add a bool flag (e.g.,
any_published = false) at the start of the parsing block and set it to true
whenever you call publish_state on any of the members (pm_0_5_number_,
pm_1_0_number_, pm_2_5_number_, pm_4_0_number_, pm_10_0_number_,
typical_particle_size_); after processing all fields return any_published
instead of true so the method returns false when no values passed CRC and
nothing was published, leaving parse_value and publish_state calls unchanged.
| cv.Optional(CONF_PM_0_5_NUMBER): sensor.sensor_schema( | ||
| unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER, | ||
| icon=ICON_CHEMICAL_WEAPON, | ||
| accuracy_decimals=0, | ||
| device_class=DEVICE_CLASS_PM25, | ||
| state_class=STATE_CLASS_MEASUREMENT, | ||
| ), | ||
| cv.Optional(CONF_PM_1_0_NUMBER): sensor.sensor_schema( | ||
| unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER, | ||
| icon=ICON_CHEMICAL_WEAPON, | ||
| accuracy_decimals=0, | ||
| device_class=DEVICE_CLASS_PM25, | ||
| state_class=STATE_CLASS_MEASUREMENT, | ||
| ), | ||
| cv.Optional(CONF_PM_2_5_NUMBER): sensor.sensor_schema( | ||
| unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER, | ||
| icon=ICON_CHEMICAL_WEAPON, | ||
| accuracy_decimals=0, | ||
| device_class=DEVICE_CLASS_PM25, | ||
| state_class=STATE_CLASS_MEASUREMENT, | ||
| ), | ||
| cv.Optional(CONF_PM_4_0_NUMBER): sensor.sensor_schema( | ||
| unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER, | ||
| icon=ICON_CHEMICAL_WEAPON, | ||
| accuracy_decimals=0, | ||
| device_class=DEVICE_CLASS_PM25, | ||
| state_class=STATE_CLASS_MEASUREMENT, | ||
| ), | ||
| cv.Optional(CONF_PM_10_0_NUMBER): sensor.sensor_schema( | ||
| unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER, | ||
| icon=ICON_CHEMICAL_WEAPON, | ||
| accuracy_decimals=0, | ||
| device_class=DEVICE_CLASS_PM25, | ||
| state_class=STATE_CLASS_MEASUREMENT, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd Integrations/ESPHome/custom_components/sen5x_number && head -40 sensor.pyRepository: ApolloAutomation/TEMP_PROJECT-1
Length of output: 206
🏁 Script executed:
rg "DEVICE_CLASS|UNIT_NUMBER_PER_CUBIC_METER" Integrations/ESPHome/custom_components/sen5x_number/ -A 2 -B 2Repository: ApolloAutomation/TEMP_PROJECT-1
Length of output: 4865
🌐 Web query:
Home Assistant sensor device_class PM25 number concentration #/m³``
💡 Result:
Home Assistant’s built‑in PM device_class (pm25 / SensorDeviceClass.PM25 or NumberDeviceClass.PM25) expects mass concentration in µg/m³ — it does not support number concentration in #/m³. Use a generic sensor (no device_class) or expose the reading with a unit you set (e.g. "#/m³") and keep device_class unset so HA won’t validate/overwrite the unit. (developers.home-assistant.io)
Citations:
Remove device_class=DEVICE_CLASS_PM25 from number-concentration sensors.
DEVICE_CLASS_PM25 is defined in Home Assistant for mass concentration (µg/m³) only. Using it with #/m³ (number concentration) violates Home Assistant's sensor specifications and will be rejected or misrepresented. Omit device_class for these five sensors.
🔧 Suggested change
cv.Optional(CONF_PM_0_5_NUMBER): sensor.sensor_schema(
unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
- device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_1_0_NUMBER): sensor.sensor_schema(
unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
- device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_2_5_NUMBER): sensor.sensor_schema(
unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
- device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_4_0_NUMBER): sensor.sensor_schema(
unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
- device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PM_10_0_NUMBER): sensor.sensor_schema(
unit_of_measurement=UNIT_NUMBER_PER_CUBIC_METER,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
- device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
),🤖 Prompt for AI Agents
In `@Integrations/ESPHome/custom_components/sen5x_number/sensor.py` around lines
34 - 67, Remove the inappropriate device_class from the number-concentration
sensor schemas: in sensor.py update the sensor.sensor_schema calls for
CONF_PM_0_5_NUMBER, CONF_PM_1_0_NUMBER, CONF_PM_2_5_NUMBER, CONF_PM_4_0_NUMBER
and CONF_PM_10_0_NUMBER to omit device_class=DEVICE_CLASS_PM25 (leave
unit_of_measurement, icon, accuracy_decimals and state_class intact); this
ensures the number sensors use no device_class as required by Home Assistant
specs.
Version: 26.1.21.1
What does this implement/fix?
Adds
Types of changes
Checklist / Checklijst:
If user-visible functionality or configuration variables are added/modified:
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.