From a270bd76de682b687c1b3ebd4ceda48c34c71002 Mon Sep 17 00:00:00 2001 From: Christopher Fenner <9592452+CFenner@users.noreply.github.com> Date: Sun, 5 Oct 2025 18:34:45 +0200 Subject: [PATCH 01/20] Add sensors for battery charge amount to ViCare integration (#153631) Co-authored-by: Josef Zweck --- homeassistant/components/vicare/sensor.py | 7 +++ homeassistant/components/vicare/strings.json | 3 + .../vicare/fixtures/VitoChargeVX3.json | 42 ++++++++++++++ .../vicare/snapshots/test_sensor.ambr | 56 +++++++++++++++++++ tests/components/vicare/test_sensor.py | 1 + 5 files changed, 109 insertions(+) create mode 100644 tests/components/vicare/fixtures/VitoChargeVX3.json diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 891992acd045d1..864439c746c4bc 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -720,6 +720,13 @@ class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysM options=["charge", "discharge", "standby"], value_getter=lambda api: api.getElectricalEnergySystemOperationState(), ), + ViCareSensorEntityDescription( + key="ess_charge_total", + translation_key="ess_charge_total", + state_class=SensorStateClass.TOTAL_INCREASING, + value_getter=lambda api: api.getElectricalEnergySystemTransferChargeCumulatedLifeCycle(), + unit_getter=lambda api: api.getElectricalEnergySystemTransferChargeCumulatedUnit(), + ), ViCareSensorEntityDescription( key="ess_discharge_today", translation_key="ess_discharge_today", diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 3135dd7acc39a6..260b51f56f3f38 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -370,6 +370,9 @@ "standby": "[%key:common::state::standby%]" } }, + "ess_charge_total": { + "name": "Battery charge total" + }, "ess_discharge_today": { "name": "Battery discharge today" }, diff --git a/tests/components/vicare/fixtures/VitoChargeVX3.json b/tests/components/vicare/fixtures/VitoChargeVX3.json new file mode 100644 index 00000000000000..fe2f94f3e067ab --- /dev/null +++ b/tests/components/vicare/fixtures/VitoChargeVX3.json @@ -0,0 +1,42 @@ +{ + "data": [ + { + "apiVersion": 1, + "commands": {}, + "deviceId": "################", + "feature": "ess.transfer.charge.cumulated", + "gatewayId": "################", + "isEnabled": true, + "isReady": true, + "properties": { + "currentDay": { + "type": "number", + "unit": "wattHour", + "value": 5449 + }, + "currentMonth": { + "type": "number", + "unit": "wattHour", + "value": 143145 + }, + "currentWeek": { + "type": "number", + "unit": "wattHour", + "value": 5450 + }, + "currentYear": { + "type": "number", + "unit": "wattHour", + "value": 1251105 + }, + "lifeCycle": { + "type": "number", + "unit": "wattHour", + "value": 1879163 + } + }, + "timestamp": "2025-09-29T16:45:15.994Z", + "uri": "https://api.viessmann-climatesolutions.com/iot/v2/features/installations/#######/gateways/################/devices/################/features/ess.transfer.charge.cumulated" + } + ] +} diff --git a/tests/components/vicare/snapshots/test_sensor.ambr b/tests/components/vicare/snapshots/test_sensor.ambr index 36bb33b8de2742..22cba704dcfa2e 100644 --- a/tests/components/vicare/snapshots/test_sensor.ambr +++ b/tests/components/vicare/snapshots/test_sensor.ambr @@ -1122,6 +1122,62 @@ 'state': '25.5', }) # --- +# name: test_all_entities[type:ess-vicare/VitoChargeVX3.json][sensor.model0_battery_charge_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.model0_battery_charge_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery charge total', + 'platform': 'vicare', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ess_charge_total', + 'unique_id': 'gateway0_deviceId0-ess_charge_total', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[type:ess-vicare/VitoChargeVX3.json][sensor.model0_battery_charge_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'model0 Battery charge total', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.model0_battery_charge_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1879163', + }) +# --- # name: test_all_entities[type:heatpump-vicare/Vitocal250A.json][sensor.model0_boiler_supply_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/vicare/test_sensor.py b/tests/components/vicare/test_sensor.py index daad6bfa1c8657..be7418291a8ae3 100644 --- a/tests/components/vicare/test_sensor.py +++ b/tests/components/vicare/test_sensor.py @@ -22,6 +22,7 @@ ("type:boiler", "vicare/Vitodens300W.json"), ("type:heatpump", "vicare/Vitocal250A.json"), ("type:ventilation", "vicare/ViAir300F.json"), + ("type:ess", "vicare/VitoChargeVX3.json"), ], ) async def test_all_entities( From f44d65e0235132725c51ffd34640d169c715b440 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sun, 5 Oct 2025 18:43:37 +0200 Subject: [PATCH 02/20] Migrate tolo to entry.runtime_data (#153744) --- homeassistant/components/tolo/__init__.py | 16 +++++----------- homeassistant/components/tolo/binary_sensor.py | 12 +++++------- homeassistant/components/tolo/button.py | 10 ++++------ homeassistant/components/tolo/climate.py | 10 ++++------ homeassistant/components/tolo/coordinator.py | 6 ++++-- homeassistant/components/tolo/entity.py | 5 ++--- homeassistant/components/tolo/fan.py | 10 ++++------ homeassistant/components/tolo/light.py | 10 ++++------ homeassistant/components/tolo/number.py | 10 ++++------ homeassistant/components/tolo/select.py | 11 +++++------ homeassistant/components/tolo/sensor.py | 10 ++++------ homeassistant/components/tolo/switch.py | 10 ++++------ 12 files changed, 49 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index d2a43ef525b27d..bbd17cc8b13574 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -2,12 +2,10 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator PLATFORMS = [ Platform.BINARY_SENSOR, @@ -22,21 +20,17 @@ ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ToloConfigEntry) -> bool: """Set up tolo from a config entry.""" coordinator = ToloSaunaUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ToloConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/tolo/binary_sensor.py b/homeassistant/components/tolo/binary_sensor.py index cb3ba46b604aed..0b94c60094f001 100644 --- a/homeassistant/components/tolo/binary_sensor.py +++ b/homeassistant/components/tolo/binary_sensor.py @@ -4,23 +4,21 @@ BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up binary sensors for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( [ ToloFlowInBinarySensor(coordinator, entry), @@ -37,7 +35,7 @@ class ToloFlowInBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.OPENING def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, coordinator: ToloSaunaUpdateCoordinator, entry: ToloConfigEntry ) -> None: """Initialize TOLO Water In Valve entity.""" super().__init__(coordinator, entry) @@ -58,7 +56,7 @@ class ToloFlowOutBinarySensor(ToloSaunaCoordinatorEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.OPENING def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, coordinator: ToloSaunaUpdateCoordinator, entry: ToloConfigEntry ) -> None: """Initialize TOLO Water Out Valve entity.""" super().__init__(coordinator, entry) diff --git a/homeassistant/components/tolo/button.py b/homeassistant/components/tolo/button.py index 9e4c8c84be9bd0..472abdcb673cb8 100644 --- a/homeassistant/components/tolo/button.py +++ b/homeassistant/components/tolo/button.py @@ -3,23 +3,21 @@ from tololib import LampMode from homeassistant.components.button import ButtonEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up buttons for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( [ ToloLampNextColorButton(coordinator, entry), @@ -34,7 +32,7 @@ class ToloLampNextColorButton(ToloSaunaCoordinatorEntity, ButtonEntity): _attr_translation_key = "next_color" def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, coordinator: ToloSaunaUpdateCoordinator, entry: ToloConfigEntry ) -> None: """Initialize lamp next color button entity.""" super().__init__(coordinator, entry) diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 0df8635fca9f78..ed7ab0c3b7621e 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -20,23 +20,21 @@ HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up climate controls for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities([SaunaClimate(coordinator, entry)]) @@ -62,7 +60,7 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity): _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, coordinator: ToloSaunaUpdateCoordinator, entry: ToloConfigEntry ) -> None: """Initialize TOLO Sauna Climate entity.""" super().__init__(coordinator, entry) diff --git a/homeassistant/components/tolo/coordinator.py b/homeassistant/components/tolo/coordinator.py index 729073b16c4c8e..372c67a4260660 100644 --- a/homeassistant/components/tolo/coordinator.py +++ b/homeassistant/components/tolo/coordinator.py @@ -17,6 +17,8 @@ _LOGGER = logging.getLogger(__name__) +type ToloConfigEntry = ConfigEntry[ToloSaunaUpdateCoordinator] + class ToloSaunaData(NamedTuple): """Compound class for reflecting full state (status and info) of a TOLO Sauna.""" @@ -28,9 +30,9 @@ class ToloSaunaData(NamedTuple): class ToloSaunaUpdateCoordinator(DataUpdateCoordinator[ToloSaunaData]): """DataUpdateCoordinator for TOLO Sauna.""" - config_entry: ConfigEntry + config_entry: ToloConfigEntry - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: ToloConfigEntry) -> None: """Initialize ToloSaunaUpdateCoordinator.""" self.client = ToloClient( address=entry.data[CONF_HOST], diff --git a/homeassistant/components/tolo/entity.py b/homeassistant/components/tolo/entity.py index 261cfc7cb0c136..c6aef0fb82463c 100644 --- a/homeassistant/components/tolo/entity.py +++ b/homeassistant/components/tolo/entity.py @@ -2,12 +2,11 @@ from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator class ToloSaunaCoordinatorEntity(CoordinatorEntity[ToloSaunaUpdateCoordinator]): @@ -16,7 +15,7 @@ class ToloSaunaCoordinatorEntity(CoordinatorEntity[ToloSaunaUpdateCoordinator]): _attr_has_entity_name = True def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, coordinator: ToloSaunaUpdateCoordinator, entry: ToloConfigEntry ) -> None: """Initialize ToloSaunaCoordinatorEntity.""" super().__init__(coordinator) diff --git a/homeassistant/components/tolo/fan.py b/homeassistant/components/tolo/fan.py index 7bddf775143c23..41ca94055ba7b0 100644 --- a/homeassistant/components/tolo/fan.py +++ b/homeassistant/components/tolo/fan.py @@ -5,22 +5,20 @@ from typing import Any from homeassistant.components.fan import FanEntity, FanEntityFeature -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up fan controls for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities([ToloFan(coordinator, entry)]) @@ -31,7 +29,7 @@ class ToloFan(ToloSaunaCoordinatorEntity, FanEntity): _attr_supported_features = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, coordinator: ToloSaunaUpdateCoordinator, entry: ToloConfigEntry ) -> None: """Initialize TOLO fan entity.""" super().__init__(coordinator, entry) diff --git a/homeassistant/components/tolo/light.py b/homeassistant/components/tolo/light.py index 9ccd4a8e407afd..25e1e91354403c 100644 --- a/homeassistant/components/tolo/light.py +++ b/homeassistant/components/tolo/light.py @@ -5,22 +5,20 @@ from typing import Any from homeassistant.components.light import ColorMode, LightEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up light controls for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities([ToloLight(coordinator, entry)]) @@ -32,7 +30,7 @@ class ToloLight(ToloSaunaCoordinatorEntity, LightEntity): _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( - self, coordinator: ToloSaunaUpdateCoordinator, entry: ConfigEntry + self, coordinator: ToloSaunaUpdateCoordinator, entry: ToloConfigEntry ) -> None: """Initialize TOLO Sauna Light entity.""" super().__init__(coordinator, entry) diff --git a/homeassistant/components/tolo/number.py b/homeassistant/components/tolo/number.py index 902fb749d236cf..db06b82d002dc5 100644 --- a/homeassistant/components/tolo/number.py +++ b/homeassistant/components/tolo/number.py @@ -15,13 +15,11 @@ ) from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity @@ -67,11 +65,11 @@ class ToloNumberEntityDescription(NumberEntityDescription): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up number controls for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( ToloNumberEntity(coordinator, entry, description) for description in NUMBERS ) @@ -85,7 +83,7 @@ class ToloNumberEntity(ToloSaunaCoordinatorEntity, NumberEntity): def __init__( self, coordinator: ToloSaunaUpdateCoordinator, - entry: ConfigEntry, + entry: ToloConfigEntry, entity_description: ToloNumberEntityDescription, ) -> None: """Initialize TOLO Number entity.""" diff --git a/homeassistant/components/tolo/select.py b/homeassistant/components/tolo/select.py index b08f37e40ae293..f487fba9664b91 100644 --- a/homeassistant/components/tolo/select.py +++ b/homeassistant/components/tolo/select.py @@ -8,13 +8,12 @@ from tololib import ToloClient, ToloSettings from homeassistant.components.select import SelectEntity, SelectEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, AromaTherapySlot, LampMode -from .coordinator import ToloSaunaUpdateCoordinator +from .const import AromaTherapySlot, LampMode +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity @@ -53,11 +52,11 @@ class ToloSelectEntityDescription(SelectEntityDescription): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up select entities for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( ToloSelectEntity(coordinator, entry, description) for description in SELECTS ) @@ -73,7 +72,7 @@ class ToloSelectEntity(ToloSaunaCoordinatorEntity, SelectEntity): def __init__( self, coordinator: ToloSaunaUpdateCoordinator, - entry: ConfigEntry, + entry: ToloConfigEntry, entity_description: ToloSelectEntityDescription, ) -> None: """Initialize TOLO select entity.""" diff --git a/homeassistant/components/tolo/sensor.py b/homeassistant/components/tolo/sensor.py index e97211c8e40e94..ba203dec8067c8 100644 --- a/homeassistant/components/tolo/sensor.py +++ b/homeassistant/components/tolo/sensor.py @@ -13,7 +13,6 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, EntityCategory, @@ -23,8 +22,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity @@ -88,11 +86,11 @@ class ToloSensorEntityDescription(SensorEntityDescription): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up (non-binary, general) sensors for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( ToloSensorEntity(coordinator, entry, description) for description in SENSORS ) @@ -106,7 +104,7 @@ class ToloSensorEntity(ToloSaunaCoordinatorEntity, SensorEntity): def __init__( self, coordinator: ToloSaunaUpdateCoordinator, - entry: ConfigEntry, + entry: ToloConfigEntry, entity_description: ToloSensorEntityDescription, ) -> None: """Initialize TOLO Number entity.""" diff --git a/homeassistant/components/tolo/switch.py b/homeassistant/components/tolo/switch.py index ce863053e2664b..686f78b04e9825 100644 --- a/homeassistant/components/tolo/switch.py +++ b/homeassistant/components/tolo/switch.py @@ -9,12 +9,10 @@ from tololib import ToloClient, ToloStatus from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN -from .coordinator import ToloSaunaUpdateCoordinator +from .coordinator import ToloConfigEntry, ToloSaunaUpdateCoordinator from .entity import ToloSaunaCoordinatorEntity @@ -44,11 +42,11 @@ class ToloSwitchEntityDescription(SwitchEntityDescription): async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: ToloConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up switch controls for TOLO Sauna.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities( ToloSwitchEntity(coordinator, entry, description) for description in SWITCHES ) @@ -62,7 +60,7 @@ class ToloSwitchEntity(ToloSaunaCoordinatorEntity, SwitchEntity): def __init__( self, coordinator: ToloSaunaUpdateCoordinator, - entry: ConfigEntry, + entry: ToloConfigEntry, entity_description: ToloSwitchEntityDescription, ) -> None: """Initialize TOLO switch entity.""" From fed8f137e97560f981bdd3da9575a9c77febbdc5 Mon Sep 17 00:00:00 2001 From: Sander Jochems Date: Sun, 5 Oct 2025 19:49:22 +0200 Subject: [PATCH 03/20] Upgrade python-melcloud to 0.1.2 (#153742) --- homeassistant/components/melcloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index a9440ad830069a..6032cd3e17d681 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/melcloud", "iot_class": "cloud_polling", "loggers": ["pymelcloud"], - "requirements": ["python-melcloud==0.1.0"] + "requirements": ["python-melcloud==0.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 09330429e91c52..3a9a3c5e8424ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2504,7 +2504,7 @@ python-linkplay==0.2.12 python-matter-server==8.1.0 # homeassistant.components.melcloud -python-melcloud==0.1.0 +python-melcloud==0.1.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fd7e67198f2fe..0d03a25ab168ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2080,7 +2080,7 @@ python-linkplay==0.2.12 python-matter-server==8.1.0 # homeassistant.components.melcloud -python-melcloud==0.1.0 +python-melcloud==0.1.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From d75ca0f5f3587412e95ed3479507bbc6e029c874 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 5 Oct 2025 20:59:02 +0200 Subject: [PATCH 04/20] Bump aioamazondevices to 6.2.9 (#153756) --- 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 1121120d4b6873..e5badd35f17fab 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.2.8"] + "requirements": ["aioamazondevices==6.2.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3a9a3c5e8424ea..5888cf6a058f44 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.2.8 +aioamazondevices==6.2.9 # homeassistant.components.ambient_network # homeassistant.components.ambient_station diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d03a25ab168ac..0f9b7b5915b79e 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.2.8 +aioamazondevices==6.2.9 # homeassistant.components.ambient_network # homeassistant.components.ambient_station From 26bfbc55e940901bc8543c727719f73bc7649b33 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sun, 5 Oct 2025 21:59:50 +0300 Subject: [PATCH 05/20] Bump anthropic to 0.69.0 (#153764) --- homeassistant/components/anthropic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/anthropic/manifest.json b/homeassistant/components/anthropic/manifest.json index 6fed0282a00172..a0991f42fdbf1a 100644 --- a/homeassistant/components/anthropic/manifest.json +++ b/homeassistant/components/anthropic/manifest.json @@ -8,5 +8,5 @@ "documentation": "https://www.home-assistant.io/integrations/anthropic", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["anthropic==0.62.0"] + "requirements": ["anthropic==0.69.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5888cf6a058f44..6c0d4a111bf8cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -495,7 +495,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.62.0 +anthropic==0.69.0 # homeassistant.components.mcp_server anyio==4.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f9b7b5915b79e..ead1ce88bd3bf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -468,7 +468,7 @@ anova-wifi==0.17.0 anthemav==1.4.1 # homeassistant.components.anthropic -anthropic==0.62.0 +anthropic==0.69.0 # homeassistant.components.mcp_server anyio==4.10.0 From 6ec7b63ebe68ba8999fab1b71331f239557e6035 Mon Sep 17 00:00:00 2001 From: Denis Shulyaka Date: Sun, 5 Oct 2025 22:29:24 +0300 Subject: [PATCH 06/20] Add support for Anthropic Claude Sonnet 4.5 (#153769) --- homeassistant/components/anthropic/const.py | 9 ++++----- homeassistant/components/anthropic/entity.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/anthropic/const.py b/homeassistant/components/anthropic/const.py index 356140ff66e20e..395f7fa8a81133 100644 --- a/homeassistant/components/anthropic/const.py +++ b/homeassistant/components/anthropic/const.py @@ -19,9 +19,8 @@ RECOMMENDED_THINKING_BUDGET = 0 MIN_THINKING_BUDGET = 1024 -THINKING_MODELS = [ - "claude-3-7-sonnet", - "claude-sonnet-4-0", - "claude-opus-4-0", - "claude-opus-4-1", +NON_THINKING_MODELS = [ + "claude-3-5", # Both sonnet and haiku + "claude-3-opus", + "claude-3-haiku", ] diff --git a/homeassistant/components/anthropic/entity.py b/homeassistant/components/anthropic/entity.py index 7338cbe2906962..7c58326515e902 100644 --- a/homeassistant/components/anthropic/entity.py +++ b/homeassistant/components/anthropic/entity.py @@ -51,11 +51,11 @@ DOMAIN, LOGGER, MIN_THINKING_BUDGET, + NON_THINKING_MODELS, RECOMMENDED_CHAT_MODEL, RECOMMENDED_MAX_TOKENS, RECOMMENDED_TEMPERATURE, RECOMMENDED_THINKING_BUDGET, - THINKING_MODELS, ) # Max number of back and forth with the LLM to generate a response @@ -364,7 +364,7 @@ async def _async_handle_chat_log( if tools: model_args["tools"] = tools if ( - model.startswith(tuple(THINKING_MODELS)) + not model.startswith(tuple(NON_THINKING_MODELS)) and thinking_budget >= MIN_THINKING_BUDGET ): model_args["thinking"] = ThinkingConfigEnabledParam( From 933b15ce36dd5c39796f85619137fe53b6e6fedf Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sun, 5 Oct 2025 22:05:04 +0200 Subject: [PATCH 07/20] Revert "AGENTS.md" (#153777) --- AGENTS.md => .github/copilot-instructions.md | 0 CLAUDE.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename AGENTS.md => .github/copilot-instructions.md (100%) diff --git a/AGENTS.md b/.github/copilot-instructions.md similarity index 100% rename from AGENTS.md rename to .github/copilot-instructions.md diff --git a/CLAUDE.md b/CLAUDE.md index 47dc3e3d863cfb..02dd134122ea96 120000 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -AGENTS.md \ No newline at end of file +.github/copilot-instructions.md \ No newline at end of file From d63d154457eecbe9b53e37b03d01ea217432ed91 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sun, 5 Oct 2025 22:18:31 +0200 Subject: [PATCH 08/20] Daikin increase timeout (#153722) Co-authored-by: Franck Nijhof Co-authored-by: Josef Zweck --- homeassistant/components/daikin/__init__.py | 6 +++--- homeassistant/components/daikin/config_flow.py | 4 ++-- homeassistant/components/daikin/const.py | 2 +- homeassistant/components/daikin/coordinator.py | 4 ++-- tests/components/daikin/test_init.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 88a7b71e3ed91d..a96918747a2441 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.util.ssl import client_context_no_verify -from .const import KEY_MAC, TIMEOUT +from .const import KEY_MAC, TIMEOUT_SEC from .coordinator import DaikinConfigEntry, DaikinCoordinator _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo session = async_get_clientsession(hass) host = conf[CONF_HOST] try: - async with asyncio.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT_SEC): device: Appliance = await DaikinFactory( host, session, @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo ) _LOGGER.debug("Connection to %s successful", host) except TimeoutError as err: - _LOGGER.debug("Connection to %s timed out in 60 seconds", host) + _LOGGER.debug("Connection to %s timed out in %s seconds", host, TIMEOUT_SEC) raise ConfigEntryNotReady from err except ClientConnectionError as err: _LOGGER.debug("ClientConnectionError to %s", host) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index f5febafc4dc714..85ed0804c668f6 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -20,7 +20,7 @@ from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.util.ssl import client_context_no_verify -from .const import DOMAIN, KEY_MAC, TIMEOUT +from .const import DOMAIN, KEY_MAC, TIMEOUT_SEC _LOGGER = logging.getLogger(__name__) @@ -84,7 +84,7 @@ async def _create_device( password = None try: - async with asyncio.timeout(TIMEOUT): + async with asyncio.timeout(TIMEOUT_SEC): device: Appliance = await DaikinFactory( host, async_get_clientsession(self.hass), diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index 690267e5c83cee..f093569ea54df7 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -24,4 +24,4 @@ KEY_MAC = "mac" KEY_IP = "ip" -TIMEOUT = 60 +TIMEOUT_SEC = 120 diff --git a/homeassistant/components/daikin/coordinator.py b/homeassistant/components/daikin/coordinator.py index 8e1713af5b2c1e..9bd8d17bf485ba 100644 --- a/homeassistant/components/daikin/coordinator.py +++ b/homeassistant/components/daikin/coordinator.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN +from .const import DOMAIN, TIMEOUT_SEC _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,7 @@ def __init__( _LOGGER, config_entry=entry, name=device.values.get("name", DOMAIN), - update_interval=timedelta(seconds=60), + update_interval=timedelta(seconds=TIMEOUT_SEC), ) self.device = device diff --git a/tests/components/daikin/test_init.py b/tests/components/daikin/test_init.py index 2380d5ad798bc4..54caa79539baf0 100644 --- a/tests/components/daikin/test_init.py +++ b/tests/components/daikin/test_init.py @@ -187,7 +187,7 @@ async def test_client_update_connection_error( type(mock_daikin).update_status.side_effect = ClientConnectionError - freezer.tick(timedelta(seconds=60)) + freezer.tick(timedelta(seconds=120)) async_fire_time_changed(hass) await hass.async_block_till_done() From 5d83c82b814fcfc1a58fbe4cc68dd6d785cdd920 Mon Sep 17 00:00:00 2001 From: David Rapan Date: Sun, 5 Oct 2025 22:32:39 +0200 Subject: [PATCH 09/20] Shelly's energy sensors naming paradigm standardization (#153729) --- homeassistant/components/shelly/sensor.py | 53 +++--- .../shelly/snapshots/test_devices.ambr | 180 +++++++++--------- .../shelly/snapshots/test_sensor.ambr | 36 ++-- tests/components/shelly/test_sensor.py | 40 ++-- 4 files changed, 149 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index ced5f46be3a83f..6bece8f9565eab 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -122,8 +122,8 @@ def native_value(self) -> StateType: return self.option_map[attribute_value] -class RpcConsumedEnergySensor(RpcSensor): - """Represent a RPC sensor.""" +class RpcEnergyConsumedSensor(RpcSensor): + """Represent a RPC energy consumed sensor.""" @property def native_value(self) -> StateType: @@ -886,8 +886,7 @@ def __init__( native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, - removal_condition=lambda _config, status, key: status[key].get("n_current") - is None, + removal_condition=lambda _, status, key: status[key].get("n_current") is None, entity_registry_enabled_default=False, ), "total_current": RpcSensorDescription( @@ -902,7 +901,7 @@ def __init__( "energy": RpcSensorDescription( key="switch", sub_key="aenergy", - name="Total energy", + name="Energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: status["total"], @@ -913,21 +912,21 @@ def __init__( "ret_energy": RpcSensorDescription( key="switch", sub_key="ret_aenergy", - name="Returned energy", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: status["total"], suggested_display_precision=2, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, - removal_condition=lambda _config, status, key: ( + removal_condition=lambda _, status, key: ( status[key].get("ret_aenergy") is None ), ), "consumed_energy_switch": RpcSensorDescription( key="switch", sub_key="ret_aenergy", - name="Consumed energy", + name="Energy consumed", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: status["total"], @@ -935,8 +934,8 @@ def __init__( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, - entity_class=RpcConsumedEnergySensor, - removal_condition=lambda _config, status, key: ( + entity_class=RpcEnergyConsumedSensor, + removal_condition=lambda _, status, key: ( status[key].get("ret_aenergy") is None ), ), @@ -954,7 +953,7 @@ def __init__( "energy_pm1": RpcSensorDescription( key="pm1", sub_key="aenergy", - name="Total energy", + name="Energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: status["total"], @@ -965,7 +964,7 @@ def __init__( "ret_energy_pm1": RpcSensorDescription( key="pm1", sub_key="ret_aenergy", - name="Returned energy", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: status["total"], @@ -976,7 +975,7 @@ def __init__( "consumed_energy_pm1": RpcSensorDescription( key="pm1", sub_key="ret_aenergy", - name="Consumed energy", + name="Energy consumed", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: status["total"], @@ -984,7 +983,7 @@ def __init__( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, - entity_class=RpcConsumedEnergySensor, + entity_class=RpcEnergyConsumedSensor, ), "energy_cct": RpcSensorDescription( key="cct", @@ -1022,7 +1021,7 @@ def __init__( "total_act": RpcSensorDescription( key="emdata", sub_key="total_act", - name="Total active energy", + name="Energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1033,7 +1032,7 @@ def __init__( "total_act_energy": RpcSensorDescription( key="em1data", sub_key="total_act_energy", - name="Total active energy", + name="Energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1045,7 +1044,7 @@ def __init__( "a_total_act_energy": RpcSensorDescription( key="emdata", sub_key="a_total_act_energy", - name="Total active energy", + name="Energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1059,7 +1058,7 @@ def __init__( "b_total_act_energy": RpcSensorDescription( key="emdata", sub_key="b_total_act_energy", - name="Total active energy", + name="Energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1073,7 +1072,7 @@ def __init__( "c_total_act_energy": RpcSensorDescription( key="emdata", sub_key="c_total_act_energy", - name="Total active energy", + name="Energy", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1087,7 +1086,7 @@ def __init__( "total_act_ret": RpcSensorDescription( key="emdata", sub_key="total_act_ret", - name="Total active returned energy", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1098,7 +1097,7 @@ def __init__( "total_act_ret_energy": RpcSensorDescription( key="em1data", sub_key="total_act_ret_energy", - name="Total active returned energy", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1110,7 +1109,7 @@ def __init__( "a_total_act_ret_energy": RpcSensorDescription( key="emdata", sub_key="a_total_act_ret_energy", - name="Total active returned energy", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1124,7 +1123,7 @@ def __init__( "b_total_act_ret_energy": RpcSensorDescription( key="emdata", sub_key="b_total_act_ret_energy", - name="Total active returned energy", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1138,7 +1137,7 @@ def __init__( "c_total_act_ret_energy": RpcSensorDescription( key="emdata", sub_key="c_total_act_ret_energy", - name="Total active returned energy", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda status, _: float(status), @@ -1337,7 +1336,7 @@ def __init__( device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - removal_condition=lambda _config, status, key: (status[key]["battery"] is None), + removal_condition=lambda _, status, key: (status[key]["battery"] is None), ), "voltmeter": RpcSensorDescription( key="voltmeter", @@ -1354,9 +1353,7 @@ def __init__( key="voltmeter", sub_key="xvoltage", name="Voltmeter value", - removal_condition=lambda _config, status, key: ( - status[key].get("xvoltage") is None - ), + removal_condition=lambda _, status, key: (status[key].get("xvoltage") is None), unit=lambda config: config["xvoltage"]["unit"] or None, ), "analoginput": RpcSensorDescription( diff --git a/tests/components/shelly/snapshots/test_devices.ambr b/tests/components/shelly/snapshots/test_devices.ambr index 65ce2cde2b0138..90ac21d1b8408d 100644 --- a/tests/components/shelly/snapshots/test_devices.ambr +++ b/tests/components/shelly/snapshots/test_devices.ambr @@ -767,7 +767,7 @@ 'state': '36.4', }) # --- -# name: test_shelly_2pm_gen3_cover[sensor.test_name_total_energy-entry] +# name: test_shelly_2pm_gen3_cover[sensor.test_name_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -782,7 +782,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_total_energy', + 'entity_id': 'sensor.test_name_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -800,7 +800,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total energy', + 'original_name': 'Energy', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -810,16 +810,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_2pm_gen3_cover[sensor.test_name_total_energy-state] +# name: test_shelly_2pm_gen3_cover[sensor.test_name_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Total energy', + 'friendly_name': 'Test name Energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_total_energy', + 'entity_id': 'sensor.test_name_energy', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1743,7 +1743,7 @@ 'state': '-52', }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_consumed_energy-entry] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_energy_consumed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1758,7 +1758,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_switch_0_consumed_energy', + 'entity_id': 'sensor.test_name_switch_0_energy_consumed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1776,7 +1776,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Consumed energy', + 'original_name': 'Energy consumed', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -1786,16 +1786,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_consumed_energy-state] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_energy_consumed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Switch 0 Consumed energy', + 'friendly_name': 'Test name Switch 0 Energy consumed', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_switch_0_consumed_energy', + 'entity_id': 'sensor.test_name_switch_0_energy_consumed', 'last_changed': , 'last_reported': , 'last_updated': , @@ -1970,7 +1970,7 @@ 'state': '0.0', }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_returned_energy-entry] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_energy_returned-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1985,7 +1985,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_switch_0_returned_energy', + 'entity_id': 'sensor.test_name_switch_0_energy_returned', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2003,7 +2003,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Returned energy', + 'original_name': 'Energy returned', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -2013,16 +2013,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_returned_energy-state] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_energy_returned-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Switch 0 Returned energy', + 'friendly_name': 'Test name Switch 0 Energy returned', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_switch_0_returned_energy', + 'entity_id': 'sensor.test_name_switch_0_energy_returned', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2085,7 +2085,7 @@ 'state': '40.6', }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_total_energy-entry] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2100,7 +2100,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_switch_0_total_energy', + 'entity_id': 'sensor.test_name_switch_0_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2118,7 +2118,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total energy', + 'original_name': 'Energy', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -2128,16 +2128,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_total_energy-state] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_0_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Switch 0 Total energy', + 'friendly_name': 'Test name Switch 0 Energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_switch_0_total_energy', + 'entity_id': 'sensor.test_name_switch_0_energy', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2200,7 +2200,7 @@ 'state': '216.2', }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_consumed_energy-entry] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_energy_consumed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2215,7 +2215,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_switch_1_consumed_energy', + 'entity_id': 'sensor.test_name_switch_1_energy_consumed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2233,7 +2233,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Consumed energy', + 'original_name': 'Energy consumed', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -2243,16 +2243,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_consumed_energy-state] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_energy_consumed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Switch 1 Consumed energy', + 'friendly_name': 'Test name Switch 1 Energy consumed', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_switch_1_consumed_energy', + 'entity_id': 'sensor.test_name_switch_1_energy_consumed', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2427,7 +2427,7 @@ 'state': '0.0', }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_returned_energy-entry] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_energy_returned-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2442,7 +2442,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_switch_1_returned_energy', + 'entity_id': 'sensor.test_name_switch_1_energy_returned', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2460,7 +2460,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Returned energy', + 'original_name': 'Energy returned', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -2470,16 +2470,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_returned_energy-state] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_energy_returned-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Switch 1 Returned energy', + 'friendly_name': 'Test name Switch 1 Energy returned', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_switch_1_returned_energy', + 'entity_id': 'sensor.test_name_switch_1_energy_returned', 'last_changed': , 'last_reported': , 'last_updated': , @@ -2542,7 +2542,7 @@ 'state': '40.6', }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_total_energy-entry] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -2557,7 +2557,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_switch_1_total_energy', + 'entity_id': 'sensor.test_name_switch_1_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -2575,7 +2575,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total energy', + 'original_name': 'Energy', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -2585,16 +2585,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_total_energy-state] +# name: test_shelly_2pm_gen3_no_relay_names[sensor.test_name_switch_1_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Switch 1 Total energy', + 'friendly_name': 'Test name Switch 1 Energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_switch_1_total_energy', + 'entity_id': 'sensor.test_name_switch_1_energy', 'last_changed': , 'last_reported': , 'last_updated': , @@ -3347,7 +3347,7 @@ 'state': '0.99', }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_a_total_active_energy-entry] +# name: test_shelly_pro_3em[sensor.test_name_phase_a_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3362,7 +3362,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_phase_a_total_active_energy', + 'entity_id': 'sensor.test_name_phase_a_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3380,7 +3380,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total active energy', + 'original_name': 'Energy', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -3390,23 +3390,23 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_a_total_active_energy-state] +# name: test_shelly_pro_3em[sensor.test_name_phase_a_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Phase A Total active energy', + 'friendly_name': 'Test name Phase A Energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_phase_a_total_active_energy', + 'entity_id': 'sensor.test_name_phase_a_energy', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '3105.57642', }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_a_total_active_returned_energy-entry] +# name: test_shelly_pro_3em[sensor.test_name_phase_a_energy_returned-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3421,7 +3421,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_phase_a_total_active_returned_energy', + 'entity_id': 'sensor.test_name_phase_a_energy_returned', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3439,7 +3439,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total active returned energy', + 'original_name': 'Energy returned', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -3449,16 +3449,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_a_total_active_returned_energy-state] +# name: test_shelly_pro_3em[sensor.test_name_phase_a_energy_returned-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Phase A Total active returned energy', + 'friendly_name': 'Test name Phase A Energy returned', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_phase_a_total_active_returned_energy', + 'entity_id': 'sensor.test_name_phase_a_energy_returned', 'last_changed': , 'last_reported': , 'last_updated': , @@ -3797,7 +3797,7 @@ 'state': '0.36', }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_b_total_active_energy-entry] +# name: test_shelly_pro_3em[sensor.test_name_phase_b_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3812,7 +3812,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_phase_b_total_active_energy', + 'entity_id': 'sensor.test_name_phase_b_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3830,7 +3830,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total active energy', + 'original_name': 'Energy', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -3840,23 +3840,23 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_b_total_active_energy-state] +# name: test_shelly_pro_3em[sensor.test_name_phase_b_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Phase B Total active energy', + 'friendly_name': 'Test name Phase B Energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_phase_b_total_active_energy', + 'entity_id': 'sensor.test_name_phase_b_energy', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '195.76572', }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_b_total_active_returned_energy-entry] +# name: test_shelly_pro_3em[sensor.test_name_phase_b_energy_returned-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -3871,7 +3871,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_phase_b_total_active_returned_energy', + 'entity_id': 'sensor.test_name_phase_b_energy_returned', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -3889,7 +3889,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total active returned energy', + 'original_name': 'Energy returned', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -3899,16 +3899,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_b_total_active_returned_energy-state] +# name: test_shelly_pro_3em[sensor.test_name_phase_b_energy_returned-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Phase B Total active returned energy', + 'friendly_name': 'Test name Phase B Energy returned', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_phase_b_total_active_returned_energy', + 'entity_id': 'sensor.test_name_phase_b_energy_returned', 'last_changed': , 'last_reported': , 'last_updated': , @@ -4247,7 +4247,7 @@ 'state': '0.72', }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_c_total_active_energy-entry] +# name: test_shelly_pro_3em[sensor.test_name_phase_c_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4262,7 +4262,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_phase_c_total_active_energy', + 'entity_id': 'sensor.test_name_phase_c_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4280,7 +4280,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total active energy', + 'original_name': 'Energy', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -4290,23 +4290,23 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_c_total_active_energy-state] +# name: test_shelly_pro_3em[sensor.test_name_phase_c_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Phase C Total active energy', + 'friendly_name': 'Test name Phase C Energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_phase_c_total_active_energy', + 'entity_id': 'sensor.test_name_phase_c_energy', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '2114.07205', }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_c_total_active_returned_energy-entry] +# name: test_shelly_pro_3em[sensor.test_name_phase_c_energy_returned-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4321,7 +4321,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_phase_c_total_active_returned_energy', + 'entity_id': 'sensor.test_name_phase_c_energy_returned', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4339,7 +4339,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total active returned energy', + 'original_name': 'Energy returned', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -4349,16 +4349,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_pro_3em[sensor.test_name_phase_c_total_active_returned_energy-state] +# name: test_shelly_pro_3em[sensor.test_name_phase_c_energy_returned-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Phase C Total active returned energy', + 'friendly_name': 'Test name Phase C Energy returned', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_phase_c_total_active_returned_energy', + 'entity_id': 'sensor.test_name_phase_c_energy_returned', 'last_changed': , 'last_reported': , 'last_updated': , @@ -4586,7 +4586,7 @@ 'state': '46.3', }) # --- -# name: test_shelly_pro_3em[sensor.test_name_total_active_energy-entry] +# name: test_shelly_pro_3em[sensor.test_name_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4601,7 +4601,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_total_active_energy', + 'entity_id': 'sensor.test_name_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4619,7 +4619,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total active energy', + 'original_name': 'Energy', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -4629,16 +4629,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_pro_3em[sensor.test_name_total_active_energy-state] +# name: test_shelly_pro_3em[sensor.test_name_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Total active energy', + 'friendly_name': 'Test name Energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_total_active_energy', + 'entity_id': 'sensor.test_name_energy', 'last_changed': , 'last_reported': , 'last_updated': , @@ -4701,7 +4701,7 @@ 'state': '2413.825', }) # --- -# name: test_shelly_pro_3em[sensor.test_name_total_active_returned_energy-entry] +# name: test_shelly_pro_3em[sensor.test_name_energy_returned-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -4716,7 +4716,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_total_active_returned_energy', + 'entity_id': 'sensor.test_name_energy_returned', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -4734,7 +4734,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Total active returned energy', + 'original_name': 'Energy returned', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -4744,16 +4744,16 @@ 'unit_of_measurement': , }) # --- -# name: test_shelly_pro_3em[sensor.test_name_total_active_returned_energy-state] +# name: test_shelly_pro_3em[sensor.test_name_energy_returned-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name Total active returned energy', + 'friendly_name': 'Test name Energy returned', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_total_active_returned_energy', + 'entity_id': 'sensor.test_name_energy_returned', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr index 3e849287bd73e3..2f09492351e7dc 100644 --- a/tests/components/shelly/snapshots/test_sensor.ambr +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -339,7 +339,7 @@ 'state': '5.0', }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_consumed_energy-entry] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy_consumed-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -354,7 +354,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_test_switch_0_consumed_energy', + 'entity_id': 'sensor.test_name_test_switch_0_energy_consumed', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -372,7 +372,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test switch_0 consumed energy', + 'original_name': 'test switch_0 energy consumed', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -382,23 +382,23 @@ 'unit_of_measurement': , }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_consumed_energy-state] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy_consumed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name test switch_0 consumed energy', + 'friendly_name': 'Test name test switch_0 energy consumed', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_test_switch_0_consumed_energy', + 'entity_id': 'sensor.test_name_test_switch_0_energy_consumed', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '1135.80246', }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_returned_energy-entry] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy_returned-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -413,7 +413,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_test_switch_0_returned_energy', + 'entity_id': 'sensor.test_name_test_switch_0_energy_returned', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -431,7 +431,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test switch_0 returned energy', + 'original_name': 'test switch_0 energy returned', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -441,23 +441,23 @@ 'unit_of_measurement': , }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_returned_energy-state] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy_returned-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name test switch_0 returned energy', + 'friendly_name': 'Test name test switch_0 energy returned', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_test_switch_0_returned_energy', + 'entity_id': 'sensor.test_name_test_switch_0_energy_returned', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '98.76543', }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_total_energy-entry] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -472,7 +472,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.test_name_test_switch_0_total_energy', + 'entity_id': 'sensor.test_name_test_switch_0_energy', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -490,7 +490,7 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'test switch_0 total energy', + 'original_name': 'test switch_0 energy', 'platform': 'shelly', 'previous_unique_id': None, 'suggested_object_id': None, @@ -500,16 +500,16 @@ 'unit_of_measurement': , }) # --- -# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_total_energy-state] +# name: test_rpc_switch_energy_sensors[sensor.test_name_test_switch_0_energy-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'energy', - 'friendly_name': 'Test name test switch_0 total energy', + 'friendly_name': 'Test name test switch_0 energy', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.test_name_test_switch_0_total_energy', + 'entity_id': 'sensor.test_name_test_switch_0_energy', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index e6d6812505b9e7..f1f41f5c188359 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -707,27 +707,19 @@ async def test_rpc_energy_meter_1_sensors( assert (entry := entity_registry.async_get("sensor.test_name_energy_meter_1_power")) assert entry.unique_id == "123456789ABC-em1:1-power_em1" - assert ( - state := hass.states.get("sensor.test_name_energy_meter_0_total_active_energy") - ) + assert (state := hass.states.get("sensor.test_name_energy_meter_0_energy")) assert state.state == "123.4564" assert ( - entry := entity_registry.async_get( - "sensor.test_name_energy_meter_0_total_active_energy" - ) + entry := entity_registry.async_get("sensor.test_name_energy_meter_0_energy") ) assert entry.unique_id == "123456789ABC-em1data:0-total_act_energy" - assert ( - state := hass.states.get("sensor.test_name_energy_meter_1_total_active_energy") - ) + assert (state := hass.states.get("sensor.test_name_energy_meter_1_energy")) assert state.state == "987.6543" assert ( - entry := entity_registry.async_get( - "sensor.test_name_energy_meter_1_total_active_energy" - ) + entry := entity_registry.async_get("sensor.test_name_energy_meter_1_energy") ) assert entry.unique_id == "123456789ABC-em1data:1-total_act_energy" @@ -1649,7 +1641,7 @@ async def test_rpc_switch_energy_sensors( monkeypatch.setattr(mock_rpc_device, "status", status) await init_integration(hass, 3) - for entity in ("total_energy", "returned_energy", "consumed_energy"): + for entity in ("energy", "energy_returned", "energy_consumed"): entity_id = f"{SENSOR_DOMAIN}.test_name_test_switch_0_{entity}" state = hass.states.get(entity_id) @@ -1660,12 +1652,12 @@ async def test_rpc_switch_energy_sensors( @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_rpc_switch_no_returned_energy_sensor( +async def test_rpc_switch_no_energy_returned_sensor( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test switch component without returned energy sensor.""" + """Test switch component without energy returned sensor.""" status = { "sys": {}, "switch:0": { @@ -1678,8 +1670,8 @@ async def test_rpc_switch_no_returned_energy_sensor( monkeypatch.setattr(mock_rpc_device, "status", status) await init_integration(hass, 3) - assert hass.states.get("sensor.test_name_test_switch_0_returned_energy") is None - assert hass.states.get("sensor.test_name_test_switch_0_consumed_energy") is None + assert hass.states.get("sensor.test_name_test_switch_0_energy_returned") is None + assert hass.states.get("sensor.test_name_test_switch_0_energy_consumed") is None async def test_rpc_shelly_ev_sensors( @@ -1877,7 +1869,7 @@ async def test_rpc_presencezone_component( @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_rpc_pm1_consumed_energy_sensor( +async def test_rpc_pm1_energy_consumed_sensor( hass: HomeAssistant, mock_rpc_device: Mock, entity_registry: EntityRegistry, @@ -1899,14 +1891,14 @@ async def test_rpc_pm1_consumed_energy_sensor( monkeypatch.setattr(mock_rpc_device, "status", status) await init_integration(hass, 3) - assert (state := hass.states.get(f"{SENSOR_DOMAIN}.test_name_total_energy")) + assert (state := hass.states.get(f"{SENSOR_DOMAIN}.test_name_energy")) assert state.state == "3.0" - assert (state := hass.states.get(f"{SENSOR_DOMAIN}.test_name_returned_energy")) + assert (state := hass.states.get(f"{SENSOR_DOMAIN}.test_name_energy_returned")) assert state.state == "1.0" - entity_id = f"{SENSOR_DOMAIN}.test_name_consumed_energy" - # consumed energy = total energy - returned energy + entity_id = f"{SENSOR_DOMAIN}.test_name_energy_consumed" + # energy consumed = energy - energy returned assert (state := hass.states.get(entity_id)) assert state.state == "2.0" @@ -1916,14 +1908,14 @@ async def test_rpc_pm1_consumed_energy_sensor( @pytest.mark.parametrize(("key"), ["aenergy", "ret_aenergy"]) @pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_rpc_pm1_consumed_energy_sensor_non_float_value( +async def test_rpc_pm1_energy_consumed_sensor_non_float_value( hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, key: str, ) -> None: """Test energy sensors for switch component.""" - entity_id = f"{SENSOR_DOMAIN}.test_name_consumed_energy" + entity_id = f"{SENSOR_DOMAIN}.test_name_energy_consumed" status = { "sys": {}, "pm1:0": { From 19f990ed31fc540f39486254b9afc66df0066c1c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 5 Oct 2025 16:33:12 -0400 Subject: [PATCH 10/20] ESPHome to set Z-Wave discovery as next_flow (#153706) --- .../components/esphome/config_flow.py | 53 ++++- tests/components/esphome/test_config_flow.py | 202 +++++++++++++++++- 2 files changed, 249 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 6197716f617e1d..fc81dfdbc43218 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -22,19 +22,23 @@ from homeassistant.components import zeroconf from homeassistant.config_entries import ( + SOURCE_ESPHOME, SOURCE_IGNORE, SOURCE_REAUTH, SOURCE_RECONFIGURE, ConfigEntry, ConfigFlow, ConfigFlowResult, + FlowType, OptionsFlow, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import AbortFlow +from homeassistant.data_entry_flow import AbortFlow, FlowResultType +from homeassistant.helpers import discovery_flow from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo @@ -75,6 +79,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" self._host: str | None = None + self._connected_address: str | None = None self.__name: str | None = None self._port: int | None = None self._password: str | None = None @@ -498,18 +503,55 @@ async def async_step_name_conflict_overwrite( await self.hass.config_entries.async_remove( self._entry_with_name_conflict.entry_id ) - return self._async_create_entry() + return await self._async_create_entry() - @callback - def _async_create_entry(self) -> ConfigFlowResult: + async def _async_create_entry(self) -> ConfigFlowResult: """Create the config entry.""" assert self._name is not None + assert self._device_info is not None + + # Check if Z-Wave capabilities are present and start discovery flow + next_flow_id: str | None = None + if self._device_info.zwave_proxy_feature_flags: + assert self._connected_address is not None + assert self._port is not None + + # Start Z-Wave discovery flow and get the flow ID + zwave_result = await self.hass.config_entries.flow.async_init( + "zwave_js", + context={ + "source": SOURCE_ESPHOME, + "discovery_key": discovery_flow.DiscoveryKey( + domain=DOMAIN, + key=self._device_info.mac_address, + version=1, + ), + }, + data=ESPHomeServiceInfo( + name=self._device_info.name, + zwave_home_id=self._device_info.zwave_home_id or None, + ip_address=self._connected_address, + port=self._port, + noise_psk=self._noise_psk, + ), + ) + if zwave_result["type"] in ( + FlowResultType.ABORT, + FlowResultType.CREATE_ENTRY, + ): + _LOGGER.debug( + "Unable to continue created Z-Wave JS config flow: %s", zwave_result + ) + else: + next_flow_id = zwave_result["flow_id"] + return self.async_create_entry( title=self._name, data=self._async_make_config_data(), options={ CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS, }, + next_flow=(FlowType.CONFIG_FLOW, next_flow_id) if next_flow_id else None, ) @callback @@ -556,7 +598,7 @@ async def _async_validated_connection(self) -> ConfigFlowResult: if entry.data.get(CONF_DEVICE_NAME) == self._device_name: self._entry_with_name_conflict = entry return await self.async_step_name_conflict() - return self._async_create_entry() + return await self._async_create_entry() async def _async_reauth_validated_connection(self) -> ConfigFlowResult: """Handle reauth validated connection.""" @@ -703,6 +745,7 @@ async def _fetch_device_info( try: await cli.connect() self._device_info = await cli.device_info() + self._connected_address = cli.connected_address except InvalidAuthAPIError: return ERROR_INVALID_PASSWORD_AUTH except RequiresEncryptionAPIError: diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 27d585bea6f369..fb7458a1a5b73f 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -3,7 +3,7 @@ from ipaddress import ip_address import json from typing import Any -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from aioesphomeapi import ( APIClient, @@ -34,7 +34,9 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import discovery_flow from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo from homeassistant.helpers.service_info.hassio import HassioServiceInfo from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo @@ -2619,3 +2621,201 @@ async def test_discovery_dhcp_no_probe_same_host_port_none( # Host should remain unchanged assert entry.data[CONF_HOST] == "192.168.43.183" + + +@pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf") +async def test_user_flow_starts_zwave_discovery( + hass: HomeAssistant, mock_client: APIClient +) -> None: + """Test that the user flow starts Z-Wave JS discovery when device has Z-Wave capabilities.""" + # Mock device with Z-Wave capabilities + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + uses_password=False, + name="test-zwave-device", + mac_address="11:22:33:44:55:BB", + zwave_proxy_feature_flags=1, + zwave_home_id=1234567890, + ) + ) + mock_client.connected_address = "mock-connected-address" + + # Track flow.async_init calls and async_get calls + original_async_init = hass.config_entries.flow.async_init + original_async_get = hass.config_entries.flow.async_get + flow_init_calls = [] + zwave_flow_id = "mock-zwave-flow-id" + + async def track_async_init(*args, **kwargs): + flow_init_calls.append((args, kwargs)) + # For the Z-Wave flow, return a mock result with the flow_id + if args and args[0] == "zwave_js": + return {"flow_id": zwave_flow_id, "type": FlowResultType.FORM} + # Otherwise call the original + return await original_async_init(*args, **kwargs) + + def mock_async_get(flow_id: str): + # Return a mock flow for the Z-Wave flow_id + if flow_id == zwave_flow_id: + return MagicMock() + return original_async_get(flow_id) + + with ( + patch.object( + hass.config_entries.flow, "async_init", side_effect=track_async_init + ), + patch.object(hass.config_entries.flow, "async_get", side_effect=mock_async_get), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "192.168.1.100", CONF_PORT: 6053}, + ) + + # Verify the entry was created + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-zwave-device" + assert result["data"] == { + CONF_HOST: "192.168.1.100", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test-zwave-device", + } + + # First call is ESPHome flow, second should be Z-Wave flow + assert len(flow_init_calls) == 2 + zwave_call_args, zwave_call_kwargs = flow_init_calls[1] + assert zwave_call_args[0] == "zwave_js" + assert zwave_call_kwargs["context"] == { + "source": config_entries.SOURCE_ESPHOME, + "discovery_key": discovery_flow.DiscoveryKey( + domain="esphome", key="11:22:33:44:55:BB", version=1 + ), + } + assert zwave_call_kwargs["data"] == ESPHomeServiceInfo( + name="test-zwave-device", + zwave_home_id=1234567890, + ip_address="mock-connected-address", + port=6053, + noise_psk=None, + ) + + # Verify next_flow was set + assert result["next_flow"] == (config_entries.FlowType.CONFIG_FLOW, zwave_flow_id) + + +@pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf") +async def test_user_flow_no_zwave_discovery_without_capabilities( + hass: HomeAssistant, mock_client: APIClient +) -> None: + """Test that the user flow does not start Z-Wave JS discovery when device has no Z-Wave capabilities.""" + # Mock device without Z-Wave capabilities + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + uses_password=False, + name="test-regular-device", + mac_address="11:22:33:44:55:CC", + ) + ) + + # Track flow.async_init calls + original_async_init = hass.config_entries.flow.async_init + flow_init_calls = [] + + async def track_async_init(*args, **kwargs): + flow_init_calls.append((args, kwargs)) + return await original_async_init(*args, **kwargs) + + with patch.object( + hass.config_entries.flow, "async_init", side_effect=track_async_init + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "192.168.1.101", CONF_PORT: 6053}, + ) + + # Verify the entry was created + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-regular-device" + + # Verify Z-Wave discovery flow was NOT started (only ESPHome flow) + assert len(flow_init_calls) == 1 + + # Verify next_flow was not set + assert "next_flow" not in result + + +@pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf") +async def test_user_flow_zwave_discovery_aborts( + hass: HomeAssistant, mock_client: APIClient +) -> None: + """Test that the user flow handles Z-Wave discovery abort gracefully.""" + # Mock device with Z-Wave capabilities + mock_client.device_info = AsyncMock( + return_value=DeviceInfo( + uses_password=False, + name="test-zwave-device", + mac_address="11:22:33:44:55:DD", + zwave_proxy_feature_flags=1, + zwave_home_id=9876543210, + ) + ) + mock_client.connected_address = "192.168.1.102" + + # Track flow.async_init calls + original_async_init = hass.config_entries.flow.async_init + flow_init_calls = [] + + async def track_async_init(*args, **kwargs): + flow_init_calls.append((args, kwargs)) + # For the Z-Wave flow, return an ABORT result + if args and args[0] == "zwave_js": + return { + "type": FlowResultType.ABORT, + "reason": "already_configured", + } + # Otherwise call the original + return await original_async_init(*args, **kwargs) + + with patch.object( + hass.config_entries.flow, "async_init", side_effect=track_async_init + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_HOST: "192.168.1.102", CONF_PORT: 6053}, + ) + + # Verify the ESPHome entry was still created despite Z-Wave flow aborting + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-zwave-device" + assert result["data"] == { + CONF_HOST: "192.168.1.102", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test-zwave-device", + } + + # Verify Z-Wave discovery flow was attempted + assert len(flow_init_calls) == 2 + zwave_call_args, zwave_call_kwargs = flow_init_calls[1] + assert zwave_call_args[0] == "zwave_js" + assert zwave_call_kwargs["context"]["source"] == config_entries.SOURCE_ESPHOME + assert zwave_call_kwargs["context"]["discovery_key"] == discovery_flow.DiscoveryKey( + domain=DOMAIN, + key="11:22:33:44:55:DD", + version=1, + ) + assert zwave_call_kwargs["data"] == ESPHomeServiceInfo( + name="test-zwave-device", + zwave_home_id=9876543210, + ip_address="192.168.1.102", + port=6053, + noise_psk=None, + ) + + # Verify next_flow was NOT set since Z-Wave flow aborted + assert "next_flow" not in result From f524edc4b979605bf43b9b22638a89adc10501d4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 5 Oct 2025 22:36:24 +0200 Subject: [PATCH 11/20] Add pytest command line option to drop recorder db before test (#153527) --- tests/conftest.py | 73 ++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 205396a5d949b6..374e8098bb9d47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -159,6 +159,7 @@ def pytest_addoption(parser: pytest.Parser) -> None: """Register custom pytest options.""" parser.addoption("--dburl", action="store", default="sqlite://") + parser.addoption("--drop-existing-db", action="store_const", const=True) def pytest_configure(config: pytest.Config) -> None: @@ -1492,44 +1493,58 @@ def recorder_db_url( assert not hass_fixture_setup db_url = cast(str, pytestconfig.getoption("dburl")) + drop_existing_db = pytestconfig.getoption("drop_existing_db") + + def drop_db() -> None: + import sqlalchemy as sa # noqa: PLC0415 + import sqlalchemy_utils # noqa: PLC0415 + + if db_url.startswith("mysql://"): + made_url = sa.make_url(db_url) + db = made_url.database + engine = sa.create_engine(db_url) + # Check for any open connections to the database before dropping it + # to ensure that InnoDB does not deadlock. + with engine.begin() as connection: + query = sa.text( + "select id FROM information_schema.processlist WHERE db=:db and id != CONNECTION_ID()" + ) + rows = connection.execute(query, parameters={"db": db}).fetchall() + if rows: + raise RuntimeError( + f"Unable to drop database {db} because it is in use by {rows}" + ) + engine.dispose() + sqlalchemy_utils.drop_database(db_url) + elif db_url.startswith("postgresql://"): + sqlalchemy_utils.drop_database(db_url) + if db_url == "sqlite://" and persistent_database: tmp_path = tmp_path_factory.mktemp("recorder") db_url = "sqlite:///" + str(tmp_path / "pytest.db") - elif db_url.startswith("mysql://"): + elif db_url.startswith(("mysql://", "postgresql://")): import sqlalchemy_utils # noqa: PLC0415 - charset = "utf8mb4' COLLATE = 'utf8mb4_unicode_ci" - assert not sqlalchemy_utils.database_exists(db_url) - sqlalchemy_utils.create_database(db_url, encoding=charset) - elif db_url.startswith("postgresql://"): - import sqlalchemy_utils # noqa: PLC0415 + if drop_existing_db and sqlalchemy_utils.database_exists(db_url): + drop_db() - assert not sqlalchemy_utils.database_exists(db_url) - sqlalchemy_utils.create_database(db_url, encoding="utf8") + if sqlalchemy_utils.database_exists(db_url): + raise RuntimeError( + f"Database {db_url} already exists. Use --drop-existing-db " + "to automatically drop existing database before start of test." + ) + + sqlalchemy_utils.create_database( + db_url, + encoding="utf8mb4' COLLATE = 'utf8mb4_unicode_ci" + if db_url.startswith("mysql://") + else "utf8", + ) yield db_url if db_url == "sqlite://" and persistent_database: rmtree(tmp_path, ignore_errors=True) - elif db_url.startswith("mysql://"): - import sqlalchemy as sa # noqa: PLC0415 - - made_url = sa.make_url(db_url) - db = made_url.database - engine = sa.create_engine(db_url) - # Check for any open connections to the database before dropping it - # to ensure that InnoDB does not deadlock. - with engine.begin() as connection: - query = sa.text( - "select id FROM information_schema.processlist WHERE db=:db and id != CONNECTION_ID()" - ) - rows = connection.execute(query, parameters={"db": db}).fetchall() - if rows: - raise RuntimeError( - f"Unable to drop database {db} because it is in use by {rows}" - ) - engine.dispose() - sqlalchemy_utils.drop_database(db_url) - elif db_url.startswith("postgresql://"): - sqlalchemy_utils.drop_database(db_url) + elif db_url.startswith(("mysql://", "postgresql://")): + drop_db() async def _async_init_recorder_component( From 1818fce1aedb5033b731f3b8933484aa639208e0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 5 Oct 2025 22:37:14 +0200 Subject: [PATCH 12/20] Validating schema outside the event loop will now fail (#153472) --- homeassistant/helpers/config_validation.py | 10 +--------- tests/helpers/test_config_validation.py | 5 ++++- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7110ad267af00f..cc46327c4c165e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -739,15 +739,7 @@ def template(value: Any | None) -> template_helper.Template: if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid("template value should be a string") if not (hass := _async_get_hass_or_none()): - from .frame import ReportBehavior, report_usage # noqa: PLC0415 - - report_usage( - ( - "validates schema outside the event loop, " - "which will stop working in HA Core 2025.10" - ), - core_behavior=ReportBehavior.LOG, - ) + raise vol.Invalid("Validates schema outside the event loop") template_value = template_helper.Template(str(value), hass) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 95e40641e79b81..0630c584989eaf 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -711,7 +711,10 @@ async def test_template_no_hass(hass: HomeAssistant) -> None: "{{ no_such_function('group.foo')|map(attribute='entity_id')|list }}", ) for value in options: - await hass.async_add_executor_job(schema, value) + with pytest.raises( + vol.Invalid, match="Validates schema outside the event loop" + ): + await hass.async_add_executor_job(schema, value) def test_dynamic_template(hass: HomeAssistant) -> None: From 9ac93920d8235edce74d00bf8a893ba98bf91725 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 5 Oct 2025 22:41:19 +0200 Subject: [PATCH 13/20] Cleanup process_fds addition in systemmonitor (#153568) --- .../components/systemmonitor/coordinator.py | 39 +++++++------------ .../components/systemmonitor/sensor.py | 36 ++--------------- .../snapshots/test_diagnostics.ambr | 10 ++++- 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/systemmonitor/coordinator.py b/homeassistant/components/systemmonitor/coordinator.py index 87e7a3eb5915e6..5cbc81eba6b155 100644 --- a/homeassistant/components/systemmonitor/coordinator.py +++ b/homeassistant/components/systemmonitor/coordinator.py @@ -8,7 +8,7 @@ import os from typing import TYPE_CHECKING, Any, NamedTuple -from psutil import AccessDenied, NoSuchProcess, Process +from psutil import Process from psutil._common import sdiskusage, shwtemp, snetio, snicaddr, sswap import psutil_home_assistant as ha_psutil @@ -67,7 +67,7 @@ def as_dict(self) -> dict[str, Any]: "boot_time": str(self.boot_time), "processes": str(self.processes), "temperatures": temperatures, - "process_fds": str(self.process_fds), + "process_fds": self.process_fds, } @@ -212,6 +212,7 @@ def update_data(self) -> dict[str, Any]: _LOGGER.debug("boot time: %s", self.boot_time) selected_processes: list[Process] = [] + process_fds: dict[str, int] = {} if self.update_subscribers[("processes", "")] or self._initial_update: processes = self._psutil.process_iter() _LOGGER.debug("processes: %s", processes) @@ -220,8 +221,12 @@ def update_data(self) -> dict[str, Any]: ).get(CONF_PROCESS, []) for process in processes: try: - if process.name() in user_options: + if (process_name := process.name()) in user_options: selected_processes.append(process) + process_fds[process_name] = ( + process_fds.get(process_name, 0) + process.num_fds() + ) + except PROCESS_ERRORS as err: if not hasattr(err, "pid") or not hasattr(err, "name"): _LOGGER.warning( @@ -235,28 +240,12 @@ def update_data(self) -> dict[str, Any]: err.name, ) continue - - # Collect file descriptor counts only for selected processes - process_fds: dict[str, int] = {} - for proc in selected_processes: - try: - process_name = proc.name() - # Our sensors are a per-process name aggregation. Not ideal, but the only - # way to do it without user specifying PIDs which are not static. - process_fds[process_name] = ( - process_fds.get(process_name, 0) + proc.num_fds() - ) - except (NoSuchProcess, AccessDenied): - _LOGGER.warning( - "Failed to get file descriptor count for process %s: access denied or process not found", - proc.pid, - ) - except OSError as err: - _LOGGER.warning( - "OS error getting file descriptor count for process %s: %s", - proc.pid, - err, - ) + except OSError as err: + _LOGGER.warning( + "OS error getting file descriptor count for process %s: %s", + process.pid if hasattr(process, "pid") else "unknown", + err, + ) temps: dict[str, list[shwtemp]] = {} if self.update_subscribers[("temperatures", "")] or self._initial_update: diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 6e3fac7d6354a4..1b7764eac00547 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -60,6 +60,7 @@ "disk_": "disk_arguments", "ipv": "network_arguments", **dict.fromkeys(NET_IO_TYPES, "network_arguments"), + "process_num_fds": "processes", } @@ -444,6 +445,9 @@ def get_arguments() -> dict[str, Any]: startup_arguments = await hass.async_add_executor_job(get_arguments) startup_arguments["cpu_temperature"] = cpu_temperature + startup_arguments["processes"] = entry.options.get(BINARY_SENSOR_DOMAIN, {}).get( + CONF_PROCESS, [] + ) _LOGGER.debug("Setup from options %s", entry.options) for _type, sensor_description in SENSOR_TYPES.items(): @@ -499,38 +503,6 @@ def get_arguments() -> dict[str, Any]: ) continue - if _type == "process_num_fds": - # Create sensors for processes configured in binary_sensor section - processes = entry.options.get(BINARY_SENSOR_DOMAIN, {}).get( - CONF_PROCESS, [] - ) - _LOGGER.debug( - "Creating process_num_fds sensors for processes: %s", processes - ) - for process in processes: - argument = process - is_enabled = check_legacy_resource( - f"{_type}_{argument}", legacy_resources - ) - unique_id = slugify(f"{_type}_{argument}") - loaded_resources.add(unique_id) - _LOGGER.debug( - "Creating process_num_fds sensor: type=%s, process=%s, unique_id=%s, enabled=%s", - _type, - process, - unique_id, - is_enabled, - ) - entities.append( - SystemMonitorSensor( - coordinator, - sensor_description, - entry.entry_id, - argument, - is_enabled, - ) - ) - continue # Ensure legacy imported disk_* resources are loaded if they are not part # of mount points automatically discovered for resource in legacy_resources: diff --git a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr index 7f53bef3fef2dd..d306fa6551400c 100644 --- a/tests/components/systemmonitor/snapshots/test_diagnostics.ambr +++ b/tests/components/systemmonitor/snapshots/test_diagnostics.ambr @@ -22,7 +22,10 @@ }), 'load': '(1, 2, 3)', 'memory': 'VirtualMemory(total=104857600, available=41943040, percent=40.0, used=62914560, free=31457280)', - 'process_fds': "{'python3': 42, 'pip': 15}", + 'process_fds': dict({ + 'pip': 15, + 'python3': 42, + }), 'processes': "[tests.components.systemmonitor.conftest.MockProcess(pid=1, name='python3', status='sleeping', started='2024-02-23 15:00:00'), tests.components.systemmonitor.conftest.MockProcess(pid=1, name='pip', status='sleeping', started='2024-02-23 15:00:00')]", 'swap': 'sswap(total=104857600, used=62914560, free=41943040, percent=60.0, sin=1, sout=1)', 'temperatures': dict({ @@ -80,7 +83,10 @@ 'io_counters': None, 'load': '(1, 2, 3)', 'memory': 'VirtualMemory(total=104857600, available=41943040, percent=40.0, used=62914560, free=31457280)', - 'process_fds': "{'python3': 42, 'pip': 15}", + 'process_fds': dict({ + 'pip': 15, + 'python3': 42, + }), 'processes': "[tests.components.systemmonitor.conftest.MockProcess(pid=1, name='python3', status='sleeping', started='2024-02-23 15:00:00'), tests.components.systemmonitor.conftest.MockProcess(pid=1, name='pip', status='sleeping', started='2024-02-23 15:00:00')]", 'swap': 'sswap(total=104857600, used=62914560, free=41943040, percent=60.0, sin=1, sout=1)', 'temperatures': dict({ From 78cd80746dff058cf5440cfea9521674347c1785 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Sun, 5 Oct 2025 22:12:05 +0100 Subject: [PATCH 14/20] Bump aiomealie to 1.0.0, update min Mealie instance version to v2. (#153203) --- homeassistant/components/mealie/__init__.py | 1 - homeassistant/components/mealie/const.py | 2 +- homeassistant/components/mealie/manifest.json | 2 +- homeassistant/components/mealie/quality_scale.yaml | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/mealie/fixtures/about.json | 2 +- tests/components/mealie/snapshots/test_diagnostics.ambr | 2 +- tests/components/mealie/snapshots/test_init.ambr | 2 +- tests/components/mealie/test_config_flow.py | 2 ++ 10 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py index 0221fd45051a96..e5ee1bc9e99a03 100644 --- a/homeassistant/components/mealie/__init__.py +++ b/homeassistant/components/mealie/__init__.py @@ -48,7 +48,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo ), ) try: - await client.define_household_support() about = await client.get_about() version = create_version(about.version) except MealieAuthenticationError as error: diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py index e729265bcbc6d4..4f8c4773b9e122 100644 --- a/homeassistant/components/mealie/const.py +++ b/homeassistant/components/mealie/const.py @@ -19,4 +19,4 @@ ATTR_SEARCH_TERMS = "search_terms" ATTR_RESULT_LIMIT = "result_limit" -MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0") +MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v2.0.0") diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index b768cc92ccd650..1fdcc4f897fa62 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["aiomealie==0.11.0"] + "requirements": ["aiomealie==1.0.0"] } diff --git a/homeassistant/components/mealie/quality_scale.yaml b/homeassistant/components/mealie/quality_scale.yaml index 93fb3ae74a0208..1fccc3add81f59 100644 --- a/homeassistant/components/mealie/quality_scale.yaml +++ b/homeassistant/components/mealie/quality_scale.yaml @@ -50,7 +50,7 @@ rules: docs-data-update: done docs-examples: done docs-known-limitations: todo - docs-supported-devices: todo + docs-supported-devices: done docs-supported-functions: done docs-troubleshooting: todo docs-use-cases: todo diff --git a/requirements_all.txt b/requirements_all.txt index 6c0d4a111bf8cd..5fc81177605a31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -310,7 +310,7 @@ aiolookin==1.0.0 aiolyric==2.0.2 # homeassistant.components.mealie -aiomealie==0.11.0 +aiomealie==1.0.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ead1ce88bd3bf2..996fcca613045d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -292,7 +292,7 @@ aiolookin==1.0.0 aiolyric==2.0.2 # homeassistant.components.mealie -aiomealie==0.11.0 +aiomealie==1.0.0 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/tests/components/mealie/fixtures/about.json b/tests/components/mealie/fixtures/about.json index 86f74ec66d61df..1ffac4bdd5a37e 100644 --- a/tests/components/mealie/fixtures/about.json +++ b/tests/components/mealie/fixtures/about.json @@ -1,3 +1,3 @@ { - "version": "v1.10.2" + "version": "v2.0.0" } diff --git a/tests/components/mealie/snapshots/test_diagnostics.ambr b/tests/components/mealie/snapshots/test_diagnostics.ambr index c569ad8e5892fd..94d5ecdeaaade2 100644 --- a/tests/components/mealie/snapshots/test_diagnostics.ambr +++ b/tests/components/mealie/snapshots/test_diagnostics.ambr @@ -2,7 +2,7 @@ # name: test_entry_diagnostics dict({ 'about': dict({ - 'version': 'v1.10.2', + 'version': 'v2.0.0', }), 'mealplans': dict({ 'breakfast': list([ diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr index 50da06ca00595e..18824686ababc9 100644 --- a/tests/components/mealie/snapshots/test_init.ambr +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -26,7 +26,7 @@ 'name_by_user': None, 'primary_config_entry': , 'serial_number': None, - 'sw_version': 'v1.10.2', + 'sw_version': 'v2.0.0', 'via_device_id': None, }) # --- diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index f86818a933f0d6..d4ff9ec8e73f5e 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -126,6 +126,8 @@ async def test_ingress_host( ("v1.0.0beta-5"), ("v1.0.0-RC2"), ("v0.1.0"), + ("v1.9.0"), + ("v2.0.0beta-2"), ], ) async def test_flow_version_error( From a04835629b174b524f4b40f77b014666fa02b64e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 5 Oct 2025 23:13:33 +0200 Subject: [PATCH 15/20] Make hassfest fail on services with device filter on targets (#152794) --- script/hassfest/services.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index b47fa90d8bbbe5..723a9ec927803a 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -118,8 +118,20 @@ def _service_schema(targeted: bool, custom: bool) -> vol.Schema: ) } + def raise_on_target_device_filter(value: dict[str, Any]) -> dict[str, Any]: + """Raise error if target has a device filter.""" + if "device" in value: + raise vol.Invalid( + "Services do not support device filters on target, use a device " + "selector instead" + ) + return value + if targeted: - schema_dict[vol.Required("target")] = selector.TargetSelector.CONFIG_SCHEMA + schema_dict[vol.Required("target")] = vol.All( + selector.TargetSelector.CONFIG_SCHEMA, + raise_on_target_device_filter, + ) if custom: schema_dict |= CUSTOM_INTEGRATION_EXTRA_SCHEMA_DICT From 7f931e4d70355488801387779f19e754b16a2406 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 5 Oct 2025 23:14:12 +0200 Subject: [PATCH 16/20] Add device class filter to hydrawise services (#153249) --- .../components/hydrawise/binary_sensor.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/binary_sensor.py b/homeassistant/components/hydrawise/binary_sensor.py index f2177d2144a9fc..b26255db3fafa5 100644 --- a/homeassistant/components/hydrawise/binary_sensor.py +++ b/homeassistant/components/hydrawise/binary_sensor.py @@ -122,11 +122,24 @@ def _add_new_zones(zones: Iterable[tuple[Zone, Controller]]) -> None: coordinators.main.new_zones_callbacks.append(_add_new_zones) platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_RESUME, None, "resume") platform.async_register_entity_service( - SERVICE_START_WATERING, SCHEMA_START_WATERING, "start_watering" + SERVICE_RESUME, + None, + "resume", + entity_device_classes=(BinarySensorDeviceClass.RUNNING,), + ) + platform.async_register_entity_service( + SERVICE_START_WATERING, + SCHEMA_START_WATERING, + "start_watering", + entity_device_classes=(BinarySensorDeviceClass.RUNNING,), + ) + platform.async_register_entity_service( + SERVICE_SUSPEND, + SCHEMA_SUSPEND, + "suspend", + entity_device_classes=(BinarySensorDeviceClass.RUNNING,), ) - platform.async_register_entity_service(SERVICE_SUSPEND, SCHEMA_SUSPEND, "suspend") class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity): From fad0e237974e17a0bb8a97effc50df81866c92f9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 5 Oct 2025 23:15:00 +0200 Subject: [PATCH 17/20] Allow to set the manufacturer in a MQTT device subentry setup (#153747) --- homeassistant/components/mqtt/config_flow.py | 2 ++ homeassistant/components/mqtt/strings.json | 6 ++++-- tests/components/mqtt/common.py | 1 + tests/components/mqtt/test_config_flow.py | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 26b6cd7cd45714..d115c13d0e77a5 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -66,6 +66,7 @@ from homeassistant.const import ( ATTR_CONFIGURATION_URL, ATTR_HW_VERSION, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_MODEL_ID, ATTR_NAME, @@ -3050,6 +3051,7 @@ class PlatformField: ), ATTR_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False), ATTR_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False), + ATTR_MANUFACTURER: PlatformField(selector=TEXT_SELECTOR, required=False), ATTR_CONFIGURATION_URL: PlatformField( selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url" ), diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 1f3892fb927de2..49449c2f52d375 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -165,13 +165,15 @@ "name": "[%key:common::config_flow::data::name%]", "configuration_url": "Configuration URL", "model": "Model", - "model_id": "Model ID" + "model_id": "Model ID", + "manufacturer": "Manufacturer" }, "data_description": { "name": "The name of the manually added MQTT device.", "configuration_url": "A link to the webpage that can manage the configuration of this device. Can be either a 'http://', 'https://' or an internal 'homeassistant://' URL.", "model": "E.g. 'Cleanmaster Pro'.", - "model_id": "E.g. '123NK2PRO'." + "model_id": "E.g. '123NK2PRO'.", + "manufacturer": "E.g. Cleanmaster Ltd." }, "sections": { "advanced_settings": { diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index af488fa613a951..a45ea4c0648c49 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -526,6 +526,7 @@ "hw_version": "2.1 rev a", "model": "Model XL", "model_id": "mn002", + "manufacturer": "Milk Masters", "configuration_url": "https://example.com", } diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index b361b0b595b1cb..e94e842b7c3098 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -4720,6 +4720,7 @@ async def test_subentry_reconfigure_update_device_properties( "advanced_settings": {"sw_version": "1.1"}, "model": "Beer bottle XL", "model_id": "bn003", + "manufacturer": "Beer Masters", "configuration_url": "https://example.com", "mqtt_settings": {"qos": 1}, }, @@ -4742,6 +4743,7 @@ async def test_subentry_reconfigure_update_device_properties( assert device["model"] == "Beer bottle XL" assert device["model_id"] == "bn003" assert device["sw_version"] == "1.1" + assert device["manufacturer"] == "Beer Masters" assert device["mqtt_settings"]["qos"] == 1 assert "qos" not in device From 19f3559345b32e217c341ce0522f2548729bac68 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 5 Oct 2025 23:16:57 +0200 Subject: [PATCH 18/20] Remove previously deprecated template attach function (#153370) --- homeassistant/helpers/template/__init__.py | 25 ---------------------- 1 file changed, 25 deletions(-) diff --git a/homeassistant/helpers/template/__init__.py b/homeassistant/helpers/template/__init__.py index ed1e6151f2a1ce..34c3955dbdd785 100644 --- a/homeassistant/helpers/template/__init__.py +++ b/homeassistant/helpers/template/__init__.py @@ -64,11 +64,9 @@ label_registry as lr, location as loc_helper, ) -from homeassistant.helpers.deprecation import deprecated_function from homeassistant.helpers.singleton import singleton from homeassistant.helpers.translation import async_translate_state from homeassistant.helpers.typing import TemplateVarsType -from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as location_util from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.hass_dict import HassKey @@ -198,29 +196,6 @@ def _async_adjust_lru_sizes(_: Any) -> None: return True -@bind_hass -@deprecated_function( - "automatic setting of Template.hass introduced by HA Core PR #89242", - breaks_in_ha_version="2025.10", -) -def attach(hass: HomeAssistant, obj: Any) -> None: - """Recursively attach hass to all template instances in list and dict.""" - return _attach(hass, obj) - - -def _attach(hass: HomeAssistant, obj: Any) -> None: - """Recursively attach hass to all template instances in list and dict.""" - if isinstance(obj, list): - for child in obj: - _attach(hass, child) - elif isinstance(obj, collections.abc.Mapping): - for child_key, child_value in obj.items(): - _attach(hass, child_key) - _attach(hass, child_value) - elif isinstance(obj, Template): - obj.hass = hass - - def render_complex( value: Any, variables: TemplateVarsType = None, From bc3fe7a18eea080402dc2658903898b62ca65a29 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 5 Oct 2025 23:17:37 +0200 Subject: [PATCH 19/20] Use automatic reload options flow in min_max (#153143) --- homeassistant/components/min_max/__init__.py | 7 ------- homeassistant/components/min_max/config_flow.py | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/min_max/__init__.py b/homeassistant/components/min_max/__init__.py index a027a029ec2fe7..9090de908fb5da 100644 --- a/homeassistant/components/min_max/__init__.py +++ b/homeassistant/components/min_max/__init__.py @@ -11,16 +11,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Min/Max from a config entry.""" await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) - return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index 36133f7394d312..2b7b38beb46b99 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -71,6 +71,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" From 9a9fd44c62a3e23f66d7b9d5e95c218f66fbbab1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 5 Oct 2025 23:21:38 +0200 Subject: [PATCH 20/20] Use yaml anchors in ci workflow (#152586) --- .github/workflows/ci.yaml | 710 +++++++++++--------------------------- 1 file changed, 206 insertions(+), 504 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1bfa93eed5d7c9..81a04bfb4c680c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -75,6 +75,7 @@ concurrency: jobs: info: name: Collect information & changes data + runs-on: &runs-on-ubuntu ubuntu-24.04 outputs: # In case of issues with the partial run, use the following line instead: # test_full_suite: 'true' @@ -95,9 +96,9 @@ jobs: tests: ${{ steps.info.outputs.tests }} lint_only: ${{ steps.info.outputs.lint_only }} skip_coverage: ${{ steps.info.outputs.skip_coverage }} - runs-on: ubuntu-24.04 steps: - - name: Check out code from GitHub + - &checkout + name: Check out code from GitHub uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Generate partial Python venv restore key id: generate_python_cache_key @@ -245,28 +246,27 @@ jobs: pre-commit: name: Prepare pre-commit base - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu + needs: [info] if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' && github.event.inputs.audit-licenses-only != 'true' - needs: - - info steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} + - *checkout + - &setup-python-default + name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: &actions-setup-python actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: &actions-cache actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: venv - key: >- + key: &key-pre-commit-venv >- ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Create Python virtual environment @@ -279,11 +279,11 @@ jobs: uv pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: *actions-cache with: path: ${{ env.PRE_COMMIT_CACHE }} lookup-only: true - key: >- + key: &key-pre-commit-env >- ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.pre-commit_cache_key }} - name: Install pre-commit dependencies @@ -294,37 +294,29 @@ jobs: lint-ruff-format: name: Check ruff-format - runs-on: ubuntu-24.04 - needs: + runs-on: *runs-on-ubuntu + needs: &needs-pre-commit - info - pre-commit steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true - - name: Restore base Python virtual environment + - *checkout + - *setup-python-default + - &cache-restore-pre-commit-venv + name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: &actions-cache-restore actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: venv fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{ - needs.info.outputs.pre-commit_cache_key }} - - name: Restore pre-commit environment from cache + key: *key-pre-commit-venv + - &cache-restore-pre-commit-env + name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: *actions-cache-restore with: path: ${{ env.PRE_COMMIT_CACHE }} fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.pre-commit_cache_key }} + key: *key-pre-commit-env - name: Run ruff-format run: | . venv/bin/activate @@ -334,37 +326,13 @@ jobs: lint-ruff: name: Check ruff - runs-on: ubuntu-24.04 - needs: - - info - - pre-commit + runs-on: *runs-on-ubuntu + needs: *needs-pre-commit steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{ - needs.info.outputs.pre-commit_cache_key }} - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.pre-commit_cache_key }} + - *checkout + - *setup-python-default + - *cache-restore-pre-commit-venv + - *cache-restore-pre-commit-env - name: Run ruff run: | . venv/bin/activate @@ -374,37 +342,13 @@ jobs: lint-other: name: Check other linters - runs-on: ubuntu-24.04 - needs: - - info - - pre-commit + runs-on: *runs-on-ubuntu + needs: *needs-pre-commit steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{ - needs.info.outputs.pre-commit_cache_key }} - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ${{ env.PRE_COMMIT_CACHE }} - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.pre-commit_cache_key }} + - *checkout + - *setup-python-default + - *cache-restore-pre-commit-venv + - *cache-restore-pre-commit-env - name: Register yamllint problem matcher run: | @@ -454,9 +398,8 @@ jobs: lint-hadolint: name: Check ${{ matrix.file }} - runs-on: ubuntu-24.04 - needs: - - info + runs-on: *runs-on-ubuntu + needs: [info] if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' @@ -469,8 +412,7 @@ jobs: - Dockerfile.dev - script/hassfest/docker/Dockerfile steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - *checkout - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" @@ -481,18 +423,18 @@ jobs: base: name: Prepare dependencies - runs-on: ubuntu-24.04 - needs: info + runs-on: *runs-on-ubuntu + needs: [info] timeout-minutes: 60 strategy: matrix: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ matrix.python-version }} + - *checkout + - &setup-python-matrix + name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + uses: *actions-setup-python with: python-version: ${{ matrix.python-version }} check-latest: true @@ -505,15 +447,15 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: *actions-cache with: path: venv - key: >- + key: &key-python-venv >- ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ needs.info.outputs.python_cache_key }} - name: Restore uv wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: *actions-cache with: path: ${{ env.UV_CACHE_DIR }} key: >- @@ -525,13 +467,13 @@ jobs: env.HA_SHORT_VERSION }}- - name: Check if apt cache exists id: cache-apt-check - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: *actions-cache with: lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }} - path: | + path: &path-apt-cache | ${{ env.APT_CACHE_DIR }} ${{ env.APT_LIST_CACHE_DIR }} - key: >- + key: &key-apt-cache >- ${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }} - name: Install additional OS dependencies if: | @@ -570,13 +512,12 @@ jobs: fi - name: Save apt cache if: steps.cache-apt-check.outputs.cache-hit != 'true' - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | ${{ env.APT_CACHE_DIR }} ${{ env.APT_LIST_CACHE_DIR }} - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }} + key: *key-apt-cache - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -596,7 +537,7 @@ jobs: python --version uv pip freeze >> pip_freeze.txt - name: Upload pip_freeze artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: &actions-upload-artifact actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: pip-freeze-${{ matrix.python-version }} path: pip_freeze.txt @@ -606,30 +547,29 @@ jobs: - name: Remove generated requirements_all if: steps.cache-venv.outputs.cache-hit != 'true' run: rm requirements_all_pytest.txt requirements_all_wheels_*.txt - - name: Check dirty + - &check-dirty + name: Check dirty run: | ./script/check_dirty hassfest: name: Check hassfest - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu + needs: &needs-base + - info + - base if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' && github.event.inputs.audit-licenses-only != 'true' - needs: - - info - - base steps: - - name: Restore apt cache - uses: actions/cache/restore@v4.3.0 + - &cache-restore-apt + name: Restore apt cache + uses: *actions-cache-restore with: - path: | - ${{ env.APT_CACHE_DIR }} - ${{ env.APT_LIST_CACHE_DIR }} + path: *path-apt-cache fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }} + key: *key-apt-cache - name: Install additional OS dependencies timeout-minutes: 10 run: | @@ -641,23 +581,16 @@ jobs: -o Dir::Cache=${{ env.APT_CACHE_DIR }} \ -o Dir::State::Lists=${{ env.APT_LIST_CACHE_DIR }} \ libturbojpeg - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true - - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment + - *checkout + - *setup-python-default + - &cache-restore-python-default + name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: *actions-cache-restore with: path: venv fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} + key: *key-python-venv - name: Run hassfest run: | . venv/bin/activate @@ -665,32 +598,16 @@ jobs: gen-requirements-all: name: Check all requirements - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu + needs: *needs-base if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' && github.event.inputs.audit-licenses-only != 'true' - needs: - - info - - base steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} + - *checkout + - *setup-python-default + - *cache-restore-python-default - name: Run gen_requirements_all.py run: | . venv/bin/activate @@ -698,18 +615,15 @@ jobs: dependency-review: name: Dependency review - runs-on: ubuntu-24.04 - needs: - - info - - base + runs-on: *runs-on-ubuntu + needs: *needs-base if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' && needs.info.outputs.requirements == 'true' && github.event_name == 'pull_request' steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - *checkout - name: Dependency review uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0 with: @@ -717,10 +631,8 @@ jobs: audit-licenses: name: Audit licenses - runs-on: ubuntu-24.04 - needs: - - info - - base + runs-on: *runs-on-ubuntu + needs: *needs-base if: | (github.event.inputs.pylint-only != 'true' && github.event.inputs.mypy-only != 'true' @@ -731,29 +643,22 @@ jobs: matrix: python-version: ${{ fromJson(needs.info.outputs.python_versions) }} steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ matrix.python-version }} - check-latest: true - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - *checkout + - *setup-python-matrix + - &cache-restore-python-matrix + name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: *actions-cache-restore with: path: venv fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} + key: *key-python-venv - name: Extract license data run: | . venv/bin/activate python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json - name: Upload licenses - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: licenses-${{ github.run_number }}-${{ matrix.python-version }} path: licenses-${{ matrix.python-version }}.json @@ -764,34 +669,19 @@ jobs: pylint: name: Check pylint - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu + needs: *needs-base timeout-minutes: 20 if: | github.event.inputs.mypy-only != 'true' && github.event.inputs.audit-licenses-only != 'true' || github.event.inputs.pylint-only == 'true' - needs: - - info - - base steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true - - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} - - name: Register pylint problem matcher + - *checkout + - *setup-python-default + - *cache-restore-python-default + - &problem-matcher-pylint + name: Register pylint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/pylint.json" - name: Run pylint (fully) @@ -810,37 +700,19 @@ jobs: pylint-tests: name: Check pylint on tests - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu + needs: *needs-base timeout-minutes: 20 if: | (github.event.inputs.mypy-only != 'true' && github.event.inputs.audit-licenses-only != 'true' || github.event.inputs.pylint-only == 'true') && (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true') - needs: - - info - - base steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true - - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} - - name: Register pylint problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/pylint.json" + - *checkout + - *setup-python-default + - *cache-restore-python-default + - *problem-matcher-pylint - name: Run pylint (fully) if: needs.info.outputs.test_full_suite == 'true' run: | @@ -857,23 +729,15 @@ jobs: mypy: name: Check mypy - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu + needs: *needs-base if: | github.event.inputs.pylint-only != 'true' && github.event.inputs.audit-licenses-only != 'true' || github.event.inputs.mypy-only == 'true' - needs: - - info - - base steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true + - *checkout + - *setup-python-default - name: Generate partial mypy restore key id: generate-mypy-key run: | @@ -881,17 +745,9 @@ jobs: echo "version=$mypy_version" >> $GITHUB_OUTPUT echo "key=mypy-${{ env.MYPY_CACHE_VERSION }}-$mypy_version-${{ env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} + - *cache-restore-python-default - name: Restore mypy cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + uses: *actions-cache with: path: .mypy_cache key: >- @@ -919,7 +775,8 @@ jobs: mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} prepare-pytest-full: - runs-on: ubuntu-24.04 + name: Split tests for full run + runs-on: *runs-on-ubuntu if: | needs.info.outputs.lint_only != 'true' && needs.info.outputs.test_full_suite == 'true' @@ -932,17 +789,8 @@ jobs: - lint-ruff - lint-ruff-format - mypy - name: Split tests for full run steps: - - name: Restore apt cache - uses: actions/cache/restore@v4.3.0 - with: - path: | - ${{ env.APT_CACHE_DIR }} - ${{ env.APT_LIST_CACHE_DIR }} - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }} + - *cache-restore-apt - name: Install additional OS dependencies timeout-minutes: 10 run: | @@ -957,39 +805,23 @@ jobs: ffmpeg \ libturbojpeg \ libgammu-dev - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - check-latest: true - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} + - *checkout + - *setup-python-default + - *cache-restore-python-default - name: Run split_tests.py run: | . venv/bin/activate python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests - name: Upload pytest_buckets - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: pytest_buckets path: pytest_buckets.txt overwrite: true pytest-full: - runs-on: ubuntu-24.04 - if: | - needs.info.outputs.lint_only != 'true' - && needs.info.outputs.test_full_suite == 'true' + name: Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) + runs-on: *runs-on-ubuntu needs: - info - base @@ -1000,23 +832,16 @@ jobs: - lint-ruff-format - mypy - prepare-pytest-full + if: | + needs.info.outputs.lint_only != 'true' + && needs.info.outputs.test_full_suite == 'true' strategy: fail-fast: false matrix: - group: ${{ fromJson(needs.info.outputs.test_groups) }} python-version: ${{ fromJson(needs.info.outputs.python_versions) }} - name: >- - Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) + group: ${{ fromJson(needs.info.outputs.test_groups) }} steps: - - name: Restore apt cache - uses: actions/cache/restore@v4.3.0 - with: - path: | - ${{ env.APT_CACHE_DIR }} - ${{ env.APT_LIST_CACHE_DIR }} - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }} + - *cache-restore-apt - name: Install additional OS dependencies timeout-minutes: 10 run: | @@ -1032,34 +857,23 @@ jobs: libturbojpeg \ libgammu-dev \ libxml2-utils - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ matrix.python-version }} - check-latest: true - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} - - name: Register Python problem matcher + - *checkout + - *setup-python-matrix + - *cache-restore-python-matrix + - &problem-matcher-python + name: Register Python problem matcher run: | echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Register pytest slow test problem matcher + - &problem-matcher-pytest-slow + name: Register pytest slow test problem matcher run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Download pytest_buckets - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: &actions-download-artifact actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: pytest_buckets - - name: Compile English translations + - &compile-english-translations + name: Compile English translations run: | . venv/bin/activate python3 -m script.translations develop --all @@ -1095,19 +909,20 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-full.conclusion == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml overwrite: true - - name: Beautify test results + - &beautify-test-results + name: Beautify test results # For easier identification of parsing errors if: needs.info.outputs.skip_coverage != 'true' run: | @@ -1115,18 +930,17 @@ jobs: mv "junit.xml-tmp" "junit.xml" - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: test-results-full-${{ matrix.python-version }}-${{ matrix.group }} path: junit.xml - name: Remove pytest_buckets run: rm pytest_buckets.txt - - name: Check dirty - run: | - ./script/check_dirty + - *check-dirty pytest-mariadb: - runs-on: ubuntu-24.04 + name: Run ${{ matrix.mariadb-group }} tests Python ${{ matrix.python-version }} + runs-on: *runs-on-ubuntu services: mariadb: image: ${{ matrix.mariadb-group }} @@ -1135,9 +949,6 @@ jobs: env: MYSQL_ROOT_PASSWORD: password options: --health-cmd="mysqladmin ping -uroot -ppassword" --health-interval=5s --health-timeout=2s --health-retries=3 - if: | - needs.info.outputs.lint_only != 'true' - && needs.info.outputs.mariadb_groups != '[]' needs: - info - base @@ -1147,23 +958,16 @@ jobs: - lint-ruff - lint-ruff-format - mypy + if: | + needs.info.outputs.lint_only != 'true' + && needs.info.outputs.mariadb_groups != '[]' strategy: fail-fast: false matrix: python-version: ${{ fromJson(needs.info.outputs.python_versions) }} mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }} - name: >- - Run ${{ matrix.mariadb-group }} tests Python ${{ matrix.python-version }} steps: - - name: Restore apt cache - uses: actions/cache/restore@v4.3.0 - with: - path: | - ${{ env.APT_CACHE_DIR }} - ${{ env.APT_LIST_CACHE_DIR }} - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }} + - *cache-restore-apt - name: Install additional OS dependencies timeout-minutes: 10 run: | @@ -1179,37 +983,16 @@ jobs: libturbojpeg \ libmariadb-dev-compat \ libxml2-utils - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ matrix.python-version }} - check-latest: true - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} - - name: Register Python problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Register pytest slow test problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" + - *checkout + - *setup-python-matrix + - *cache-restore-python-matrix + - *problem-matcher-python + - *problem-matcher-pytest-slow - name: Install SQL Python libraries run: | . venv/bin/activate uv pip install mysqlclient sqlalchemy_utils - - name: Compile English translations - run: | - . venv/bin/activate - python3 -m script.translations develop --all + - *compile-english-translations - name: Run pytest (partially) timeout-minutes: 20 id: pytest-partial @@ -1248,7 +1031,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} @@ -1256,31 +1039,25 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} path: coverage.xml overwrite: true - - name: Beautify test results - # For easier identification of parsing errors - if: needs.info.outputs.skip_coverage != 'true' - run: | - xmllint --format "junit.xml" > "junit.xml-tmp" - mv "junit.xml-tmp" "junit.xml" + - *beautify-test-results - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: test-results-mariadb-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.mariadb }} path: junit.xml - - name: Check dirty - run: | - ./script/check_dirty + - *check-dirty pytest-postgres: - runs-on: ubuntu-24.04 + name: Run ${{ matrix.postgresql-group }} tests Python ${{ matrix.python-version }} + runs-on: *runs-on-ubuntu services: postgres: image: ${{ matrix.postgresql-group }} @@ -1289,9 +1066,6 @@ jobs: env: POSTGRES_PASSWORD: password options: --health-cmd="pg_isready -hlocalhost -Upostgres" --health-interval=5s --health-timeout=2s --health-retries=3 - if: | - needs.info.outputs.lint_only != 'true' - && needs.info.outputs.postgresql_groups != '[]' needs: - info - base @@ -1301,23 +1075,16 @@ jobs: - lint-ruff - lint-ruff-format - mypy + if: | + needs.info.outputs.lint_only != 'true' + && needs.info.outputs.postgresql_groups != '[]' strategy: fail-fast: false matrix: python-version: ${{ fromJson(needs.info.outputs.python_versions) }} postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }} - name: >- - Run ${{ matrix.postgresql-group }} tests Python ${{ matrix.python-version }} steps: - - name: Restore apt cache - uses: actions/cache/restore@v4.3.0 - with: - path: | - ${{ env.APT_CACHE_DIR }} - ${{ env.APT_LIST_CACHE_DIR }} - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }} + - *cache-restore-apt - name: Install additional OS dependencies timeout-minutes: 10 run: | @@ -1335,37 +1102,16 @@ jobs: sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y sudo apt-get -y install \ postgresql-server-dev-14 - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ matrix.python-version }} - check-latest: true - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} - - name: Register Python problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Register pytest slow test problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" + - *checkout + - *setup-python-matrix + - *cache-restore-python-matrix + - *problem-matcher-python + - *problem-matcher-pytest-slow - name: Install SQL Python libraries run: | . venv/bin/activate uv pip install psycopg2 sqlalchemy_utils - - name: Compile English translations - run: | - . venv/bin/activate - python3 -m script.translations develop --all + - *compile-english-translations - name: Run pytest (partially) timeout-minutes: 20 id: pytest-partial @@ -1405,7 +1151,7 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} @@ -1413,44 +1159,36 @@ jobs: overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: coverage-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} path: coverage.xml overwrite: true - - name: Beautify test results - # For easier identification of parsing errors - if: needs.info.outputs.skip_coverage != 'true' - run: | - xmllint --format "junit.xml" > "junit.xml-tmp" - mv "junit.xml-tmp" "junit.xml" + - *beautify-test-results - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: test-results-postgres-${{ matrix.python-version }}-${{ steps.pytest-partial.outputs.postgresql }} path: junit.xml - - name: Check dirty - run: | - ./script/check_dirty + - *check-dirty coverage-full: name: Upload test coverage to Codecov (full suite) - if: needs.info.outputs.skip_coverage != 'true' - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu needs: - info - pytest-full - pytest-postgres - pytest-mariadb timeout-minutes: 10 + if: needs.info.outputs.skip_coverage != 'true' steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - *checkout - name: Download all coverage artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: *actions-download-artifact with: pattern: coverage-* - name: Upload coverage to Codecov @@ -1462,11 +1200,8 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} pytest-partial: - runs-on: ubuntu-24.04 - if: | - needs.info.outputs.lint_only != 'true' - && needs.info.outputs.tests_glob - && needs.info.outputs.test_full_suite == 'false' + name: Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) + runs-on: *runs-on-ubuntu needs: - info - base @@ -1476,23 +1211,17 @@ jobs: - lint-ruff - lint-ruff-format - mypy + if: | + needs.info.outputs.lint_only != 'true' + && needs.info.outputs.tests_glob + && needs.info.outputs.test_full_suite == 'false' strategy: fail-fast: false matrix: - group: ${{ fromJson(needs.info.outputs.test_groups) }} python-version: ${{ fromJson(needs.info.outputs.python_versions) }} - name: >- - Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) + group: ${{ fromJson(needs.info.outputs.test_groups) }} steps: - - name: Restore apt cache - uses: actions/cache/restore@v4.3.0 - with: - path: | - ${{ env.APT_CACHE_DIR }} - ${{ env.APT_LIST_CACHE_DIR }} - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }} + - *cache-restore-apt - name: Install additional OS dependencies timeout-minutes: 10 run: | @@ -1508,33 +1237,12 @@ jobs: libturbojpeg \ libgammu-dev \ libxml2-utils - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 - with: - python-version: ${{ matrix.python-version }} - check-latest: true - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: venv - fail-on-cache-miss: true - key: >- - ${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{ - needs.info.outputs.python_cache_key }} - - name: Register Python problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Register pytest slow test problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - - name: Compile English translations - run: | - . venv/bin/activate - python3 -m script.translations develop --all + - *checkout + - *setup-python-matrix + - *cache-restore-python-matrix + - *problem-matcher-python + - *problem-matcher-pytest-slow + - *compile-english-translations - name: Run pytest timeout-minutes: 10 id: pytest-partial @@ -1574,47 +1282,39 @@ jobs: 2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt - name: Upload pytest output if: success() || failure() && steps.pytest-partial.conclusion == 'failure' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }} path: pytest-*.txt overwrite: true - name: Upload coverage artifact if: needs.info.outputs.skip_coverage != 'true' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml overwrite: true - - name: Beautify test results - # For easier identification of parsing errors - if: needs.info.outputs.skip_coverage != 'true' - run: | - xmllint --format "junit.xml" > "junit.xml-tmp" - mv "junit.xml-tmp" "junit.xml" + - *beautify-test-results - name: Upload test results artifact if: needs.info.outputs.skip_coverage != 'true' && !cancelled() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: *actions-upload-artifact with: name: test-results-partial-${{ matrix.python-version }}-${{ matrix.group }} path: junit.xml - - name: Check dirty - run: | - ./script/check_dirty + - *check-dirty coverage-partial: name: Upload test coverage to Codecov (partial suite) if: needs.info.outputs.skip_coverage != 'true' - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu + timeout-minutes: 10 needs: - info - pytest-partial - timeout-minutes: 10 steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - *checkout - name: Download all coverage artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: *actions-download-artifact with: pattern: coverage-* - name: Upload coverage to Codecov @@ -1626,10 +1326,7 @@ jobs: upload-test-results: name: Upload test results to Codecov - # codecov/test-results-action currently doesn't support tokenless uploads - # therefore we can't run it on forks - if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) && needs.info.outputs.skip_coverage != 'true' && !cancelled() }} - runs-on: ubuntu-24.04 + runs-on: *runs-on-ubuntu needs: - info - pytest-partial @@ -1637,9 +1334,14 @@ jobs: - pytest-postgres - pytest-mariadb timeout-minutes: 10 + # codecov/test-results-action currently doesn't support tokenless uploads + # therefore we can't run it on forks + if: | + (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork) + && needs.info.outputs.skip_coverage != 'true' && !cancelled() steps: - name: Download all coverage artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: *actions-download-artifact with: pattern: test-results-* - name: Upload test results to Codecov