mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 23:27:37 +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 tibber
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP
|
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 import discovery
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
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_call_later
|
|
||||||
from homeassistant.util import dt as dt_util
|
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(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
|
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
|
||||||
@ -25,12 +28,30 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_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."""
|
"""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(
|
tibber_connection = tibber.Tibber(
|
||||||
conf[CONF_ACCESS_TOKEN],
|
access_token=entry.data[CONF_ACCESS_TOKEN],
|
||||||
websession=async_get_clientsession(hass),
|
websession=async_get_clientsession(hass),
|
||||||
time_zone=dt_util.DEFAULT_TIME_ZONE,
|
time_zone=dt_util.DEFAULT_TIME_ZONE,
|
||||||
)
|
)
|
||||||
@ -44,15 +65,7 @@ async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME):
|
|||||||
try:
|
try:
|
||||||
await tibber_connection.update_info()
|
await tibber_connection.update_info()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.warning("Timeout connecting to Tibber. Will retry in %ss", retry_delay)
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
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
|
|
||||||
except aiohttp.ClientError as err:
|
except aiohttp.ClientError as err:
|
||||||
_LOGGER.error("Error connecting to Tibber: %s ", err)
|
_LOGGER.error("Error connecting to Tibber: %s ", err)
|
||||||
return False
|
return False
|
||||||
@ -60,7 +73,34 @@ async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME):
|
|||||||
_LOGGER.error("Failed to login. %s", exp)
|
_LOGGER.error("Failed to login. %s", exp)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for component in ["sensor", "notify"]:
|
for component in PLATFORMS:
|
||||||
discovery.load_platform(hass, component, DOMAIN, {CONF_NAME: DOMAIN}, config)
|
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
|
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",
|
"domain": "tibber",
|
||||||
"name": "Tibber",
|
"name": "Tibber",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||||
"requirements": ["pyTibber==0.13.8"],
|
"requirements": ["pyTibber==0.14.0"],
|
||||||
"codeowners": ["@danielhiversen"],
|
"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.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle, dt as dt_util
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -20,10 +20,8 @@ SCAN_INTERVAL = timedelta(minutes=1)
|
|||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
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."""
|
"""Set up the Tibber sensor."""
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
tibber_connection = hass.data.get(TIBBER_DOMAIN)
|
tibber_connection = hass.data.get(TIBBER_DOMAIN)
|
||||||
|
|
||||||
@ -66,11 +64,34 @@ class TibberSensor(Entity):
|
|||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return self._device_state_attributes
|
return self._device_state_attributes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
"""Return the model of the sensor."""
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
return self._state
|
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):
|
class TibberSensorElPrice(TibberSensor):
|
||||||
"""Representation of a Tibber sensor for el price."""
|
"""Representation of a Tibber sensor for el price."""
|
||||||
@ -112,6 +133,11 @@ class TibberSensorElPrice(TibberSensor):
|
|||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return f"Electricity price {self._name}"
|
return f"Electricity price {self._name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
"""Return the model of the sensor."""
|
||||||
|
return "Price Sensor"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
"""Return the icon to use in the frontend."""
|
"""Return the icon to use in the frontend."""
|
||||||
@ -125,8 +151,7 @@ class TibberSensorElPrice(TibberSensor):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
home = self._tibber_home.info["viewer"]["home"]
|
return self.device_id
|
||||||
return home["meteringPointData"]["consumptionEan"]
|
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
async def _fetch_data(self):
|
async def _fetch_data(self):
|
||||||
@ -149,7 +174,7 @@ class TibberSensorRT(TibberSensor):
|
|||||||
"""Representation of a Tibber sensor for real time consumption."""
|
"""Representation of a Tibber sensor for real time consumption."""
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
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)
|
await self._tibber_home.rt_subscribe(self.hass.loop, self._async_callback)
|
||||||
|
|
||||||
async def _async_callback(self, payload):
|
async def _async_callback(self, payload):
|
||||||
@ -177,6 +202,11 @@ class TibberSensorRT(TibberSensor):
|
|||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._tibber_home.rt_subscription_running
|
return self._tibber_home.rt_subscription_running
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
"""Return the model of the sensor."""
|
||||||
|
return "Tibber Pulse"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
@ -200,6 +230,4 @@ class TibberSensorRT(TibberSensor):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
home = self._tibber_home.info["viewer"]["home"]
|
return f"{self.device_id}_rt_consumption"
|
||||||
_id = home["meteringPointData"]["consumptionEan"]
|
|
||||||
return f"{_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",
|
"tado",
|
||||||
"tellduslive",
|
"tellduslive",
|
||||||
"tesla",
|
"tesla",
|
||||||
|
"tibber",
|
||||||
"toon",
|
"toon",
|
||||||
"totalconnect",
|
"totalconnect",
|
||||||
"tplink",
|
"tplink",
|
||||||
|
@ -1158,7 +1158,7 @@ pyRFXtrx==0.25
|
|||||||
# pySwitchmate==0.4.6
|
# pySwitchmate==0.4.6
|
||||||
|
|
||||||
# homeassistant.components.tibber
|
# homeassistant.components.tibber
|
||||||
pyTibber==0.13.8
|
pyTibber==0.14.0
|
||||||
|
|
||||||
# homeassistant.components.dlink
|
# homeassistant.components.dlink
|
||||||
pyW215==0.7.0
|
pyW215==0.7.0
|
||||||
|
@ -475,6 +475,9 @@ pyMetno==0.4.6
|
|||||||
# homeassistant.components.rfxtrx
|
# homeassistant.components.rfxtrx
|
||||||
pyRFXtrx==0.25
|
pyRFXtrx==0.25
|
||||||
|
|
||||||
|
# homeassistant.components.tibber
|
||||||
|
pyTibber==0.14.0
|
||||||
|
|
||||||
# homeassistant.components.nextbus
|
# homeassistant.components.nextbus
|
||||||
py_nextbusnext==0.1.4
|
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