Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion homeassistant/components/knx/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:

@property
@override
def fan_mode(self) -> str:
def fan_mode(self) -> str | None:
"""Return the fan setting."""

fan_speed = self._device.current_fan_speed
Expand All @@ -563,6 +563,10 @@ def fan_mode(self) -> str:
return self.fan_zero_mode

if self._device.fan_speed_mode is FanSpeedMode.STEP:
# DPT 5.010 fits any 1-byte value (0-255), so a gateway may report
# a step beyond the configured fan_max_step
if fan_speed >= len(self._attr_fan_modes):
return None
return self._attr_fan_modes[fan_speed]

# Find the closest fan mode percentage
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/teslemetry/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ async def async_step_authorize(
LOGGER.error("Bluetooth pairing was rejected: %s", err)
self._pair_error = {"base": "pair_failed"}
return self.async_show_progress_done(next_step_id="instructions")
except Exception:
# async_remove() only runs if the flow is still tracked when this step raises.
await self._async_disconnect()
raise
return self.async_show_progress_done(next_step_id="pair")

async def _async_disconnect(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/waterfurnace/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["waterfurnace"],
"quality_scale": "bronze",
"requirements": ["waterfurnace==1.9.0"]
"requirements": ["waterfurnace==1.9.2"]
}
24 changes: 13 additions & 11 deletions homeassistant/components/withings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.start import async_at_started

from .const import DEFAULT_TITLE, DOMAIN, LOGGER
from .coordinator import (
Expand Down Expand Up @@ -164,22 +165,23 @@ async def manage_cloudhook(state: cloud.CloudConnectionState) -> None:
async_call_later(hass, 30, webhook_manager.register_webhook)
)

@callback
def _async_register_webhook_later(_: HomeAssistant) -> None:
"""Register the webhook once Home Assistant answers for it."""
entry.async_on_unload(
async_call_later(
hass, WEBHOOK_REGISTER_DELAY, webhook_manager.register_webhook
)
)

if cloud.async_active_subscription(hass):
if cloud.async_is_connected(hass):
entry.async_on_unload(
async_call_later(
hass, WEBHOOK_REGISTER_DELAY, webhook_manager.register_webhook
)
)
entry.async_on_unload(async_at_started(hass, _async_register_webhook_later))
entry.async_on_unload(
cloud.async_listen_connection_change(hass, manage_cloudhook)
)
else:
entry.async_on_unload(
async_call_later(
hass, WEBHOOK_REGISTER_DELAY, webhook_manager.register_webhook
)
)
entry.async_on_unload(async_at_started(hass, _async_register_webhook_later))

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions tests/components/knx/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,45 @@ async def test_fan_speed_3_steps(hass: HomeAssistant, knx: KNXTestKit) -> None:
knx.assert_state("climate.test", HVACMode.HEAT, fan_mode="off")


@pytest.mark.parametrize("raw_value", [0x04, 0xFF])
async def test_fan_speed_step_out_of_range(
hass: HomeAssistant, knx: KNXTestKit, raw_value: int
) -> None:
"""Test that fan step values beyond fan_max_step don't crash the entity."""
await knx.setup_integration(
{
ClimateSchema.PLATFORM: {
CONF_NAME: "test",
ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3",
ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4",
ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5",
ClimateSchema.CONF_FAN_SPEED_ADDRESS: "1/2/6",
ClimateSchema.CONF_FAN_SPEED_STATE_ADDRESS: "1/2/7",
ClimateConf.FAN_SPEED_MODE: "step",
ClimateConf.FAN_MAX_STEP: 3,
}
}
)

# read states state updater
await knx.assert_read("1/2/3")
await knx.assert_read("1/2/5")

# StateUpdater initialize state
await knx.receive_response("1/2/5", RAW_FLOAT_22_0)
await knx.receive_response("1/2/3", RAW_FLOAT_21_0)

# Query status
await knx.assert_read("1/2/7")

# a gateway may report a step value beyond the configured fan_max_step
await knx.receive_write("1/2/7", (raw_value,))
await hass.async_block_till_done()

# the invalid telegram is published without breaking the state write
knx.assert_state("climate.test", HVACMode.HEAT, fan_mode=None)


async def test_fan_speed_2_steps(hass: HomeAssistant, knx: KNXTestKit) -> None:
"""Test KNX climate fan speed 2 steps."""
await knx.setup_integration(
Expand Down
31 changes: 31 additions & 0 deletions tests/components/teslemetry/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,37 @@ async def _pair() -> None:
vehicle.pair.assert_awaited_once()


async def test_subentry_authorize_unexpected_error_disconnects(
hass: HomeAssistant,
) -> None:
"""An exception outside the handled set still disconnects before propagating."""
entry = await _setup_account_entry(hass)
vehicle = _mock_vehicle(on_whitelist=False)
vehicle.pair = AsyncMock(side_effect=ValueError("boom"))

with (
patch(
"homeassistant.components.teslemetry.config_flow.async_discovered_service_info",
return_value=[_discovered_info()],
),
patch(
"homeassistant.components.teslemetry.config_flow.async_get_ble_parent",
return_value=_mock_ble_parent(vehicle),
),
):
result = await _start_pairing_at_scan(hass, entry)
result = await hass.config_entries.subentries.async_configure(
result["flow_id"], {}
)
assert result["step_id"] == "instructions"

with pytest.raises(ValueError, match="boom"):
await hass.config_entries.subentries.async_configure(result["flow_id"], {})

vehicle.disconnect.assert_awaited_once()
assert not entry.get_subentries_of_type(SUBENTRY_TYPE_VEHICLE)


async def test_subentry_authorize_existing_key_finishes(hass: HomeAssistant) -> None:
"""Approving the key after a timeout, then retrying, completes the pairing."""
entry = await _setup_account_entry(hass)
Expand Down
28 changes: 26 additions & 2 deletions tests/components/withings/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
from homeassistant.components.webhook import async_generate_url
from homeassistant.components.withings.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
Expand Down Expand Up @@ -857,6 +857,30 @@ async def test_webhook_subscription_auth_failure(
)


async def test_webhook_subscription_waits_for_start(
hass: HomeAssistant,
withings: AsyncMock,
webhook_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the webhook is only subscribed once Home Assistant has started."""
hass.set_state(CoreState.not_running)

await setup_integration(hass, webhook_config_entry)
await prepare_webhook_setup(hass, freezer)

# Withings validates the callback URL by calling it, which cannot work
# while our own HTTP server is still starting up
assert withings.subscribe_notification.call_count == 0

hass.set_state(CoreState.running)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
await hass.async_block_till_done()
await prepare_webhook_setup(hass, freezer)

assert withings.subscribe_notification.call_count == 6


async def test_webhook_subscription_invalid_params(
hass: HomeAssistant,
withings: AsyncMock,
Expand Down
Loading