From 34c5748132a93fb9d82d837565ccd71caea1277c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 12 Oct 2025 18:41:54 +0300 Subject: [PATCH 01/21] Align Shelly `async_setup_entry` in platforms (#154142) Co-authored-by: Simone Chemelli --- .../components/shelly/binary_sensor.py | 71 +++++---- homeassistant/components/shelly/button.py | 2 +- homeassistant/components/shelly/climate.py | 29 ++-- homeassistant/components/shelly/cover.py | 12 +- homeassistant/components/shelly/event.py | 144 ++++++++++-------- homeassistant/components/shelly/light.py | 12 +- homeassistant/components/shelly/number.py | 61 +++++--- homeassistant/components/shelly/select.py | 52 ++++--- homeassistant/components/shelly/sensor.py | 71 +++++---- homeassistant/components/shelly/switch.py | 12 +- homeassistant/components/shelly/text.py | 56 ++++--- homeassistant/components/shelly/update.py | 47 ++++-- homeassistant/components/shelly/valve.py | 38 ++--- 13 files changed, 368 insertions(+), 239 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index a4f62c0d803af..78885d767bdb0 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -14,7 +14,7 @@ BinarySensorEntityDescription, ) from homeassistant.const import STATE_ON, EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -337,33 +337,20 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up sensors for device.""" + """Set up binary sensor entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - if config_entry.data[CONF_SLEEP_PERIOD]: - async_setup_entry_rpc( - hass, - config_entry, - async_add_entities, - RPC_SENSORS, - RpcSleepingBinarySensor, - ) - else: - coordinator = config_entry.runtime_data.rpc - assert coordinator - - async_setup_entry_rpc( - hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor - ) - - async_remove_orphaned_entities( - hass, - config_entry.entry_id, - coordinator.mac, - BINARY_SENSOR_PLATFORM, - coordinator.device.status, - ) - return + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) + return _async_setup_block_entry(hass, config_entry, async_add_entities) + + +@callback +def _async_setup_block_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for BLOCK device.""" if config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_attribute_entities( hass, @@ -389,6 +376,38 @@ async def async_setup_entry( ) +@callback +def _async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + if config_entry.data[CONF_SLEEP_PERIOD]: + async_setup_entry_rpc( + hass, + config_entry, + async_add_entities, + RPC_SENSORS, + RpcSleepingBinarySensor, + ) + else: + coordinator = config_entry.runtime_data.rpc + assert coordinator + + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor + ) + + async_remove_orphaned_entities( + hass, + config_entry.entry_id, + coordinator.mac, + BINARY_SENSOR_PLATFORM, + coordinator.device.status, + ) + + class BlockBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity): """Represent a block binary sensor entity.""" diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index cf467209c3965..b921255fbc1e8 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -173,7 +173,7 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set buttons for device.""" + """Set up button entities.""" entry_data = config_entry.runtime_data coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None if get_device_entry_gen(config_entry) in RPC_GENERATIONS: diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index e0cffbc7f179d..4cf1bc848e0f6 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -269,27 +269,36 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up climate device.""" + """Set up climate entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - async_setup_rpc_entry(hass, config_entry, async_add_entities) - return + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) + return _async_setup_block_entry(hass, config_entry, async_add_entities) + + +@callback +def _async_setup_block_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for BLOCK device.""" coordinator = config_entry.runtime_data.block assert coordinator if coordinator.device.initialized: - async_setup_climate_entities(async_add_entities, coordinator) + _async_setup_block_climate_entities(async_add_entities, coordinator) else: - async_restore_climate_entities( + _async_restore_block_climate_entities( hass, config_entry, async_add_entities, coordinator ) @callback -def async_setup_climate_entities( +def _async_setup_block_climate_entities( async_add_entities: AddConfigEntryEntitiesCallback, coordinator: ShellyBlockCoordinator, ) -> None: - """Set up online climate devices.""" + """Set up online BLOCK climate devices.""" device_block: Block | None = None sensor_block: Block | None = None @@ -310,13 +319,13 @@ def async_setup_climate_entities( @callback -def async_restore_climate_entities( +def _async_restore_block_climate_entities( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, coordinator: ShellyBlockCoordinator, ) -> None: - """Restore sleeping climate devices.""" + """Restore sleeping BLOCK climate devices.""" ent_reg = er.async_get(hass) entries = er.async_entries_for_config_entry(ent_reg, config_entry.entry_id) @@ -332,7 +341,7 @@ def async_restore_climate_entities( @callback -def async_setup_rpc_entry( +def _async_setup_rpc_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index c7ed54752387c..b56a2b103ac93 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -63,20 +63,20 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up covers for device.""" + """Set up cover entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - return async_setup_rpc_entry(hass, config_entry, async_add_entities) + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) - return async_setup_block_entry(hass, config_entry, async_add_entities) + return _async_setup_block_entry(hass, config_entry, async_add_entities) @callback -def async_setup_block_entry( +def _async_setup_block_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up cover for device.""" + """Set up entities for BLOCK device.""" coordinator = config_entry.runtime_data.block assert coordinator @@ -86,7 +86,7 @@ def async_setup_block_entry( @callback -def async_setup_rpc_entry( +def _async_setup_rpc_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, diff --git a/homeassistant/components/shelly/event.py b/homeassistant/components/shelly/event.py index 8b2b92e11ce31..5e92abb7a7e43 100644 --- a/homeassistant/components/shelly/event.py +++ b/homeassistant/components/shelly/event.py @@ -84,71 +84,91 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up sensors for device.""" - entities: list[ShellyBlockEvent | ShellyRpcEvent] = [] + """Set up event entities.""" + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) - coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None = None + return _async_setup_block_entry(hass, config_entry, async_add_entities) - if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - coordinator = config_entry.runtime_data.rpc - if TYPE_CHECKING: - assert coordinator - - key_instances = get_rpc_key_instances(coordinator.device.status, RPC_EVENT.key) - - for key in key_instances: - if RPC_EVENT.removal_condition and RPC_EVENT.removal_condition( - coordinator.device.config, coordinator.device.status, key - ): - unique_id = f"{coordinator.mac}-{key}" - async_remove_shelly_entity(hass, EVENT_DOMAIN, unique_id) - else: - entities.append(ShellyRpcEvent(coordinator, key, RPC_EVENT)) - - script_instances = get_rpc_key_instances( - coordinator.device.status, SCRIPT_EVENT.key - ) - script_events = config_entry.runtime_data.rpc_script_events - for script in script_instances: - script_name = get_rpc_entity_name(coordinator.device, script) - if script_name == BLE_SCRIPT_NAME: - continue - - script_id = int(script.split(":")[-1]) - if script_events and (event_types := script_events[script_id]): - entities.append(ShellyRpcScriptEvent(coordinator, script, event_types)) - - # If a script is removed, from the device configuration, we need to remove orphaned entities - async_remove_orphaned_entities( - hass, - config_entry.entry_id, - coordinator.mac, - EVENT_DOMAIN, - coordinator.device.status, - "script", - ) - else: - coordinator = config_entry.runtime_data.block - if TYPE_CHECKING: - assert coordinator - assert coordinator.device.blocks - - for block in coordinator.device.blocks: - if ( - "inputEvent" not in block.sensor_ids - or "inputEventCnt" not in block.sensor_ids - ): - continue - - if BLOCK_EVENT.removal_condition and BLOCK_EVENT.removal_condition( - coordinator.device.settings, block - ): - channel = int(block.channel or 0) + 1 - unique_id = f"{coordinator.mac}-{block.description}-{channel}" - async_remove_shelly_entity(hass, EVENT_DOMAIN, unique_id) - else: - entities.append(ShellyBlockEvent(coordinator, block, BLOCK_EVENT)) +@callback +def _async_setup_block_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for BLOCK device.""" + entities: list[ShellyBlockEvent] = [] + + coordinator = config_entry.runtime_data.block + if TYPE_CHECKING: + assert coordinator and coordinator.device.blocks + + for block in coordinator.device.blocks: + if ( + "inputEvent" not in block.sensor_ids + or "inputEventCnt" not in block.sensor_ids + ): + continue + + if BLOCK_EVENT.removal_condition and BLOCK_EVENT.removal_condition( + coordinator.device.settings, block + ): + channel = int(block.channel or 0) + 1 + unique_id = f"{coordinator.mac}-{block.description}-{channel}" + async_remove_shelly_entity(hass, EVENT_DOMAIN, unique_id) + else: + entities.append(ShellyBlockEvent(coordinator, block, BLOCK_EVENT)) + + async_add_entities(entities) + + +@callback +def _async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + entities: list[ShellyRpcEvent] = [] + + coordinator = config_entry.runtime_data.rpc + if TYPE_CHECKING: + assert coordinator + + key_instances = get_rpc_key_instances(coordinator.device.status, RPC_EVENT.key) + + for key in key_instances: + if RPC_EVENT.removal_condition and RPC_EVENT.removal_condition( + coordinator.device.config, coordinator.device.status, key + ): + unique_id = f"{coordinator.mac}-{key}" + async_remove_shelly_entity(hass, EVENT_DOMAIN, unique_id) + else: + entities.append(ShellyRpcEvent(coordinator, key, RPC_EVENT)) + + script_instances = get_rpc_key_instances( + coordinator.device.status, SCRIPT_EVENT.key + ) + script_events = config_entry.runtime_data.rpc_script_events + for script in script_instances: + script_name = get_rpc_entity_name(coordinator.device, script) + if script_name == BLE_SCRIPT_NAME: + continue + + script_id = int(script.split(":")[-1]) + if script_events and (event_types := script_events[script_id]): + entities.append(ShellyRpcScriptEvent(coordinator, script, event_types)) + + # If a script is removed, from the device configuration, we need to remove orphaned entities + async_remove_orphaned_entities( + hass, + config_entry.entry_id, + coordinator.mac, + EVENT_DOMAIN, + coordinator.device.status, + "script", + ) async_add_entities(entities) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 66d523ab20a90..c66039cdca5dd 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -82,20 +82,20 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up lights for device.""" + """Set up light entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - return async_setup_rpc_entry(hass, config_entry, async_add_entities) + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) - return async_setup_block_entry(hass, config_entry, async_add_entities) + return _async_setup_block_entry(hass, config_entry, async_add_entities) @callback -def async_setup_block_entry( +def _async_setup_block_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up entities for block device.""" + """Set up entities for BLOCK device.""" coordinator = config_entry.runtime_data.block assert coordinator @@ -538,7 +538,7 @@ class RpcShellyRgbwLight(RpcShellyLightBase): @callback -def async_setup_rpc_entry( +def _async_setup_rpc_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 8ba67233a6837..a7531f47a6b77 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -19,7 +19,7 @@ RestoreNumber, ) from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry @@ -331,30 +331,20 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up numbers for device.""" + """Set up number entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - coordinator = config_entry.runtime_data.rpc - assert coordinator + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) - async_setup_entry_rpc( - hass, config_entry, async_add_entities, RPC_NUMBERS, RpcNumber - ) + return _async_setup_block_entry(hass, config_entry, async_add_entities) - # the user can remove virtual components from the device configuration, so - # we need to remove orphaned entities - virtual_number_ids = get_virtual_component_ids( - coordinator.device.config, NUMBER_PLATFORM - ) - async_remove_orphaned_entities( - hass, - config_entry.entry_id, - coordinator.mac, - NUMBER_PLATFORM, - virtual_number_ids, - "number", - ) - return +@callback +def _async_setup_block_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for BLOCK device.""" if config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_attribute_entities( hass, @@ -365,6 +355,35 @@ async def async_setup_entry( ) +@callback +def _async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + coordinator = config_entry.runtime_data.rpc + assert coordinator + + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_NUMBERS, RpcNumber + ) + + # the user can remove virtual components from the device configuration, so + # we need to remove orphaned entities + virtual_number_ids = get_virtual_component_ids( + coordinator.device.config, NUMBER_PLATFORM + ) + async_remove_orphaned_entities( + hass, + config_entry.entry_id, + coordinator.mac, + NUMBER_PLATFORM, + virtual_number_ids, + "number", + ) + + class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, RestoreNumber): """Represent a block sleeping number.""" diff --git a/homeassistant/components/shelly/select.py b/homeassistant/components/shelly/select.py index 617e2d900097c..165e9bc21a235 100644 --- a/homeassistant/components/shelly/select.py +++ b/homeassistant/components/shelly/select.py @@ -12,7 +12,7 @@ SelectEntity, SelectEntityDescription, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import ShellyConfigEntry, ShellyRpcCoordinator @@ -54,28 +54,40 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up selectors for device.""" + """Set up select entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - coordinator = config_entry.runtime_data.rpc - assert coordinator + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) - async_setup_entry_rpc( - hass, config_entry, async_add_entities, RPC_SELECT_ENTITIES, RpcSelect - ) + return None - # the user can remove virtual components from the device configuration, so - # we need to remove orphaned entities - virtual_text_ids = get_virtual_component_ids( - coordinator.device.config, SELECT_PLATFORM - ) - async_remove_orphaned_entities( - hass, - config_entry.entry_id, - coordinator.mac, - SELECT_PLATFORM, - virtual_text_ids, - "enum", - ) + +@callback +def _async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + coordinator = config_entry.runtime_data.rpc + assert coordinator + + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_SELECT_ENTITIES, RpcSelect + ) + + # the user can remove virtual components from the device configuration, so + # we need to remove orphaned entities + virtual_text_ids = get_virtual_component_ids( + coordinator.device.config, SELECT_PLATFORM + ) + async_remove_orphaned_entities( + hass, + config_entry.entry_id, + coordinator.mac, + SELECT_PLATFORM, + virtual_text_ids, + "enum", + ) class RpcSelect(ShellyRpcAttributeEntity, SelectEntity): diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 824d44f2fb50b..8cb3736e09063 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -36,7 +36,7 @@ UnitOfVolume, UnitOfVolumeFlowRate, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.typing import StateType @@ -1710,33 +1710,20 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up sensors for device.""" + """Set up sensor entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - if config_entry.data[CONF_SLEEP_PERIOD]: - async_setup_entry_rpc( - hass, - config_entry, - async_add_entities, - RPC_SENSORS, - RpcSleepingSensor, - ) - else: - coordinator = config_entry.runtime_data.rpc - assert coordinator - - async_setup_entry_rpc( - hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor - ) - - async_remove_orphaned_entities( - hass, - config_entry.entry_id, - coordinator.mac, - SENSOR_PLATFORM, - coordinator.device.status, - ) - return + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) + return _async_setup_block_entry(hass, config_entry, async_add_entities) + + +@callback +def _async_setup_block_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for BLOCK device.""" if config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_attribute_entities( hass, @@ -1758,6 +1745,38 @@ async def async_setup_entry( ) +@callback +def _async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + if config_entry.data[CONF_SLEEP_PERIOD]: + async_setup_entry_rpc( + hass, + config_entry, + async_add_entities, + RPC_SENSORS, + RpcSleepingSensor, + ) + else: + coordinator = config_entry.runtime_data.rpc + assert coordinator + + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor + ) + + async_remove_orphaned_entities( + hass, + config_entry.entry_id, + coordinator.mac, + SENSOR_PLATFORM, + coordinator.device.status, + ) + + class BlockSensor(ShellyBlockAttributeEntity, SensorEntity): """Represent a block sensor.""" diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 43395f3f8da3a..20b72ce8933bd 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -264,20 +264,20 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up switches for device.""" + """Set up switch entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - return async_setup_rpc_entry(hass, config_entry, async_add_entities) + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) - return async_setup_block_entry(hass, config_entry, async_add_entities) + return _async_setup_block_entry(hass, config_entry, async_add_entities) @callback -def async_setup_block_entry( +def _async_setup_block_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up entities for block device.""" + """Set up entities for BLOCK device.""" coordinator = config_entry.runtime_data.block assert coordinator @@ -295,7 +295,7 @@ def async_setup_block_entry( @callback -def async_setup_rpc_entry( +def _async_setup_rpc_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, diff --git a/homeassistant/components/shelly/text.py b/homeassistant/components/shelly/text.py index ef30ec310ed6a..5643c34c7272a 100644 --- a/homeassistant/components/shelly/text.py +++ b/homeassistant/components/shelly/text.py @@ -12,7 +12,7 @@ TextEntity, TextEntityDescription, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import ShellyConfigEntry @@ -54,28 +54,40 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up sensors for device.""" + """Set up text entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - coordinator = config_entry.runtime_data.rpc - assert coordinator - - async_setup_entry_rpc( - hass, config_entry, async_add_entities, RPC_TEXT_ENTITIES, RpcText - ) - - # the user can remove virtual components from the device configuration, so - # we need to remove orphaned entities - virtual_text_ids = get_virtual_component_ids( - coordinator.device.config, TEXT_PLATFORM - ) - async_remove_orphaned_entities( - hass, - config_entry.entry_id, - coordinator.mac, - TEXT_PLATFORM, - virtual_text_ids, - "text", - ) + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) + + return None + + +@callback +def _async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + coordinator = config_entry.runtime_data.rpc + assert coordinator + + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_TEXT_ENTITIES, RpcText + ) + + # the user can remove virtual components from the device configuration, so + # we need to remove orphaned entities + virtual_text_ids = get_virtual_component_ids( + coordinator.device.config, TEXT_PLATFORM + ) + async_remove_orphaned_entities( + hass, + config_entry.entry_id, + coordinator.mac, + TEXT_PLATFORM, + virtual_text_ids, + "text", + ) class RpcText(ShellyRpcAttributeEntity, TextEntity): diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 2ff2462bd7989..703050f4b2770 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -115,22 +115,20 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up update entities for Shelly component.""" + """Set up update entities.""" if get_device_entry_gen(config_entry) in RPC_GENERATIONS: - if config_entry.data[CONF_SLEEP_PERIOD]: - async_setup_entry_rpc( - hass, - config_entry, - async_add_entities, - RPC_UPDATES, - RpcSleepingUpdateEntity, - ) - else: - async_setup_entry_rpc( - hass, config_entry, async_add_entities, RPC_UPDATES, RpcUpdateEntity - ) - return + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) + + return _async_setup_block_entry(hass, config_entry, async_add_entities) + +@callback +def _async_setup_block_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for BLOCK device.""" if not config_entry.data[CONF_SLEEP_PERIOD]: async_setup_entry_rest( hass, @@ -141,6 +139,27 @@ async def async_setup_entry( ) +@callback +def _async_setup_rpc_entry( + hass: HomeAssistant, + config_entry: ShellyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up entities for RPC device.""" + if config_entry.data[CONF_SLEEP_PERIOD]: + async_setup_entry_rpc( + hass, + config_entry, + async_add_entities, + RPC_UPDATES, + RpcSleepingUpdateEntity, + ) + else: + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_UPDATES, RpcUpdateEntity + ) + + class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity): """Represent a REST update entity.""" diff --git a/homeassistant/components/shelly/valve.py b/homeassistant/components/shelly/valve.py index e723b793fe6fc..ada262e7bbf3c 100644 --- a/homeassistant/components/shelly/valve.py +++ b/homeassistant/components/shelly/valve.py @@ -6,7 +6,7 @@ from typing import Any, cast from aioshelly.block_device import Block -from aioshelly.const import BLOCK_GENERATIONS, MODEL_GAS +from aioshelly.const import MODEL_GAS, RPC_GENERATIONS from homeassistant.components.valve import ( ValveDeviceClass, @@ -135,44 +135,44 @@ async def async_setup_entry( config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up valves for device.""" - if get_device_entry_gen(config_entry) in BLOCK_GENERATIONS: - return async_setup_block_entry(hass, config_entry, async_add_entities) + """Set up valve entities.""" + if get_device_entry_gen(config_entry) in RPC_GENERATIONS: + return _async_setup_rpc_entry(hass, config_entry, async_add_entities) - return async_setup_rpc_entry(hass, config_entry, async_add_entities) + return _async_setup_block_entry(hass, config_entry, async_add_entities) @callback -def async_setup_rpc_entry( +def _async_setup_block_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up entities for RPC device.""" - coordinator = config_entry.runtime_data.rpc + """Set up entities for BLOCK device.""" + coordinator = config_entry.runtime_data.block assert coordinator - async_setup_entry_rpc( - hass, config_entry, async_add_entities, RPC_VALVES, RpcShellyWaterValve + async_setup_block_attribute_entities( + hass, + async_add_entities, + coordinator, + BLOCK_VALVES, + BlockShellyValve, ) @callback -def async_setup_block_entry( +def _async_setup_rpc_entry( hass: HomeAssistant, config_entry: ShellyConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: - """Set up valve for device.""" - coordinator = config_entry.runtime_data.block + """Set up entities for RPC device.""" + coordinator = config_entry.runtime_data.rpc assert coordinator - async_setup_block_attribute_entities( - hass, - async_add_entities, - coordinator, - BLOCK_VALVES, - BlockShellyValve, + async_setup_entry_rpc( + hass, config_entry, async_add_entities, RPC_VALVES, RpcShellyWaterValve ) From de4bfd6f05e813ade604c27e71e21882011af65d Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 12 Oct 2025 18:07:19 +0200 Subject: [PATCH 02/21] Bump pyOverkiz to 1.19.0 in Overkiz (#154310) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 48f06ffe35389..8beb6b14099a8 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.17.2"], + "requirements": ["pyoverkiz==1.19.0"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 00002e1388a1d..ee47e014b2a36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2263,7 +2263,7 @@ pyotgw==2.2.2 pyotp==2.9.0 # homeassistant.components.overkiz -pyoverkiz==1.17.2 +pyoverkiz==1.19.0 # homeassistant.components.onewire pyownet==0.10.0.post1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c988f7686ddc..dedab121e7fe6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1893,7 +1893,7 @@ pyotgw==2.2.2 pyotp==2.9.0 # homeassistant.components.overkiz -pyoverkiz==1.17.2 +pyoverkiz==1.19.0 # homeassistant.components.onewire pyownet==0.10.0.post1 From d457787639154d871effbcf0f7e4e2a2615d9f0d Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sun, 12 Oct 2025 18:23:24 +0200 Subject: [PATCH 03/21] Move URL out of Overkiz Config Flow descriptions (#154315) --- homeassistant/components/overkiz/config_flow.py | 4 +++- homeassistant/components/overkiz/strings.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 520e94601474f..7a02268376947 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -210,7 +210,9 @@ async def async_step_local( ) -> ConfigFlowResult: """Handle the local authentication step via config flow.""" errors = {} - description_placeholders = {} + description_placeholders = { + "somfy-developer-mode-docs": "https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started" + } if user_input: self._host = user_input[CONF_HOST] diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index b82b45de16c7b..6f0001691a620 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -32,7 +32,7 @@ } }, "local": { - "description": "By activating the [Developer Mode of your TaHoma box](https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode#getting-started), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway.", + "description": "By activating the [Developer Mode of your TaHoma box]({somfy-developer-mode-docs}), you can authorize third-party software (like Home Assistant) to connect to it via your local network.\n\n1. Open the TaHoma By Somfy application on your device.\n2. Navigate to the Help & advanced features -> Advanced features menu in the application.\n3. Activate Developer Mode by tapping 7 times on the version number of your gateway (like 2025.1.4-11).\n4. Generate a token from the Developer Mode menu to authenticate your API calls.\n\n5. Enter the generated token below and update the host to include your Gateway PIN or the IP address of your gateway.", "data": { "host": "[%key:common::config_flow::data::host%]", "token": "[%key:common::config_flow::data::api_token%]", From 3c1496d2bbeecfde1cb53b4bcf50c3ab9471d357 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sun, 12 Oct 2025 19:44:38 +0300 Subject: [PATCH 04/21] Add gpt-image-1-mini support (#154316) --- .../components/openai_conversation/ai_task.py | 10 ++++++++-- .../openai_conversation/config_flow.py | 15 +++++++++++++++ .../components/openai_conversation/const.py | 2 ++ .../components/openai_conversation/entity.py | 16 ++++++++++------ .../components/openai_conversation/strings.json | 4 ++++ .../openai_conversation/test_ai_task.py | 10 +++++++++- .../openai_conversation/test_config_flow.py | 2 ++ 7 files changed, 50 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/openai_conversation/ai_task.py b/homeassistant/components/openai_conversation/ai_task.py index bc05671e48fae..91933a36bb97e 100644 --- a/homeassistant/components/openai_conversation/ai_task.py +++ b/homeassistant/components/openai_conversation/ai_task.py @@ -16,7 +16,13 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.json import json_loads -from .const import CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL, UNSUPPORTED_IMAGE_MODELS +from .const import ( + CONF_CHAT_MODEL, + CONF_IMAGE_MODEL, + RECOMMENDED_CHAT_MODEL, + RECOMMENDED_IMAGE_MODEL, + UNSUPPORTED_IMAGE_MODELS, +) from .entity import OpenAIBaseLLMEntity if TYPE_CHECKING: @@ -142,7 +148,7 @@ async def _async_generate_image( mime_type=mime_type, width=int(width) if width else None, height=int(height) if height else None, - model="gpt-image-1", + model=self.subentry.data.get(CONF_IMAGE_MODEL, RECOMMENDED_IMAGE_MODEL), revised_prompt=image_call.revised_prompt if hasattr(image_call, "revised_prompt") else None, diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py index e21005d85415c..a9fdf5fd77152 100644 --- a/homeassistant/components/openai_conversation/config_flow.py +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -43,6 +43,7 @@ from .const import ( CONF_CHAT_MODEL, CONF_CODE_INTERPRETER, + CONF_IMAGE_MODEL, CONF_MAX_TOKENS, CONF_PROMPT, CONF_REASONING_EFFORT, @@ -64,6 +65,7 @@ RECOMMENDED_CHAT_MODEL, RECOMMENDED_CODE_INTERPRETER, RECOMMENDED_CONVERSATION_OPTIONS, + RECOMMENDED_IMAGE_MODEL, RECOMMENDED_MAX_TOKENS, RECOMMENDED_REASONING_EFFORT, RECOMMENDED_TEMPERATURE, @@ -72,6 +74,7 @@ RECOMMENDED_WEB_SEARCH, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE, RECOMMENDED_WEB_SEARCH_USER_LOCATION, + UNSUPPORTED_IMAGE_MODELS, UNSUPPORTED_MODELS, UNSUPPORTED_WEB_SEARCH_MODELS, ) @@ -411,6 +414,18 @@ async def async_step_model( ) } + if self._subentry_type == "ai_task_data" and not model.startswith( + tuple(UNSUPPORTED_IMAGE_MODELS) + ): + step_schema[ + vol.Optional(CONF_IMAGE_MODEL, default=RECOMMENDED_IMAGE_MODEL) + ] = SelectSelector( + SelectSelectorConfig( + options=["gpt-image-1", "gpt-image-1-mini"], + mode=SelectSelectorMode.DROPDOWN, + ) + ) + if user_input is not None: if user_input.get(CONF_WEB_SEARCH): if user_input.get(CONF_WEB_SEARCH_USER_LOCATION): diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py index fda862e1dbe77..9d936e03348ac 100644 --- a/homeassistant/components/openai_conversation/const.py +++ b/homeassistant/components/openai_conversation/const.py @@ -13,6 +13,7 @@ DEFAULT_NAME = "OpenAI Conversation" CONF_CHAT_MODEL = "chat_model" +CONF_IMAGE_MODEL = "image_model" CONF_CODE_INTERPRETER = "code_interpreter" CONF_FILENAMES = "filenames" CONF_MAX_TOKENS = "max_tokens" @@ -31,6 +32,7 @@ CONF_WEB_SEARCH_TIMEZONE = "timezone" RECOMMENDED_CODE_INTERPRETER = False RECOMMENDED_CHAT_MODEL = "gpt-4o-mini" +RECOMMENDED_IMAGE_MODEL = "gpt-image-1" RECOMMENDED_MAX_TOKENS = 3000 RECOMMENDED_REASONING_EFFORT = "low" RECOMMENDED_TEMPERATURE = 1.0 diff --git a/homeassistant/components/openai_conversation/entity.py b/homeassistant/components/openai_conversation/entity.py index 0ff6e662918a8..845c8a7bfed03 100644 --- a/homeassistant/components/openai_conversation/entity.py +++ b/homeassistant/components/openai_conversation/entity.py @@ -67,6 +67,7 @@ from .const import ( CONF_CHAT_MODEL, CONF_CODE_INTERPRETER, + CONF_IMAGE_MODEL, CONF_MAX_TOKENS, CONF_REASONING_EFFORT, CONF_TEMPERATURE, @@ -82,6 +83,7 @@ DOMAIN, LOGGER, RECOMMENDED_CHAT_MODEL, + RECOMMENDED_IMAGE_MODEL, RECOMMENDED_MAX_TOKENS, RECOMMENDED_REASONING_EFFORT, RECOMMENDED_TEMPERATURE, @@ -516,13 +518,15 @@ async def _async_handle_chat_log( model_args.setdefault("include", []).append("code_interpreter_call.outputs") # type: ignore[union-attr] if force_image: - tools.append( - ImageGeneration( - type="image_generation", - input_fidelity="high", - output_format="png", - ) + image_model = options.get(CONF_IMAGE_MODEL, RECOMMENDED_IMAGE_MODEL) + image_tool = ImageGeneration( + type="image_generation", + model=image_model, + output_format="png", ) + if image_model == "gpt-image-1": + image_tool["input_fidelity"] = "high" + tools.append(image_tool) model_args["tool_choice"] = ToolChoiceTypesParam(type="image_generation") model_args["store"] = True # Avoid sending image data back and forth diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json index 190e86e87b894..e5b3cb3064657 100644 --- a/homeassistant/components/openai_conversation/strings.json +++ b/homeassistant/components/openai_conversation/strings.json @@ -50,6 +50,7 @@ "data": { "code_interpreter": "Enable code interpreter tool", "reasoning_effort": "Reasoning effort", + "image_model": "Image generation model", "web_search": "Enable web search", "search_context_size": "Search context size", "user_location": "Include home location" @@ -57,6 +58,7 @@ "data_description": { "code_interpreter": "This tool, also known as the python tool to the model, allows it to run code to answer questions", "reasoning_effort": "How many reasoning tokens the model should generate before creating a response to the prompt", + "image_model": "The model to use when generating images", "web_search": "Allow the model to search the web for the latest information before generating a response", "search_context_size": "High level guidance for the amount of context window space to use for the search", "user_location": "Refine search results based on geography" @@ -97,12 +99,14 @@ "title": "[%key:component::openai_conversation::config_subentries::conversation::step::model::title%]", "data": { "reasoning_effort": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::reasoning_effort%]", + "image_model": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::image_model%]", "web_search": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::web_search%]", "search_context_size": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::search_context_size%]", "user_location": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::user_location%]" }, "data_description": { "reasoning_effort": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::reasoning_effort%]", + "image_model": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::image_model%]", "web_search": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::web_search%]", "search_context_size": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::search_context_size%]", "user_location": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::user_location%]" diff --git a/tests/components/openai_conversation/test_ai_task.py b/tests/components/openai_conversation/test_ai_task.py index b9a69e5f77ed6..77d878bcfa136 100644 --- a/tests/components/openai_conversation/test_ai_task.py +++ b/tests/components/openai_conversation/test_ai_task.py @@ -213,12 +213,14 @@ async def test_generate_data_with_attachments( @pytest.mark.usefixtures("mock_init_component") @freeze_time("2025-06-14 22:59:00") +@pytest.mark.parametrize("image_model", ["gpt-image-1", "gpt-image-1-mini"]) async def test_generate_image( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_create_stream: AsyncMock, entity_registry: er.EntityRegistry, issue_registry: ir.IssueRegistry, + image_model: str, ) -> None: """Test AI Task image generation.""" entity_id = "ai_task.openai_ai_task" @@ -232,6 +234,12 @@ async def test_generate_image( if entry.subentry_type == "ai_task_data" ) ) + hass.config_entries.async_update_subentry( + mock_config_entry, + ai_task_entry, + data={"image_model": image_model}, + ) + await hass.async_block_till_done() assert entity_entry is not None assert entity_entry.config_entry_id == mock_config_entry.entry_id assert entity_entry.config_subentry_id == ai_task_entry.subentry_id @@ -258,7 +266,7 @@ async def test_generate_image( assert result["width"] == 1536 assert result["revised_prompt"] == "Mock revised prompt." assert result["mime_type"] == "image/png" - assert result["model"] == "gpt-image-1" + assert result["model"] == image_model mock_upload_media.assert_called_once() image_data = mock_upload_media.call_args[0][1] diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py index 4a87365a4a699..ce0a64ea718f5 100644 --- a/tests/components/openai_conversation/test_config_flow.py +++ b/tests/components/openai_conversation/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components.openai_conversation.const import ( CONF_CHAT_MODEL, CONF_CODE_INTERPRETER, + CONF_IMAGE_MODEL, CONF_MAX_TOKENS, CONF_PROMPT, CONF_REASONING_EFFORT, @@ -917,6 +918,7 @@ async def test_creating_ai_task_subentry_advanced( assert result4.get("data") == { CONF_RECOMMENDED: False, CONF_CHAT_MODEL: "gpt-4o", + CONF_IMAGE_MODEL: "gpt-image-1", CONF_MAX_TOKENS: 200, CONF_TEMPERATURE: 0.5, CONF_TOP_P: 0.9, From bf4f8b48a3d6eb5e05bc3b97d3028bc8015d843a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:46:04 +0200 Subject: [PATCH 05/21] Update pylint to 4.0.0 + astroid to 4.0.1 (#154311) --- pyproject.toml | 2 ++ requirements_test.txt | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 79f974b6d33ba..6f4a8db8b00ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,6 +161,7 @@ class-const-naming-style = "any" # possibly-used-before-assignment - too many errors / not necessarily issues # --- # Pylint CodeStyle plugin +# consider-math-not-float # consider-using-namedtuple-or-dataclass - too opinionated # consider-using-assignment-expr - decision to use := better left to devs disable = [ @@ -181,6 +182,7 @@ disable = [ "too-many-boolean-expressions", "too-many-positional-arguments", "wrong-import-order", + "consider-math-not-float", "consider-using-namedtuple-or-dataclass", "consider-using-assignment-expr", "possibly-used-before-assignment", diff --git a/requirements_test.txt b/requirements_test.txt index 3ed5357422570..799b697e7533e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -astroid==3.3.11 +astroid==4.0.1 coverage==7.10.6 freezegun==1.5.2 go2rtc-client==0.2.1 @@ -18,7 +18,7 @@ mock-open==1.4.0 mypy-dev==1.19.0a4 pre-commit==4.2.0 pydantic==2.12.0 -pylint==3.3.9 +pylint==4.0.0 pylint-per-file-ignores==1.4.0 pipdeptree==2.26.1 pytest-asyncio==1.2.0 From ef60d16659a2932d5e04cbd88d0074117dd4efb0 Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Sun, 12 Oct 2025 12:47:22 -0400 Subject: [PATCH 06/21] Fix Environment Canada camera entity initialization (#154302) Co-authored-by: Claude --- .../components/environment_canada/__init__.py | 11 +++++------ homeassistant/components/environment_canada/camera.py | 8 ++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 6afea2f983d1f..ae2e569bc103b 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -47,11 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) -> radar_coordinator = ECDataUpdateCoordinator( hass, config_entry, radar_data, "radar", DEFAULT_RADAR_UPDATE_INTERVAL ) - try: - await radar_coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady: - errors = errors + 1 - _LOGGER.warning("Unable to retrieve Environment Canada radar") + # Skip initial refresh for radar since the camera entity is disabled by default. + # The coordinator will fetch data when the entity is enabled. aqhi_data = ECAirQuality(coordinates=(lat, lon)) aqhi_coordinator = ECDataUpdateCoordinator( @@ -63,7 +60,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) -> errors = errors + 1 _LOGGER.warning("Unable to retrieve Environment Canada AQHI") - if errors == 3: + # Require at least one coordinator to succeed (weather or AQHI) + # Radar is optional since the camera entity is disabled by default + if errors >= 2: raise ConfigEntryNotReady config_entry.runtime_data = ECRuntimeData( diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index b051c572816bd..eb3c5a1bfb798 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -59,6 +59,14 @@ def __init__(self, coordinator: ECDataUpdateCoordinator[ECRadar]) -> None: self.content_type = "image/gif" + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + # Trigger coordinator refresh when entity is enabled + # since radar coordinator skips initial refresh during setup + if not self.coordinator.last_update_success: + await self.coordinator.async_request_refresh() + def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: From 38e46f7a53ddd14edf59914caf818be2333cd2f1 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 12 Oct 2025 18:50:46 +0200 Subject: [PATCH 07/21] Bump plugwise to v1.8.0 - add initial support for Emma (#154277) --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 69b456ca8d8be..bb3efc6a7f5c8 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["plugwise"], "quality_scale": "platinum", - "requirements": ["plugwise==1.7.8"], + "requirements": ["plugwise==1.8.0"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index ee47e014b2a36..db080dd76d78b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1720,7 +1720,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==1.7.8 +plugwise==1.8.0 # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dedab121e7fe6..631bdbf6abbbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1461,7 +1461,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==1.7.8 +plugwise==1.8.0 # homeassistant.components.poolsense poolsense==0.0.8 From 6ef1b3bad39c2ecf82c6eb964a3a175bec6d00e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 06:51:05 -1000 Subject: [PATCH 08/21] Bump aioesphomeapi to 41.14.0 (#154275) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index adc0ffab70b0e..0e18b416db9d8 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -17,7 +17,7 @@ "mqtt": ["esphome/discover/#"], "quality_scale": "platinum", "requirements": [ - "aioesphomeapi==41.13.0", + "aioesphomeapi==41.14.0", "esphome-dashboard-api==1.3.0", "bleak-esphome==3.4.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index db080dd76d78b..15364701525ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioelectricitymaps==1.1.1 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==41.13.0 +aioesphomeapi==41.14.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 631bdbf6abbbc..1f75f4130802b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,7 +235,7 @@ aioelectricitymaps==1.1.1 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==41.13.0 +aioesphomeapi==41.14.0 # homeassistant.components.flo aioflo==2021.11.0 From 09e539bf0e10bdf366067401d88e6cab4915cb86 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 12 Oct 2025 18:51:50 +0200 Subject: [PATCH 09/21] Fix home wiziard total increasing sensors returning 0 (#154264) --- homeassistant/components/homewizard/sensor.py | 20 ++++++++--------- .../homewizard/snapshots/test_sensor.ambr | 22 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index a35f841175e1b..75bafa5ddb1a0 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -158,7 +158,7 @@ def uptime_to_datetime(value: int) -> datetime: device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, has_fn=lambda data: data.measurement.energy_import_kwh is not None, - value_fn=lambda data: data.measurement.energy_import_kwh, + value_fn=lambda data: data.measurement.energy_import_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_import_t1_kwh", @@ -172,7 +172,7 @@ def uptime_to_datetime(value: int) -> datetime: data.measurement.energy_import_t1_kwh is not None and data.measurement.energy_export_t2_kwh is not None ), - value_fn=lambda data: data.measurement.energy_import_t1_kwh, + value_fn=lambda data: data.measurement.energy_import_t1_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_import_t2_kwh", @@ -182,7 +182,7 @@ def uptime_to_datetime(value: int) -> datetime: device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, has_fn=lambda data: data.measurement.energy_import_t2_kwh is not None, - value_fn=lambda data: data.measurement.energy_import_t2_kwh, + value_fn=lambda data: data.measurement.energy_import_t2_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_import_t3_kwh", @@ -192,7 +192,7 @@ def uptime_to_datetime(value: int) -> datetime: device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, has_fn=lambda data: data.measurement.energy_import_t3_kwh is not None, - value_fn=lambda data: data.measurement.energy_import_t3_kwh, + value_fn=lambda data: data.measurement.energy_import_t3_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_import_t4_kwh", @@ -202,7 +202,7 @@ def uptime_to_datetime(value: int) -> datetime: device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, has_fn=lambda data: data.measurement.energy_import_t4_kwh is not None, - value_fn=lambda data: data.measurement.energy_import_t4_kwh, + value_fn=lambda data: data.measurement.energy_import_t4_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_export_kwh", @@ -212,7 +212,7 @@ def uptime_to_datetime(value: int) -> datetime: state_class=SensorStateClass.TOTAL_INCREASING, has_fn=lambda data: data.measurement.energy_export_kwh is not None, enabled_fn=lambda data: data.measurement.energy_export_kwh != 0, - value_fn=lambda data: data.measurement.energy_export_kwh, + value_fn=lambda data: data.measurement.energy_export_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_export_t1_kwh", @@ -227,7 +227,7 @@ def uptime_to_datetime(value: int) -> datetime: and data.measurement.energy_export_t2_kwh is not None ), enabled_fn=lambda data: data.measurement.energy_export_t1_kwh != 0, - value_fn=lambda data: data.measurement.energy_export_t1_kwh, + value_fn=lambda data: data.measurement.energy_export_t1_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_export_t2_kwh", @@ -238,7 +238,7 @@ def uptime_to_datetime(value: int) -> datetime: state_class=SensorStateClass.TOTAL_INCREASING, has_fn=lambda data: data.measurement.energy_export_t2_kwh is not None, enabled_fn=lambda data: data.measurement.energy_export_t2_kwh != 0, - value_fn=lambda data: data.measurement.energy_export_t2_kwh, + value_fn=lambda data: data.measurement.energy_export_t2_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_export_t3_kwh", @@ -249,7 +249,7 @@ def uptime_to_datetime(value: int) -> datetime: state_class=SensorStateClass.TOTAL_INCREASING, has_fn=lambda data: data.measurement.energy_export_t3_kwh is not None, enabled_fn=lambda data: data.measurement.energy_export_t3_kwh != 0, - value_fn=lambda data: data.measurement.energy_export_t3_kwh, + value_fn=lambda data: data.measurement.energy_export_t3_kwh or None, ), HomeWizardSensorEntityDescription( key="total_power_export_t4_kwh", @@ -260,7 +260,7 @@ def uptime_to_datetime(value: int) -> datetime: state_class=SensorStateClass.TOTAL_INCREASING, has_fn=lambda data: data.measurement.energy_export_t4_kwh is not None, enabled_fn=lambda data: data.measurement.energy_export_t4_kwh != 0, - value_fn=lambda data: data.measurement.energy_export_t4_kwh, + value_fn=lambda data: data.measurement.energy_export_t4_kwh or None, ), HomeWizardSensorEntityDescription( key="active_power_w", diff --git a/tests/components/homewizard/snapshots/test_sensor.ambr b/tests/components/homewizard/snapshots/test_sensor.ambr index 1bde08b3201ec..4ee0a4feb5cdc 100644 --- a/tests/components/homewizard/snapshots/test_sensor.ambr +++ b/tests/components/homewizard/snapshots/test_sensor.ambr @@ -12599,7 +12599,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_export_tariff_1:device-registry] @@ -12690,7 +12690,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_export_tariff_2:device-registry] @@ -12781,7 +12781,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_export_tariff_3:device-registry] @@ -12872,7 +12872,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_export_tariff_4:device-registry] @@ -12963,7 +12963,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_import:device-registry] @@ -13054,7 +13054,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_import_tariff_1:device-registry] @@ -13145,7 +13145,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_import_tariff_2:device-registry] @@ -13236,7 +13236,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_import_tariff_3:device-registry] @@ -13327,7 +13327,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_energy_import_tariff_4:device-registry] @@ -13418,7 +13418,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-P1-zero-values-entity_ids1][sensor.device_frequency:device-registry] @@ -15249,7 +15249,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.0', + 'state': 'unavailable', }) # --- # name: test_sensors[HWE-SKT-11-entity_ids2][sensor.device_energy_import:device-registry] From ac7be97245e61ff8e22b7ffc0ad96accf03ea757 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 12 Oct 2025 19:25:53 +0200 Subject: [PATCH 10/21] Bump aioamazondevices to 6.4.3 (#154293) --- homeassistant/components/alexa_devices/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa_devices/manifest.json b/homeassistant/components/alexa_devices/manifest.json index 165b0c6a74fcb..3e87cc5590266 100644 --- a/homeassistant/components/alexa_devices/manifest.json +++ b/homeassistant/components/alexa_devices/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["aioamazondevices"], "quality_scale": "platinum", - "requirements": ["aioamazondevices==6.4.1"] + "requirements": ["aioamazondevices==6.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 15364701525ce..3a4c3e0d48a56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.1 # homeassistant.components.alexa_devices -aioamazondevices==6.4.1 +aioamazondevices==6.4.3 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f75f4130802b..22507e9121a3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -173,7 +173,7 @@ aioairzone-cloud==0.7.2 aioairzone==1.0.1 # homeassistant.components.alexa_devices -aioamazondevices==6.4.1 +aioamazondevices==6.4.3 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 53b65b2fb4c9b7160ce0e9a787960f5c18449fde Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Sun, 12 Oct 2025 14:31:02 -0400 Subject: [PATCH 11/21] Bump env-canada to v0.12.1 (#154303) Co-authored-by: Claude --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 7dcfadd9ffe74..ce1b6b49d9f06 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/environment_canada", "iot_class": "cloud_polling", "loggers": ["env_canada"], - "requirements": ["env-canada==0.11.3"] + "requirements": ["env-canada==0.12.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3a4c3e0d48a56..c15899387c9fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -895,7 +895,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.11.3 +env-canada==0.12.1 # homeassistant.components.season ephem==4.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22507e9121a3f..ecd1f6bde273e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -777,7 +777,7 @@ energyzero==2.1.1 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.11.3 +env-canada==0.12.1 # homeassistant.components.season ephem==4.1.6 From be843970fded03c5ab2d8ad90bf08a5d4b164972 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sun, 12 Oct 2025 20:38:27 +0200 Subject: [PATCH 12/21] bump tilt-ble to 1.0.1 (#154320) --- homeassistant/components/tilt_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tilt_ble/manifest.json b/homeassistant/components/tilt_ble/manifest.json index 1b178cdb2a60e..7d75d186bc94f 100644 --- a/homeassistant/components/tilt_ble/manifest.json +++ b/homeassistant/components/tilt_ble/manifest.json @@ -12,5 +12,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/tilt_ble", "iot_class": "local_push", - "requirements": ["tilt-ble==0.3.1"] + "requirements": ["tilt-ble==1.0.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index c15899387c9fb..af2f435dc9697 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2991,7 +2991,7 @@ thinqconnect==1.0.8 tikteck==0.4 # homeassistant.components.tilt_ble -tilt-ble==0.3.1 +tilt-ble==1.0.1 # homeassistant.components.tilt_pi tilt-pi==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecd1f6bde273e..acf23826d8e65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2474,7 +2474,7 @@ thermopro-ble==0.13.1 thinqconnect==1.0.8 # homeassistant.components.tilt_ble -tilt-ble==0.3.1 +tilt-ble==1.0.1 # homeassistant.components.tilt_pi tilt-pi==0.2.1 From 7948b352659b495e1f02d4742aa82e622d1d787a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 09:16:02 -1000 Subject: [PATCH 13/21] Fix Yale integration to handle unavailable OAuth implementation at startup (#154245) --- homeassistant/components/yale/__init__.py | 13 ++++++++----- tests/components/yale/test_init.py | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/yale/__init__.py b/homeassistant/components/yale/__init__.py index 1cbd9c87b572e..f3db044a7852b 100644 --- a/homeassistant/components/yale/__init__.py +++ b/homeassistant/components/yale/__init__.py @@ -27,13 +27,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool: - """Set up yale from a config entry.""" + """Set up Yale from a config entry.""" session = async_create_yale_clientsession(hass) - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry + try: + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) ) - ) + except ValueError as err: + raise ConfigEntryNotReady("OAuth implementation not available") from err oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session) try: diff --git a/tests/components/yale/test_init.py b/tests/components/yale/test_init.py index ec43c07f1eefd..1276907133a54 100644 --- a/tests/components/yale/test_init.py +++ b/tests/components/yale/test_init.py @@ -1,6 +1,6 @@ """The tests for the yale platform.""" -from unittest.mock import Mock +from unittest.mock import Mock, patch from aiohttp import ClientResponseError import pytest @@ -28,6 +28,8 @@ _mock_inoperative_yale_lock_detail, _mock_lock_with_offline_key, _mock_operative_yale_lock_detail, + mock_client_credentials, + mock_yale_config_entry, ) from tests.typing import WebSocketGenerator @@ -234,3 +236,18 @@ async def test_device_remove_devices( ) response = await client.remove_device(dead_device_entry.id, config_entry.entry_id) assert response["success"] + + +async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None: + """Test that unavailable OAuth implementation raises ConfigEntryNotReady.""" + await mock_client_credentials(hass) + entry = await mock_yale_config_entry(hass) + + with patch( + "homeassistant.components.yale.config_entry_oauth2_flow.async_get_config_entry_implementation", + side_effect=ValueError("Implementation not available"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY From c9d67d596b4566a809ddfd9d4d81e3a37f96ebd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Oct 2025 09:16:22 -1000 Subject: [PATCH 14/21] Fix August integration to handle unavailable OAuth implementation at startup (#154244) --- homeassistant/components/august/__init__.py | 11 +++++++---- tests/components/august/test_init.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 38a3ade2d905f..3267fbfb0bc75 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -36,11 +36,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo raise ConfigEntryAuthFailed("Migration to OAuth required") session = async_create_august_clientsession(hass) - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry + try: + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) ) - ) + except ValueError as err: + raise ConfigEntryNotReady("OAuth implementation not available") from err oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session) try: diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 33517e9e13071..ae30c8ad97796 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,6 +1,6 @@ """The tests for the august platform.""" -from unittest.mock import Mock +from unittest.mock import Mock, patch from aiohttp import ClientResponseError import pytest @@ -33,6 +33,8 @@ _mock_inoperative_august_lock_detail, _mock_lock_with_offline_key, _mock_operative_august_lock_detail, + mock_august_config_entry, + mock_client_credentials, ) from tests.common import MockConfigEntry @@ -284,3 +286,18 @@ async def test_oauth_migration_on_legacy_entry(hass: HomeAssistant) -> None: assert len(flows) == 1 assert flows[0]["step_id"] == "pick_implementation" assert flows[0]["context"]["source"] == "reauth" + + +async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None: + """Test that unavailable OAuth implementation raises ConfigEntryNotReady.""" + await mock_client_credentials(hass) + entry = await mock_august_config_entry(hass) + + with patch( + "homeassistant.components.august.config_entry_oauth2_flow.async_get_config_entry_implementation", + side_effect=ValueError("Implementation not available"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY From 9b9c55b37b2f91ba31cf575e23c2206551bcd2c5 Mon Sep 17 00:00:00 2001 From: Marcus Gustavsson Date: Sun, 12 Oct 2025 20:17:43 +0100 Subject: [PATCH 15/21] Updated prowlpy to 1.1.1 and changed the usage to do asynchronous calls (#154193) --- homeassistant/components/prowl/helpers.py | 6 +-- homeassistant/components/prowl/manifest.json | 2 +- homeassistant/components/prowl/notify.py | 57 ++++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/prowl/conftest.py | 29 +++++++--- tests/components/prowl/test_config_flow.py | 16 +++--- tests/components/prowl/test_init.py | 6 +-- tests/components/prowl/test_notify.py | 28 +++++----- 9 files changed, 85 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/prowl/helpers.py b/homeassistant/components/prowl/helpers.py index 89eebf2720a92..b567c9ffc04f2 100644 --- a/homeassistant/components/prowl/helpers.py +++ b/homeassistant/components/prowl/helpers.py @@ -1,19 +1,19 @@ """Helper functions for Prowl.""" import asyncio -from functools import partial import prowlpy from homeassistant.core import HomeAssistant +from homeassistant.helpers.httpx_client import get_async_client async def async_verify_key(hass: HomeAssistant, api_key: str) -> bool: """Validate API key.""" - prowl = await hass.async_add_executor_job(partial(prowlpy.Prowl, api_key)) + prowl = prowlpy.AsyncProwl(api_key, client=get_async_client(hass)) try: async with asyncio.timeout(10): - await hass.async_add_executor_job(prowl.verify_key) + await prowl.verify_key() return True except prowlpy.APIError as ex: if str(ex).startswith("Invalid API key"): diff --git a/homeassistant/components/prowl/manifest.json b/homeassistant/components/prowl/manifest.json index 086bc62aa829e..70cdc0844e7b9 100644 --- a/homeassistant/components/prowl/manifest.json +++ b/homeassistant/components/prowl/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_push", "loggers": ["prowl"], "quality_scale": "legacy", - "requirements": ["prowlpy==1.0.2"] + "requirements": ["prowlpy==1.1.1"] } diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index c878c58271ef8..d013480417ac3 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -3,10 +3,10 @@ from __future__ import annotations import asyncio -from functools import partial import logging from typing import Any +import httpx import prowlpy import voluptuous as vol @@ -24,6 +24,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -37,9 +38,7 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> ProwlNotificationService: """Get the Prowl notification service.""" - return await hass.async_add_executor_job( - partial(ProwlNotificationService, hass, config[CONF_API_KEY]) - ) + return ProwlNotificationService(hass, config[CONF_API_KEY], get_async_client(hass)) async def async_setup_entry( @@ -48,7 +47,9 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the notify entities.""" - prowl = ProwlNotificationEntity(hass, entry.title, entry.data[CONF_API_KEY]) + prowl = ProwlNotificationEntity( + hass, entry.title, entry.data[CONF_API_KEY], get_async_client(hass) + ) async_add_entities([prowl]) @@ -58,10 +59,12 @@ class ProwlNotificationService(BaseNotificationService): This class is used for legacy configuration via configuration.yaml """ - def __init__(self, hass: HomeAssistant, api_key: str) -> None: + def __init__( + self, hass: HomeAssistant, api_key: str, httpx_client: httpx.AsyncClient + ) -> None: """Initialize the service.""" self._hass = hass - self._prowl = prowlpy.Prowl(api_key) + self._prowl = prowlpy.AsyncProwl(api_key, client=httpx_client) async def async_send_message(self, message: str, **kwargs: Any) -> None: """Send the message to the user.""" @@ -71,15 +74,12 @@ async def async_send_message(self, message: str, **kwargs: Any) -> None: try: async with asyncio.timeout(10): - await self._hass.async_add_executor_job( - partial( - self._prowl.send, - application="Home-Assistant", - event=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - description=message, - priority=data.get("priority", 0), - url=data.get("url"), - ) + await self._prowl.post( + application="Home-Assistant", + event=kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), + description=message, + priority=data.get("priority", 0), + url=data.get("url"), ) except TimeoutError as ex: _LOGGER.error("Timeout accessing Prowl API") @@ -103,10 +103,16 @@ class ProwlNotificationEntity(NotifyEntity): This class is used for Prowl config entries. """ - def __init__(self, hass: HomeAssistant, name: str, api_key: str) -> None: + def __init__( + self, + hass: HomeAssistant, + name: str, + api_key: str, + httpx_client: httpx.AsyncClient, + ) -> None: """Initialize the service.""" self._hass = hass - self._prowl = prowlpy.Prowl(api_key) + self._prowl = prowlpy.AsyncProwl(api_key, client=httpx_client) self._attr_name = name self._attr_unique_id = name @@ -115,15 +121,12 @@ async def async_send_message(self, message: str, title: str | None = None) -> No _LOGGER.debug("Sending Prowl notification from entity %s", self.name) try: async with asyncio.timeout(10): - await self._hass.async_add_executor_job( - partial( - self._prowl.send, - application="Home-Assistant", - event=title or ATTR_TITLE_DEFAULT, - description=message, - priority=0, - url=None, - ) + await self._prowl.post( + application="Home-Assistant", + event=title or ATTR_TITLE_DEFAULT, + description=message, + priority=0, + url=None, ) except TimeoutError as ex: _LOGGER.error("Timeout accessing Prowl API") diff --git a/requirements_all.txt b/requirements_all.txt index af2f435dc9697..348591a1ffb6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1744,7 +1744,7 @@ proliphix==0.4.1 prometheus-client==0.21.0 # homeassistant.components.prowl -prowlpy==1.0.2 +prowlpy==1.1.1 # homeassistant.components.proxmoxve proxmoxer==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index acf23826d8e65..1dbcef26b69e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1479,7 +1479,7 @@ prayer-times-calculator-offline==1.0.3 prometheus-client==0.21.0 # homeassistant.components.prowl -prowlpy==1.0.2 +prowlpy==1.1.1 # homeassistant.components.hardware # homeassistant.components.recorder diff --git a/tests/components/prowl/conftest.py b/tests/components/prowl/conftest.py index 9374115bdd6e4..7194bd7397b46 100644 --- a/tests/components/prowl/conftest.py +++ b/tests/components/prowl/conftest.py @@ -1,7 +1,7 @@ """Test fixtures for Prowl.""" from collections.abc import Generator -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, patch import pytest @@ -27,7 +27,7 @@ @pytest.fixture async def configure_prowl_through_yaml( - hass: HomeAssistant, mock_prowlpy: Generator[Mock] + hass: HomeAssistant, mock_prowlpy: Generator[AsyncMock] ) -> Generator[None]: """Configure the notify domain with YAML for the Prowl platform.""" await async_setup_component( @@ -48,7 +48,9 @@ async def configure_prowl_through_yaml( @pytest.fixture async def prowl_notification_entity( - hass: HomeAssistant, mock_prowlpy: Mock, mock_prowlpy_config_entry: MockConfigEntry + hass: HomeAssistant, + mock_prowlpy: AsyncMock, + mock_prowlpy_config_entry: MockConfigEntry, ) -> Generator[MockConfigEntry]: """Configure a Prowl Notification Entity.""" mock_prowlpy.verify_key.return_value = True @@ -61,11 +63,24 @@ async def prowl_notification_entity( @pytest.fixture -def mock_prowlpy() -> Generator[Mock]: +def mock_prowlpy() -> Generator[AsyncMock]: """Mock the prowlpy library.""" - - with patch("homeassistant.components.prowl.notify.prowlpy.Prowl") as MockProwl: - mock_instance = MockProwl.return_value + mock_instance = AsyncMock() + + with ( + patch( + "homeassistant.components.prowl.notify.prowlpy.AsyncProwl", + return_value=mock_instance, + ), + patch( + "homeassistant.components.prowl.helpers.prowlpy.AsyncProwl", + return_value=mock_instance, + ), + patch( + "homeassistant.components.prowl.__init__.prowlpy.AsyncProwl", + return_value=mock_instance, + ), + ): yield mock_instance diff --git a/tests/components/prowl/test_config_flow.py b/tests/components/prowl/test_config_flow.py index 72fa038062216..4ceb3c89d1781 100644 --- a/tests/components/prowl/test_config_flow.py +++ b/tests/components/prowl/test_config_flow.py @@ -1,6 +1,6 @@ """Test Prowl config flow.""" -from unittest.mock import Mock +from unittest.mock import AsyncMock import prowlpy @@ -13,7 +13,7 @@ from .conftest import BAD_API_RESPONSE, CONF_INPUT, INVALID_API_KEY_ERROR, TIMEOUT_ERROR -async def test_flow_user(hass: HomeAssistant, mock_prowlpy: Mock) -> None: +async def test_flow_user(hass: HomeAssistant, mock_prowlpy: AsyncMock) -> None: """Test user initialized flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -30,7 +30,9 @@ async def test_flow_user(hass: HomeAssistant, mock_prowlpy: Mock) -> None: assert result["data"] == {CONF_API_KEY: CONF_INPUT[CONF_API_KEY]} -async def test_flow_duplicate_api_key(hass: HomeAssistant, mock_prowlpy: Mock) -> None: +async def test_flow_duplicate_api_key( + hass: HomeAssistant, mock_prowlpy: AsyncMock +) -> None: """Test user initialized flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -52,7 +54,7 @@ async def test_flow_duplicate_api_key(hass: HomeAssistant, mock_prowlpy: Mock) - assert result["type"] is FlowResultType.ABORT -async def test_flow_user_bad_key(hass: HomeAssistant, mock_prowlpy: Mock) -> None: +async def test_flow_user_bad_key(hass: HomeAssistant, mock_prowlpy: AsyncMock) -> None: """Test user submitting a bad API key.""" mock_prowlpy.verify_key.side_effect = prowlpy.APIError("Invalid API key") @@ -70,7 +72,9 @@ async def test_flow_user_bad_key(hass: HomeAssistant, mock_prowlpy: Mock) -> Non assert result["errors"] == INVALID_API_KEY_ERROR -async def test_flow_user_prowl_timeout(hass: HomeAssistant, mock_prowlpy: Mock) -> None: +async def test_flow_user_prowl_timeout( + hass: HomeAssistant, mock_prowlpy: AsyncMock +) -> None: """Test Prowl API timeout.""" mock_prowlpy.verify_key.side_effect = TimeoutError @@ -88,7 +92,7 @@ async def test_flow_user_prowl_timeout(hass: HomeAssistant, mock_prowlpy: Mock) assert result["errors"] == TIMEOUT_ERROR -async def test_flow_api_failure(hass: HomeAssistant, mock_prowlpy: Mock) -> None: +async def test_flow_api_failure(hass: HomeAssistant, mock_prowlpy: AsyncMock) -> None: """Test Prowl API failure.""" mock_prowlpy.verify_key.side_effect = prowlpy.APIError(BAD_API_RESPONSE) diff --git a/tests/components/prowl/test_init.py b/tests/components/prowl/test_init.py index 67c38423581de..d222a098633d1 100644 --- a/tests/components/prowl/test_init.py +++ b/tests/components/prowl/test_init.py @@ -1,6 +1,6 @@ """Testing the Prowl initialisation.""" -from unittest.mock import Mock +from unittest.mock import AsyncMock import prowlpy import pytest @@ -18,7 +18,7 @@ async def test_load_reload_unload_config_entry( hass: HomeAssistant, mock_prowlpy_config_entry: MockConfigEntry, - mock_prowlpy: Mock, + mock_prowlpy: AsyncMock, ) -> None: """Test the Prowl configuration entry loading/reloading/unloading.""" mock_prowlpy_config_entry.add_to_hass(hass) @@ -57,7 +57,7 @@ async def test_load_reload_unload_config_entry( async def test_config_entry_failures( hass: HomeAssistant, mock_prowlpy_config_entry: MockConfigEntry, - mock_prowlpy: Mock, + mock_prowlpy: AsyncMock, prowlpy_side_effect, expected_config_state: ConfigEntryState, ) -> None: diff --git a/tests/components/prowl/test_notify.py b/tests/components/prowl/test_notify.py index 231d067b18005..b2cd52d31690e 100644 --- a/tests/components/prowl/test_notify.py +++ b/tests/components/prowl/test_notify.py @@ -1,7 +1,7 @@ """Test the Prowl notifications.""" from typing import Any -from unittest.mock import Mock +from unittest.mock import AsyncMock import prowlpy import pytest @@ -29,7 +29,7 @@ @pytest.mark.usefixtures("configure_prowl_through_yaml") async def test_send_notification_service( hass: HomeAssistant, - mock_prowlpy: Mock, + mock_prowlpy: AsyncMock, ) -> None: """Set up Prowl, call notify service, and check API call.""" assert hass.services.has_service(notify.DOMAIN, DOMAIN) @@ -40,12 +40,12 @@ async def test_send_notification_service( blocking=True, ) - mock_prowlpy.send.assert_called_once_with(**EXPECTED_SEND_PARAMETERS) + mock_prowlpy.post.assert_called_once_with(**EXPECTED_SEND_PARAMETERS) async def test_send_notification_entity_service( hass: HomeAssistant, - mock_prowlpy: Mock, + mock_prowlpy: AsyncMock, mock_prowlpy_config_entry: MockConfigEntry, ) -> None: """Set up Prowl via config entry, call notify service, and check API call.""" @@ -65,7 +65,7 @@ async def test_send_notification_entity_service( blocking=True, ) - mock_prowlpy.send.assert_called_once_with(**EXPECTED_SEND_PARAMETERS) + mock_prowlpy.post.assert_called_once_with(**EXPECTED_SEND_PARAMETERS) @pytest.mark.parametrize( @@ -102,7 +102,7 @@ async def test_send_notification_entity_service( ) async def test_fail_send_notification_entity_service( hass: HomeAssistant, - mock_prowlpy: Mock, + mock_prowlpy: AsyncMock, mock_prowlpy_config_entry: MockConfigEntry, prowlpy_side_effect: Exception, raised_exception: type[Exception], @@ -113,7 +113,7 @@ async def test_fail_send_notification_entity_service( await hass.config_entries.async_setup(mock_prowlpy_config_entry.entry_id) await hass.async_block_till_done() - mock_prowlpy.send.side_effect = prowlpy_side_effect + mock_prowlpy.post.side_effect = prowlpy_side_effect assert hass.services.has_service(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE) with pytest.raises(raised_exception, match=exception_message): @@ -128,7 +128,7 @@ async def test_fail_send_notification_entity_service( blocking=True, ) - mock_prowlpy.send.assert_called_once_with(**EXPECTED_SEND_PARAMETERS) + mock_prowlpy.post.assert_called_once_with(**EXPECTED_SEND_PARAMETERS) @pytest.mark.parametrize( @@ -166,13 +166,13 @@ async def test_fail_send_notification_entity_service( @pytest.mark.usefixtures("configure_prowl_through_yaml") async def test_fail_send_notification( hass: HomeAssistant, - mock_prowlpy: Mock, + mock_prowlpy: AsyncMock, prowlpy_side_effect: Exception, raised_exception: type[Exception], exception_message: str | None, ) -> None: """Sending a message via Prowl with a failure.""" - mock_prowlpy.send.side_effect = prowlpy_side_effect + mock_prowlpy.post.side_effect = prowlpy_side_effect assert hass.services.has_service(notify.DOMAIN, DOMAIN) with pytest.raises(raised_exception, match=exception_message): @@ -183,7 +183,7 @@ async def test_fail_send_notification( blocking=True, ) - mock_prowlpy.send.assert_called_once_with(**EXPECTED_SEND_PARAMETERS) + mock_prowlpy.post.assert_called_once_with(**EXPECTED_SEND_PARAMETERS) @pytest.mark.parametrize( @@ -204,12 +204,12 @@ async def test_fail_send_notification( @pytest.mark.usefixtures("configure_prowl_through_yaml") async def test_other_exception_send_notification( hass: HomeAssistant, - mock_prowlpy: Mock, + mock_prowlpy: AsyncMock, service_data: dict[str, Any], expected_send_parameters: dict[str, Any], ) -> None: """Sending a message via Prowl with a general unhandled exception.""" - mock_prowlpy.send.side_effect = SyntaxError + mock_prowlpy.post.side_effect = SyntaxError assert hass.services.has_service(notify.DOMAIN, DOMAIN) with pytest.raises(SyntaxError): @@ -220,4 +220,4 @@ async def test_other_exception_send_notification( blocking=True, ) - mock_prowlpy.send.assert_called_once_with(**expected_send_parameters) + mock_prowlpy.post.assert_called_once_with(**expected_send_parameters) From 083277d1ff46c3754b56d0d3992d060543d16e96 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sun, 12 Oct 2025 21:45:01 +0200 Subject: [PATCH 16/21] Add model_id to Husqvarna Automower (#154335) --- homeassistant/components/husqvarna_automower/entity.py | 8 ++++---- .../husqvarna_automower/snapshots/test_init.ambr | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/entity.py b/homeassistant/components/husqvarna_automower/entity.py index 99df51c7fe764..c1f20e8e7d37e 100644 --- a/homeassistant/components/husqvarna_automower/entity.py +++ b/homeassistant/components/husqvarna_automower/entity.py @@ -89,12 +89,12 @@ def __init__( """Initialize AutomowerEntity.""" super().__init__(coordinator) self.mower_id = mower_id + parts = self.mower_attributes.system.model.split(maxsplit=2) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mower_id)}, - manufacturer="Husqvarna", - model=self.mower_attributes.system.model.removeprefix( - "HUSQVARNA " - ).removeprefix("Husqvarna "), + manufacturer=parts[0], + model=parts[1], + model_id=parts[2], name=self.mower_attributes.system.name, serial_number=self.mower_attributes.system.serial_number, suggested_area="Garden", diff --git a/tests/components/husqvarna_automower/snapshots/test_init.ambr b/tests/components/husqvarna_automower/snapshots/test_init.ambr index 82116391f4f8b..57ff86634d3c2 100644 --- a/tests/components/husqvarna_automower/snapshots/test_init.ambr +++ b/tests/components/husqvarna_automower/snapshots/test_init.ambr @@ -19,9 +19,9 @@ }), 'labels': set({ }), - 'manufacturer': 'Husqvarna', - 'model': 'AUTOMOWER® 450XH', - 'model_id': None, + 'manufacturer': 'HUSQVARNA', + 'model': 'AUTOMOWER®', + 'model_id': '450XH', 'name': 'Test Mower 1', 'name_by_user': None, 'primary_config_entry': , From 59781422f78d36dbb0bd7243ea7d07c60233c310 Mon Sep 17 00:00:00 2001 From: Dan Schafer Date: Sun, 12 Oct 2025 12:57:47 -0700 Subject: [PATCH 17/21] Update Snoo strings.json to include weaning_baseline (#154268) --- homeassistant/components/snoo/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/snoo/strings.json b/homeassistant/components/snoo/strings.json index c86e0dd890733..f096acc57a5ae 100644 --- a/homeassistant/components/snoo/strings.json +++ b/homeassistant/components/snoo/strings.json @@ -76,6 +76,7 @@ "name": "State", "state": { "baseline": "Baseline", + "weaning_baseline": "Baseline (Weaning)", "level1": "Level 1", "level2": "Level 2", "level3": "Level 3", From 19dedb038e1e8df84be5880bef44a20585590bfd Mon Sep 17 00:00:00 2001 From: "Glenn Vandeuren (aka Iondependent)" Date: Sun, 12 Oct 2025 21:58:01 +0200 Subject: [PATCH 18/21] Update nhc requirement to version 0.7.0 (#154250) --- homeassistant/components/niko_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index 07f3aed63e2b9..160d84ffabc1e 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "iot_class": "local_push", "loggers": ["nikohomecontrol"], - "requirements": ["nhc==0.6.1"] + "requirements": ["nhc==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 348591a1ffb6f..5feb59cf465be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1545,7 +1545,7 @@ nextcord==3.1.0 nextdns==4.1.0 # homeassistant.components.niko_home_control -nhc==0.6.1 +nhc==0.7.0 # homeassistant.components.nibe_heatpump nibe==2.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1dbcef26b69e0..2753e2c81b469 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1328,7 +1328,7 @@ nextcord==3.1.0 nextdns==4.1.0 # homeassistant.components.niko_home_control -nhc==0.6.1 +nhc==0.7.0 # homeassistant.components.nibe_heatpump nibe==2.19.0 From 2b38f33d500dc9c45bfade432b5e3d8aff7f9acd Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 12 Oct 2025 23:01:22 +0300 Subject: [PATCH 19/21] Bump aioshelly to 13.13.0 (#154337) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index a370b855c4548..9ee2f096fb826 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["aioshelly"], "quality_scale": "silver", - "requirements": ["aioshelly==13.12.0"], + "requirements": ["aioshelly==13.13.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 5feb59cf465be..6412a9d636f65 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -384,7 +384,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.12.0 +aioshelly==13.13.0 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2753e2c81b469..c8d3a4558214c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -366,7 +366,7 @@ aioruuvigateway==0.1.0 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==13.12.0 +aioshelly==13.13.0 # homeassistant.components.skybell aioskybell==22.7.0 From 3d130a9bdf468d4959a8deff0d05b2f1038569cb Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 12 Oct 2025 21:06:13 +0100 Subject: [PATCH 20/21] Simplify generic camera tests (#154313) --- tests/components/generic/test_config_flow.py | 53 +++++++------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 19af6cd7a0921..30b1cced57df9 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -32,7 +32,6 @@ from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import ( CONF_AUTHENTICATION, - CONF_NAME, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL, @@ -56,16 +55,17 @@ CONF_VERIFY_SSL: False, } +TESTDATA_ONLYSTILL = TESTDATA.copy() +TESTDATA_ONLYSTILL.pop(CONF_STREAM_SOURCE) + +TESTDATA_ONLYSTREAM = TESTDATA.copy() +TESTDATA_ONLYSTREAM.pop(CONF_STILL_IMAGE_URL) + TESTDATA_OPTIONS = { CONF_LIMIT_REFETCH_TO_URL_CHANGE: False, **TESTDATA, } -TESTDATA_YAML = { - CONF_NAME: "Yaml Defined Name", - **TESTDATA, -} - @respx.mock @pytest.mark.usefixtures("fakeimg_png") @@ -135,11 +135,9 @@ async def test_form_only_stillimage( mock_setup_entry: _patch[MagicMock], ) -> None: """Test we complete ok if the user wants still images only.""" - data = TESTDATA.copy() - data.pop(CONF_STREAM_SOURCE) result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], - data, + TESTDATA_ONLYSTILL, ) await hass.async_block_till_done() assert result1["type"] is FlowResultType.FORM @@ -235,11 +233,9 @@ async def test_form_only_stillimage_gif( mock_setup_entry: _patch[MagicMock], ) -> None: """Test we complete ok if the user wants a gif.""" - data = TESTDATA.copy() - data.pop(CONF_STREAM_SOURCE) result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], - data, + TESTDATA_ONLYSTILL, ) assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm" @@ -262,11 +258,9 @@ async def test_form_only_svg_whitespace( """Test we complete ok if svg starts with whitespace, issue #68889.""" fakeimgbytes_wspace_svg = bytes(" \n ", encoding="utf-8") + fakeimgbytes_svg respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_wspace_svg) - data = TESTDATA.copy() - data.pop(CONF_STREAM_SOURCE) result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], - data, + TESTDATA_ONLYSTILL, ) assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm" @@ -296,11 +290,9 @@ async def test_form_only_still_sample( image_path = os.path.join(os.path.dirname(__file__), image_file) image_bytes = await hass.async_add_executor_job(Path(image_path).read_bytes) respx.get("http://127.0.0.1/testurl/1").respond(stream=image_bytes) - data = TESTDATA.copy() - data.pop(CONF_STREAM_SOURCE) result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], - data, + TESTDATA_ONLYSTILL, ) assert result1["type"] is FlowResultType.FORM assert result1["step_id"] == "user_confirm" @@ -364,8 +356,7 @@ async def test_form_still_template( # There is no need to mock the request if its an # invalid url because we will never make the request respx.get(url).respond(stream=fakeimgbytes_png) - data = TESTDATA.copy() - data.pop(CONF_STREAM_SOURCE) + data = TESTDATA_ONLYSTILL.copy() data[CONF_STILL_IMAGE_URL] = template result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], @@ -417,8 +408,7 @@ async def test_form_only_stream( mock_create_stream: _patch[MagicMock], ) -> None: """Test we complete ok if the user wants stream only.""" - data = TESTDATA.copy() - data.pop(CONF_STILL_IMAGE_URL) + data = TESTDATA_ONLYSTREAM.copy() data[CONF_STREAM_SOURCE] = "rtsp://user:pass@127.0.0.1/testurl/2" result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], @@ -592,8 +582,6 @@ async def test_form_stream_timeout( @respx.mock async def test_form_stream_not_set_up(hass: HomeAssistant, user_flow) -> None: """Test we handle if stream has not been set up.""" - TESTDATA_ONLY_STREAM = TESTDATA.copy() - TESTDATA_ONLY_STREAM.pop(CONF_STILL_IMAGE_URL) with patch( "homeassistant.components.generic.config_flow.create_stream", @@ -601,7 +589,7 @@ async def test_form_stream_not_set_up(hass: HomeAssistant, user_flow) -> None: ): result1 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], - TESTDATA_ONLY_STREAM, + TESTDATA_ONLYSTREAM, ) await hass.async_block_till_done() @@ -612,8 +600,6 @@ async def test_form_stream_not_set_up(hass: HomeAssistant, user_flow) -> None: @respx.mock async def test_form_stream_other_error(hass: HomeAssistant, user_flow) -> None: """Test the unknown error for streams.""" - TESTDATA_ONLY_STREAM = TESTDATA.copy() - TESTDATA_ONLY_STREAM.pop(CONF_STILL_IMAGE_URL) with ( patch( @@ -624,7 +610,7 @@ async def test_form_stream_other_error(hass: HomeAssistant, user_flow) -> None: ): await hass.config_entries.flow.async_configure( user_flow["flow_id"], - TESTDATA_ONLY_STREAM, + TESTDATA_ONLYSTREAM, ) await hass.async_block_till_done() @@ -794,14 +780,12 @@ async def test_options_only_stream( mock_create_stream: _patch[MagicMock], ) -> None: """Test the options flow without a still_image_url.""" - data = TESTDATA.copy() - data.pop(CONF_STILL_IMAGE_URL) mock_entry = MockConfigEntry( title="Test Camera", domain=DOMAIN, data={}, - options=data, + options=TESTDATA_ONLYSTREAM, ) mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) @@ -813,7 +797,7 @@ async def test_options_only_stream( # try updating the config options result2 = await hass.config_entries.options.async_configure( result["flow_id"], - user_input=data, + user_input=TESTDATA_ONLYSTREAM, ) assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "user_confirm" @@ -830,7 +814,8 @@ async def test_options_still_and_stream_not_provided( mock_setup_entry: _patch[MagicMock], ) -> None: """Test we show a suitable error if neither still or stream URL are provided.""" - data = TESTDATA.copy() + data = TESTDATA_ONLYSTILL.copy() + data.pop(CONF_STILL_IMAGE_URL) mock_entry = MockConfigEntry( title="Test Camera", @@ -845,8 +830,6 @@ async def test_options_still_and_stream_not_provided( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" - data.pop(CONF_STILL_IMAGE_URL) - data.pop(CONF_STREAM_SOURCE) result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input=data, From 4ca1ae61aabf101a62e2da0aa72710ff16583fbe Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Sun, 12 Oct 2025 16:34:16 -0400 Subject: [PATCH 21/21] Environment Canada station selector (#154307) Co-authored-by: Claude Co-authored-by: Joost Lekkerkerker --- .../environment_canada/config_flow.py | 40 +++++++++++-- .../environment_canada/strings.json | 6 +- .../environment_canada/test_config_flow.py | 57 +++++++++++++++++-- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py index debe1c5ae4381..871025d00b464 100644 --- a/homeassistant/components/environment_canada/config_flow.py +++ b/homeassistant/components/environment_canada/config_flow.py @@ -6,11 +6,18 @@ import aiohttp from env_canada import ECWeather, ec_exc +from env_canada.ec_weather import get_ec_sites_list import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) from .const import CONF_STATION, CONF_TITLE, DOMAIN @@ -25,14 +32,16 @@ async def validate_input(data): lang = data.get(CONF_LANGUAGE).lower() if station: + # When station is provided, use it and get the coordinates from ECWeather weather_data = ECWeather(station_id=station, language=lang) - else: - weather_data = ECWeather(coordinates=(lat, lon), language=lang) - await weather_data.update() - - if lat is None or lon is None: + await weather_data.update() + # Always use the station's coordinates, not the user-provided ones lat = weather_data.lat lon = weather_data.lon + else: + # When no station is provided, use coordinates to find nearest station + weather_data = ECWeather(coordinates=(lat, lon), language=lang) + await weather_data.update() return { CONF_TITLE: weather_data.metadata.location, @@ -46,6 +55,13 @@ class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Environment Canada weather.""" VERSION = 1 + _station_codes: list[dict[str, str]] | None = None + + async def _get_station_codes(self) -> list[dict[str, str]]: + """Get station codes, cached after first call.""" + if self._station_codes is None: + self._station_codes = await get_ec_sites_list() + return self._station_codes async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -80,9 +96,21 @@ async def async_step_user( self._abort_if_unique_id_configured() return self.async_create_entry(title=info[CONF_TITLE], data=user_input) + station_codes = await self._get_station_codes() + data_schema = vol.Schema( { - vol.Optional(CONF_STATION): str, + vol.Optional(CONF_STATION): SelectSelector( + SelectSelectorConfig( + options=[ + SelectOptionDict( + value=station["value"], label=station["label"] + ) + for station in station_codes + ], + mode=SelectSelectorMode.DROPDOWN, + ) + ), vol.Optional( CONF_LATITUDE, default=self.hass.config.latitude ): cv.latitude, diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index 3e58f15e0fe73..0412dcb69612e 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -2,12 +2,12 @@ "config": { "step": { "user": { - "title": "Environment Canada: Location and language", - "description": "You can specify a location using either a station code or latitude/longitude coordinates.\n\nDefault behavior: If no station code is entered, the system uses the latitude/longitude values configured in your Home Assistant installation.\n\nStation code format: Station codes follow the format \"s0000123\" or simply \"123\".\n\nFind station codes at https://dd.weather.gc.ca/today/citypage_weather/docs/site_list_towns_en.csv.", + "title": "Environment Canada: weather location and language", + "description": "Select a weather station from the dropdown, or specify coordinates to use the closest station. The default coordinates are from your Home Assistant installation. Weather information can be retrieved in English or French.", "data": { "latitude": "[%key:common::config_flow::data::latitude%]", "longitude": "[%key:common::config_flow::data::longitude%]", - "station": "Station code", + "station": "Weather station", "language": "Weather information language" } } diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index 9f3fdbd43dca4..681777ed85adc 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -15,12 +15,17 @@ from tests.common import MockConfigEntry FAKE_CONFIG = { - CONF_STATION: "ON/s1234567", + CONF_STATION: "123", CONF_LANGUAGE: "English", CONF_LATITUDE: 42.42, CONF_LONGITUDE: -42.42, } FAKE_TITLE = "Universal title!" +FAKE_STATIONS = [ + {"label": "Toronto, ON", "value": "123"}, + {"label": "Ottawa, ON", "value": "456"}, + {"label": "Montreal, QC", "value": "789"}, +] def mocked_ec(): @@ -40,10 +45,19 @@ def mocked_ec(): ) +def mocked_stations(): + """Mock the station list.""" + return patch( + "homeassistant.components.environment_canada.config_flow.get_ec_sites_list", + return_value=FAKE_STATIONS, + ) + + async def test_create_entry(hass: HomeAssistant) -> None: """Test creating an entry.""" with ( mocked_ec(), + mocked_stations(), patch( "homeassistant.components.environment_canada.async_setup_entry", return_value=True, @@ -66,12 +80,13 @@ async def test_create_same_entry_twice(hass: HomeAssistant) -> None: entry = MockConfigEntry( domain=DOMAIN, data=FAKE_CONFIG, - unique_id="ON/s1234567-english", + unique_id="123-english", ) entry.add_to_hass(hass) with ( mocked_ec(), + mocked_stations(), patch( "homeassistant.components.environment_canada.async_setup_entry", return_value=True, @@ -101,9 +116,12 @@ async def test_create_same_entry_twice(hass: HomeAssistant) -> None: async def test_exception_handling(hass: HomeAssistant, error) -> None: """Test exception handling.""" exc, base_error = error - with patch( - "homeassistant.components.environment_canada.config_flow.ECWeather", - side_effect=exc, + with ( + mocked_stations(), + patch( + "homeassistant.components.environment_canada.config_flow.ECWeather", + side_effect=exc, + ), ): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -121,6 +139,7 @@ async def test_lat_lon_not_specified(hass: HomeAssistant) -> None: """Test that the import step works when coordinates are not specified.""" with ( mocked_ec(), + mocked_stations(), patch( "homeassistant.components.environment_canada.async_setup_entry", return_value=True, @@ -136,3 +155,31 @@ async def test_lat_lon_not_specified(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE + + +async def test_coordinates_without_station(hass: HomeAssistant) -> None: + """Test setup with coordinates but no station ID.""" + with ( + mocked_ec(), + mocked_stations(), + patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ), + ): + # Config with coordinates but no station + config_no_station = { + CONF_LANGUAGE: "English", + CONF_LATITUDE: 42.42, + CONF_LONGITUDE: -42.42, + } + flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], config_no_station + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == FAKE_CONFIG + assert result["title"] == FAKE_TITLE