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/sochain/sensor.py
homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/__init__.py
homeassistant/components/solaredge/sensor.py
homeassistant/components/solaredge_local/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."""
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",
"stringcase==1.2.0"
],
"config_flow": true,
"dependencies": [],
"codeowners": []
}

View File

@ -1,102 +1,39 @@
"""Support for SolarEdge Monitoring API."""
from datetime import timedelta
import logging
import voluptuous as vol
import solaredge
from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.components.sensor import PLATFORM_SCHEMA
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.const import CONF_API_KEY
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
# Config for solaredge monitoring api requests.
CONF_SITE_ID = "site_id"
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]
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)]
),
}
from .const import (
CONF_SITE_ID,
OVERVIEW_UPDATE_DELAY,
DETAILS_UPDATE_DELAY,
INVENTORY_UPDATE_DELAY,
POWER_FLOW_UPDATE_DELAY,
SENSOR_TYPES,
)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the SolarEdge Monitoring API sensor."""
import solaredge
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old configuration."""
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
api = solaredge.Solaredge(api_key)
async def async_setup_entry(hass, entry, async_add_entities):
"""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
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":
_LOGGER.error("SolarEdge site is not active")
return
@ -108,17 +45,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.error("Could not retrieve details from SolarEdge API")
return
# Create sensor factory that will create sensors based on sensor_key.
sensor_factory = SolarEdgeSensorFactory(platform_name, site_id, api)
# Create a new sensor for each sensor type.
sensor_factory = SolarEdgeSensorFactory(entry.title, entry.data[CONF_SITE_ID], api)
entities = []
for sensor_key in config[CONF_MONITORED_CONDITIONS]:
for sensor_key in SENSOR_TYPES:
sensor = sensor_factory.create_sensor(sensor_key)
if sensor is not None:
entities.append(sensor)
add_entities(entities, True)
async_add_entities(entities)
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",
"smartthings",
"smhi",
"solaredge",
"somfy",
"sonos",
"tellduslive",

View File

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

View File

@ -157,6 +157,7 @@ TEST_REQUIREMENTS = (
"simplisafe-python",
"sleepyq",
"smhi-pkg",
"solaredge",
"somecomfort",
"sqlalchemy",
"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"}