mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
Implement SMA config flow (#48003)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Johann Kellerman <kellerza@gmail.com>
This commit is contained in:
parent
e9f0891354
commit
926c2489f0
@ -899,6 +899,7 @@ omit =
|
|||||||
homeassistant/components/slack/notify.py
|
homeassistant/components/slack/notify.py
|
||||||
homeassistant/components/sinch/*
|
homeassistant/components/sinch/*
|
||||||
homeassistant/components/slide/*
|
homeassistant/components/slide/*
|
||||||
|
homeassistant/components/sma/__init__.py
|
||||||
homeassistant/components/sma/sensor.py
|
homeassistant/components/sma/sensor.py
|
||||||
homeassistant/components/smappee/__init__.py
|
homeassistant/components/smappee/__init__.py
|
||||||
homeassistant/components/smappee/api.py
|
homeassistant/components/smappee/api.py
|
||||||
|
@ -428,7 +428,7 @@ homeassistant/components/sisyphus/* @jkeljo
|
|||||||
homeassistant/components/sky_hub/* @rogerselwyn
|
homeassistant/components/sky_hub/* @rogerselwyn
|
||||||
homeassistant/components/slack/* @bachya
|
homeassistant/components/slack/* @bachya
|
||||||
homeassistant/components/slide/* @ualex73
|
homeassistant/components/slide/* @ualex73
|
||||||
homeassistant/components/sma/* @kellerza
|
homeassistant/components/sma/* @kellerza @rklomp
|
||||||
homeassistant/components/smappee/* @bsmappee
|
homeassistant/components/smappee/* @bsmappee
|
||||||
homeassistant/components/smart_meter_texas/* @grahamwetzler
|
homeassistant/components/smart_meter_texas/* @grahamwetzler
|
||||||
homeassistant/components/smarthab/* @outadoc
|
homeassistant/components/smarthab/* @outadoc
|
||||||
|
@ -1 +1,199 @@
|
|||||||
"""The sma component."""
|
"""The sma integration."""
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pysma
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PATH,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
CONF_SENSORS,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_CUSTOM,
|
||||||
|
CONF_FACTOR,
|
||||||
|
CONF_GROUP,
|
||||||
|
CONF_KEY,
|
||||||
|
CONF_UNIT,
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
|
DOMAIN,
|
||||||
|
PLATFORMS,
|
||||||
|
PYSMA_COORDINATOR,
|
||||||
|
PYSMA_OBJECT,
|
||||||
|
PYSMA_REMOVE_LISTENER,
|
||||||
|
PYSMA_SENSORS,
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def _parse_legacy_options(entry: ConfigEntry, sensor_def: pysma.Sensors) -> None:
|
||||||
|
"""Parse legacy configuration options.
|
||||||
|
|
||||||
|
This will parse the legacy CONF_SENSORS and CONF_CUSTOM configuration options
|
||||||
|
to support deprecated yaml config from platform setup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add sensors from the custom config
|
||||||
|
sensor_def.add(
|
||||||
|
[
|
||||||
|
pysma.Sensor(o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH))
|
||||||
|
for n, o in entry.data.get(CONF_CUSTOM).items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parsing of sensors configuration
|
||||||
|
config_sensors = entry.data.get(CONF_SENSORS)
|
||||||
|
if not config_sensors:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find and replace sensors removed from pysma
|
||||||
|
# This only alters the config, the actual sensor migration takes place in _migrate_old_unique_ids
|
||||||
|
for sensor in config_sensors.copy():
|
||||||
|
if sensor in pysma.LEGACY_MAP:
|
||||||
|
config_sensors.remove(sensor)
|
||||||
|
config_sensors.append(pysma.LEGACY_MAP[sensor]["new_sensor"])
|
||||||
|
|
||||||
|
# Only sensors from config should be enabled
|
||||||
|
for sensor in sensor_def:
|
||||||
|
sensor.enabled = sensor.name in config_sensors
|
||||||
|
|
||||||
|
|
||||||
|
async def _migrate_old_unique_ids(
|
||||||
|
hass: HomeAssistant, entry: ConfigEntry, sensor_def: pysma.Sensors
|
||||||
|
) -> None:
|
||||||
|
"""Migrate legacy sensor entity_id format to new format."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
# Create list of all possible sensor names
|
||||||
|
possible_sensors = list(
|
||||||
|
set(
|
||||||
|
entry.data.get(CONF_SENSORS)
|
||||||
|
+ [s.name for s in sensor_def]
|
||||||
|
+ list(pysma.LEGACY_MAP)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for sensor in possible_sensors:
|
||||||
|
if sensor in sensor_def:
|
||||||
|
pysma_sensor = sensor_def[sensor]
|
||||||
|
original_key = pysma_sensor.key
|
||||||
|
elif sensor in pysma.LEGACY_MAP:
|
||||||
|
# If sensor was removed from pysma we will remap it to the new sensor
|
||||||
|
legacy_sensor = pysma.LEGACY_MAP[sensor]
|
||||||
|
pysma_sensor = sensor_def[legacy_sensor["new_sensor"]]
|
||||||
|
original_key = legacy_sensor["old_key"]
|
||||||
|
else:
|
||||||
|
_LOGGER.error("%s does not exist", sensor)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find entity_id using previous format of unique ID
|
||||||
|
entity_id = entity_registry.async_get_entity_id(
|
||||||
|
"sensor", "sma", f"sma-{original_key}-{sensor}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not entity_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Change entity_id to new format using the device serial in entry.unique_id
|
||||||
|
new_unique_id = f"{entry.unique_id}-{pysma_sensor.key}_{pysma_sensor.key_idx}"
|
||||||
|
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up sma from a config entry."""
|
||||||
|
# Init all default sensors
|
||||||
|
sensor_def = pysma.Sensors()
|
||||||
|
|
||||||
|
if entry.source == SOURCE_IMPORT:
|
||||||
|
await _parse_legacy_options(entry, sensor_def)
|
||||||
|
await _migrate_old_unique_ids(hass, entry, sensor_def)
|
||||||
|
|
||||||
|
# Init the SMA interface
|
||||||
|
protocol = "https" if entry.data.get(CONF_SSL) else "http"
|
||||||
|
url = f"{protocol}://{entry.data.get(CONF_HOST)}"
|
||||||
|
verify_ssl = entry.data.get(CONF_VERIFY_SSL)
|
||||||
|
group = entry.data.get(CONF_GROUP)
|
||||||
|
password = entry.data.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
session = async_get_clientsession(hass, verify_ssl=verify_ssl)
|
||||||
|
sma = pysma.SMA(session, url, password, group)
|
||||||
|
|
||||||
|
# Define the coordinator
|
||||||
|
async def async_update_data():
|
||||||
|
"""Update the used SMA sensors."""
|
||||||
|
values = await sma.read(sensor_def)
|
||||||
|
if not values:
|
||||||
|
raise UpdateFailed
|
||||||
|
|
||||||
|
interval = timedelta(
|
||||||
|
seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||||
|
)
|
||||||
|
|
||||||
|
coordinator = DataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name="sma",
|
||||||
|
update_method=async_update_data,
|
||||||
|
update_interval=interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
except ConfigEntryNotReady:
|
||||||
|
await sma.close_session()
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Ensure we logout on shutdown
|
||||||
|
async def async_close_session(event):
|
||||||
|
"""Close the session."""
|
||||||
|
await sma.close_session()
|
||||||
|
|
||||||
|
remove_stop_listener = hass.bus.async_listen_once(
|
||||||
|
EVENT_HOMEASSISTANT_STOP, async_close_session
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
PYSMA_OBJECT: sma,
|
||||||
|
PYSMA_COORDINATOR: coordinator,
|
||||||
|
PYSMA_SENSORS: sensor_def,
|
||||||
|
PYSMA_REMOVE_LISTENER: remove_stop_listener,
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
|
for component in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if unload_ok:
|
||||||
|
data = hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
await data[PYSMA_OBJECT].close_session()
|
||||||
|
data[PYSMA_REMOVE_LISTENER]()
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
141
homeassistant/components/sma/config_flow.py
Normal file
141
homeassistant/components/sma/config_flow.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
"""Config flow for the sma integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import pysma
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core, exceptions
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SENSORS,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
from .const import CONF_CUSTOM, CONF_GROUP, DEVICE_INFO, GROUPS
|
||||||
|
from .const import DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(
|
||||||
|
hass: core.HomeAssistant, data: dict[str, Any]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Validate the user input allows us to connect."""
|
||||||
|
session = async_get_clientsession(hass, verify_ssl=data[CONF_VERIFY_SSL])
|
||||||
|
|
||||||
|
protocol = "https" if data[CONF_SSL] else "http"
|
||||||
|
url = f"{protocol}://{data[CONF_HOST]}"
|
||||||
|
|
||||||
|
sma = pysma.SMA(session, url, data[CONF_PASSWORD], group=data[CONF_GROUP])
|
||||||
|
|
||||||
|
if await sma.new_session() is False:
|
||||||
|
raise InvalidAuth
|
||||||
|
|
||||||
|
device_info = await sma.device_info()
|
||||||
|
await sma.close_session()
|
||||||
|
|
||||||
|
if not device_info:
|
||||||
|
raise CannotRetrieveDeviceInfo
|
||||||
|
|
||||||
|
return device_info
|
||||||
|
|
||||||
|
|
||||||
|
class SmaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for SMA."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
self._data = {
|
||||||
|
CONF_HOST: vol.UNDEFINED,
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
CONF_GROUP: GROUPS[0],
|
||||||
|
CONF_PASSWORD: vol.UNDEFINED,
|
||||||
|
CONF_SENSORS: [],
|
||||||
|
CONF_CUSTOM: {},
|
||||||
|
DEVICE_INFO: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""First step in config flow."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
self._data[CONF_HOST] = user_input[CONF_HOST]
|
||||||
|
self._data[CONF_SSL] = user_input[CONF_SSL]
|
||||||
|
self._data[CONF_VERIFY_SSL] = user_input[CONF_VERIFY_SSL]
|
||||||
|
self._data[CONF_GROUP] = user_input[CONF_GROUP]
|
||||||
|
self._data[CONF_PASSWORD] = user_input[CONF_PASSWORD]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._data[DEVICE_INFO] = await validate_input(self.hass, user_input)
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except CannotRetrieveDeviceInfo:
|
||||||
|
errors["base"] = "cannot_retrieve_device_info"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
await self.async_set_unique_id(self._data[DEVICE_INFO]["serial"])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self._data[CONF_HOST], data=self._data
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST, default=self._data[CONF_HOST]): cv.string,
|
||||||
|
vol.Optional(CONF_SSL, default=self._data[CONF_SSL]): cv.boolean,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_VERIFY_SSL, default=self._data[CONF_VERIFY_SSL]
|
||||||
|
): cv.boolean,
|
||||||
|
vol.Optional(CONF_GROUP, default=self._data[CONF_GROUP]): vol.In(
|
||||||
|
GROUPS
|
||||||
|
),
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(
|
||||||
|
self, import_config: dict[str, Any] | None
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Import a config flow from configuration."""
|
||||||
|
device_info = await validate_input(self.hass, import_config)
|
||||||
|
import_config[DEVICE_INFO] = device_info
|
||||||
|
|
||||||
|
# If unique is configured import was already run
|
||||||
|
# This means remap was already done, so we can abort
|
||||||
|
await self.async_set_unique_id(device_info["serial"])
|
||||||
|
self._abort_if_unique_id_configured(import_config)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=import_config[CONF_HOST], data=import_config
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
||||||
|
|
||||||
|
|
||||||
|
class CannotRetrieveDeviceInfo(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot retrieve the device information."""
|
21
homeassistant/components/sma/const.py
Normal file
21
homeassistant/components/sma/const.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
"""Constants for the sma integration."""
|
||||||
|
|
||||||
|
DOMAIN = "sma"
|
||||||
|
|
||||||
|
PYSMA_COORDINATOR = "coordinator"
|
||||||
|
PYSMA_OBJECT = "pysma"
|
||||||
|
PYSMA_REMOVE_LISTENER = "remove_listener"
|
||||||
|
PYSMA_SENSORS = "pysma_sensors"
|
||||||
|
|
||||||
|
PLATFORMS = ["sensor"]
|
||||||
|
|
||||||
|
CONF_CUSTOM = "custom"
|
||||||
|
CONF_FACTOR = "factor"
|
||||||
|
CONF_GROUP = "group"
|
||||||
|
CONF_KEY = "key"
|
||||||
|
CONF_UNIT = "unit"
|
||||||
|
DEVICE_INFO = "device_info"
|
||||||
|
|
||||||
|
DEFAULT_SCAN_INTERVAL = 5
|
||||||
|
|
||||||
|
GROUPS = ["user", "installer"]
|
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"domain": "sma",
|
"domain": "sma",
|
||||||
"name": "SMA Solar",
|
"name": "SMA Solar",
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/sma",
|
"documentation": "https://www.home-assistant.io/integrations/sma",
|
||||||
"requirements": ["pysma==0.3.5"],
|
"requirements": ["pysma==0.4.3"],
|
||||||
"codeowners": ["@kellerza"]
|
"codeowners": ["@kellerza", "@rklomp"]
|
||||||
}
|
}
|
||||||
|
@ -1,41 +1,51 @@
|
|||||||
"""SMA Solar Webconnect interface."""
|
"""SMA Solar Webconnect interface."""
|
||||||
from datetime import timedelta
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any, Callable, Coroutine
|
||||||
|
|
||||||
import pysma
|
import pysma
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PATH,
|
CONF_PATH,
|
||||||
CONF_SCAN_INTERVAL,
|
|
||||||
CONF_SENSORS,
|
CONF_SENSORS,
|
||||||
CONF_SSL,
|
CONF_SSL,
|
||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.helpers.update_coordinator import (
|
||||||
|
CoordinatorEntity,
|
||||||
|
DataUpdateCoordinator,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_CUSTOM,
|
||||||
|
CONF_FACTOR,
|
||||||
|
CONF_GROUP,
|
||||||
|
CONF_KEY,
|
||||||
|
CONF_UNIT,
|
||||||
|
DEVICE_INFO,
|
||||||
|
DOMAIN,
|
||||||
|
GROUPS,
|
||||||
|
PYSMA_COORDINATOR,
|
||||||
|
PYSMA_SENSORS,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_CUSTOM = "custom"
|
|
||||||
CONF_FACTOR = "factor"
|
|
||||||
CONF_GROUP = "group"
|
|
||||||
CONF_KEY = "key"
|
|
||||||
CONF_UNIT = "unit"
|
|
||||||
|
|
||||||
GROUPS = ["user", "installer"]
|
def _check_sensor_schema(conf: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
|
||||||
|
|
||||||
def _check_sensor_schema(conf):
|
|
||||||
"""Check sensors and attributes are valid."""
|
"""Check sensors and attributes are valid."""
|
||||||
try:
|
try:
|
||||||
valid = [s.name for s in pysma.Sensors()]
|
valid = [s.name for s in pysma.Sensors()]
|
||||||
|
valid += pysma.LEGACY_MAP.keys()
|
||||||
except (ImportError, AttributeError):
|
except (ImportError, AttributeError):
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
@ -83,146 +93,114 @@ PLATFORM_SCHEMA = vol.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_platform(
|
||||||
"""Set up SMA WebConnect sensor."""
|
hass: HomeAssistant,
|
||||||
# Check config again during load - dependency available
|
config: ConfigEntry,
|
||||||
config = _check_sensor_schema(config)
|
async_add_entities: Callable[[], Coroutine],
|
||||||
|
discovery_info=None,
|
||||||
# Init all default sensors
|
) -> None:
|
||||||
sensor_def = pysma.Sensors()
|
"""Import the platform into a config entry."""
|
||||||
|
_LOGGER.warning(
|
||||||
# Sensor from the custom config
|
"Loading SMA via platform setup is deprecated. "
|
||||||
sensor_def.add(
|
"Please remove it from your configuration"
|
||||||
[
|
|
||||||
pysma.Sensor(o[CONF_KEY], n, o[CONF_UNIT], o[CONF_FACTOR], o.get(CONF_PATH))
|
|
||||||
for n, o in config[CONF_CUSTOM].items()
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use all sensors by default
|
hass.async_create_task(
|
||||||
config_sensors = config[CONF_SENSORS]
|
hass.config_entries.flow.async_init(
|
||||||
hass_sensors = []
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||||
used_sensors = []
|
)
|
||||||
|
)
|
||||||
if isinstance(config_sensors, dict): # will be remove from 0.99
|
|
||||||
if not config_sensors: # Use all sensors by default
|
|
||||||
config_sensors = {s.name: [] for s in sensor_def}
|
|
||||||
|
|
||||||
# Prepare all Home Assistant sensor entities
|
|
||||||
for name, attr in config_sensors.items():
|
|
||||||
sub_sensors = [sensor_def[s] for s in attr]
|
|
||||||
hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors))
|
|
||||||
used_sensors.append(name)
|
|
||||||
used_sensors.extend(attr)
|
|
||||||
|
|
||||||
if isinstance(config_sensors, list):
|
|
||||||
if not config_sensors: # Use all sensors by default
|
|
||||||
config_sensors = [s.name for s in sensor_def]
|
|
||||||
used_sensors = list(set(config_sensors + list(config[CONF_CUSTOM])))
|
|
||||||
for sensor in used_sensors:
|
|
||||||
hass_sensors.append(SMAsensor(sensor_def[sensor], []))
|
|
||||||
|
|
||||||
used_sensors = [sensor_def[s] for s in set(used_sensors)]
|
|
||||||
async_add_entities(hass_sensors)
|
|
||||||
|
|
||||||
# Init the SMA interface
|
|
||||||
session = async_get_clientsession(hass, verify_ssl=config[CONF_VERIFY_SSL])
|
|
||||||
grp = config[CONF_GROUP]
|
|
||||||
|
|
||||||
protocol = "https" if config[CONF_SSL] else "http"
|
|
||||||
url = f"{protocol}://{config[CONF_HOST]}"
|
|
||||||
|
|
||||||
sma = pysma.SMA(session, url, config[CONF_PASSWORD], group=grp)
|
|
||||||
|
|
||||||
# Ensure we logout on shutdown
|
|
||||||
async def async_close_session(event):
|
|
||||||
"""Close the session."""
|
|
||||||
await sma.close_session()
|
|
||||||
|
|
||||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_close_session)
|
|
||||||
|
|
||||||
backoff = 0
|
|
||||||
backoff_step = 0
|
|
||||||
|
|
||||||
async def async_sma(event):
|
|
||||||
"""Update all the SMA sensors."""
|
|
||||||
nonlocal backoff, backoff_step
|
|
||||||
if backoff > 1:
|
|
||||||
backoff -= 1
|
|
||||||
return
|
|
||||||
|
|
||||||
values = await sma.read(used_sensors)
|
|
||||||
if not values:
|
|
||||||
try:
|
|
||||||
backoff = [1, 1, 1, 6, 30][backoff_step]
|
|
||||||
backoff_step += 1
|
|
||||||
except IndexError:
|
|
||||||
backoff = 60
|
|
||||||
return
|
|
||||||
backoff_step = 0
|
|
||||||
|
|
||||||
for sensor in hass_sensors:
|
|
||||||
sensor.async_update_values()
|
|
||||||
|
|
||||||
interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=5)
|
|
||||||
async_track_time_interval(hass, async_sma, interval)
|
|
||||||
|
|
||||||
|
|
||||||
class SMAsensor(SensorEntity):
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[], Coroutine],
|
||||||
|
) -> None:
|
||||||
|
"""Set up SMA sensors."""
|
||||||
|
sma_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
coordinator = sma_data[PYSMA_COORDINATOR]
|
||||||
|
used_sensors = sma_data[PYSMA_SENSORS]
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
for sensor in used_sensors:
|
||||||
|
entities.append(
|
||||||
|
SMAsensor(
|
||||||
|
coordinator,
|
||||||
|
config_entry.unique_id,
|
||||||
|
config_entry.data[DEVICE_INFO],
|
||||||
|
sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class SMAsensor(CoordinatorEntity, SensorEntity):
|
||||||
"""Representation of a SMA sensor."""
|
"""Representation of a SMA sensor."""
|
||||||
|
|
||||||
def __init__(self, pysma_sensor, sub_sensors):
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: DataUpdateCoordinator,
|
||||||
|
config_entry_unique_id: str,
|
||||||
|
device_info: dict[str, Any],
|
||||||
|
pysma_sensor: pysma.Sensor,
|
||||||
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
self._sensor = pysma_sensor
|
self._sensor = pysma_sensor
|
||||||
self._sub_sensors = sub_sensors # Can be remove from 0.99
|
self._enabled_default = self._sensor.enabled
|
||||||
|
self._config_entry_unique_id = config_entry_unique_id
|
||||||
|
self._device_info = device_info
|
||||||
|
|
||||||
self._attr = {s.name: "" for s in sub_sensors}
|
# Set sensor enabled to False.
|
||||||
self._state = self._sensor.value
|
# Will be enabled by async_added_to_hass if actually used.
|
||||||
|
self._sensor.enabled = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self) -> str:
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return self._sensor.name
|
return self._sensor.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self._state
|
return self._sensor.value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self) -> str | None:
|
||||||
"""Return the unit the value is expressed in."""
|
"""Return the unit the value is expressed in."""
|
||||||
return self._sensor.unit
|
return self._sensor.unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self): # Can be remove from 0.99
|
def unique_id(self) -> str:
|
||||||
"""Return the state attributes of the sensor."""
|
|
||||||
return self._attr
|
|
||||||
|
|
||||||
@property
|
|
||||||
def poll(self):
|
|
||||||
"""SMA sensors are updated & don't poll."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_update_values(self):
|
|
||||||
"""Update this sensor."""
|
|
||||||
update = False
|
|
||||||
|
|
||||||
for sens in self._sub_sensors: # Can be remove from 0.99
|
|
||||||
newval = f"{sens.value} {sens.unit}"
|
|
||||||
if self._attr[sens.name] != newval:
|
|
||||||
update = True
|
|
||||||
self._attr[sens.name] = newval
|
|
||||||
|
|
||||||
if self._sensor.value != self._state:
|
|
||||||
update = True
|
|
||||||
self._state = self._sensor.value
|
|
||||||
|
|
||||||
if update:
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
"""Return a unique identifier for this sensor."""
|
"""Return a unique identifier for this sensor."""
|
||||||
return f"sma-{self._sensor.key}-{self._sensor.name}"
|
return (
|
||||||
|
f"{self._config_entry_unique_id}-{self._sensor.key}_{self._sensor.key_idx}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> dict[str, Any]:
|
||||||
|
"""Return the device information."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self._config_entry_unique_id)},
|
||||||
|
"name": self._device_info["name"],
|
||||||
|
"manufacturer": self._device_info["manufacturer"],
|
||||||
|
"model": self._device_info["type"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||||
|
return self._enabled_default
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when entity about to be added to hass."""
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
self._sensor.enabled = True
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Run when entity will be removed from hass."""
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
|
self._sensor.enabled = False
|
||||||
|
27
homeassistant/components/sma/strings.json
Normal file
27
homeassistant/components/sma/strings.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"cannot_retrieve_device_info": "Successfully connected, but unable to retrieve the device information",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"group": "Group",
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||||
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
|
},
|
||||||
|
"description": "Enter your SMA device information.",
|
||||||
|
"title": "Set up SMA Solar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
homeassistant/components/sma/translations/en.json
Normal file
27
homeassistant/components/sma/translations/en.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Device is already configured",
|
||||||
|
"already_in_progress": "Configuration flow is already in progress"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"cannot_retrieve_device_info": "Successfully connected, but unable to retrieve the device information",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"group": "Group",
|
||||||
|
"host": "Host",
|
||||||
|
"password": "Password",
|
||||||
|
"ssl": "Uses an SSL certificate",
|
||||||
|
"verify_ssl": "Verify SSL certificate"
|
||||||
|
},
|
||||||
|
"description": "Enter your SMA device information.",
|
||||||
|
"title": "Set up SMA Solar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -210,6 +210,7 @@ FLOWS = [
|
|||||||
"shelly",
|
"shelly",
|
||||||
"shopping_list",
|
"shopping_list",
|
||||||
"simplisafe",
|
"simplisafe",
|
||||||
|
"sma",
|
||||||
"smappee",
|
"smappee",
|
||||||
"smart_meter_texas",
|
"smart_meter_texas",
|
||||||
"smarthab",
|
"smarthab",
|
||||||
|
@ -1708,7 +1708,7 @@ pysignalclirestapi==0.3.4
|
|||||||
pyskyqhub==0.1.3
|
pyskyqhub==0.1.3
|
||||||
|
|
||||||
# homeassistant.components.sma
|
# homeassistant.components.sma
|
||||||
pysma==0.3.5
|
pysma==0.4.3
|
||||||
|
|
||||||
# homeassistant.components.smappee
|
# homeassistant.components.smappee
|
||||||
pysmappee==0.2.17
|
pysmappee==0.2.17
|
||||||
|
@ -935,7 +935,7 @@ pyserial==3.5
|
|||||||
pysignalclirestapi==0.3.4
|
pysignalclirestapi==0.3.4
|
||||||
|
|
||||||
# homeassistant.components.sma
|
# homeassistant.components.sma
|
||||||
pysma==0.3.5
|
pysma==0.4.3
|
||||||
|
|
||||||
# homeassistant.components.smappee
|
# homeassistant.components.smappee
|
||||||
pysmappee==0.2.17
|
pysmappee==0.2.17
|
||||||
|
@ -1 +1,126 @@
|
|||||||
"""SMA tests."""
|
"""Tests for the sma integration."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from homeassistant.components.sma.const import DOMAIN
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
MOCK_DEVICE = {
|
||||||
|
"manufacturer": "SMA",
|
||||||
|
"name": "SMA Device Name",
|
||||||
|
"type": "Sunny Boy 3.6",
|
||||||
|
"serial": "123456789",
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_USER_INPUT = {
|
||||||
|
"host": "1.1.1.1",
|
||||||
|
"ssl": True,
|
||||||
|
"verify_ssl": False,
|
||||||
|
"group": "user",
|
||||||
|
"password": "password",
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_IMPORT = {
|
||||||
|
"platform": "sma",
|
||||||
|
"host": "1.1.1.1",
|
||||||
|
"ssl": True,
|
||||||
|
"verify_ssl": False,
|
||||||
|
"group": "user",
|
||||||
|
"password": "password",
|
||||||
|
"sensors": ["pv_power", "daily_yield", "total_yield", "not_existing_sensors"],
|
||||||
|
"custom": {
|
||||||
|
"yesterday_consumption": {
|
||||||
|
"factor": 1000.0,
|
||||||
|
"key": "6400_00543A01",
|
||||||
|
"unit": "kWh",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_CUSTOM_SENSOR = {
|
||||||
|
"name": "yesterday_consumption",
|
||||||
|
"key": "6400_00543A01",
|
||||||
|
"unit": "kWh",
|
||||||
|
"factor": 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_CUSTOM_SENSOR2 = {
|
||||||
|
"name": "device_type_id",
|
||||||
|
"key": "6800_08822000",
|
||||||
|
"unit": "",
|
||||||
|
"path": '"1"[0].val[0].tag',
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_SETUP_DATA = dict(
|
||||||
|
{
|
||||||
|
"custom": {},
|
||||||
|
"device_info": MOCK_DEVICE,
|
||||||
|
"sensors": [],
|
||||||
|
},
|
||||||
|
**MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
MOCK_CUSTOM_SETUP_DATA = dict(
|
||||||
|
{
|
||||||
|
"custom": {
|
||||||
|
MOCK_CUSTOM_SENSOR["name"]: {
|
||||||
|
"factor": MOCK_CUSTOM_SENSOR["factor"],
|
||||||
|
"key": MOCK_CUSTOM_SENSOR["key"],
|
||||||
|
"path": None,
|
||||||
|
"unit": MOCK_CUSTOM_SENSOR["unit"],
|
||||||
|
},
|
||||||
|
MOCK_CUSTOM_SENSOR2["name"]: {
|
||||||
|
"factor": 1.0,
|
||||||
|
"key": MOCK_CUSTOM_SENSOR2["key"],
|
||||||
|
"path": MOCK_CUSTOM_SENSOR2["path"],
|
||||||
|
"unit": MOCK_CUSTOM_SENSOR2["unit"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"device_info": MOCK_DEVICE,
|
||||||
|
"sensors": [],
|
||||||
|
},
|
||||||
|
**MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
MOCK_LEGACY_ENTRY = er.RegistryEntry(
|
||||||
|
entity_id="sensor.pv_power",
|
||||||
|
unique_id="sma-6100_0046C200-pv_power",
|
||||||
|
platform="sma",
|
||||||
|
unit_of_measurement="W",
|
||||||
|
original_name="pv_power",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def init_integration(hass):
|
||||||
|
"""Create a fake SMA Config Entry."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title=MOCK_DEVICE["name"],
|
||||||
|
unique_id=MOCK_DEVICE["serial"],
|
||||||
|
data=MOCK_CUSTOM_SETUP_DATA,
|
||||||
|
source="import",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.update_coordinator.DataUpdateCoordinator.async_config_entry_first_refresh"
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_validate_input(return_value=MOCK_DEVICE, side_effect=None):
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.sma.config_flow.validate_input",
|
||||||
|
return_value=return_value,
|
||||||
|
side_effect=side_effect,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_async_setup_entry(return_value=True):
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.sma.async_setup_entry",
|
||||||
|
return_value=return_value,
|
||||||
|
)
|
||||||
|
170
tests/components/sma/test_config_flow.py
Normal file
170
tests/components/sma/test_config_flow.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
"""Test the sma config flow."""
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from homeassistant import setup
|
||||||
|
from homeassistant.components.sma.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||||
|
from homeassistant.data_entry_flow import (
|
||||||
|
RESULT_TYPE_ABORT,
|
||||||
|
RESULT_TYPE_CREATE_ENTRY,
|
||||||
|
RESULT_TYPE_FORM,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
MOCK_DEVICE,
|
||||||
|
MOCK_IMPORT,
|
||||||
|
MOCK_LEGACY_ENTRY,
|
||||||
|
MOCK_SETUP_DATA,
|
||||||
|
MOCK_USER_INPUT,
|
||||||
|
_patch_async_setup_entry,
|
||||||
|
_patch_validate_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass, aioclient_mock):
|
||||||
|
"""Test we get the form."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch("pysma.SMA.new_session", return_value=True), patch(
|
||||||
|
"pysma.SMA.device_info", return_value=MOCK_DEVICE
|
||||||
|
), _patch_async_setup_entry() as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == MOCK_USER_INPUT["host"]
|
||||||
|
assert result["data"] == MOCK_SETUP_DATA
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass, aioclient_mock):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
aioclient_mock.get("https://1.1.1.1/data/l10n/en-US.json", exc=aiohttp.ClientError)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_invalid_auth(hass, aioclient_mock):
|
||||||
|
"""Test we handle invalid auth error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("pysma.SMA.new_session", return_value=False):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_retrieve_device_info(hass, aioclient_mock):
|
||||||
|
"""Test we handle cannot retrieve device info error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("pysma.SMA.new_session", return_value=True), patch(
|
||||||
|
"pysma.SMA.read", return_value=False
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "cannot_retrieve_device_info"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_unexpected_exception(hass):
|
||||||
|
"""Test we handle unexpected exception."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with _patch_validate_input(side_effect=Exception):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_already_configured(hass):
|
||||||
|
"""Test starting a flow by user when already configured."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with _patch_validate_input():
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["result"].unique_id == MOCK_DEVICE["serial"]
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with _patch_validate_input():
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
MOCK_USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import(hass):
|
||||||
|
"""Test we can import."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
entity_registry._register_entry(MOCK_LEGACY_ENTRY)
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
with _patch_validate_input():
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=MOCK_IMPORT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == MOCK_USER_INPUT["host"]
|
||||||
|
assert result["data"] == MOCK_IMPORT
|
||||||
|
|
||||||
|
assert MOCK_LEGACY_ENTRY.original_name not in result["data"]["sensors"]
|
||||||
|
assert "pv_power_a" in result["data"]["sensors"]
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(MOCK_LEGACY_ENTRY.entity_id)
|
||||||
|
assert entity.unique_id == f"{MOCK_DEVICE['serial']}-6380_40251E00_0"
|
@ -1,32 +1,21 @@
|
|||||||
"""SMA sensor tests."""
|
"""Test the sma sensor platform."""
|
||||||
from homeassistant.components.sensor import DOMAIN
|
from homeassistant.const import (
|
||||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, VOLT
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
from homeassistant.setup import async_setup_component
|
ENERGY_KILO_WATT_HOUR,
|
||||||
|
POWER_WATT,
|
||||||
|
)
|
||||||
|
|
||||||
from tests.common import assert_setup_component
|
from . import MOCK_CUSTOM_SENSOR, init_integration
|
||||||
|
|
||||||
BASE_CFG = {
|
|
||||||
"platform": "sma",
|
|
||||||
"host": "1.1.1.1",
|
|
||||||
"password": "",
|
|
||||||
"custom": {"my_sensor": {"key": "1234567890123", "unit": VOLT}},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_sma_config(hass):
|
async def test_sensors(hass):
|
||||||
"""Test new config."""
|
"""Test states of the sensors."""
|
||||||
sensors = ["current_consumption"]
|
await init_integration(hass)
|
||||||
|
|
||||||
with assert_setup_component(1):
|
|
||||||
assert await async_setup_component(
|
|
||||||
hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)}
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
state = hass.states.get("sensor.current_consumption")
|
state = hass.states.get("sensor.current_consumption")
|
||||||
assert state
|
assert state
|
||||||
assert ATTR_UNIT_OF_MEASUREMENT in state.attributes
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT
|
||||||
assert "current_consumption" not in state.attributes
|
|
||||||
|
|
||||||
state = hass.states.get("sensor.my_sensor")
|
state = hass.states.get(f"sensor.{MOCK_CUSTOM_SENSOR['name']}")
|
||||||
assert state
|
assert state
|
||||||
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR
|
||||||
|
Loading…
x
Reference in New Issue
Block a user