mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
add authentication
This commit is contained in:
parent
ef38bf75b5
commit
73fa6c18a7
9 changed files with 137 additions and 61 deletions
|
|
@ -10,13 +10,13 @@ from __future__ import annotations
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.loader import async_get_loaded_integration
|
from homeassistant.loader import async_get_loaded_integration
|
||||||
|
|
||||||
from .api import TibberPricesApiClient
|
from .api import TibberPricesApiClient
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
from .coordinator import BlueprintDataUpdateCoordinator
|
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||||
from .data import TibberPricesData
|
from .data import TibberPricesData
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -37,7 +37,7 @@ async def async_setup_entry(
|
||||||
entry: TibberPricesConfigEntry,
|
entry: TibberPricesConfigEntry,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up this integration using UI."""
|
"""Set up this integration using UI."""
|
||||||
coordinator = BlueprintDataUpdateCoordinator(
|
coordinator = TibberPricesDataUpdateCoordinator(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
logger=LOGGER,
|
logger=LOGGER,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
|
|
@ -45,8 +45,7 @@ async def async_setup_entry(
|
||||||
)
|
)
|
||||||
entry.runtime_data = TibberPricesData(
|
entry.runtime_data = TibberPricesData(
|
||||||
client=TibberPricesApiClient(
|
client=TibberPricesApiClient(
|
||||||
username=entry.data[CONF_USERNAME],
|
access_token=entry.data[CONF_ACCESS_TOKEN],
|
||||||
password=entry.data[CONF_PASSWORD],
|
|
||||||
session=async_get_clientsession(hass),
|
session=async_get_clientsession(hass),
|
||||||
),
|
),
|
||||||
integration=async_get_loaded_integration(hass, entry.domain),
|
integration=async_get_loaded_integration(hass, entry.domain),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
"""Sample API Client."""
|
"""Tibber API Client."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -7,17 +7,26 @@ from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
from homeassistant.const import __version__ as ha_version
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesApiClientError(Exception):
|
class TibberPricesApiClientError(Exception):
|
||||||
"""Exception to indicate a general API error."""
|
"""Exception to indicate a general API error."""
|
||||||
|
|
||||||
|
UNKNOWN_ERROR = "Unknown GraphQL error"
|
||||||
|
MALFORMED_ERROR = "Malformed GraphQL error: {error}"
|
||||||
|
GRAPHQL_ERROR = "GraphQL error: {message}"
|
||||||
|
GENERIC_ERROR = "Something went wrong! {exception}"
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesApiClientCommunicationError(
|
class TibberPricesApiClientCommunicationError(
|
||||||
TibberPricesApiClientError,
|
TibberPricesApiClientError,
|
||||||
):
|
):
|
||||||
"""Exception to indicate a communication error."""
|
"""Exception to indicate a communication error."""
|
||||||
|
|
||||||
|
TIMEOUT_ERROR = "Timeout error fetching information - {exception}"
|
||||||
|
CONNECTION_ERROR = "Error fetching information - {exception}"
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesApiClientAuthenticationError(
|
class TibberPricesApiClientAuthenticationError(
|
||||||
TibberPricesApiClientError,
|
TibberPricesApiClientError,
|
||||||
|
|
@ -35,67 +44,143 @@ def _verify_response_or_raise(response: aiohttp.ClientResponse) -> None:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
async def _verify_graphql_response(response_json: dict) -> None:
|
||||||
|
"""
|
||||||
|
Verify the GraphQL response for errors.
|
||||||
|
|
||||||
|
GraphQL errors follow this structure:
|
||||||
|
{
|
||||||
|
"errors": [{
|
||||||
|
"message": "error message",
|
||||||
|
"locations": [...],
|
||||||
|
"path": [...],
|
||||||
|
"extensions": {
|
||||||
|
"code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if "errors" not in response_json:
|
||||||
|
return
|
||||||
|
|
||||||
|
errors = response_json["errors"]
|
||||||
|
if not errors:
|
||||||
|
raise TibberPricesApiClientError(TibberPricesApiClientError.UNKNOWN_ERROR)
|
||||||
|
|
||||||
|
error = errors[0] # Take first error
|
||||||
|
if not isinstance(error, dict):
|
||||||
|
raise TibberPricesApiClientError(
|
||||||
|
TibberPricesApiClientError.MALFORMED_ERROR.format(error=error)
|
||||||
|
)
|
||||||
|
|
||||||
|
message = error.get("message", "Unknown error")
|
||||||
|
extensions = error.get("extensions", {})
|
||||||
|
|
||||||
|
# Check for authentication errors first
|
||||||
|
if extensions.get("code") == "UNAUTHENTICATED":
|
||||||
|
raise TibberPricesApiClientAuthenticationError(message)
|
||||||
|
|
||||||
|
# Handle all other GraphQL errors
|
||||||
|
raise TibberPricesApiClientError(
|
||||||
|
TibberPricesApiClientError.GRAPHQL_ERROR.format(message=message)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesApiClient:
|
class TibberPricesApiClient:
|
||||||
"""Sample API Client."""
|
"""Tibber API Client."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
username: str,
|
access_token: str,
|
||||||
password: str,
|
|
||||||
session: aiohttp.ClientSession,
|
session: aiohttp.ClientSession,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Sample API Client."""
|
"""Tibber API Client."""
|
||||||
self._username = username
|
self._access_token = access_token
|
||||||
self._password = password
|
|
||||||
self._session = session
|
self._session = session
|
||||||
|
|
||||||
|
async def async_test_connection(self) -> Any:
|
||||||
|
"""Test connection to the API."""
|
||||||
|
return await self._api_wrapper(
|
||||||
|
data={
|
||||||
|
"query": """
|
||||||
|
query {
|
||||||
|
viewer {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_data(self) -> Any:
|
async def async_get_data(self) -> Any:
|
||||||
"""Get data from the API."""
|
"""Get data from the API."""
|
||||||
return await self._api_wrapper(
|
return await self._api_wrapper(
|
||||||
method="get",
|
data={
|
||||||
url="https://jsonplaceholder.typicode.com/posts/1",
|
"query": """
|
||||||
|
query {
|
||||||
|
viewer {
|
||||||
|
homes {
|
||||||
|
timeZone
|
||||||
|
currentSubscription {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_set_title(self, value: str) -> Any:
|
async def async_set_title(self, value: str) -> Any:
|
||||||
"""Get data from the API."""
|
"""Get data from the API."""
|
||||||
return await self._api_wrapper(
|
return await self._api_wrapper(
|
||||||
method="patch",
|
|
||||||
url="https://jsonplaceholder.typicode.com/posts/1",
|
|
||||||
data={"title": value},
|
data={"title": value},
|
||||||
headers={"Content-type": "application/json; charset=UTF-8"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _api_wrapper(
|
async def _api_wrapper(
|
||||||
self,
|
self,
|
||||||
method: str,
|
|
||||||
url: str,
|
|
||||||
data: dict | None = None,
|
data: dict | None = None,
|
||||||
headers: dict | None = None,
|
headers: dict | None = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Get information from the API."""
|
"""Get information from the API."""
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(10):
|
async with async_timeout.timeout(10):
|
||||||
|
headers = headers or {}
|
||||||
|
if headers.get("Authorization") is None:
|
||||||
|
headers["Authorization"] = f"Bearer {self._access_token}"
|
||||||
|
if headers.get("Accept") is None:
|
||||||
|
headers["Accept"] = "application/json"
|
||||||
|
if headers.get("User-Agent") is None:
|
||||||
|
headers["User-Agent"] = (
|
||||||
|
f"HomeAssistant/{ha_version} (tibber_prices; +https://github.com/jpawlowski/hass.tibber_prices/)"
|
||||||
|
)
|
||||||
response = await self._session.request(
|
response = await self._session.request(
|
||||||
method=method,
|
method="POST",
|
||||||
url=url,
|
url="https://api.tibber.com/v1-beta/gql",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
json=data,
|
json=data,
|
||||||
)
|
)
|
||||||
_verify_response_or_raise(response)
|
_verify_response_or_raise(response)
|
||||||
return await response.json()
|
response_json = await response.json()
|
||||||
|
await _verify_graphql_response(response_json)
|
||||||
|
return response_json
|
||||||
|
|
||||||
except TimeoutError as exception:
|
except TimeoutError as exception:
|
||||||
msg = f"Timeout error fetching information - {exception}"
|
|
||||||
raise TibberPricesApiClientCommunicationError(
|
raise TibberPricesApiClientCommunicationError(
|
||||||
msg,
|
TibberPricesApiClientCommunicationError.TIMEOUT_ERROR.format(
|
||||||
|
exception=exception
|
||||||
|
)
|
||||||
) from exception
|
) from exception
|
||||||
except (aiohttp.ClientError, socket.gaierror) as exception:
|
except (aiohttp.ClientError, socket.gaierror) as exception:
|
||||||
msg = f"Error fetching information - {exception}"
|
|
||||||
raise TibberPricesApiClientCommunicationError(
|
raise TibberPricesApiClientCommunicationError(
|
||||||
msg,
|
TibberPricesApiClientCommunicationError.CONNECTION_ERROR.format(
|
||||||
|
exception=exception
|
||||||
|
)
|
||||||
) from exception
|
) from exception
|
||||||
|
except TibberPricesApiClientAuthenticationError:
|
||||||
|
# Re-raise authentication errors directly
|
||||||
|
raise
|
||||||
except Exception as exception: # pylint: disable=broad-except
|
except Exception as exception: # pylint: disable=broad-except
|
||||||
msg = f"Something really wrong happened! - {exception}"
|
|
||||||
raise TibberPricesApiClientError(
|
raise TibberPricesApiClientError(
|
||||||
msg,
|
TibberPricesApiClientError.GENERIC_ERROR.format(exception=exception)
|
||||||
) from exception
|
) from exception
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .coordinator import BlueprintDataUpdateCoordinator
|
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||||
from .data import TibberPricesConfigEntry
|
from .data import TibberPricesConfigEntry
|
||||||
|
|
||||||
ENTITY_DESCRIPTIONS = (
|
ENTITY_DESCRIPTIONS = (
|
||||||
|
|
@ -48,7 +48,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: BlueprintDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
entity_description: BinarySensorEntityDescription,
|
entity_description: BinarySensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the binary_sensor class."""
|
"""Initialize the binary_sensor class."""
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
"""Adds config flow for Blueprint."""
|
"""Adds config flow for tibber_prices."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
from homeassistant.helpers import selector
|
from homeassistant.helpers import selector
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
from slugify import slugify
|
from slugify import slugify
|
||||||
|
|
@ -19,7 +19,7 @@ from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Config flow for Blueprint."""
|
"""Config flow for tibber_prices."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
|
|
@ -31,10 +31,7 @@ class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
_errors = {}
|
_errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
await self._test_credentials(
|
await self._test_credentials(access_token=user_input[CONF_ACCESS_TOKEN])
|
||||||
username=user_input[CONF_USERNAME],
|
|
||||||
password=user_input[CONF_PASSWORD],
|
|
||||||
)
|
|
||||||
except TibberPricesApiClientAuthenticationError as exception:
|
except TibberPricesApiClientAuthenticationError as exception:
|
||||||
LOGGER.warning(exception)
|
LOGGER.warning(exception)
|
||||||
_errors["base"] = "auth"
|
_errors["base"] = "auth"
|
||||||
|
|
@ -49,11 +46,11 @@ class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
## Do NOT use this in production code
|
## Do NOT use this in production code
|
||||||
## The unique_id should never be something that can change
|
## The unique_id should never be something that can change
|
||||||
## https://developers.home-assistant.io/docs/config_entries_config_flow_handler#unique-ids
|
## https://developers.home-assistant.io/docs/config_entries_config_flow_handler#unique-ids
|
||||||
unique_id=slugify(user_input[CONF_USERNAME])
|
unique_id=slugify(user_input[CONF_ACCESS_TOKEN])
|
||||||
)
|
)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_input[CONF_USERNAME],
|
title=user_input[CONF_ACCESS_TOKEN],
|
||||||
data=user_input,
|
data=user_input,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -62,28 +59,24 @@ class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_USERNAME,
|
CONF_ACCESS_TOKEN,
|
||||||
default=(user_input or {}).get(CONF_USERNAME, vol.UNDEFINED),
|
default=(user_input or {}).get(
|
||||||
|
CONF_ACCESS_TOKEN, vol.UNDEFINED
|
||||||
|
),
|
||||||
): selector.TextSelector(
|
): selector.TextSelector(
|
||||||
selector.TextSelectorConfig(
|
selector.TextSelectorConfig(
|
||||||
type=selector.TextSelectorType.TEXT,
|
type=selector.TextSelectorType.TEXT,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
vol.Required(CONF_PASSWORD): selector.TextSelector(
|
|
||||||
selector.TextSelectorConfig(
|
|
||||||
type=selector.TextSelectorType.PASSWORD,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
errors=_errors,
|
errors=_errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _test_credentials(self, username: str, password: str) -> None:
|
async def _test_credentials(self, access_token: str) -> None:
|
||||||
"""Validate credentials."""
|
"""Validate credentials."""
|
||||||
client = TibberPricesApiClient(
|
client = TibberPricesApiClient(
|
||||||
username=username,
|
access_token=access_token,
|
||||||
password=password,
|
|
||||||
session=async_create_clientsession(self.hass),
|
session=async_create_clientsession(self.hass),
|
||||||
)
|
)
|
||||||
await client.async_get_data()
|
await client.async_test_connection()
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
|
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
|
||||||
class BlueprintDataUpdateCoordinator(DataUpdateCoordinator):
|
class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Class to manage fetching data from the API."""
|
"""Class to manage fetching data from the API."""
|
||||||
|
|
||||||
config_entry: TibberPricesConfigEntry
|
config_entry: TibberPricesConfigEntry
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
||||||
from homeassistant.loader import Integration
|
from homeassistant.loader import Integration
|
||||||
|
|
||||||
from .api import TibberPricesApiClient
|
from .api import TibberPricesApiClient
|
||||||
from .coordinator import BlueprintDataUpdateCoordinator
|
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
type TibberPricesConfigEntry = ConfigEntry[TibberPricesData]
|
type TibberPricesConfigEntry = ConfigEntry[TibberPricesData]
|
||||||
|
|
@ -21,5 +21,5 @@ class TibberPricesData:
|
||||||
"""Data for the Blueprint integration."""
|
"""Data for the Blueprint integration."""
|
||||||
|
|
||||||
client: TibberPricesApiClient
|
client: TibberPricesApiClient
|
||||||
coordinator: BlueprintDataUpdateCoordinator
|
coordinator: TibberPricesDataUpdateCoordinator
|
||||||
integration: Integration
|
integration: Integration
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import ATTRIBUTION
|
from .const import ATTRIBUTION
|
||||||
from .coordinator import BlueprintDataUpdateCoordinator
|
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesEntity(CoordinatorEntity[BlueprintDataUpdateCoordinator]):
|
class TibberPricesEntity(CoordinatorEntity[TibberPricesDataUpdateCoordinator]):
|
||||||
"""BlueprintEntity class."""
|
"""BlueprintEntity class."""
|
||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
_attr_attribution = ATTRIBUTION
|
||||||
|
|
||||||
def __init__(self, coordinator: BlueprintDataUpdateCoordinator) -> None:
|
def __init__(self, coordinator: TibberPricesDataUpdateCoordinator) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_unique_id = coordinator.config_entry.entry_id
|
self._attr_unique_id = coordinator.config_entry.entry_id
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .coordinator import BlueprintDataUpdateCoordinator
|
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||||
from .data import TibberPricesConfigEntry
|
from .data import TibberPricesConfigEntry
|
||||||
|
|
||||||
ENTITY_DESCRIPTIONS = (
|
ENTITY_DESCRIPTIONS = (
|
||||||
|
|
@ -44,7 +44,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: BlueprintDataUpdateCoordinator,
|
coordinator: TibberPricesDataUpdateCoordinator,
|
||||||
entity_description: SensorEntityDescription,
|
entity_description: SensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor class."""
|
"""Initialize the sensor class."""
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,12 @@
|
||||||
"user": {
|
"user": {
|
||||||
"description": "If you need help with the configuration have a look here: https://github.com/jpawlowski/hass.tibber_prices",
|
"description": "If you need help with the configuration have a look here: https://github.com/jpawlowski/hass.tibber_prices",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "Username",
|
"access_token": "Tibber Access Token"
|
||||||
"password": "Password"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"auth": "Username/Password is wrong.",
|
"auth": "Tibber Access Token is wrong.",
|
||||||
"connection": "Unable to connect to the server.",
|
"connection": "Unable to connect to the server.",
|
||||||
"unknown": "Unknown error occurred."
|
"unknown": "Unknown error occurred."
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue