Enable SolarEdge config entries (#26282)

* Initial commit for the solaredge configflow

* rerun the hassfest script

* Adding testcases

* Rerun hassfest, problem with black?

* Requirements for the tests

* Remove CONF_MONITORED_CONDITIONS from configuration.yaml

* Remove the options flow strings

* Resolve some comments

* Comments

* More comments

* Move the config from the sensor platform to the component itself

* More comments

* More comments

* Added solaredge __init__

* Added more test to increase coverage
This commit is contained in:
Maikel Punie 2019-09-08 21:49:20 +02:00 committed by Martin Hjelmare
parent 0983367abe
commit 28beebac61
13 changed files with 412 additions and 89 deletions

View file

@ -576,6 +576,7 @@ omit =
homeassistant/components/snmp/* homeassistant/components/snmp/*
homeassistant/components/sochain/sensor.py homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/__init__.py
homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge/sensor.py
homeassistant/components/solaredge_local/sensor.py homeassistant/components/solaredge_local/sensor.py
homeassistant/components/solax/sensor.py homeassistant/components/solax/sensor.py

View file

@ -0,0 +1,21 @@
{
"config": {
"title": "SolarEdge",
"step": {
"user": {
"title": "Define the API parameters for this installation",
"data": {
"name": "The name of this installation",
"site_id": "The SolarEdge site-id",
"api_key": "The API key for this site"
}
}
},
"error": {
"site_exists": "This site_id is already configured"
},
"abort": {
"site_exists": "This site_id is already configured"
}
}
}

View file

@ -1 +1,43 @@
"""The solaredge component.""" """The solaredge component."""
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
from .const import DEFAULT_NAME, DOMAIN, CONF_SITE_ID
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_SITE_ID): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Platform setup, do nothing."""
if DOMAIN not in config:
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config[DOMAIN])
)
)
return True
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Load the saved entities."""
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True

View file

@ -0,0 +1,98 @@
"""Config flow for the SolarEdge platform."""
import solaredge
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify
from .const import DOMAIN, DEFAULT_NAME, CONF_SITE_ID
@callback
def solaredge_entries(hass: HomeAssistant):
"""Return the site_ids for the domain."""
return set(
(entry.data[CONF_SITE_ID])
for entry in hass.config_entries.async_entries(DOMAIN)
)
class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
def __init__(self) -> None:
"""Initialize the config flow."""
self._errors = {}
def _site_in_configuration_exists(self, site_id) -> bool:
"""Return True if site_id exists in configuration."""
if site_id in solaredge_entries(self.hass):
return True
return False
def _check_site(self, site_id, api_key) -> bool:
"""Check if we can connect to the soleredge api service."""
api = solaredge.Solaredge(api_key)
try:
response = api.get_details(site_id)
except (ConnectTimeout, HTTPError):
self._errors[CONF_SITE_ID] = "could_not_connect"
return False
try:
if response["details"]["status"].lower() != "active":
self._errors[CONF_SITE_ID] = "site_not_active"
return False
except KeyError:
self._errors[CONF_SITE_ID] = "api_failure"
return False
return True
async def async_step_user(self, user_input=None):
"""Step when user intializes a integration."""
self._errors = {}
if user_input is not None:
name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME))
if self._site_in_configuration_exists(user_input[CONF_SITE_ID]):
self._errors[CONF_SITE_ID] = "site_exists"
else:
site = user_input[CONF_SITE_ID]
api = user_input[CONF_API_KEY]
can_connect = await self.hass.async_add_executor_job(
self._check_site, site, api
)
if can_connect:
return self.async_create_entry(
title=name, data={CONF_SITE_ID: site, CONF_API_KEY: api}
)
else:
user_input = {}
user_input[CONF_NAME] = DEFAULT_NAME
user_input[CONF_SITE_ID] = ""
user_input[CONF_API_KEY] = ""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(
CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME)
): str,
vol.Required(CONF_SITE_ID, default=user_input[CONF_SITE_ID]): str,
vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str,
}
),
errors=self._errors,
)
async def async_step_import(self, user_input=None):
"""Import a config entry."""
if self._site_in_configuration_exists(user_input[CONF_SITE_ID]):
return self.async_abort(reason="site_exists")
return await self.async_step_user(user_input)

View file

@ -0,0 +1,68 @@
"""Constants for the SolarEdge Monitoring API."""
from datetime import timedelta
from homeassistant.const import POWER_WATT, ENERGY_WATT_HOUR
DOMAIN = "solaredge"
# Config for solaredge monitoring api requests.
CONF_SITE_ID = "site_id"
DEFAULT_NAME = "SolarEdge"
OVERVIEW_UPDATE_DELAY = timedelta(minutes=10)
DETAILS_UPDATE_DELAY = timedelta(hours=12)
INVENTORY_UPDATE_DELAY = timedelta(hours=12)
POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10)
SCAN_INTERVAL = timedelta(minutes=10)
# Supported overview sensor types:
# Key: ['json_key', 'name', unit, icon, default]
SENSOR_TYPES = {
"lifetime_energy": [
"lifeTimeData",
"Lifetime energy",
ENERGY_WATT_HOUR,
"mdi:solar-power",
False,
],
"energy_this_year": [
"lastYearData",
"Energy this year",
ENERGY_WATT_HOUR,
"mdi:solar-power",
False,
],
"energy_this_month": [
"lastMonthData",
"Energy this month",
ENERGY_WATT_HOUR,
"mdi:solar-power",
False,
],
"energy_today": [
"lastDayData",
"Energy today",
ENERGY_WATT_HOUR,
"mdi:solar-power",
False,
],
"current_power": [
"currentPower",
"Current Power",
POWER_WATT,
"mdi:solar-power",
True,
],
"site_details": [None, "Site details", None, None, False],
"meters": ["meters", "Meters", None, None, False],
"sensors": ["sensors", "Sensors", None, None, False],
"gateways": ["gateways", "Gateways", None, None, False],
"batteries": ["batteries", "Batteries", None, None, False],
"inverters": ["inverters", "Inverters", None, None, False],
"power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash", False],
"solar_power": ["PV", "Solar Power", None, "mdi:solar-power", False],
"grid_power": ["GRID", "Grid Power", None, "mdi:power-plug", False],
"storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery", False],
}

View file

@ -6,6 +6,7 @@
"solaredge==0.0.2", "solaredge==0.0.2",
"stringcase==1.2.0" "stringcase==1.2.0"
], ],
"config_flow": true,
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []
} }

View file

@ -1,102 +1,39 @@
"""Support for SolarEdge Monitoring API.""" """Support for SolarEdge Monitoring API."""
from datetime import timedelta
import logging import logging
import solaredge
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_API_KEY
from homeassistant.const import (
CONF_API_KEY,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
POWER_WATT,
ENERGY_WATT_HOUR,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
# Config for solaredge monitoring api requests. from .const import (
CONF_SITE_ID = "site_id" CONF_SITE_ID,
OVERVIEW_UPDATE_DELAY,
OVERVIEW_UPDATE_DELAY = timedelta(minutes=10) DETAILS_UPDATE_DELAY,
DETAILS_UPDATE_DELAY = timedelta(hours=12) INVENTORY_UPDATE_DELAY,
INVENTORY_UPDATE_DELAY = timedelta(hours=12) POWER_FLOW_UPDATE_DELAY,
POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10) SENSOR_TYPES,
SCAN_INTERVAL = timedelta(minutes=10)
# Supported overview sensor types:
# Key: ['json_key', 'name', unit, icon]
SENSOR_TYPES = {
"lifetime_energy": [
"lifeTimeData",
"Lifetime energy",
ENERGY_WATT_HOUR,
"mdi:solar-power",
],
"energy_this_year": [
"lastYearData",
"Energy this year",
ENERGY_WATT_HOUR,
"mdi:solar-power",
],
"energy_this_month": [
"lastMonthData",
"Energy this month",
ENERGY_WATT_HOUR,
"mdi:solar-power",
],
"energy_today": [
"lastDayData",
"Energy today",
ENERGY_WATT_HOUR,
"mdi:solar-power",
],
"current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"],
"site_details": [None, "Site details", None, None],
"meters": ["meters", "Meters", None, None],
"sensors": ["sensors", "Sensors", None, None],
"gateways": ["gateways", "Gateways", None, None],
"batteries": ["batteries", "Batteries", None, None],
"inverters": ["inverters", "Inverters", None, None],
"power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash"],
"solar_power": ["PV", "Solar Power", None, "mdi:solar-power"],
"grid_power": ["GRID", "Grid Power", None, "mdi:power-plug"],
"storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery"],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_SITE_ID): cv.string,
vol.Optional(CONF_NAME, default="SolarEdge"): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=["current_power"]): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]
),
}
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Create the SolarEdge Monitoring API sensor.""" """Old configuration."""
import solaredge pass
api_key = config[CONF_API_KEY]
site_id = config[CONF_SITE_ID]
platform_name = config[CONF_NAME]
# Create new SolarEdge object to retrieve data async def async_setup_entry(hass, entry, async_add_entities):
api = solaredge.Solaredge(api_key) """Add an solarEdge entry."""
# Add the needed sensors to hass
api = solaredge.Solaredge(entry.data[CONF_API_KEY])
# Check if api can be reached and site is active # Check if api can be reached and site is active
try: try:
response = api.get_details(site_id) response = await hass.async_add_executor_job(
api.get_details, entry.data[CONF_SITE_ID]
)
if response["details"]["status"].lower() != "active": if response["details"]["status"].lower() != "active":
_LOGGER.error("SolarEdge site is not active") _LOGGER.error("SolarEdge site is not active")
return return
@ -108,17 +45,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.error("Could not retrieve details from SolarEdge API") _LOGGER.error("Could not retrieve details from SolarEdge API")
return return
# Create sensor factory that will create sensors based on sensor_key. sensor_factory = SolarEdgeSensorFactory(entry.title, entry.data[CONF_SITE_ID], api)
sensor_factory = SolarEdgeSensorFactory(platform_name, site_id, api)
# Create a new sensor for each sensor type.
entities = [] entities = []
for sensor_key in config[CONF_MONITORED_CONDITIONS]: for sensor_key in SENSOR_TYPES:
sensor = sensor_factory.create_sensor(sensor_key) sensor = sensor_factory.create_sensor(sensor_key)
if sensor is not None: if sensor is not None:
entities.append(sensor) entities.append(sensor)
async_add_entities(entities)
add_entities(entities, True)
class SolarEdgeSensorFactory: class SolarEdgeSensorFactory:

View file

@ -0,0 +1,21 @@
{
"config": {
"title": "SolarEdge",
"step": {
"user": {
"title": "Define the API parameters for this installation",
"data": {
"name": "The name of this installation",
"site_id": "The SolarEdge site-id",
"api_key": "The API key for this site"
}
}
},
"error": {
"site_exists": "This site_id is already configured"
},
"abort": {
"site_exists": "This site_id is already configured"
}
}
}

View file

@ -50,6 +50,7 @@ FLOWS = [
"simplisafe", "simplisafe",
"smartthings", "smartthings",
"smhi", "smhi",
"solaredge",
"somfy", "somfy",
"sonos", "sonos",
"tellduslive", "tellduslive",

View file

@ -386,6 +386,9 @@ sleepyq==0.7
# homeassistant.components.smhi # homeassistant.components.smhi
smhi-pkg==1.0.10 smhi-pkg==1.0.10
# homeassistant.components.solaredge
solaredge==0.0.2
# homeassistant.components.honeywell # homeassistant.components.honeywell
somecomfort==0.5.2 somecomfort==0.5.2

View file

@ -157,6 +157,7 @@ TEST_REQUIREMENTS = (
"simplisafe-python", "simplisafe-python",
"sleepyq", "sleepyq",
"smhi-pkg", "smhi-pkg",
"solaredge",
"somecomfort", "somecomfort",
"sqlalchemy", "sqlalchemy",
"srpenergy", "srpenergy",

View file

@ -0,0 +1 @@
"""Tests for the SolarEdge component."""

View file

@ -0,0 +1,132 @@
"""Tests for the SolarEdge config flow."""
import pytest
from requests.exceptions import HTTPError, ConnectTimeout
from unittest.mock import patch, Mock
from homeassistant import data_entry_flow
from homeassistant.components.solaredge import config_flow
from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME
from homeassistant.const import CONF_NAME, CONF_API_KEY
from tests.common import MockConfigEntry
NAME = "solaredge site 1 2 3"
SITE_ID = "1a2b3c4d5e6f7g8h"
API_KEY = "a1b2c3d4e5f6g7h8"
@pytest.fixture(name="test_api")
def mock_controller():
"""Mock a successfull Solaredge API."""
api = Mock()
api.get_details.return_value = {"details": {"status": "active"}}
with patch("solaredge.Solaredge", return_value=api):
yield api
def init_config_flow(hass):
"""Init a configuration flow."""
flow = config_flow.SolarEdgeConfigFlow()
flow.hass = hass
return flow
async def test_user(hass, test_api):
"""Test user config."""
flow = init_config_flow(hass)
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
# tets with all provided
result = await flow.async_step_user(
{CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "solaredge_site_1_2_3"
assert result["data"][CONF_SITE_ID] == SITE_ID
assert result["data"][CONF_API_KEY] == API_KEY
async def test_import(hass, test_api):
"""Test import step."""
flow = init_config_flow(hass)
# import with site_id and api_key
result = await flow.async_step_import(
{CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "solaredge"
assert result["data"][CONF_SITE_ID] == SITE_ID
assert result["data"][CONF_API_KEY] == API_KEY
# import with all
result = await flow.async_step_import(
{CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID, CONF_NAME: NAME}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "solaredge_site_1_2_3"
assert result["data"][CONF_SITE_ID] == SITE_ID
assert result["data"][CONF_API_KEY] == API_KEY
async def test_abort_if_already_setup(hass, test_api):
"""Test we abort if the site_id is already setup."""
flow = init_config_flow(hass)
MockConfigEntry(
domain="solaredge",
data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY},
).add_to_hass(hass)
# import: Should fail, same SITE_ID
result = await flow.async_step_import(
{CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "site_exists"
# user: Should fail, same SITE_ID
result = await flow.async_step_user(
{CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_SITE_ID: "site_exists"}
async def test_asserts(hass, test_api):
"""Test the _site_in_configuration_exists method."""
flow = init_config_flow(hass)
# test with inactive site
test_api.get_details.return_value = {"details": {"status": "NOK"}}
result = await flow.async_step_user(
{CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_SITE_ID: "site_not_active"}
# test with api_failure
test_api.get_details.return_value = {}
result = await flow.async_step_user(
{CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_SITE_ID: "api_failure"}
# test with ConnectionTimeout
test_api.get_details.side_effect = ConnectTimeout()
result = await flow.async_step_user(
{CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_SITE_ID: "could_not_connect"}
# test with HTTPError
test_api.get_details.side_effect = HTTPError()
result = await flow.async_step_user(
{CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {CONF_SITE_ID: "could_not_connect"}