1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Bump pysma to 1.0.2 and enable type checking (#154977)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Johann Kellerman
2025-10-22 23:01:51 +02:00
committed by GitHub
parent a3d760156f
commit b116619af1
14 changed files with 83 additions and 68 deletions

View File

@@ -477,6 +477,7 @@ homeassistant.components.skybell.*
homeassistant.components.slack.* homeassistant.components.slack.*
homeassistant.components.sleep_as_android.* homeassistant.components.sleep_as_android.*
homeassistant.components.sleepiq.* homeassistant.components.sleepiq.*
homeassistant.components.sma.*
homeassistant.components.smhi.* homeassistant.components.smhi.*
homeassistant.components.smlight.* homeassistant.components.smlight.*
homeassistant.components.smtp.* homeassistant.components.smtp.*

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import logging import logging
from pysma import SMA from pysma import SMAWebConnect
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( 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" protocol = "https" if entry.data[CONF_SSL] else "http"
url = f"{protocol}://{entry.data[CONF_HOST]}" url = f"{protocol}://{entry.data[CONF_HOST]}"
sma = SMA( sma = SMAWebConnect(
session=async_get_clientsession( session=async_get_clientsession(
hass=hass, verify_ssl=entry.data[CONF_VERIFY_SSL] hass=hass, verify_ssl=entry.data[CONF_VERIFY_SSL]
), ),

View File

@@ -6,7 +6,13 @@ from collections.abc import Mapping
import logging import logging
from typing import Any from typing import Any
import pysma import attrs
from pysma import (
SmaAuthenticationException,
SmaConnectionException,
SmaReadException,
SMAWebConnect,
)
import voluptuous as vol import voluptuous as vol
from yarl import URL 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] host = data[CONF_HOST] if data is not None else user_input[CONF_HOST]
url = URL.build(scheme=protocol, host=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] 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() device_info = await sma.device_info()
await sma.close_session() await sma.close_session()
return device_info return attrs.asdict(device_info)
class SmaConfigFlow(ConfigFlow, domain=DOMAIN): class SmaConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -90,11 +96,11 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN):
device_info = await validate_input( device_info = await validate_input(
self.hass, user_input=user_input, data=self._data self.hass, user_input=user_input, data=self._data
) )
except pysma.exceptions.SmaConnectionException: except SmaConnectionException:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except pysma.exceptions.SmaAuthenticationException: except SmaAuthenticationException:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except pysma.exceptions.SmaReadException: except SmaReadException:
errors["base"] = "cannot_retrieve_device_info" errors["base"] = "cannot_retrieve_device_info"
except Exception: except Exception:
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")

View File

@@ -6,13 +6,14 @@ from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
import logging import logging
from pysma import SMA from pysma import (
from pysma.exceptions import (
SmaAuthenticationException, SmaAuthenticationException,
SmaConnectionException, SmaConnectionException,
SmaReadException, 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.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.const import CONF_SCAN_INTERVAL
@@ -29,8 +30,8 @@ _LOGGER = logging.getLogger(__name__)
class SMACoordinatorData: class SMACoordinatorData:
"""Data class for SMA sensors.""" """Data class for SMA sensors."""
sma_device_info: dict[str, str] sma_device_info: DeviceInfo
sensors: list[Sensor] sensors: Sensors
class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]): class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]):
@@ -42,7 +43,7 @@ class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]):
self, self,
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
sma: SMA, sma: SMAWebConnect,
) -> None: ) -> None:
"""Initialize the SMA Data Update Coordinator.""" """Initialize the SMA Data Update Coordinator."""
super().__init__( super().__init__(
@@ -57,8 +58,8 @@ class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]):
), ),
) )
self.sma = sma self.sma = sma
self._sma_device_info: dict[str, str] = {} self._sma_device_info = DeviceInfo()
self._sensors: list[Sensor] = [] self._sensors = Sensors()
async def _async_setup(self) -> None: async def _async_setup(self) -> None:
"""Setup the SMA Data Update Coordinator.""" """Setup the SMA Data Update Coordinator."""

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/sma", "documentation": "https://www.home-assistant.io/integrations/sma",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pysma"], "loggers": ["pysma"],
"requirements": ["pysma==0.7.5"] "requirements": ["pysma==1.0.2"]
} }

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import pysma from pysma.sensor import Sensor
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@@ -859,7 +859,7 @@ class SMAsensor(CoordinatorEntity[SMADataUpdateCoordinator], SensorEntity):
self, self,
coordinator: SMADataUpdateCoordinator, coordinator: SMADataUpdateCoordinator,
description: SensorEntityDescription | None, description: SensorEntityDescription | None,
pysma_sensor: pysma.sensor.Sensor, pysma_sensor: Sensor,
entry: SMAConfigEntry, entry: SMAConfigEntry,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
@@ -873,17 +873,17 @@ class SMAsensor(CoordinatorEntity[SMADataUpdateCoordinator], SensorEntity):
url = f"{protocol}://{entry.data[CONF_HOST]}" url = f"{protocol}://{entry.data[CONF_HOST]}"
self._sensor = pysma_sensor self._sensor = pysma_sensor
self._serial = coordinator.data.sma_device_info["serial"] self._serial = coordinator.data.sma_device_info.serial
assert entry.unique_id assert entry.unique_id
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
configuration_url=url, configuration_url=url,
identifiers={(DOMAIN, entry.unique_id)}, identifiers={(DOMAIN, entry.unique_id)},
manufacturer=coordinator.data.sma_device_info["manufacturer"], manufacturer=coordinator.data.sma_device_info.manufacturer,
model=coordinator.data.sma_device_info["type"], model=coordinator.data.sma_device_info.type,
name=coordinator.data.sma_device_info["name"], name=coordinator.data.sma_device_info.name,
sw_version=coordinator.data.sma_device_info["sw_version"], sw_version=coordinator.data.sma_device_info.sw_version,
serial_number=coordinator.data.sma_device_info["serial"], serial_number=coordinator.data.sma_device_info.serial,
) )
self._attr_unique_id = ( self._attr_unique_id = (
f"{entry.unique_id}-{pysma_sensor.key}_{pysma_sensor.key_idx}" 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 if the device is available."""
return ( return (
super().available super().available
and self._serial == self.coordinator.data.sma_device_info["serial"] and self._serial == self.coordinator.data.sma_device_info.serial
) )
@property @property

10
mypy.ini generated
View File

@@ -4526,6 +4526,16 @@ disallow_untyped_defs = true
warn_return_any = true warn_return_any = true
warn_unreachable = 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.*] [mypy-homeassistant.components.smhi.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true

2
requirements_all.txt generated
View File

@@ -2387,7 +2387,7 @@ pysignalclirestapi==0.3.24
pyskyqhub==0.1.4 pyskyqhub==0.1.4
# homeassistant.components.sma # homeassistant.components.sma
pysma==0.7.5 pysma==1.0.2
# homeassistant.components.smappee # homeassistant.components.smappee
pysmappee==0.2.29 pysmappee==0.2.29

View File

@@ -1993,7 +1993,7 @@ pysiaalarm==3.1.1
pysignalclirestapi==0.3.24 pysignalclirestapi==0.3.24
# homeassistant.components.sma # homeassistant.components.sma
pysma==0.7.5 pysma==1.0.2
# homeassistant.components.smappee # homeassistant.components.smappee
pysmappee==0.2.29 pysmappee==0.2.29

View File

@@ -1,5 +1,7 @@
"""Tests for the sma integration.""" """Tests for the sma integration."""
from pysma.helpers import DeviceInfo
from homeassistant.components.sma.const import CONF_GROUP from homeassistant.components.sma.const import CONF_GROUP
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@@ -12,13 +14,14 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
MOCK_DEVICE = { MOCK_DEVICE = DeviceInfo(
"manufacturer": "SMA", manufacturer="SMA",
"name": "SMA Device Name", name="SMA Device Name",
"type": "Sunny Boy 3.6", type="Sunny Boy 3.6",
"serial": 123456789, serial=123456789,
"sw_version": "1.0.0", sw_version="1.0.0",
} )
MOCK_USER_INPUT = { MOCK_USER_INPUT = {
CONF_HOST: "1.1.1.1", CONF_HOST: "1.1.1.1",

View File

@@ -8,7 +8,7 @@ from pysma.const import (
GENERIC_SENSORS, GENERIC_SENSORS,
OPTIMIZERS_VIA_INVERTER, OPTIMIZERS_VIA_INVERTER,
) )
from pysma.definitions import sensor_map from pysma.definitions.webconnect import sensor_map
from pysma.sensor import Sensors from pysma.sensor import Sensors
import pytest import pytest
@@ -26,8 +26,8 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
return MockConfigEntry( return MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title=MOCK_DEVICE["name"], title=MOCK_DEVICE.name,
unique_id=str(MOCK_DEVICE["serial"]), unique_id=str(MOCK_DEVICE.serial),
data=MOCK_USER_INPUT, data=MOCK_USER_INPUT,
minor_version=2, minor_version=2,
entry_id="sma_entry_123", entry_id="sma_entry_123",
@@ -47,7 +47,7 @@ def mock_setup_entry() -> Generator[AsyncMock]:
def mock_sma_client() -> Generator[MagicMock]: def mock_sma_client() -> Generator[MagicMock]:
"""Mock the SMA client.""" """Mock the SMA client."""
with patch( with patch(
"homeassistant.components.sma.coordinator.SMA", autospec=True "homeassistant.components.sma.coordinator.SMAWebConnect", autospec=True
) as sma_cls: ) as sma_cls:
sma_instance: MagicMock = sma_cls.return_value sma_instance: MagicMock = sma_cls.return_value
sma_instance.device_info = AsyncMock(return_value=MOCK_DEVICE) 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) sma_instance.read = AsyncMock(side_effect=_async_mock_read)
with ( with (
patch("homeassistant.components.sma.config_flow.pysma.SMA", new=sma_cls), patch(
patch("homeassistant.components.sma.SMA", new=sma_cls), "homeassistant.components.sma.config_flow.SMAWebConnect",
patch("pysma.SMA", new=sma_cls), new=sma_cls,
),
patch("homeassistant.components.sma.SMAWebConnect", new=sma_cls),
patch("pysma.SMAWebConnect", new=sma_cls),
): ):
yield sma_instance yield sma_instance

View File

@@ -2,11 +2,7 @@
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from pysma.exceptions import ( from pysma import SmaAuthenticationException, SmaConnectionException, SmaReadException
SmaAuthenticationException,
SmaConnectionException,
SmaReadException,
)
import pytest import pytest
from homeassistant.components.sma.const import DOMAIN from homeassistant.components.sma.const import DOMAIN
@@ -91,7 +87,7 @@ async def test_form_exceptions(
) )
with patch( with patch(
"homeassistant.components.sma.config_flow.pysma.SMA.new_session", "homeassistant.components.sma.config_flow.SMAWebConnect.new_session",
side_effect=exception, side_effect=exception,
): ):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -211,7 +207,7 @@ async def test_dhcp_exceptions(
data=DHCP_DISCOVERY, 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 = mock_sma.return_value
mock_sma_instance.new_session = AsyncMock(side_effect=exception) 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["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error} 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 = mock_sma.return_value
mock_sma_instance.new_session = AsyncMock(return_value=True) mock_sma_instance.new_session = AsyncMock(return_value=True)
mock_sma_instance.device_info = AsyncMock(return_value=MOCK_DEVICE) 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) 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 = mock_sma.return_value
mock_sma_instance.new_session = AsyncMock(side_effect=exception) mock_sma_instance.new_session = AsyncMock(side_effect=exception)
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(

View File

@@ -1,12 +1,9 @@
"""Test the sma init file.""" """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 ( from pysma import SmaAuthenticationException, SmaConnectionException, SmaReadException
SmaAuthenticationException,
SmaConnectionException,
SmaReadException,
)
import pytest import pytest
from homeassistant.components.sma.const import DOMAIN 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.""" """Test migrating a 1.1 config entry to 1.2."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title=MOCK_DEVICE["name"], title=MOCK_DEVICE.name,
unique_id=MOCK_DEVICE["serial"], # Not converted to str unique_id=MOCK_DEVICE.serial,
data=MOCK_USER_INPUT, data=MOCK_USER_INPUT,
source=SOURCE_IMPORT, source=SOURCE_IMPORT,
minor_version=1, 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 await hass.config_entries.async_setup(entry.entry_id)
assert entry.version == 1 assert entry.version == 1
assert entry.minor_version == 2 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( @pytest.mark.parametrize(
@@ -49,7 +47,7 @@ async def test_migrate_entry_minor_version_1_2(
) )
async def test_setup_exceptions( async def test_setup_exceptions(
hass: HomeAssistant, hass: HomeAssistant,
mock_sma_client: Generator, mock_sma_client: MagicMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
exception: Exception, exception: Exception,
expected_state: ConfigEntryState, expected_state: ConfigEntryState,

View File

@@ -1,14 +1,10 @@
"""Test the SMA sensor platform.""" """Test the SMA sensor platform."""
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import patch from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from pysma.exceptions import ( from pysma import SmaAuthenticationException, SmaConnectionException, SmaReadException
SmaAuthenticationException,
SmaConnectionException,
SmaReadException,
)
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@@ -56,7 +52,7 @@ async def test_all_entities(
async def test_refresh_exceptions( async def test_refresh_exceptions(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
mock_sma_client: Generator, mock_sma_client: MagicMock,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
exception: Exception, exception: Exception,
) -> None: ) -> None:
@@ -71,4 +67,5 @@ async def test_refresh_exceptions(
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("sensor.sma_device_name_battery_capacity_a") state = hass.states.get("sensor.sma_device_name_battery_capacity_a")
assert state
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE