From 9fe5cfcd55a6b3bbb09e461514f2f225d142f825 Mon Sep 17 00:00:00 2001 From: soldier2008 <217476753+soldier2008@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:33:02 -0300 Subject: [PATCH 1/4] Avoid a naive datetime.now() in lg_netcast (#178255) --- homeassistant/components/lg_netcast/media_player.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index f523c277aa6e00..87d20a0318f808 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -1,7 +1,7 @@ """Support for LG TV running on NetCast 3 or 4.""" from collections import Counter -from datetime import datetime +import time from typing import TYPE_CHECKING, Any, override from pylgnetcast import LG_COMMAND, LgNetCastError @@ -223,9 +223,7 @@ def supported_features(self) -> MediaPlayerEntityFeature: @override def media_image_url(self): """URL for obtaining a screen capture.""" - return ( - f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}" # pylint: disable=home-assistant-enforce-naive-now - ) + return f"{self._client.url}data?target=screen_image&_={time.time()}" @override def turn_off(self) -> None: From d250c8577f84130336a2a94fcb3e2707a3a64959 Mon Sep 17 00:00:00 2001 From: Heig Gregorian Date: Sun, 23 Aug 2026 14:40:46 -0700 Subject: [PATCH 2/4] Handle empty effect before filtering reserved effects (#177496) --- homeassistant/components/nanoleaf/light.py | 5 ++-- tests/components/nanoleaf/test_light.py | 32 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index b24d0b5cd4d0bb..f8a03cb24f4df4 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -66,9 +66,8 @@ def effect(self) -> str | None: # The effects *Static* and *Dynamic* are not supported by Home Assistant. # These reserved effects are implicitly set and are not in the effect_list. # https://forum.nanoleaf.me/docs/openapi#_byoot0bams8f - return ( - None if self._nanoleaf.effect in RESERVED_EFFECTS else self._nanoleaf.effect - ) + effect = self._nanoleaf.effect + return None if not effect or effect in RESERVED_EFFECTS else effect @property @override diff --git a/tests/components/nanoleaf/test_light.py b/tests/components/nanoleaf/test_light.py index 3260c2e2609253..97d24a02c5a319 100644 --- a/tests/components/nanoleaf/test_light.py +++ b/tests/components/nanoleaf/test_light.py @@ -5,7 +5,11 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.light import ATTR_EFFECT_LIST, DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ( + ATTR_EFFECT, + ATTR_EFFECT_LIST, + DOMAIN as LIGHT_DOMAIN, +) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -66,3 +70,29 @@ async def test_turning_on_or_off_writes_state( "Nemo", "Something Else", ] + + +@pytest.mark.parametrize( + ("effect", "expected_effect"), + [ + ("", None), + ("*Solid*", None), + ("Rainbow", "Rainbow"), + ], +) +async def test_effect( + hass: HomeAssistant, + mock_nanoleaf: AsyncMock, + mock_config_entry: MockConfigEntry, + effect: str, + expected_effect: str | None, +) -> None: + """Test the current effect.""" + mock_nanoleaf.is_on = True + mock_nanoleaf.effect = effect + + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("light.nanoleaf") + assert state is not None + assert state.attributes.get(ATTR_EFFECT) == expected_effect From 1db381611fbe5f80cf8210d5ab36b73ef9ddeaaa Mon Sep 17 00:00:00 2001 From: soldier2008 <217476753+soldier2008@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:43:00 -0300 Subject: [PATCH 3/4] Avoid a naive datetime.now() in nice_go (#178248) --- homeassistant/components/nice_go/config_flow.py | 6 +++--- homeassistant/components/nice_go/coordinator.py | 6 +++--- tests/components/nice_go/conftest.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nice_go/config_flow.py b/homeassistant/components/nice_go/config_flow.py index 918ae42e4aee21..7e2555bd914b97 100644 --- a/homeassistant/components/nice_go/config_flow.py +++ b/homeassistant/components/nice_go/config_flow.py @@ -1,8 +1,8 @@ """Config flow for Nice G.O. integration.""" from collections.abc import Mapping -from datetime import datetime import logging +import time from typing import Any, override from nice_go import AuthFailedError, NiceGOApi @@ -59,7 +59,7 @@ async def async_step_user( CONF_EMAIL: user_input[CONF_EMAIL], CONF_PASSWORD: user_input[CONF_PASSWORD], CONF_REFRESH_TOKEN: refresh_token, - CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now + CONF_REFRESH_TOKEN_CREATION_TIME: time.time(), }, ) @@ -100,7 +100,7 @@ async def async_step_reauth_confirm( data={ **user_input, CONF_REFRESH_TOKEN: refresh_token, - CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now + CONF_REFRESH_TOKEN_CREATION_TIME: time.time(), }, unique_id=user_input[CONF_EMAIL], ) diff --git a/homeassistant/components/nice_go/coordinator.py b/homeassistant/components/nice_go/coordinator.py index 2903091620c926..5136ce97672859 100644 --- a/homeassistant/components/nice_go/coordinator.py +++ b/homeassistant/components/nice_go/coordinator.py @@ -3,9 +3,9 @@ import asyncio from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime import json import logging +import time from typing import TYPE_CHECKING, Any, override from nice_go import ( @@ -174,7 +174,7 @@ async def authenticate(self) -> None: self.refresh_token_creation_time + REFRESH_TOKEN_EXPIRY_TIME.total_seconds() ) try: - if datetime.now().timestamp() >= expiry_time: # pylint: disable=home-assistant-enforce-naive-now + if time.time() >= expiry_time: await self.update_refresh_token() else: await self.api.authenticate_refresh( @@ -205,7 +205,7 @@ async def update_refresh_token(self) -> None: data = { **self.config_entry.data, CONF_REFRESH_TOKEN: refresh_token, - CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now + CONF_REFRESH_TOKEN_CREATION_TIME: time.time(), } self.hass.config_entries.async_update_entry(self.config_entry, data=data) diff --git a/tests/components/nice_go/conftest.py b/tests/components/nice_go/conftest.py index 3bfc845df08c2d..2c29f5b90c7b1b 100644 --- a/tests/components/nice_go/conftest.py +++ b/tests/components/nice_go/conftest.py @@ -1,7 +1,7 @@ """Common fixtures for the Nice G.O. tests.""" from collections.abc import Generator -from datetime import datetime +import time from unittest.mock import AsyncMock, patch from nice_go import Barrier, BarrierState, ConnectionState @@ -74,7 +74,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password", CONF_REFRESH_TOKEN: "test-refresh-token", - CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(), # pylint: disable=home-assistant-enforce-naive-now + CONF_REFRESH_TOKEN_CREATION_TIME: time.time(), }, version=1, unique_id="test-email", From 24d099832a02a890e8d2e761232959b750dd5997 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 24 Aug 2026 00:10:22 +0200 Subject: [PATCH 4/4] Log Netatmo publisher availability changes only once (#176727) Co-authored-by: Ariel Ebersberger --- .../components/netatmo/coordinator.py | 17 +++++--- .../components/netatmo/quality_scale.yaml | 2 +- tests/components/netatmo/test_init.py | 43 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/coordinator.py b/homeassistant/components/netatmo/coordinator.py index 637ed38c6c6921..95ed48e3deca5b 100644 --- a/homeassistant/components/netatmo/coordinator.py +++ b/homeassistant/components/netatmo/coordinator.py @@ -140,6 +140,7 @@ class NetatmoPublisher: kwargs: dict available: bool = True error_count: int = 0 + unavailable_logged: bool = False class NetatmoDataHandler: @@ -262,11 +263,10 @@ async def handle_event(self, event: dict) -> None: async def async_fetch_data(self, signal_name: str) -> bool: """Fetch data and notify.""" self.poll_count += 1 + publisher = self.publisher[signal_name] has_error = False try: - await getattr(self.account, self.publisher[signal_name].method)( - **self.publisher[signal_name].kwargs - ) + await getattr(self.account, publisher.method)(**publisher.kwargs) except ( pyatmo.NoDeviceError, @@ -274,10 +274,17 @@ async def async_fetch_data(self, signal_name: str) -> bool: TimeoutError, aiohttp.ClientConnectorError, ) as err: - _LOGGER.debug(err) has_error = True + if not publisher.unavailable_logged: + _LOGGER.info("Error while fetching %s data: %s", signal_name, err) + publisher.unavailable_logged = True + else: + _LOGGER.debug(err) + else: + if publisher.unavailable_logged: + _LOGGER.info("Fetching %s data recovered", signal_name) + publisher.unavailable_logged = False - publisher = self.publisher[signal_name] if has_error: publisher.error_count += 1 else: diff --git a/homeassistant/components/netatmo/quality_scale.yaml b/homeassistant/components/netatmo/quality_scale.yaml index 98a46008ee602a..637fe541fa4d05 100644 --- a/homeassistant/components/netatmo/quality_scale.yaml +++ b/homeassistant/components/netatmo/quality_scale.yaml @@ -36,7 +36,7 @@ rules: docs-installation-parameters: todo entity-unavailable: done integration-owner: done - log-when-unavailable: todo + log-when-unavailable: done parallel-updates: done reauthentication-flow: done test-coverage: todo diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 83ba08c6edba9c..af3277d964c96b 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -5,6 +5,7 @@ from datetime import timedelta from functools import partial from itertools import pairwise +import logging from time import time from typing import Any from unittest.mock import AsyncMock, patch @@ -18,6 +19,7 @@ from homeassistant.components import cloud, webhook from homeassistant.components.netatmo import DOMAIN, coordinator +from homeassistant.components.netatmo.coordinator import ACCOUNT from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_WEBHOOK_ID, @@ -1197,3 +1199,44 @@ async def fake_post(*args: Any, **kwargs: Any): # scheduled update), the delay then doubles per consecutive error until the # patched cap of 600s is reached assert gaps == [180, 300, 600, 600] + + +async def test_log_when_unavailable( + hass: HomeAssistant, + config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that unavailability and recovery are each logged exactly once.""" + with ( + patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, + patch( + "homeassistant.components.netatmo.async_get_config_entry_implementation", + return_value=AsyncMock(), + ), + patch("homeassistant.components.netatmo.webhook.webhook_generate_url"), + ): + post_request = mock_auth.return_value.async_post_api_request + post_request.side_effect = partial(fake_post_request, hass) + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + data_handler = config_entry.runtime_data + + with caplog.at_level( + logging.INFO, logger="homeassistant.components.netatmo.coordinator" + ): + post_request.side_effect = pyatmo.ApiError("boom") + await data_handler.async_fetch_data(ACCOUNT) + await data_handler.async_fetch_data(ACCOUNT) + + assert caplog.text.count("Error while fetching") == 1 + + post_request.side_effect = partial(fake_post_request, hass) + await data_handler.async_fetch_data(ACCOUNT) + await data_handler.async_fetch_data(ACCOUNT) + + assert caplog.text.count("recovered") == 1