From d6cad546e10afd4b6f51cad8fc28893c4cc02931 Mon Sep 17 00:00:00 2001 From: Josef Zweck Date: Sat, 3 Jan 2026 17:12:53 +0100 Subject: [PATCH 1/3] Remove referral link from fish_audio (#160193) --- homeassistant/components/fish_audio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fish_audio/const.py b/homeassistant/components/fish_audio/const.py index 3861cd99fdbca..bbff953e3bf85 100644 --- a/homeassistant/components/fish_audio/const.py +++ b/homeassistant/components/fish_audio/const.py @@ -35,6 +35,6 @@ SORT_BY_OPTIONS = ["task_count", "score", "created_at"] LATENCY_OPTIONS = ["normal", "balanced"] -SIGNUP_URL = "https://fish.audio/?fpr=homeassistant" # codespell:ignore fpr +SIGNUP_URL = "https://fish.audio/" BILLING_URL = "https://fish.audio/app/billing/" API_KEYS_URL = "https://fish.audio/app/api-keys/" From 667b1db5942b6038f5ad3cc6d85c1d59f0da004b Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Sat, 3 Jan 2026 21:30:26 +0100 Subject: [PATCH 2/3] Bump python-bsblan dependency to version 3.1.6 (#160202) --- homeassistant/components/bsblan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 5e12600b4b168..4545e601719dc 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -7,7 +7,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["bsblan"], - "requirements": ["python-bsblan==3.1.4"], + "requirements": ["python-bsblan==3.1.6"], "zeroconf": [ { "name": "bsb-lan*", diff --git a/requirements_all.txt b/requirements_all.txt index 02be23368cdca..96dfc23fbf483 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2481,7 +2481,7 @@ python-awair==0.2.5 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==3.1.4 +python-bsblan==3.1.6 # homeassistant.components.citybikes python-citybikes==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3139f465fd57b..c2061936534d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2095,7 +2095,7 @@ python-MotionMount==2.3.0 python-awair==0.2.5 # homeassistant.components.bsblan -python-bsblan==3.1.4 +python-bsblan==3.1.6 # homeassistant.components.ecobee python-ecobee-api==0.3.2 From f1eaf78923bca88f69fe99f2d76a2ce879d26312 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Sat, 3 Jan 2026 21:32:07 +0100 Subject: [PATCH 3/3] Portainer polish ephemeral container ID (#160186) --- .../components/portainer/binary_sensor.py | 6 +- homeassistant/components/portainer/button.py | 4 +- .../components/portainer/coordinator.py | 81 ++++++++++--------- homeassistant/components/portainer/sensor.py | 22 +++-- homeassistant/components/portainer/switch.py | 10 ++- 5 files changed, 77 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/portainer/binary_sensor.py b/homeassistant/components/portainer/binary_sensor.py index 13f05c89ddbfc..6d38e0125481e 100644 --- a/homeassistant/components/portainer/binary_sensor.py +++ b/homeassistant/components/portainer/binary_sensor.py @@ -164,7 +164,11 @@ def __init__( @property def available(self) -> bool: """Return if the device is available.""" - return super().available and self.endpoint_id in self.coordinator.data + return ( + super().available + and self.endpoint_id in self.coordinator.data + and self.device_name in self.coordinator.data[self.endpoint_id].containers + ) @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/portainer/button.py b/homeassistant/components/portainer/button.py index 2cbf2dea9dfc9..e36fdfcaf9392 100644 --- a/homeassistant/components/portainer/button.py +++ b/homeassistant/components/portainer/button.py @@ -113,7 +113,9 @@ async def async_press(self) -> None: """Trigger the Portainer button press service.""" try: await self.entity_description.press_action( - self.coordinator.portainer, self.endpoint_id, self.device_id + self.coordinator.portainer, + self.endpoint_id, + self.container_data.container.id, ) except PortainerConnectionError as err: raise HomeAssistantError( diff --git a/homeassistant/components/portainer/coordinator.py b/homeassistant/components/portainer/coordinator.py index 9aff45f8afbb3..1684fd578a5cc 100644 --- a/homeassistant/components/portainer/coordinator.py +++ b/homeassistant/components/portainer/coordinator.py @@ -50,7 +50,7 @@ class PortainerContainerData: """Container data held by the Portainer coordinator.""" container: DockerContainer - stats: DockerContainerStats + stats: DockerContainerStats | None stats_pre: DockerContainerStats | None @@ -147,47 +147,52 @@ async def _async_update_data(self) -> dict[int, PortainerCoordinatorData]: docker_version = await self.portainer.docker_version(endpoint.id) docker_info = await self.portainer.docker_info(endpoint.id) + prev_endpoint = self.data.get(endpoint.id) if self.data else None container_map: dict[str, PortainerContainerData] = {} - container_stats_task = [ - ( - container, - self.portainer.container_stats( - endpoint_id=endpoint.id, - container_id=container.id, - ), + # Map containers, started and stopped + for container in containers: + container_name = self._get_container_name(container.names[0]) + prev_container = ( + prev_endpoint.containers[container_name] + if prev_endpoint + else None ) + container_map[container_name] = PortainerContainerData( + container=container, + stats=None, + stats_pre=prev_container.stats if prev_container else None, + ) + + # Separately fetch stats for running containers + running_containers = [ + container for container in containers if container.state == CONTAINER_STATE_RUNNING ] - - container_stats_gather = await asyncio.gather( - *[task for _, task in container_stats_task] - ) - for (container, _), container_stats in zip( - container_stats_task, container_stats_gather, strict=False - ): - container_name = container.names[0].replace("/", " ").strip() - - # Store previous stats if available. This is used to calculate deltas for CPU and network usage - # In the first call it will be None, since it has nothing to compare with - # Added a walrus pattern to check if not None on prev_container, to keep mypy happy. :) - container_map[container_name] = PortainerContainerData( - container=container, - stats=container_stats, - stats_pre=( - prev_container.stats - if self.data - and (prev_data := self.data.get(endpoint.id)) is not None - and ( - prev_container := prev_data.containers.get( - container_name + if running_containers: + container_stats = dict( + zip( + ( + self._get_container_name(container.names[0]) + for container in running_containers + ), + await asyncio.gather( + *( + self.portainer.container_stats( + endpoint_id=endpoint.id, + container_id=container.id, + ) + for container in running_containers ) - ) - is not None - else None - ), + ), + strict=False, + ) ) + + # Now assign stats to the containers + for container_name, stats in container_stats.items(): + container_map[container_name].stats = stats except PortainerConnectionError as err: _LOGGER.exception("Connection error") raise UpdateFailed( @@ -228,11 +233,15 @@ def _async_add_remove_endpoints( # Surprise, we also handle containers here :) current_containers = { - (endpoint.id, container.container.id) + (endpoint.id, container_name) for endpoint in mapped_endpoints.values() - for container in endpoint.containers.values() + for container_name in endpoint.containers } new_containers = current_containers - self.known_containers if new_containers: _LOGGER.debug("New containers found: %s", new_containers) self.known_containers.update(new_containers) + + def _get_container_name(self, container_name: str) -> str: + """Sanitize to get a proper container name.""" + return container_name.replace("/", " ").strip() diff --git a/homeassistant/components/portainer/sensor.py b/homeassistant/components/portainer/sensor.py index cc42eecf43541..7fa59912ee72d 100644 --- a/homeassistant/components/portainer/sensor.py +++ b/homeassistant/components/portainer/sensor.py @@ -59,7 +59,9 @@ class PortainerEndpointSensorEntityDescription(SensorEntityDescription): PortainerContainerSensorEntityDescription( key="memory_limit", translation_key="memory_limit", - value_fn=lambda data: data.stats.memory_stats.limit, + value_fn=lambda data: ( + data.stats.memory_stats.limit if data.stats is not None else 0 + ), device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.BYTES, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -70,7 +72,9 @@ class PortainerEndpointSensorEntityDescription(SensorEntityDescription): PortainerContainerSensorEntityDescription( key="memory_usage", translation_key="memory_usage", - value_fn=lambda data: data.stats.memory_stats.usage, + value_fn=lambda data: ( + data.stats.memory_stats.usage if data.stats is not None else 0 + ), device_class=SensorDeviceClass.DATA_SIZE, native_unit_of_measurement=UnitOfInformation.BYTES, suggested_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -83,7 +87,9 @@ class PortainerEndpointSensorEntityDescription(SensorEntityDescription): translation_key="memory_usage_percentage", value_fn=lambda data: ( (data.stats.memory_stats.usage / data.stats.memory_stats.limit) * 100.0 - if data.stats.memory_stats.limit > 0 and data.stats.memory_stats.usage > 0 + if data.stats is not None + and data.stats.memory_stats.limit > 0 + and data.stats.memory_stats.usage > 0 else 0.0 ), native_unit_of_measurement=PERCENTAGE, @@ -96,7 +102,8 @@ class PortainerEndpointSensorEntityDescription(SensorEntityDescription): translation_key="cpu_usage_total", value_fn=lambda data: ( (total_delta / system_delta) * data.stats.cpu_stats.online_cpus * 100.0 - if (prev := data.stats_pre) is not None + if data.stats is not None + and (prev := data.stats_pre) is not None and ( system_delta := ( data.stats.cpu_stats.system_cpu_usage @@ -254,7 +261,6 @@ def _async_add_new_containers( ) for (endpoint, container) in containers for entity_description in CONTAINER_SENSORS - if entity_description.value_fn(container) is not None ) coordinator.new_endpoints_callbacks.append(_async_add_new_endpoints) @@ -297,7 +303,11 @@ def __init__( @property def available(self) -> bool: """Return if the device is available.""" - return super().available and self.endpoint_id in self.coordinator.data + return ( + super().available + and self.endpoint_id in self.coordinator.data + and self.device_name in self.coordinator.data[self.endpoint_id].containers + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/portainer/switch.py b/homeassistant/components/portainer/switch.py index 66372032aaa6e..7920215968563 100644 --- a/homeassistant/components/portainer/switch.py +++ b/homeassistant/components/portainer/switch.py @@ -137,13 +137,19 @@ def is_on(self) -> bool | None: async def async_turn_on(self, **kwargs: Any) -> None: """Start (turn on) the container.""" await self.entity_description.turn_on_fn( - "start", self.coordinator.portainer, self.endpoint_id, self.device_id + "start", + self.coordinator.portainer, + self.endpoint_id, + self.container_data.container.id, ) await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Stop (turn off) the container.""" await self.entity_description.turn_off_fn( - "stop", self.coordinator.portainer, self.endpoint_id, self.device_id + "stop", + self.coordinator.portainer, + self.endpoint_id, + self.container_data.container.id, ) await self.coordinator.async_request_refresh()