mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
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:
parent
d644b610b7
commit
5a2528b0f1
@ -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
|
||||
|
68
homeassistant/components/tibber/config_flow.py
Normal file
68
homeassistant/components/tibber/config_flow.py
Normal 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={},)
|
5
homeassistant/components/tibber/const.py
Normal file
5
homeassistant/components/tibber/const.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Constants for Tibber integration."""
|
||||
|
||||
DATA_HASS_CONFIG = "tibber_hass_config"
|
||||
DOMAIN = "tibber"
|
||||
MANUFACTURER = "Tibber"
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
22
homeassistant/components/tibber/strings.json
Normal file
22
homeassistant/components/tibber/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -125,6 +125,7 @@ FLOWS = [
|
||||
"tado",
|
||||
"tellduslive",
|
||||
"tesla",
|
||||
"tibber",
|
||||
"toon",
|
||||
"totalconnect",
|
||||
"tplink",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
1
tests/components/tibber/__init__.py
Normal file
1
tests/components/tibber/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for Tibber."""
|
69
tests/components/tibber/test_config_flow.py
Normal file
69
tests/components/tibber/test_config_flow.py
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user