Skip to content

test(levoit): compile tests + CI on every PR, and the build failures they found - #68

Merged
tuct merged 4 commits into
mainfrom
test/levoit-compile-tests-ci
Sep 26, 2026
Merged

tuct merged 4 commits into
mainfrom
test/levoit-compile-tests-ci

Conversation

@tuct

@tuct tuct commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Follow-up to #67, which merged before this landed.

Compile tests

Three self-contained configs in components/levoit/tests/. They need no secrets.yaml (inline dummy credentials) and point at ../.., so they always build the working copy.

Config Catches
test-all-entities.yaml Codegen for all 66 entity types across all 7 platforms
test-fan-only.yaml Sources including a platform header without a USE_LEVOIT_* guard
test-foreign-platforms.yaml The subtler case: template entities define the generic USE_SWITCH etc. while no levoit platform directory is copied

CI

.github/workflows/compile-tests.yml runs them on every PR touching components/, as a fail-fast: false matrix with PlatformIO/pip caching. This is the repo's first workflow.

Two real bugs they found immediately

A levoit config with only a fan did not build at all. core_commands.cpp, sprout_commands.cpp, superior_commands.cpp and superior_status.cpp included number/levoit_number.h / sensor/levoit_sensor.h unguarded; core_status.cpp and vital_status.cpp did the same with fan/levoit_fan.h. ESPHome only copies levoit/<platform>/ when a levoit entity of that platform is configured, so those headers were absent.

levoit.cpp guarded on the generic macros — USE_SWITCH, USE_NUMBER, … are defined by any component using that platform, so a config with a template switch but no levoit switch failed the same way. Same flaw previously fixed in the philips component.

Fix

Each platform now defines its own USE_LEVOIT_* macro. Rather than scatter #ifdef through switch-cases, the entity classes are confined to levoit.cpp behind new accessors — try_get_number_state(), try_get_select_index(), try_get_switch_state(), apply_fan_status() — so *_commands.cpp and *_status.cpp no longer reference those types at all. Each returns false when the entity is absent or has no state, preserving the existing "leave the default alone" behaviour at every call site.

Changelog correction

The 1.5.0 entry claimed the USE_LEVOIT_* guards were already in place. They were not — that had only ever been done for philips — so the entry is corrected to describe what actually shipped.

Verified

All three test configs compile, and the Core 200S / Core 400S / Vital 200S / Sprout device configs still build.

🤖 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.

Copilot review overview

🟡 Changes recommended

Critical issues remain with sentinel preservation, no-fan dereferences, and nullable status handling.

Review effort: Lite
Findings: 4 High severity

Open (4)
What changed in this PR

Adds Levoit compile-test coverage, CI validation, and fixes optional-platform build dependencies.

Changes:

  • Adds three self-contained compile configurations and GitHub Actions CI.
  • Introduces USE_LEVOIT_* guards and accessor-based entity handling.
  • Updates documentation and changelog details.
File Summary
README.md Documents build fixes and compile tests.
components/​levoit/​vital_status.cpp Routes status updates through accessors.
components/​levoit/​vital_commands.cpp Removes direct platform dependencies.
components/​levoit/​text_sensor/​__init__.py Defines platform build configuration.
components/​levoit/​tests/​test-foreign-platforms.yaml Adds foreign-platform compile coverage.
components/​levoit/​tests/​test-fan-only.yaml Adds fan-only compile coverage.
components/​levoit/​tests/​test-all-entities.yaml Adds all-entities compile coverage.
components/​levoit/​tests/​README.md Documents compile tests.
components/​levoit/​tests/​.gitignore Ignores test artifacts.
components/​levoit/​switch/​__init__.py Defines the Levoit switch guard.
components/​levoit/​superior_status.cpp Uses accessor-based status handling.
components/​levoit/​superior_commands.cpp Removes direct platform dependencies.
components/​levoit/​sprout_commands.cpp Removes direct platform dependencies.
components/​levoit/​sensor/​__init__.py Defines the Levoit sensor guard.
components/​levoit/​select/​__init__.py Defines the Levoit select guard.
components/​levoit/​number/​__init__.py Defines the Levoit number guard.
components/​levoit/​light/​__init__.py Defines the Levoit light guard.
components/​levoit/​levoit.h Declares entity accessors.
components/​levoit/​levoit.cpp Implements guarded accessors and entity handling.
components/​levoit/​fan/​__init__.py Defines the Levoit fan guard.
components/​levoit/​core_status.cpp Uses accessor-based status handling.
components/​levoit/​core_commands.cpp Removes direct platform dependencies.
components/​levoit/​button/​__init__.py Defines the Levoit button guard.
components/​levoit/​binary_sensor/​__init__.py Defines the Levoit binary sensor guard.
.github/​workflows/​compile-tests.yml Runs compile tests in CI.

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

