Skip to content

LevoitSwitch: set has_state on publish to match Select/Number behavior#31

Open
TheDave94 wants to merge 1 commit into
tuct:mainfrom
TheDave94:fix/switch-has-state
Open

LevoitSwitch: set has_state on publish to match Select/Number behavior#31
TheDave94 wants to merge 1 commit into
tuct:mainfrom
TheDave94:fix/switch-has-state

Conversation

@TheDave94
Copy link
Copy Markdown

Summary

Switch::publish_state in core ESPHome sets the public state field but does not flip has_state_ on EntityBase, unlike Select::publish_state and Number::publish_state which both call set_has_state(true).

For callers that use if (entity->has_state()) as a guard — typically to decide whether an entity holds a fresh published value worth preferring over a cached default — the check works for select and number but always returns false for switch. The bug is silent: the public state field updates correctly, callbacks fire, the device behaves as expected; only code that explicitly checks has_state() sees stale results.

Fix

Two paths get explicit set_has_state(true) calls:

  • LevoitSwitch::write_state — after the optimistic publish_state(state). Handles the user-toggle path (HA service → Switch::turn_onLevoitSwitch::write_stateon_switch_command).
  • Levoit::publish_switch — hoisted above the dedup early-return if (sw->state == state). Without this, a decoder publish whose value happens to match the entity's default-initialized state would skip publish_state entirely and leave has_state_ at false forever.

Behavior change

None for callers that don't use has_state(). The public state field, value-change callbacks, and ControllerRegistry notifications work exactly as before.

Context

Discovered while developing additional Vital 200S Pro support that uses has_state() to distinguish "user just toggled this entity" from "never published yet" in a command builder. The asymmetry meant switches always took the "cached default" branch and the user's new value was silently discarded over the wire. The fix is independent of any Vital-specific work and applies to upstream main directly.

Verified by building against upstream main and flashing to a Vital 200S Pro running MCU FW 2.0.0; switches now correctly report has_state()=true after both user toggles and decoder publishes.

Switch::publish_state in core ESPHome sets the public `state` field
and fires callbacks but does not flip has_state_ on EntityBase.
Select::publish_state and Number::publish_state both call
set_has_state(true). The asymmetry breaks any caller that uses
`if (entity->has_state())` to decide whether the entity carries a
fresh user-supplied value worth preferring over a cached default —
the check works for select / number but always returns false for
switch, even after the user has just toggled it and even after the
decoder published the device's reported value.

Two paths fixed:

  LevoitSwitch::write_state — explicit set_has_state(true) after
  publish_state(state). Handles the user-toggle path
  (HA service → Switch::turn_on → LevoitSwitch::write_state →
  on_switch_command).

  Levoit::publish_switch — set_has_state(true) hoisted above the
  dedup early-return `if (sw->state == state)`. A decoder publish
  whose value matches the entity's default (e.g. first decoded
  false against default sw->state=false) would otherwise skip
  publish_state entirely and leave has_state_ at false for the
  rest of the session.

No behavior change for callers that don't use has_state(): the
public `state` field, callbacks, and ControllerRegistry
notifications work exactly as before.
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.

1 participant