From 715f682387db9c0743bdcef8616c7ecfe3a07d35 Mon Sep 17 00:00:00 2001 From: Paul Tarjan Date: Sun, 30 Aug 2026 23:13:49 -0600 Subject: [PATCH 01/10] Bump pyHik to 0.4.5 (#180885) Co-authored-by: Claude Opus 5 --- homeassistant/components/hikvision/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index fd508fcb53821b..b26a79e6b39a6a 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["pyhik"], - "requirements": ["pyHik==0.4.4"] + "requirements": ["pyHik==0.4.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6fd651aec216ee..8d779d3658c837 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2026,7 +2026,7 @@ pyElectra==1.2.4 pyEmby==1.10 # homeassistant.components.hikvision -pyHik==0.4.4 +pyHik==0.4.5 # homeassistant.components.homee pyHomee==1.4.4 From a3975e9047cb9dee611ba7c1e2304868af98d2b0 Mon Sep 17 00:00:00 2001 From: Paul Tarjan Date: Sun, 30 Aug 2026 23:15:23 -0600 Subject: [PATCH 02/10] Stop the Hikvision event stream on Home Assistant shutdown (#180873) Co-authored-by: Claude Opus 5 --- .../components/hikvision/__init__.py | 18 +++++++++- tests/components/hikvision/test_init.py | 34 +++++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hikvision/__init__.py b/homeassistant/components/hikvision/__init__.py index 2725c0dfba10f5..cc45552d5dc605 100644 --- a/homeassistant/components/hikvision/__init__.py +++ b/homeassistant/components/hikvision/__init__.py @@ -15,9 +15,10 @@ CONF_PORT, CONF_SSL, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -142,6 +143,21 @@ def fetch_and_inject_nvr_events() -> None: # Start the event stream await hass.async_add_executor_job(camera.start_stream) + async def _async_stop_stream(event: Event) -> None: + await hass.async_add_executor_job(camera.disconnect) + + # pyHik's stream thread is non-daemonic and publishes straight into hass, so + # it has to be joined before the event loop closes. Starting it yields, so + # the stop event may already have been fired by the time we get here, and + # listening for it now would never hear it. + if hass.is_stopping: + await hass.async_add_executor_job(camera.disconnect) + raise ConfigEntryNotReady("Home Assistant is stopping") + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_stream) + ) + # Register the main device before platforms that use via_device device_registry = dr.async_get(hass) device_registry.async_get_or_create( diff --git a/tests/components/hikvision/test_init.py b/tests/components/hikvision/test_init.py index 7508dac4923e6d..05f30c11e7a289 100644 --- a/tests/components/hikvision/test_init.py +++ b/tests/components/hikvision/test_init.py @@ -8,8 +8,8 @@ import requests from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_SSL -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_SSL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import CoreState, HomeAssistant from . import setup_integration from .conftest import TEST_HOST, TEST_PASSWORD, TEST_PORT, TEST_USERNAME @@ -40,6 +40,36 @@ async def test_setup_and_unload_entry( mock_hikcamera.return_value.disconnect.assert_called_once() +async def test_stream_stopped_on_shutdown( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hikcamera: MagicMock, +) -> None: + """Test the event stream thread is joined before the event loop closes.""" + await setup_integration(hass, mock_config_entry) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + mock_hikcamera.return_value.disconnect.assert_called_once() + + +async def test_stream_stopped_when_shutdown_starts_during_setup( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hikcamera: MagicMock, +) -> None: + """Test the stream is stopped when shutdown begins while setting up.""" + hass.set_state(CoreState.stopping) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + mock_hikcamera.return_value.disconnect.assert_called_once() + + async def test_setup_entry_with_ssl( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From 85ce797b12bbae8fd3e97d09f1be9f0975745c34 Mon Sep 17 00:00:00 2001 From: Linkplay2020 <65423368+Linkplay2020@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:25:35 +0800 Subject: [PATCH 03/10] Bump wiim to 0.1.7 (#180884) Co-authored-by: Tao Jiang --- homeassistant/components/wiim/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wiim/manifest.json b/homeassistant/components/wiim/manifest.json index 9ec4a419616405..5079d1d8491e2b 100644 --- a/homeassistant/components/wiim/manifest.json +++ b/homeassistant/components/wiim/manifest.json @@ -9,6 +9,6 @@ "iot_class": "local_push", "loggers": ["wiim.sdk", "async_upnp_client"], "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.48.1", "wiim==0.1.6"], + "requirements": ["async-upnp-client==0.48.1", "wiim==0.1.7"], "zeroconf": ["_linkplay._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8d779d3658c837..2906f3e72c4520 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3449,7 +3449,7 @@ whoisdomain==2.20260806.3 wiffi==1.1.2 # homeassistant.components.wiim -wiim==0.1.6 +wiim==0.1.7 # homeassistant.components.wirelesstag wirelesstagpy==0.8.1 From 1db0cf4997f6eb30f4eee1ab00c51d24f6884211 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 07:26:11 +0200 Subject: [PATCH 04/10] Update pnpm to 11.23.0 (#180882) --- tests/e2e/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 236a2c80aa1b06..9a8abca50c668f 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "End-to-end browser tests for Home Assistant Core", "private": true, - "packageManager": "pnpm@11.22.0", + "packageManager": "pnpm@11.23.0", "scripts": { "test": "playwright test" }, From 1100225b16eb32d1a93c1e41ddfebd927e6d83db Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 31 Aug 2026 07:26:32 +0200 Subject: [PATCH 05/10] Bump reolink_aio to 0.21.14 (#180870) --- homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 45c2f0a0910363..00b1514a4ed445 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -20,5 +20,5 @@ "iot_class": "local_push", "loggers": ["reolink_aio"], "quality_scale": "platinum", - "requirements": ["reolink-aio==0.21.13"] + "requirements": ["reolink-aio==0.21.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2906f3e72c4520..32e834e02cc37a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2962,7 +2962,7 @@ renault-api==0.5.13 renson-endura-delta==1.7.2 # homeassistant.components.reolink -reolink-aio==0.21.13 +reolink-aio==0.21.14 # homeassistant.components.radio_frequency rf-protocols==4.3.0 From 5d75f39cb4ec71b4e690ee9e6b785a7f1caa01bb Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 31 Aug 2026 09:16:12 +0200 Subject: [PATCH 06/10] Use core `varh` unit constant for Fronius (#180890) --- homeassistant/components/fronius/sensor.py | 9 ++++----- .../fronius/snapshots/test_sensor.ambr | 16 ++++++++-------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 66aad951f8468e..cb245f9d719414 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -2,7 +2,7 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Final, override +from typing import TYPE_CHECKING, Any, override from homeassistant.components.sensor import ( SensorDeviceClass, @@ -20,6 +20,7 @@ UnitOfEnergy, UnitOfFrequency, UnitOfPower, + UnitOfReactiveEnergy, UnitOfReactivePower, UnitOfTemperature, ) @@ -59,8 +60,6 @@ PARALLEL_UPDATES = 0 -ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh" - async def async_setup_entry( hass: HomeAssistant, @@ -405,14 +404,14 @@ def _modbus_mppt_descriptions( ), FroniusSensorEntityDescription( key="energy_reactive_ac_consumed", - native_unit_of_measurement=ENERGY_VOLT_AMPERE_REACTIVE_HOUR, + native_unit_of_measurement=UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, invalid_when_falsy=True, ), FroniusSensorEntityDescription( key="energy_reactive_ac_produced", - native_unit_of_measurement=ENERGY_VOLT_AMPERE_REACTIVE_HOUR, + native_unit_of_measurement=UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, invalid_when_falsy=True, diff --git a/tests/components/fronius/snapshots/test_sensor.ambr b/tests/components/fronius/snapshots/test_sensor.ambr index 4236f3f7bfccb1..916ed03c89b20d 100644 --- a/tests/components/fronius/snapshots/test_sensor.ambr +++ b/tests/components/fronius/snapshots/test_sensor.ambr @@ -1823,7 +1823,7 @@ 'supported_features': 0, 'translation_key': 'energy_reactive_ac_consumed', 'unique_id': '1234567890-energy_reactive_ac_consumed', - 'unit_of_measurement': 'varh', + 'unit_of_measurement': , }) # --- # name: test_gen24[sensor.smart_meter_ts_65a_3_reactive_energy_consumed-state] @@ -1831,7 +1831,7 @@ 'attributes': ReadOnlyDict({ : 'Smart Meter TS 65A-3 Reactive energy consumed', : , - : 'varh', + : , }), 'context': , 'entity_id': 'sensor.smart_meter_ts_65a_3_reactive_energy_consumed', @@ -1877,7 +1877,7 @@ 'supported_features': 0, 'translation_key': 'energy_reactive_ac_produced', 'unique_id': '1234567890-energy_reactive_ac_produced', - 'unit_of_measurement': 'varh', + 'unit_of_measurement': , }) # --- # name: test_gen24[sensor.smart_meter_ts_65a_3_reactive_energy_produced-state] @@ -1885,7 +1885,7 @@ 'attributes': ReadOnlyDict({ : 'Smart Meter TS 65A-3 Reactive energy produced', : , - : 'varh', + : , }), 'context': , 'entity_id': 'sensor.smart_meter_ts_65a_3_reactive_energy_produced', @@ -6008,7 +6008,7 @@ 'supported_features': 0, 'translation_key': 'energy_reactive_ac_consumed', 'unique_id': '1234567890-energy_reactive_ac_consumed', - 'unit_of_measurement': 'varh', + 'unit_of_measurement': , }) # --- # name: test_gen24_storage[sensor.smart_meter_ts_65a_3_reactive_energy_consumed-state] @@ -6016,7 +6016,7 @@ 'attributes': ReadOnlyDict({ : 'Smart Meter TS 65A-3 Reactive energy consumed', : , - : 'varh', + : , }), 'context': , 'entity_id': 'sensor.smart_meter_ts_65a_3_reactive_energy_consumed', @@ -6062,7 +6062,7 @@ 'supported_features': 0, 'translation_key': 'energy_reactive_ac_produced', 'unique_id': '1234567890-energy_reactive_ac_produced', - 'unit_of_measurement': 'varh', + 'unit_of_measurement': , }) # --- # name: test_gen24_storage[sensor.smart_meter_ts_65a_3_reactive_energy_produced-state] @@ -6070,7 +6070,7 @@ 'attributes': ReadOnlyDict({ : 'Smart Meter TS 65A-3 Reactive energy produced', : , - : 'varh', + : , }), 'context': , 'entity_id': 'sensor.smart_meter_ts_65a_3_reactive_energy_produced', From 3739bc729ee7ad1bbc7e14aad68e856b6ce340e0 Mon Sep 17 00:00:00 2001 From: minimicro34 Date: Mon, 31 Aug 2026 09:27:53 +0200 Subject: [PATCH 07/10] Add Tuya indicator light on translation (#180865) --- homeassistant/components/tuya/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index bcf19a3ba00342..71e756e57f4c37 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -502,6 +502,7 @@ "name": "Indicator light mode", "state": { "none": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]", "pos": "Indicate inverted switch state", "relay": "Indicate switch on/off state" } From dda620f4bbafe51cc1316f8876925fde534cd83d Mon Sep 17 00:00:00 2001 From: SvenC81 Date: Mon, 31 Aug 2026 09:29:42 +0200 Subject: [PATCH 08/10] Add support for Miele KM7740 and KM7899 (#180632) --- homeassistant/components/miele/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index efdcded1aad75a..1f4c9f3b2f1253 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -61,8 +61,10 @@ "KM7678": 6, "KM7697": 6, "KM7699": 5, + "KM7740": 5, "KM7878": 6, "KM7897": 6, + "KM7899": 5, "KMDA7633": 5, "KMDA7634": 5, "KMDA7774": 5, From 93cf9c8d8d001bcbb35798881500474ec75bd66f Mon Sep 17 00:00:00 2001 From: darkrain-nl <24763370+darkrain-nl@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:47:16 +0200 Subject: [PATCH 09/10] Remove the Sofar waiting-ends sensor (#180899) --- homeassistant/components/sofar/__init__.py | 11 ++ homeassistant/components/sofar/sensor.py | 62 +------- homeassistant/components/sofar/strings.json | 3 - .../sofar/snapshots/test_sensor.ambr | 51 ------- tests/components/sofar/test_init.py | 27 ++++ tests/components/sofar/test_sensor.py | 140 +----------------- 6 files changed, 45 insertions(+), 249 deletions(-) diff --git a/homeassistant/components/sofar/__init__.py b/homeassistant/components/sofar/__init__.py index b3740591935936..c232ba686ae4b2 100644 --- a/homeassistant/components/sofar/__init__.py +++ b/homeassistant/components/sofar/__init__.py @@ -37,6 +37,16 @@ _IDENTITY_ATTEMPTS = 3 +def _async_remove_stale_waiting_time(hass: HomeAssistant, serial: str) -> None: + """Drop the removed waiting-time entity so it doesn't linger unavailable.""" + registry = er.async_get(hass) + entity_id = registry.async_get_entity_id( + SENSOR_DOMAIN, DOMAIN, f"{serial}_waiting_time" + ) + if entity_id is not None: + registry.async_remove(entity_id) + + async def _async_read_identity(entry: SofarConfigEntry, device: SofarInverter) -> None: """Read identity once, retrying a few times against a transient blip.""" for attempt in range(_IDENTITY_ATTEMPTS): @@ -77,6 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SofarConfigEntry) -> boo """Set up Sofar Inverter Modbus from a config entry.""" serial = entry.unique_id assert serial is not None + _async_remove_stale_waiting_time(hass, serial) inverter_type, model = identify(serial) if not inverter_type: raise ConfigEntryError( diff --git a/homeassistant/components/sofar/sensor.py b/homeassistant/components/sofar/sensor.py index 01d78cedfb9a06..85912f8b55d876 100644 --- a/homeassistant/components/sofar/sensor.py +++ b/homeassistant/components/sofar/sensor.py @@ -1,13 +1,12 @@ """Support for Sofar sensors.""" -from collections.abc import Callable, Mapping +from collections.abc import Mapping from dataclasses import dataclass -from datetime import date, datetime, timedelta +from datetime import date from enum import IntEnum from typing import cast, override from sofar_modbus.modern.device import SofarInverter -from sofar_modbus.modern.enums import SystemState from homeassistant.components.sensor import ( RestoreSensor, @@ -31,27 +30,12 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.util import dt as dt_util -from homeassistant.util.variance import ignore_variance -# Aliased: a module-level SCAN_INTERVAL would set the platform's poll. -from .const import SCAN_INTERVAL as _POLL_INTERVAL -from .coordinator import SofarConfigEntry, SofarRuntimeData +from .coordinator import SofarConfigEntry from .entity import SofarEntity, SofarEntityDescription PARALLEL_UPDATES = 0 -# Two polls of slack, so jitter does not republish a steady countdown. -_COUNTDOWN_VARIANCE = timedelta(seconds=_POLL_INTERVAL * 2) - - -def _deadline_filter() -> Callable[[int], datetime]: - """Turn remaining seconds into a deadline, holding it steady.""" - return ignore_variance( - lambda seconds: dt_util.utcnow() + timedelta(seconds=seconds), - _COUNTDOWN_VARIANCE, - ) - async def async_setup_entry( hass: HomeAssistant, @@ -114,8 +98,6 @@ def _sensor_class( description: SofarSensorDescription, ) -> type[SofarSensor | SofarTotalSensor]: """Pick the entity class a description's semantics ask for.""" - if description.device_class is SensorDeviceClass.TIMESTAMP: - return SofarCountdownSensor if description.state_class in ( SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING, @@ -140,37 +122,6 @@ def native_value(self) -> str | int | float | date | None: return cast(str | int | float | date | None, value) -class SofarCountdownSensor(SofarSensor): - """Defines a Sofar countdown, published as the moment it runs out.""" - - # A positive register alone doesn't mean the countdown is active. - _ACTIVE_STATES = (SystemState.WAITING, SystemState.CHECKING) - - def __init__( - self, - runtime_data: SofarRuntimeData, - entity_description: SofarSensorDescription, - ) -> None: - """Initialize the entity.""" - super().__init__(runtime_data, entity_description) - self._deadline = _deadline_filter() - - @property - @override - def native_value(self) -> datetime | None: - component = getattr(self.coordinator.device, self.entity_description.component) - seconds = getattr(component, self.entity_description.key) - if ( - not isinstance(seconds, int) - or seconds <= 0 - or component.system_state not in self._ACTIVE_STATES - ): - # A restart must not land inside the finished countdown's slack. - self._deadline = _deadline_filter() - return None - return self._deadline(seconds) - - class SofarTotalSensor(SofarEntity, RestoreSensor): """Defines a Sofar cumulative total sensor.""" @@ -381,13 +332,6 @@ def _part_sensors( "self_charging", ], ), - SofarSensorDescription( - key="waiting_time", - component="state", - translation_key="waiting_ends", - device_class=SensorDeviceClass.TIMESTAMP, - entity_category=EntityCategory.DIAGNOSTIC, - ), SofarSensorDescription( key="inverter_temperature_1", component="state", diff --git a/homeassistant/components/sofar/strings.json b/homeassistant/components/sofar/strings.json index 06348487f51e59..c9be7334977bc4 100644 --- a/homeassistant/components/sofar/strings.json +++ b/homeassistant/components/sofar/strings.json @@ -512,9 +512,6 @@ }, "voltage_phase_l2n": { "name": "Voltage phase L2N" - }, - "waiting_ends": { - "name": "Waiting ends" } } }, diff --git a/tests/components/sofar/snapshots/test_sensor.ambr b/tests/components/sofar/snapshots/test_sensor.ambr index 5655ad95c02e72..3cbc58f261061d 100644 --- a/tests/components/sofar/snapshots/test_sensor.ambr +++ b/tests/components/sofar/snapshots/test_sensor.ambr @@ -7411,57 +7411,6 @@ 'state': '0.0', }) # --- -# name: test_all_entities[sensor.hydxxktl_3p_waiting_ends-entry] - EntityRegistryEntrySnapshot({ - 'aliases': list([ - None, - ]), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.hydxxktl_3p_waiting_ends', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'object_id_base': 'Waiting ends', - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Waiting ends', - 'platform': 'sofar', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'waiting_ends', - 'unique_id': 'SP1XXES100XX_waiting_time', - 'unit_of_measurement': None, - }) -# --- -# name: test_all_entities[sensor.hydxxktl_3p_waiting_ends-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - : 'timestamp', - : 'HYDxxKTL-3P Waiting ends', - }), - 'context': , - 'entity_id': 'sensor.hydxxktl_3p_waiting_ends', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- # name: test_all_entities[sensor.pv_string_1_current-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/sofar/test_init.py b/tests/components/sofar/test_init.py index 2c1f9340fc9004..d956782289f51d 100644 --- a/tests/components/sofar/test_init.py +++ b/tests/components/sofar/test_init.py @@ -81,6 +81,33 @@ async def test_setup_and_unload_entry( assert entry.state is ConfigEntryState.NOT_LOADED +async def test_setup_removes_the_stale_waiting_time_entity( + hass: HomeAssistant, + mock_connection: MockModbusConnection, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test an upgrade drops the removed waiting-time entity too.""" + mock_config_entry.add_to_hass(hass) + entry = entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"{MOCK_SERIAL}_waiting_time", + config_entry=mock_config_entry, + ) + + with patch( + "homeassistant.components.sofar.async_get_unit", + side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit( + unit_id + ), + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert entity_registry.async_get(entry.entity_id) is None + + async def test_setup_entry_unrecognized_inverter_raises_setup_error( hass: HomeAssistant, ) -> None: diff --git a/tests/components/sofar/test_sensor.py b/tests/components/sofar/test_sensor.py index ab325c2584c52a..5e37e43f0c1db6 100644 --- a/tests/components/sofar/test_sensor.py +++ b/tests/components/sofar/test_sensor.py @@ -20,10 +20,9 @@ SofarSensorDescription, SofarTotalSensor, ) -from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt as dt_util from . import ( MOCK_HYBRID_MODEL, @@ -103,13 +102,13 @@ async def test_sensor_entities_created_and_state( @pytest.mark.parametrize( ("serial", "model", "seed", "created", "enabled"), [ - pytest.param(MOCK_SERIAL, MOCK_MODEL, seed_pv_inverter, 72, 22, id="pv"), + pytest.param(MOCK_SERIAL, MOCK_MODEL, seed_pv_inverter, 71, 21, id="pv"), pytest.param( MOCK_HYBRID_SERIAL, MOCK_HYBRID_MODEL, seed_hybrid_inverter, - 138, - 45, + 137, + 44, id="hybrid", ), ], @@ -401,134 +400,3 @@ async def test_total_sensor_total_increasing_uses_corrected_value( assert sensor.native_value == 42.0 mock_corrected.assert_called_once_with("load_consumption_total") assert sensor.available - - -async def test_idle_countdown_reports_no_deadline( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - init_integration: MockConfigEntry, -) -> None: - """Test a countdown at zero reports nothing, not a moment already past.""" - entity_id = entity_registry.async_get_entity_id( - SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_waiting_time" - ) - assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_UNKNOWN - - -async def test_countdown_holds_its_deadline_until_it_restarts( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - entity_registry: er.EntityRegistry, - mock_connection: MockModbusConnection, - mock_config_entry: MockConfigEntry, -) -> None: - """Test a countdown ticking with the clock keeps one deadline.""" - mock_config_entry.add_to_hass(hass) - unit = mock_connection.for_unit(1) - unit.holding[0x0404] = 0 # Waiting - unit.holding[0x0417] = 300 - - with patch( - "homeassistant.components.sofar.async_get_unit", - side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit( - unit_id - ), - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done(wait_background_tasks=True) - - entity_id = entity_registry.async_get_entity_id( - SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_waiting_time" - ) - assert entity_id is not None - # The exact moment: a wrong sign or unit must not slip through. - deadline = (dt_util.utcnow() + timedelta(seconds=300)).isoformat(timespec="seconds") - assert hass.states.get(entity_id).state == deadline - - # A second of poll jitter must not republish the deadline as a new one. - unit.holding[0x0417] = 296 - freezer.tick(timedelta(seconds=SCAN_INTERVAL)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == deadline - - # Restarted, so it really is a different moment now. - unit.holding[0x0417] = 600 - freezer.tick(timedelta(seconds=SCAN_INTERVAL)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state != deadline - - -async def test_countdown_ignores_a_stale_register_while_grid_connected( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - mock_connection: MockModbusConnection, - mock_config_entry: MockConfigEntry, -) -> None: - """Test a positive register is ignored once the inverter is connected.""" - mock_config_entry.add_to_hass(hass) - unit = mock_connection.for_unit(1) - unit.holding[0x0404] = 2 # Grid connected - unit.holding[0x0417] = 60 # Left over from the last startup wait - - with patch( - "homeassistant.components.sofar.async_get_unit", - side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit( - unit_id - ), - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done(wait_background_tasks=True) - - entity_id = entity_registry.async_get_entity_id( - SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_waiting_time" - ) - assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_UNKNOWN - - -async def test_countdown_restarting_after_idle_gets_a_new_deadline( - hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - entity_registry: er.EntityRegistry, - mock_connection: MockModbusConnection, - mock_config_entry: MockConfigEntry, -) -> None: - """Test a finished countdown's deadline is not reused by the next one.""" - mock_config_entry.add_to_hass(hass) - unit = mock_connection.for_unit(1) - unit.holding[0x0404] = 0 # Waiting - unit.holding[0x0417] = 10 - - with patch( - "homeassistant.components.sofar.async_get_unit", - side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit( - unit_id - ), - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done(wait_background_tasks=True) - - entity_id = entity_registry.async_get_entity_id( - SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_waiting_time" - ) - assert entity_id is not None - finished = hass.states.get(entity_id).state - - unit.holding[0x0417] = 0 - freezer.tick(timedelta(seconds=SCAN_INTERVAL)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_UNKNOWN - - # Close enough to the old deadline to fall inside the variance window. - unit.holding[0x0417] = 5 - freezer.tick(timedelta(seconds=SCAN_INTERVAL)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - restarted = (dt_util.utcnow() + timedelta(seconds=5)).isoformat(timespec="seconds") - assert hass.states.get(entity_id).state == restarted - assert hass.states.get(entity_id).state != finished From fd59f8ce04419029bed1936d946ed8c50c0b6fd4 Mon Sep 17 00:00:00 2001 From: darkrain-nl <24763370+darkrain-nl@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:18:49 +0200 Subject: [PATCH 10/10] Bump sofar-modbus to 0.7.0 (#180894) --- homeassistant/components/sofar/manifest.json | 2 +- requirements_all.txt | 2 +- .../sofar/snapshots/test_diagnostics.ambr | 24 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sofar/manifest.json b/homeassistant/components/sofar/manifest.json index ee28bda72f9483..4572cc84be463c 100644 --- a/homeassistant/components/sofar/manifest.json +++ b/homeassistant/components/sofar/manifest.json @@ -8,5 +8,5 @@ "integration_type": "device", "iot_class": "local_polling", "quality_scale": "silver", - "requirements": ["sofar-modbus==0.6.0"] + "requirements": ["sofar-modbus==0.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 32e834e02cc37a..a9c6714b604c9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3111,7 +3111,7 @@ snapcast==2.3.8 soco==0.31.2 # homeassistant.components.sofar -sofar-modbus==0.6.0 +sofar-modbus==0.7.0 # homeassistant.components.solaredge_local solaredge-local==0.2.3 diff --git a/tests/components/sofar/snapshots/test_diagnostics.ambr b/tests/components/sofar/snapshots/test_diagnostics.ambr index e8c1ff9856b1a5..4770d42925d2b7 100644 --- a/tests/components/sofar/snapshots/test_diagnostics.ambr +++ b/tests/components/sofar/snapshots/test_diagnostics.ambr @@ -35,12 +35,34 @@ '1055': 0, '1056': 0, '1057': 0, + '1058': 0, + '1059': 0, + '1060': 0, + '1061': 0, + '1062': 0, + '1063': 0, + '1064': 0, + '1065': 0, + '1066': 0, + '1067': 0, '1068': 0, '1069': 0, '1070': 0, '1071': 0, '1072': 0, '1073': 0, + '1074': 0, + '1075': 0, + '1076': 0, + '1077': 0, + '1078': 0, + '1079': 0, + '1080': 0, + '1081': 0, + '1082': 0, + '1083': 0, + '1084': 0, + '1085': 0, '1100': 0, '1101': 22065, '1102': 12336, @@ -105,6 +127,7 @@ '1210': 0, '1211': 0, '1212': 0, + '1213': 0, '1412': 0, '1413': 0, '1414': 250, @@ -128,6 +151,7 @@ '1681': 0, '1682': 0, '1683': 0, + '1773': 0, '4131': 0, '4132': 0, '4356': 0,