Comment thread components/levoit/levoit.cpp
Comment thread components/levoit/levoit.cpp Outdated
Comment thread components/levoit/levoit.h Outdated
Comment thread components/levoit/vital_status.cpp Outdated
tuct added a commit that referenced this pull request Sep 26, 2026
Review findings on #68, all four real:

  - apply_fan_status() took (bool, uint8_t, uint32_t), but the Vital and
    Superior decoders deliberately pass -1 for "not reported in this frame,
    leave the field alone". Narrowing turned an unknown power into ON and an
    unknown speed into 255, which then clamps to maximum - so a partial status
    frame would switch the fan on at full speed. Takes (int, int, int) now,
    matching LevoitFan::apply_device_status().

  - The rewrite of vital_status.cpp dropped the `self != nullptr` check at that
    call site, even though decode_vital_status() accepts a nullable self and
    guards its other entity updates. Restored.

  - Guarding the fan include was not enough on its own: track_cadr_usage() and
    calculate_current_cadr_per_hour() still dereferenced fan_ against a
    forward-declared type. Both are now inside USE_LEVOIT_FAN, with the latter
    returning 0 when there is no fan entity to derive a duty cycle from.

The first two were regressions I introduced in the previous commit; only the
third was pre-existing.

Verified: all three test configs compile, plus Core 400S and Vital 200S.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tuct
tuct requested a lite review from Copilot September 26, 2026 16:21

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.

Copilot review overview

🔵 Needs a closer look

The all-entities compile test omits the Levoit light platform, leaving a changed code path untested.

Review effort: Lite
Findings: None

Resolved since last review (4)

tuct and others added 4 commits September 26, 2026 18:27
… found

Adds three self-contained configs under components/levoit/tests that build the
component without a secrets.yaml, plus a GitHub Actions workflow running them
on every PR touching components/.

The tests failed immediately on two real bugs, both now fixed:

  - A levoit config with only a fan did not build. core_commands.cpp,
    sprout_commands.cpp, superior_commands.cpp and superior_status.cpp included
    number/levoit_number.h and sensor/levoit_sensor.h unguarded, and
    core_status.cpp / vital_status.cpp included fan/levoit_fan.h the same way.
    ESPHome only copies levoit/<platform>/ into the build when a levoit entity
    of that platform is configured, so those headers were simply absent.

  - levoit.cpp guarded its includes on the generic USE_SWITCH / USE_NUMBER /
    ... macros, which ANY component defines. A config with a template switch
    but no levoit switch took the include and failed the same way - the same
    flaw already fixed in the philips component.

Each platform now defines its own USE_LEVOIT_* macro. Rather than scatter
guards through switch-cases, the entity classes are confined to levoit.cpp
behind new accessors - try_get_number_state(), try_get_select_index(),
try_get_switch_state() and apply_fan_status() - so the *_commands.cpp and
*_status.cpp sources no longer reference those types at all. Each accessor
returns false when the entity is absent or has no state, preserving the
existing "leave the default alone" behaviour at every call site.

The 1.5.0 changelog entry claimed the USE_LEVOIT_* guards were already in
place. They were not - that was only ever done for philips - so the entry is
corrected here.

Verified: all three test configs compile, and Core 200S/400S, Vital 200S and
Sprout device configs still build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings on #68, all four real:

  - apply_fan_status() took (bool, uint8_t, uint32_t), but the Vital and
    Superior decoders deliberately pass -1 for "not reported in this frame,
    leave the field alone". Narrowing turned an unknown power into ON and an
    unknown speed into 255, which then clamps to maximum - so a partial status
    frame would switch the fan on at full speed. Takes (int, int, int) now,
    matching LevoitFan::apply_device_status().

  - The rewrite of vital_status.cpp dropped the `self != nullptr` check at that
    call site, even though decode_vital_status() accepts a nullable self and
    guards its other entity updates. Restored.

  - Guarding the fan include was not enough on its own: track_cadr_usage() and
    calculate_current_cadr_per_hour() still dereferenced fan_ against a
    forward-declared type. Both are now inside USE_LEVOIT_FAN, with the latter
    returning 0 when there is no fan entity to derive a duty cycle from.

The first two were regressions I introduced in the previous commit; only the
third was pre-existing.

Verified: all three test configs compile, plus Core 400S and Vital 200S.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the levoit suite for philips: every entity type, a fan-only config,
and one where template entities supply the platforms.

That last one is the regression test for a bug that was real - before the
USE_PHILIPS_* guards, an AC0650 config with an unrelated template switch failed
with "fatal error: switch/philips_switch.h: No such file". Nothing was guarding
that fix until now.

All three pass on the first run, so the philips guards were already correct.

The workflow becomes a component x config matrix, so both components build on
every PR - six jobs instead of three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rrection

Review finding: test-all-entities.yaml omitted the light platform, so
USE_LEVOIT_LIGHT and the Sprout LED-ring codegen went untested. The generator
built the entity list from each platform's TYPE_MAP and light has none - it is
a single light_schema entity - so it was skipped silently. Listed explicitly
now, with a comment saying why. All nine platforms that define a USE_LEVOIT_*
macro are exercised.

The changelog correction in this branch targeted the root README, but #69 moved
the levoit change log to components/levoit/README.md in the meantime, so the
rebase dropped it and the inaccurate claim survived at its new location.
Re-applied there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tuct
tuct force-pushed the test/levoit-compile-tests-ci branch from 71e8bdd to 5104f88 Compare September 26, 2026 16:29
@tuct
tuct requested a lite review from Copilot September 26, 2026 18:12

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.

Copilot review overview

🟢 Approval recommended

Only a non-blocking documentation nit remains; no approval-blocking issues were found.

Review effort: Lite
Findings: None

@tuct
tuct merged commit c2b8f49 into main Sep 26, 2026
7 checks passed
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