Tibber config flow (#34469)

* tibber config, wip

* read config from yaml

* sync requirements

* style

* add model property

* unique id

* unique id

* Tibber config, unique id

* test doc

* tibber config, update title

* append _el_price

* Update homeassistant/components/tibber/__init__.py

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* unique id

* tibber config flow

* tibber config flow

* fix test for python 3.8

* update test imports

* move _async_current_entries

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Daniel Høyer Iversen 2020-05-03 14:40:19 +02:00 committed by GitHub
parent d644b610b7
commit 5a2528b0f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 268 additions and 30 deletions

View File

@ -6,16 +6,19 @@ import aiohttp
import tibber
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_call_later
from homeassistant.util import dt as dt_util
DOMAIN = "tibber"
from .const import DATA_HASS_CONFIG, DOMAIN
FIRST_RETRY_TIME = 60
PLATFORMS = [
"sensor",
]
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
@ -25,12 +28,30 @@ CONFIG_SCHEMA = vol.Schema(
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME):
async def async_setup(hass, config):
"""Set up the Tibber component."""
conf = config.get(DOMAIN)
hass.data[DATA_HASS_CONFIG] = config
if DOMAIN not in config:
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=config[DOMAIN],
)
)
return True
async def async_setup_entry(hass, entry):
"""Set up a config entry."""
tibber_connection = tibber.Tibber(
conf[CONF_ACCESS_TOKEN],
access_token=entry.data[CONF_ACCESS_TOKEN],
websession=async_get_clientsession(hass),
time_zone=dt_util.DEFAULT_TIME_ZONE,
)
@ -44,15 +65,7 @@ async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME):
try:
await tibber_connection.update_info()
except asyncio.TimeoutError:
_LOGGER.warning("Timeout connecting to Tibber. Will retry in %ss", retry_delay)
async def retry_setup(now):
"""Retry setup if a timeout happens on Tibber API."""
await async_setup(hass, config, retry_delay=min(2 * retry_delay, 900))
async_call_later(hass, retry_delay, retry_setup)
return True
raise ConfigEntryNotReady
except aiohttp.ClientError as err:
_LOGGER.error("Error connecting to Tibber: %s ", err)
return False
@ -60,7 +73,34 @@ async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME):
_LOGGER.error("Failed to login. %s", exp)
return False
for component in ["sensor", "notify"]:
discovery.load_platform(hass, component, DOMAIN, {CONF_NAME: DOMAIN}, config)
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
hass.async_create_task(
discovery.async_load_platform(
hass, "notify", DOMAIN, {CONF_NAME: DOMAIN}, hass.data[DATA_HASS_CONFIG]
)
)
return True
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
tibber_connection = hass.data.get(DOMAIN)
await tibber_connection.rt_disconnect()
return unload_ok

View File

@ -0,0 +1,68 @@
"""Adds config flow for Tibber integration."""
import asyncio
import logging
import aiohttp
import tibber
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Tibber integration."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
async def async_step_import(self, import_info):
"""Set the config entry up from yaml."""
return await self.async_step_user(import_info)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if self._async_current_entries():
return self.async_abort(reason="already_configured")
if user_input is not None:
access_token = user_input[CONF_ACCESS_TOKEN].replace(" ", "")
tibber_connection = tibber.Tibber(
access_token=access_token,
websession=async_get_clientsession(self.hass),
)
errors = {}
try:
await tibber_connection.update_info()
except asyncio.TimeoutError:
errors[CONF_ACCESS_TOKEN] = "timeout"
except aiohttp.ClientError:
errors[CONF_ACCESS_TOKEN] = "connection_error"
except tibber.InvalidLogin:
errors[CONF_ACCESS_TOKEN] = "invalid_access_token"
if errors:
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors,
)
unique_id = tibber_connection.user_id
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=tibber_connection.name, data={CONF_ACCESS_TOKEN: access_token},
)
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors={},)

View File

@ -0,0 +1,5 @@
"""Constants for Tibber integration."""
DATA_HASS_CONFIG = "tibber_hass_config"
DOMAIN = "tibber"
MANUFACTURER = "Tibber"

View File

@ -2,7 +2,8 @@
"domain": "tibber",
"name": "Tibber",
"documentation": "https://www.home-assistant.io/integrations/tibber",
"requirements": ["pyTibber==0.13.8"],
"requirements": ["pyTibber==0.14.0"],
"codeowners": ["@danielhiversen"],
"quality_scale": "silver"
"quality_scale": "silver",
"config_flow": true
}

View File

