From b116619af183560c97d070b7d88b225c71c1369e Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Wed, 22 Oct 2025 23:01:51 +0200 Subject: [PATCH] Bump pysma to 1.0.2 and enable type checking (#154977) Co-authored-by: Joost Lekkerkerker --- .strict-typing | 1 + homeassistant/components/sma/__init__.py | 4 ++-- homeassistant/components/sma/config_flow.py | 18 ++++++++++++------ homeassistant/components/sma/coordinator.py | 17 +++++++++-------- homeassistant/components/sma/manifest.json | 2 +- homeassistant/components/sma/sensor.py | 18 +++++++++--------- mypy.ini | 10 ++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sma/__init__.py | 17 ++++++++++------- tests/components/sma/conftest.py | 17 ++++++++++------- tests/components/sma/test_config_flow.py | 14 +++++--------- tests/components/sma/test_init.py | 18 ++++++++---------- tests/components/sma/test_sensor.py | 11 ++++------- 14 files changed, 83 insertions(+), 68 deletions(-) diff --git a/.strict-typing b/.strict-typing index 91aa90df028..14f1540c533 100644 --- a/.strict-typing +++ b/.strict-typing @@ -477,6 +477,7 @@ homeassistant.components.skybell.* homeassistant.components.slack.* homeassistant.components.sleep_as_android.* homeassistant.components.sleepiq.* +homeassistant.components.sma.* homeassistant.components.smhi.* homeassistant.components.smlight.* homeassistant.components.smtp.* diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 7097308ebf7..f97b2ee25b5 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging -from pysma import SMA +from pysma import SMAWebConnect from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SMAConfigEntry) -> bool: protocol = "https" if entry.data[CONF_SSL] else "http" url = f"{protocol}://{entry.data[CONF_HOST]}" - sma = SMA( + sma = SMAWebConnect( session=async_get_clientsession( hass=hass, verify_ssl=entry.data[CONF_VERIFY_SSL] ), diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 66dd9c9993d..7d76fbe0ec9 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -6,7 +6,13 @@ from collections.abc import Mapping import logging from typing import Any -import pysma +import attrs +from pysma import ( + SmaAuthenticationException, + SmaConnectionException, + SmaReadException, + SMAWebConnect, +) import voluptuous as vol from yarl import URL @@ -42,7 +48,7 @@ async def validate_input( host = data[CONF_HOST] if data is not None else user_input[CONF_HOST] url = URL.build(scheme=protocol, host=host) - sma = pysma.SMA( + sma = SMAWebConnect( session, str(url), user_input[CONF_PASSWORD], group=user_input[CONF_GROUP] ) @@ -51,7 +57,7 @@ async def validate_input( device_info = await sma.device_info() await sma.close_session() - return device_info + return attrs.asdict(device_info) class SmaConfigFlow(ConfigFlow, domain=DOMAIN): @@ -90,11 +96,11 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): device_info = await validate_input( self.hass, user_input=user_input, data=self._data ) - except pysma.exceptions.SmaConnectionException: + except SmaConnectionException: errors["base"] = "cannot_connect" - except pysma.exceptions.SmaAuthenticationException: + except SmaAuthenticationException: errors["base"] = "invalid_auth" - except pysma.exceptions.SmaReadException: + except SmaReadException: errors["base"] = "cannot_retrieve_device_info" except Exception: _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/sma/coordinator.py b/homeassistant/components/sma/coordinator.py index b4adc4eb9d6..5fd00ad9f50 100644 --- a/homeassistant/components/sma/coordinator.py +++ b/homeassistant/components/sma/coordinator.py @@ -6,13 +6,14 @@ from dataclasses import dataclass from datetime import timedelta import logging -from pysma import SMA -from pysma.exceptions import ( +from pysma import ( SmaAuthenticationException, SmaConnectionException, SmaReadException, + SMAWebConnect, ) -from pysma.sensor import Sensor +from pysma.helpers import DeviceInfo +from pysma.sensor import Sensors from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL @@ -29,8 +30,8 @@ _LOGGER = logging.getLogger(__name__) class SMACoordinatorData: """Data class for SMA sensors.""" - sma_device_info: dict[str, str] - sensors: list[Sensor] + sma_device_info: DeviceInfo + sensors: Sensors class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]): @@ -42,7 +43,7 @@ class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]): self, hass: HomeAssistant, config_entry: ConfigEntry, - sma: SMA, + sma: SMAWebConnect, ) -> None: """Initialize the SMA Data Update Coordinator.""" super().__init__( @@ -57,8 +58,8 @@ class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]): ), ) self.sma = sma - self._sma_device_info: dict[str, str] = {} - self._sensors: list[Sensor] = [] + self._sma_device_info = DeviceInfo() + self._sensors = Sensors() async def _async_setup(self) -> None: """Setup the SMA Data Update Coordinator.""" diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index bb3f5318280..89b8fafc6ae 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/sma", "iot_class": "local_polling", "loggers": ["pysma"], - "requirements": ["pysma==0.7.5"] + "requirements": ["pysma==1.0.2"] } diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 1c103b53cc4..3f90014eb90 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations -import pysma +from pysma.sensor import Sensor from homeassistant.components.sensor import ( SensorDeviceClass, @@ -859,7 +859,7 @@ class SMAsensor(CoordinatorEntity[SMADataUpdateCoordinator], SensorEntity): self, coordinator: SMADataUpdateCoordinator, description: SensorEntityDescription | None, - pysma_sensor: pysma.sensor.Sensor, + pysma_sensor: Sensor, entry: SMAConfigEntry, ) -> None: """Initialize the sensor.""" @@ -873,17 +873,17 @@ class SMAsensor(CoordinatorEntity[SMADataUpdateCoordinator], SensorEntity): url = f"{protocol}://{entry.data[CONF_HOST]}" self._sensor = pysma_sensor - self._serial = coordinator.data.sma_device_info["serial"] + self._serial = coordinator.data.sma_device_info.serial assert entry.unique_id self._attr_device_info = DeviceInfo( configuration_url=url, identifiers={(DOMAIN, entry.unique_id)}, - manufacturer=coordinator.data.sma_device_info["manufacturer"], - model=coordinator.data.sma_device_info["type"], - name=coordinator.data.sma_device_info["name"], - sw_version=coordinator.data.sma_device_info["sw_version"], - serial_number=coordinator.data.sma_device_info["serial"], + manufacturer=coordinator.data.sma_device_info.manufacturer, + model=coordinator.data.sma_device_info.type, + name=coordinator.data.sma_device_info.name, + sw_version=coordinator.data.sma_device_info.sw_version, + serial_number=coordinator.data.sma_device_info.serial, ) self._attr_unique_id = ( f"{entry.unique_id}-{pysma_sensor.key}_{pysma_sensor.key_idx}" @@ -908,7 +908,7 @@ class SMAsensor(CoordinatorEntity[SMADataUpdateCoordinator], SensorEntity): """Return if the device is available.""" return ( super().available - and self._serial == self.coordinator.data.sma_device_info["serial"] + and self._serial == self.coordinator.data.sma_device_info.serial ) @property diff --git a/mypy.ini b/mypy.ini index 3f987800262..4ef7b2a826f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -4526,6 +4526,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.sma.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.smhi.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index b1cdde4bd84..d35a164b6fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,7 +2387,7 @@ pysignalclirestapi==0.3.24 pyskyqhub==0.1.4 # homeassistant.components.sma -pysma==0.7.5 +pysma==1.0.2 # homeassistant.components.smappee pysmappee==0.2.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce93a5996b5..0f321b6f716 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1993,7 +1993,7 @@ pysiaalarm==3.1.1 pysignalclirestapi==0.3.24 # homeassistant.components.sma -pysma==0.7.5 +pysma==1.0.2 # homeassistant.components.smappee pysmappee==0.2.29 diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index 61d3f81a9fc..eebaf43ccd8 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -1,5 +1,7 @@ """Tests for the sma integration.""" +from pysma.helpers import DeviceInfo + from homeassistant.components.sma.const import CONF_GROUP from homeassistant.const import ( CONF_HOST, @@ -12,13 +14,14 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -MOCK_DEVICE = { - "manufacturer": "SMA", - "name": "SMA Device Name", - "type": "Sunny Boy 3.6", - "serial": 123456789, - "sw_version": "1.0.0", -} +MOCK_DEVICE = DeviceInfo( + manufacturer="SMA", + name="SMA Device Name", + type="Sunny Boy 3.6", + serial=123456789, + sw_version="1.0.0", +) + MOCK_USER_INPUT = { CONF_HOST: "1.1.1.1", diff --git a/tests/components/sma/conftest.py b/tests/components/sma/conftest.py index d06cbe0912e..a81334b4727 100644 --- a/tests/components/sma/conftest.py +++ b/tests/components/sma/conftest.py @@ -8,7 +8,7 @@ from pysma.const import ( GENERIC_SENSORS, OPTIMIZERS_VIA_INVERTER, ) -from pysma.definitions import sensor_map +from pysma.definitions.webconnect import sensor_map from pysma.sensor import Sensors import pytest @@ -26,8 +26,8 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: return MockConfigEntry( domain=DOMAIN, - title=MOCK_DEVICE["name"], - unique_id=str(MOCK_DEVICE["serial"]), + title=MOCK_DEVICE.name, + unique_id=str(MOCK_DEVICE.serial), data=MOCK_USER_INPUT, minor_version=2, entry_id="sma_entry_123", @@ -47,7 +47,7 @@ def mock_setup_entry() -> Generator[AsyncMock]: def mock_sma_client() -> Generator[MagicMock]: """Mock the SMA client.""" with patch( - "homeassistant.components.sma.coordinator.SMA", autospec=True + "homeassistant.components.sma.coordinator.SMAWebConnect", autospec=True ) as sma_cls: sma_instance: MagicMock = sma_cls.return_value sma_instance.device_info = AsyncMock(return_value=MOCK_DEVICE) @@ -80,8 +80,11 @@ def mock_sma_client() -> Generator[MagicMock]: sma_instance.read = AsyncMock(side_effect=_async_mock_read) with ( - patch("homeassistant.components.sma.config_flow.pysma.SMA", new=sma_cls), - patch("homeassistant.components.sma.SMA", new=sma_cls), - patch("pysma.SMA", new=sma_cls), + patch( + "homeassistant.components.sma.config_flow.SMAWebConnect", + new=sma_cls, + ), + patch("homeassistant.components.sma.SMAWebConnect", new=sma_cls), + patch("pysma.SMAWebConnect", new=sma_cls), ): yield sma_instance diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 7bcc2bcfee5..f13da7198bc 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -2,11 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch -from pysma.exceptions import ( - SmaAuthenticationException, - SmaConnectionException, - SmaReadException, -) +from pysma import SmaAuthenticationException, SmaConnectionException, SmaReadException import pytest from homeassistant.components.sma.const import DOMAIN @@ -91,7 +87,7 @@ async def test_form_exceptions( ) with patch( - "homeassistant.components.sma.config_flow.pysma.SMA.new_session", + "homeassistant.components.sma.config_flow.SMAWebConnect.new_session", side_effect=exception, ): result = await hass.config_entries.flow.async_configure( @@ -211,7 +207,7 @@ async def test_dhcp_exceptions( data=DHCP_DISCOVERY, ) - with patch("homeassistant.components.sma.config_flow.pysma.SMA") as mock_sma: + with patch("homeassistant.components.sma.config_flow.SMAWebConnect") as mock_sma: mock_sma_instance = mock_sma.return_value mock_sma_instance.new_session = AsyncMock(side_effect=exception) @@ -223,7 +219,7 @@ async def test_dhcp_exceptions( assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": error} - with patch("homeassistant.components.sma.config_flow.pysma.SMA") as mock_sma: + with patch("homeassistant.components.sma.config_flow.SMAWebConnect") as mock_sma: mock_sma_instance = mock_sma.return_value mock_sma_instance.new_session = AsyncMock(return_value=True) mock_sma_instance.device_info = AsyncMock(return_value=MOCK_DEVICE) @@ -291,7 +287,7 @@ async def test_reauth_flow_exceptions( result = await entry.start_reauth_flow(hass) - with patch("homeassistant.components.sma.config_flow.pysma.SMA") as mock_sma: + with patch("homeassistant.components.sma.config_flow.SMAWebConnect") as mock_sma: mock_sma_instance = mock_sma.return_value mock_sma_instance.new_session = AsyncMock(side_effect=exception) result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/sma/test_init.py b/tests/components/sma/test_init.py index 0c20a1acc43..b9f78f568c6 100644 --- a/tests/components/sma/test_init.py +++ b/tests/components/sma/test_init.py @@ -1,12 +1,9 @@ """Test the sma init file.""" -from collections.abc import AsyncGenerator, Generator +from collections.abc import AsyncGenerator +from unittest.mock import MagicMock -from pysma.exceptions import ( - SmaAuthenticationException, - SmaConnectionException, - SmaReadException, -) +from pysma import SmaAuthenticationException, SmaConnectionException, SmaReadException import pytest from homeassistant.components.sma.const import DOMAIN @@ -26,8 +23,8 @@ async def test_migrate_entry_minor_version_1_2( """Test migrating a 1.1 config entry to 1.2.""" entry = MockConfigEntry( domain=DOMAIN, - title=MOCK_DEVICE["name"], - unique_id=MOCK_DEVICE["serial"], # Not converted to str + title=MOCK_DEVICE.name, + unique_id=MOCK_DEVICE.serial, data=MOCK_USER_INPUT, source=SOURCE_IMPORT, minor_version=1, @@ -36,7 +33,8 @@ async def test_migrate_entry_minor_version_1_2( assert await hass.config_entries.async_setup(entry.entry_id) assert entry.version == 1 assert entry.minor_version == 2 - assert entry.unique_id == str(MOCK_DEVICE["serial"]) + assert isinstance(MOCK_DEVICE.serial, str) + assert entry.unique_id == MOCK_DEVICE.serial @pytest.mark.parametrize( @@ -49,7 +47,7 @@ async def test_migrate_entry_minor_version_1_2( ) async def test_setup_exceptions( hass: HomeAssistant, - mock_sma_client: Generator, + mock_sma_client: MagicMock, mock_config_entry: MockConfigEntry, exception: Exception, expected_state: ConfigEntryState, diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index d289962d798..991898b2229 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -1,14 +1,10 @@ """Test the SMA sensor platform.""" from collections.abc import Generator -from unittest.mock import patch +from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory -from pysma.exceptions import ( - SmaAuthenticationException, - SmaConnectionException, - SmaReadException, -) +from pysma import SmaAuthenticationException, SmaConnectionException, SmaReadException import pytest from syrupy.assertion import SnapshotAssertion @@ -56,7 +52,7 @@ async def test_all_entities( async def test_refresh_exceptions( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_sma_client: Generator, + mock_sma_client: MagicMock, freezer: FrozenDateTimeFactory, exception: Exception, ) -> None: @@ -71,4 +67,5 @@ async def test_refresh_exceptions( await hass.async_block_till_done() state = hass.states.get("sensor.sma_device_name_battery_capacity_a") + assert state assert state.state == STATE_UNAVAILABLE