Repository navigation
Conversation
… filter guess The 1.5.0 pre-release read status byte 6 as an MCU filter-life percentage and moved the Display switch to byte 7. Both were wrong. Byte 6 reads 0x64 in every frame of every capture, which looked like a fresh filter at 100% but is simply the display brightness with the display on. New stock-firmware captures settle every remaining byte by matching a command the app sent against the status frame it produced: display_on_off.txt 01 05 A1 00/64 -> bytes 6 and 7 move together childlock.txt 01 00 D1 00/01 -> byte 10, 1:1 nightlight.txt 01 03 A0 00/32/64 -> byte 11, 1:1 And filter_99.txt, captured while the stock app displayed 99% and covering a full module boot with its own 01 61 40 status query, still reports byte 6 as 0x64. The value 0x63 appears nowhere in the file and no message in any capture carries a filter percentage or a runtime counter, so the Core 200S filter counter lives in the stock Wi-Fi module and the cloud, not in the MCU. The ESP-side CADR estimate is authoritative again, as on the other Core models. Also fixes a real defect found while checking that estimate: the CADR calculation used a 4-speed divisor for the Core 200S, which has 3 speeds, so every level accumulated three quarters of the air it should have and the filter percentage decayed 25% too slowly. That is the likely cause of the reports that Core 200S filter life does not work. Kept: the MCU filter reset (01 E4 A5), which is genuinely captured from the stock firmware. It no longer gates on filter_life_from_mcu(), which is gone; the button sends it in addition to clearing the ESP counters. The full byte map, the capture index and the reasoning are written up in devices/levoit-core200s/README.md so the wrong inference is not repeated. All three levoit compile tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e estimate 01 E4 A5 turns out to be bidirectional. A new capture with the filter reset using the button on the unit rather than the app shows the MCU originating it: 12.755 MCU -> module typ=22 01 E4 A5 01 12.761 module -> MCU typ=52 01 E4 A5 So the MCU does push a filter event on change, in the same family as the reset command - which was the hypothesis this capture was taken to test. It carries no percentage, just the fact that a reset happened, so it does not change the conclusion that the MCU never reports filter life. The component now clears used_cadr and total_runtime on that push, the same way it already did for the Superior 6000S panel reset, so the sensor follows the button on the unit. Handled ahead of the payload-dedup cache, because two panel resets in a row carry an identical payload and the second would be skipped. Two corrections to the byte map from the same capture: * Byte 4 is not always 00. It reads 01 here, which is the Sleep preset, so it is a real mode field. * Byte 7 does not simply follow byte 6. In display_on_off.txt they moved together under 01 05 A1, but here byte 6 goes 00 -> 64 while byte 7 stays 00. Byte 6 is the byte the display command writes and keeps driving the Display switch; byte 7 is marked undecoded rather than claimed as display. Compile tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
track_cadr_usage() converted CADR/h to CADR/min with `cadr_per_hour / 60` in integer arithmetic. Any model under 60 m3/h at the current speed truncated to zero and accumulated nothing at all: Core200S speed 1 167*1/3 = 55 m3/h -> 0 Core200S Sleep 55 * 0.63 = 34 -> 0 Sprout speed 1 145*1/4 = 36 -> 0 Core400S speed 1 442*1/4 = 110 -> 1 (unaffected) So a Core 200S left on low or Sleep never moved off 100%, which is what users reported: six months of running with the sensor pinned at 100%. At higher speeds the same truncation lost 28-46%, and because the loss is all-or-nothing per speed the graph became a staircase - flat on speed 1, stepping down on speed 2 and 3 - against the smooth slope a Core 400S shows on the same code. The sub-m3 remainder is now carried between minutes, which is exact. It is not persisted: a reboot loses under 1 m3 against a capacity of several hundred thousand. Also drops the `speed <= 4` gate around the accumulation. It silently excluded the Superior 6000S's speeds 5-9, and it was redundant - calculate_current_cadr_per_hour() already validates the speed against the model's own maximum and returns 0 when it is out of range. Compile tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the red light The MCU does track filter life - it just never reports a number. Units running ESPHome rather than the stock module still get the red filter light after a few months, and ESPHome only drives that LED for Wi-Fi status during boot, stopping the moment Wi-Fi connects. So the lamp is the MCU's own decision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
The 1.5.0 pre-release read Core 200S status byte 6 as an MCU filter-life percentage and moved the Display switch to byte 7. Both were wrong, and the combination left the Core 200S filter sensor reporting a hard-coded 100% forever — worse than the bug it was meant to fix.
Byte 6 reads
0x64in every frame of every capture. That looked exactly like a fresh filter at 100%, and the stock app did show 100% at the time. It is the display brightness, with the display on.How it was settled
New stock-firmware captures, each matching a command the app sent against the status frame it produced:
display_on_off.txt01 05 A100/64childlock.txt01 00 D100/01nightlight.txt01 03 A000/32/64And the decisive one:
filter_99.txtwas captured while the stock app displayed 99%, covering a full module boot including the module's own01 61 40status query. Byte 6 still came back0x64.0x63appears nowhere in the file, and no message in any capture carries a filter percentage or a runtime counter.So the Core 200S filter counter lives in the stock Wi-Fi module and the cloud, not the MCU. The ESP-side CADR estimate is authoritative again, as on every other Core model.
Ten of the twelve payload bytes are now confirmed by capture; bytes 8 and 9 are
0x00in every frame of every file.Also: a real defect, probably the original report
calculate_current_cadr_per_hour()used a 4-speed divisor for the Core 200S, which has 3 speeds — only the Core 300S was special-cased, thoughlevoit_fan.cppsetsspeed_count_ = 3for both. Every level accumulated three quarters of the air it should have, so the filter percentage decayed 25% too slowly. This is the most likely cause of the reports that Core 200S filter life "does not work".Kept
The MCU filter reset (
01 E4 A5) is genuinely captured from the stock firmware and stays. It no longer gates onfilter_life_from_mcu(), which is removed;has_mcu_filter_reset()replaces it and the button sends the command in addition to clearing the ESP counters.Documentation
devices/levoit-core200s/README.mdgains a full protocol section: capture index, frame layout, the byte map with the evidence for each row, the observed-payload-only command table, and a written-up account of why the byte 6 inference was wrong — so it is not repeated. The command table deliberately separates payloads that were observed from meanings inherited from the component's existing tables; assuming the unseen half is exactly how this went wrong.Two loose threads are recorded rather than papered over: the
0Bpayload on the MCU's reply to01 E4 A5, and the off-state behaviour of bytes 6/7.Testing
All three levoit compile tests pass locally (
test-all-entities,test-fan-only,test-foreign-platforms). Not yet re-verified on hardware.🤖 Generated with Claude Code