Skip to content

Commit b3e16bd

Browse files
authored
Refactor the SMA integration to use a dedicated DataUpdateCoordinator (home-assistant#154863)
1 parent 18d5035 commit b3e16bd

File tree

6 files changed

+201
-163
lines changed

6 files changed

+201
-163
lines changed

homeassistant/components/sma/__init__.py

Lines changed: 32 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,153 +1,69 @@
1-
"""The sma integration."""
1+
"""The SMA integration."""
22

33
from __future__ import annotations
44

5-
from datetime import timedelta
65
import logging
7-
from typing import TYPE_CHECKING
86

9-
import pysma
7+
from pysma import SMA
108

119
from homeassistant.config_entries import ConfigEntry
1210
from homeassistant.const import (
13-
ATTR_CONNECTIONS,
1411
CONF_HOST,
15-
CONF_MAC,
1612
CONF_PASSWORD,
17-
CONF_SCAN_INTERVAL,
1813
CONF_SSL,
1914
CONF_VERIFY_SSL,
2015
EVENT_HOMEASSISTANT_STOP,
16+
Platform,
2117
)
22-
from homeassistant.core import HomeAssistant
23-
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
24-
from homeassistant.helpers import device_registry as dr
18+
from homeassistant.core import Event, HomeAssistant
2519
from homeassistant.helpers.aiohttp_client import async_get_clientsession
26-
from homeassistant.helpers.device_registry import DeviceInfo
27-
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
28-
29-
from .const import (
30-
CONF_GROUP,
31-
DEFAULT_SCAN_INTERVAL,
32-
DOMAIN,
33-
PLATFORMS,
34-
PYSMA_COORDINATOR,
35-
PYSMA_DEVICE_INFO,
36-
PYSMA_OBJECT,
37-
PYSMA_REMOVE_LISTENER,
38-
PYSMA_SENSORS,
39-
)
20+
21+
from .const import CONF_GROUP
22+
from .coordinator import SMADataUpdateCoordinator
23+
24+
PLATFORMS = [Platform.SENSOR]
4025

4126
_LOGGER = logging.getLogger(__name__)
4227

4328

44-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
29+
type SMAConfigEntry = ConfigEntry[SMADataUpdateCoordinator]
30+
31+
32+
async def async_setup_entry(hass: HomeAssistant, entry: SMAConfigEntry) -> bool:
4533
"""Set up sma from a config entry."""
46-
# Init the SMA interface
34+
4735
protocol = "https" if entry.data[CONF_SSL] else "http"
4836
url = f"{protocol}://{entry.data[CONF_HOST]}"
49-
verify_ssl = entry.data[CONF_VERIFY_SSL]
50-
group = entry.data[CONF_GROUP]
51-
password = entry.data[CONF_PASSWORD]
52-
53-
session = async_get_clientsession(hass, verify_ssl=verify_ssl)
54-
sma = pysma.SMA(session, url, password, group)
55-
56-
try:
57-
# Get updated device info
58-
sma_device_info = await sma.device_info()
59-
# Get all device sensors
60-
sensor_def = await sma.get_sensors()
61-
except (
62-
pysma.exceptions.SmaReadException,
63-
pysma.exceptions.SmaConnectionException,
64-
) as exc:
65-
raise ConfigEntryNotReady from exc
66-
except pysma.exceptions.SmaAuthenticationException as exc:
67-
raise ConfigEntryAuthFailed from exc
68-
69-
if TYPE_CHECKING:
70-
assert entry.unique_id
71-
72-
# Create DeviceInfo object from sma_device_info
73-
device_info = DeviceInfo(
74-
configuration_url=url,
75-
identifiers={(DOMAIN, entry.unique_id)},
76-
manufacturer=sma_device_info["manufacturer"],
77-
model=sma_device_info["type"],
78-
name=sma_device_info["name"],
79-
sw_version=sma_device_info["sw_version"],
80-
serial_number=sma_device_info["serial"],
81-
)
8237

83-
# Add the MAC address to connections, if it comes via DHCP
84-
if CONF_MAC in entry.data:
85-
device_info[ATTR_CONNECTIONS] = {
86-
(dr.CONNECTION_NETWORK_MAC, entry.data[CONF_MAC])
87-
}
88-
89-
# Define the coordinator
90-
async def async_update_data():
91-
"""Update the used SMA sensors."""
92-
try:
93-
await sma.read(sensor_def)
94-
except (
95-
pysma.exceptions.SmaReadException,
96-
pysma.exceptions.SmaConnectionException,
97-
) as exc:
98-
raise UpdateFailed(exc) from exc
99-
100-
interval = timedelta(
101-
seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
38+
sma = SMA(
39+
session=async_get_clientsession(
40+
hass=hass, verify_ssl=entry.data[CONF_VERIFY_SSL]
41+
),
42+
url=url,
43+
password=entry.data[CONF_PASSWORD],
44+
group=entry.data[CONF_GROUP],
10245
)
10346

104-
coordinator = DataUpdateCoordinator(
105-
hass,
106-
_LOGGER,
107-
config_entry=entry,
108-
name="sma",
109-
update_method=async_update_data,
110-
update_interval=interval,
111-
)
47+
coordinator = SMADataUpdateCoordinator(hass, entry, sma)
48+
await coordinator.async_config_entry_first_refresh()
11249

113-
try:
114-
await coordinator.async_config_entry_first_refresh()
115-
except ConfigEntryNotReady:
116-
await sma.close_session()
117-
raise
50+
entry.runtime_data = coordinator
51+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
11852

119-
# Ensure we logout on shutdown
120-
async def async_close_session(event):
121-
"""Close the session."""
122-
await sma.close_session()
53+
# Ensure the SMA session closes when Home Assistant stops
54+
async def _async_handle_shutdown(event: Event) -> None:
55+
await coordinator.async_close_sma_session()
12356

124-
remove_stop_listener = hass.bus.async_listen_once(
125-
EVENT_HOMEASSISTANT_STOP, async_close_session
57+
entry.async_on_unload(
58+
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_handle_shutdown)
12659
)
12760

128-
hass.data.setdefault(DOMAIN, {})
129-
hass.data[DOMAIN][entry.entry_id] = {
130-
PYSMA_OBJECT: sma,
131-
PYSMA_COORDINATOR: coordinator,
132-
PYSMA_SENSORS: sensor_def,
133-
PYSMA_REMOVE_LISTENER: remove_stop_listener,
134-
PYSMA_DEVICE_INFO: device_info,
135-
}
136-
137-
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
138-
13961
return True
14062

14163

142-
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
64+
async def async_unload_entry(hass: HomeAssistant, entry: SMAConfigEntry) -> bool:
14365
"""Unload a config entry."""
144-
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
145-
if unload_ok:
146-
data = hass.data[DOMAIN].pop(entry.entry_id)
147-
await data[PYSMA_OBJECT].close_session()
148-
data[PYSMA_REMOVE_LISTENER]()
149-
150-
return unload_ok
66+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
15167

15268

15369
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -156,7 +72,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
15672
_LOGGER.debug("Migrating from version %s", entry.version)
15773

15874
if entry.version == 1:
159-
# 1 -> 2: Unique ID from integer to string
16075
if entry.minor_version == 1:
16176
minor_version = 2
16277
hass.config_entries.async_update_entry(

homeassistant/components/sma/const.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Constants for the sma integration."""
22

3-
from homeassistant.const import Platform
4-
53
DOMAIN = "sma"
64

75
PYSMA_COORDINATOR = "coordinator"
@@ -10,7 +8,6 @@
108
PYSMA_SENSORS = "pysma_sensors"
119
PYSMA_DEVICE_INFO = "device_info"
1210

13-
PLATFORMS = [Platform.SENSOR]
1411

1512
CONF_GROUP = "group"
1613

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Coordinator for the SMA integration."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from datetime import timedelta
7+
import logging
8+
9+
from pysma import SMA
10+
from pysma.exceptions import (
11+
SmaAuthenticationException,
12+
SmaConnectionException,
13+
SmaReadException,
14+
)
15+
from pysma.sensor import Sensor
16+
17+
from homeassistant.config_entries import ConfigEntry
18+
from homeassistant.const import CONF_SCAN_INTERVAL
19+
from homeassistant.core import HomeAssistant
20+
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
21+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
22+
23+
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
24+
25+
_LOGGER = logging.getLogger(__name__)
26+
27+
28+
@dataclass(slots=True)
29+
class SMACoordinatorData:
30+
"""Data class for SMA sensors."""
31+
32+
sma_device_info: dict[str, str]
33+
sensors: list[Sensor]
34+
35+
36+
class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]):
37+
"""Data Update Coordinator for SMA."""
38+
39+
config_entry: ConfigEntry
40+
41+
def __init__(
42+
self,
43+
hass: HomeAssistant,
44+
config_entry: ConfigEntry,
45+
sma: SMA,
46+
) -> None:
47+
"""Initialize the SMA Data Update Coordinator."""
48+
super().__init__(
49+
hass,
50+
_LOGGER,
51+
config_entry=config_entry,
52+
name=DOMAIN,
53+
update_interval=timedelta(
54+
seconds=config_entry.options.get(
55+
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
56+
)
57+
),
58+
)
59+
self.sma = sma
60+
self._sma_device_info: dict[str, str] = {}
61+
self._sensors: list[Sensor] = []
62+
63+
async def _async_setup(self) -> None:
64+
"""Setup the SMA Data Update Coordinator."""
65+
try:
66+
self._sma_device_info = await self.sma.device_info()
67+
self._sensors = await self.sma.get_sensors()
68+
except (
69+
SmaReadException,
70+
SmaConnectionException,
71+
) as err:
72+
await self.async_close_sma_session()
73+
raise ConfigEntryNotReady(
74+
translation_domain=DOMAIN,
75+
translation_key="cannot_connect",
76+
translation_placeholders={"error": repr(err)},
77+
) from err
78+
except SmaAuthenticationException as err:
79+
raise ConfigEntryAuthFailed(
80+
translation_domain=DOMAIN,
81+
translation_key="invalid_auth",
82+
translation_placeholders={"error": repr(err)},
83+
) from err
84+
85+
async def _async_update_data(self) -> SMACoordinatorData:
86+
"""Update the used SMA sensors."""
87+
try:
88+
await self.sma.read(self._sensors)
89+
except (
90+
SmaReadException,
91+
SmaConnectionException,
92+
) as err:
93+
raise UpdateFailed(
94+
translation_domain=DOMAIN,
95+
translation_key="cannot_connect",
96+
translation_placeholders={"error": repr(err)},
97+
) from err
98+
except SmaAuthenticationException as err:
99+
raise ConfigEntryAuthFailed(
100+
translation_domain=DOMAIN,
101+
translation_key="invalid_auth",
102+
translation_placeholders={"error": repr(err)},
103+
) from err
104+
105+
return SMACoordinatorData(
106+
sma_device_info=self._sma_device_info,
107+
sensors=self._sensors,
108+
)
109+
110+
async def async_close_sma_session(self) -> None:
111+
"""Close the SMA session."""
112+
await self.sma.close_session()
113+
_LOGGER.debug("SMA session closed")

0 commit comments

Comments
 (0)