@ -10,7 +10,7 @@ from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle, dt as dt_util
from . import DOMAIN as TIBBER_DOMAIN
from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
@ -20,10 +20,8 @@ SCAN_INTERVAL = timedelta(minutes=1)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the Tibber sensor."""
if discovery_info is None:
return
tibber_connection = hass.data.get(TIBBER_DOMAIN)
@ -66,11 +64,34 @@ class TibberSensor(Entity):
"""Return the state attributes."""
return self._device_state_attributes
@property
def model(self):
"""Return the model of the sensor."""
return None
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def device_id(self):
"""Return the ID of the physical device this sensor is part of."""
home = self._tibber_home.info["viewer"]["home"]
return home["meteringPointData"]["consumptionEan"]
@property
def device_info(self):
"""Return the device_info of the device."""
device_info = {
"identifiers": {(TIBBER_DOMAIN, self.device_id)},
"name": self.name,
"manufacturer": MANUFACTURER,
}
if self.model is not None:
device_info["model"] = self.model
return device_info
class TibberSensorElPrice(TibberSensor):
"""Representation of a Tibber sensor for el price."""
@ -112,6 +133,11 @@ class TibberSensorElPrice(TibberSensor):
"""Return the name of the sensor."""
return f"Electricity price {self._name}"
@property
def model(self):
"""Return the model of the sensor."""
return "Price Sensor"
@property
def icon(self):
"""Return the icon to use in the frontend."""
@ -125,8 +151,7 @@ class TibberSensorElPrice(TibberSensor):
@property
def unique_id(self):
"""Return a unique ID."""
home = self._tibber_home.info["viewer"]["home"]
return home["meteringPointData"]["consumptionEan"]
return self.device_id
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def _fetch_data(self):
@ -149,7 +174,7 @@ class TibberSensorRT(TibberSensor):
"""Representation of a Tibber sensor for real time consumption."""
async def async_added_to_hass(self):
"""Start unavailability tracking."""
"""Start listen for real time data."""
await self._tibber_home.rt_subscribe(self.hass.loop, self._async_callback)
async def _async_callback(self, payload):
@ -177,6 +202,11 @@ class TibberSensorRT(TibberSensor):
"""Return True if entity is available."""
return self._tibber_home.rt_subscription_running
@property
def model(self):
"""Return the model of the sensor."""
return "Tibber Pulse"
@property
def name(self):
"""Return the name of the sensor."""
@ -200,6 +230,4 @@ class TibberSensorRT(TibberSensor):
@property
def unique_id(self):
"""Return a unique ID."""
home = self._tibber_home.info["viewer"]["home"]
_id = home["meteringPointData"]["consumptionEan"]
return f"{_id}_rt_consumption"
return f"{self.device_id}_rt_consumption"

View File

@ -0,0 +1,22 @@
{
"title": "Tibber",
"config": {
"abort": {
"already_configured": "A Tibber account is already configured."
},
"error": {
"timeout": "Timeout connecting to Tibber",
"connection_error": "Error connecting to Tibber",
"invalid_access_token": "Invalid access token"
},
"step": {
"user": {
"data": {
"access_token": "Access token"
},
"description": "Enter your access token from https://developer.tibber.com/settings/accesstoken",
"title": "Tibber"
}
}
}
}

View File

@ -125,6 +125,7 @@ FLOWS = [
"tado",
"tellduslive",
"tesla",
"tibber",
"toon",
"totalconnect",
"tplink",

View File

@ -1158,7 +1158,7 @@ pyRFXtrx==0.25
# pySwitchmate==0.4.6
# homeassistant.components.tibber
pyTibber==0.13.8
pyTibber==0.14.0
# homeassistant.components.dlink
pyW215==0.7.0

View File

@ -475,6 +475,9 @@ pyMetno==0.4.6
# homeassistant.components.rfxtrx
pyRFXtrx==0.25
# homeassistant.components.tibber
pyTibber==0.14.0
# homeassistant.components.nextbus
py_nextbusnext==0.1.4

View File

@ -0,0 +1 @@
"""Tests for Tibber."""

View File

@ -0,0 +1,69 @@
"""Tests for Tibber config flow."""
import pytest
from homeassistant.components.tibber.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN
from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch
from tests.common import MockConfigEntry
@pytest.fixture(name="tibber_setup", autouse=True)
def tibber_setup_fixture():
"""Patch tibber setup entry."""
with patch("homeassistant.components.tibber.async_setup_entry", return_value=True):
yield
async def test_show_config_form(hass):
"""Test show configuration form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
async def test_create_entry(hass):
"""Test create entry from user input."""
test_data = {
CONF_ACCESS_TOKEN: "valid",
}
unique_user_id = "unique_user_id"
title = "title"
tibber_mock = MagicMock()
type(tibber_mock).update_info = AsyncMock(return_value=True)
type(tibber_mock).user_id = PropertyMock(return_value=unique_user_id)
type(tibber_mock).name = PropertyMock(return_value=title)
with patch("tibber.Tibber", return_value=tibber_mock):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=test_data
)
assert result["type"] == "create_entry"
assert result["title"] == title
assert result["data"] == test_data
async def test_flow_entry_already_exists(hass):
"""Test user input for config_entry that already exists."""
first_entry = MockConfigEntry(
domain="tibber", data={CONF_ACCESS_TOKEN: "valid"}, unique_id="tibber",
)
first_entry.add_to_hass(hass)
test_data = {
CONF_ACCESS_TOKEN: "valid",
}
with patch("tibber.Tibber.update_info", return_value=None):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=test_data
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"