diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 7b28065e4e..2b13b40a16 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -213,17 +213,19 @@ LightColorValues LightCall::validate_() { // Flag whether an explicit turn off was requested, in which case we'll also stop the effect. bool explicit_turn_off_request = this->has_state() && !this->state_; - // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on). - if (this->has_brightness() && this->brightness_ == 0.0f) { + // Treat zero brightness as an implicit turn-off when no state was explicitly requested. + if (this->has_brightness() && this->brightness_ == 0.0f && !this->has_state()) { this->state_ = false; this->set_flag_(FLAG_HAS_STATE); - if (color_mode & ColorCapability::BRIGHTNESS) { - // Reset brightness so the light has nonzero brightness when turned back on. + } + + // Make sure a turn-on makes the light visible: if the resulting brightness would be zero + // (e.g. restored from a brightness=0 turn-off), reset it to full brightness. + if (this->has_state() && this->state_ && (color_mode & ColorCapability::BRIGHTNESS)) { + float brightness = this->has_brightness() ? this->brightness_ : this->parent_->remote_values.get_brightness(); + if (brightness == 0.0f) { this->brightness_ = 1.0f; - } else { - // Light doesn't support brightness; clear the flag to avoid a spurious - // "brightness not supported" warning during capability validation. - this->clear_flag_(FLAG_HAS_BRIGHTNESS); + this->set_flag_(FLAG_HAS_BRIGHTNESS); } } diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py index 0eaf5af91b..a3a4103f5c 100644 --- a/tests/integration/test_light_calls.py +++ b/tests/integration/test_light_calls.py @@ -322,6 +322,49 @@ async def test_light_calls( assert state.state is True assert state.brightness == pytest.approx(0.75) + # Test 31: Setting brightness to 0 without an explicit state implicitly turns + # the light off; turning it back on (without an explicit brightness) then + # restores full brightness so the light is visible again. + client.light_command(key=rgbcw_light.key, state=True, brightness=0.5) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.5) + + # Brightness 0 with no explicit state -> implicit turn-off + client.light_command(key=rgbcw_light.key, brightness=0.0) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is False + assert state.brightness == pytest.approx(0.0) + # Turning on without an explicit brightness restores it to full brightness + client.light_command(key=rgbcw_light.key, state=True) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(1.0) + + # Test 31b: An explicit turn-on with brightness 0 still resets to full + # brightness - a turn-on must never leave the light on-but-invisible. This + # is the same path the restore logic exercises (set_state(true) + + # set_brightness(0) from a persisted brightness=0 turn-off). + client.light_command(key=rgbcw_light.key, state=True, brightness=0.0) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(1.0) + + # Test 32: Turning a light on when it already has nonzero brightness leaves + # the brightness unchanged (the reset only happens when brightness is 0). + client.light_command(key=rgbcw_light.key, state=True, brightness=0.4) + state = await wait_for_state_change(rgbcw_light.key) + assert state.brightness == pytest.approx(0.4) + + client.light_command(key=rgbcw_light.key, state=False) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is False + + client.light_command(key=rgbcw_light.key, state=True) + state = await wait_for_state_change(rgbcw_light.key) + assert state.state is True + assert state.brightness == pytest.approx(0.4) + # Final cleanup - turn all lights off for light in lights: client.light_command(