Add authentication support to Nightscout (#40602)
* Add API Key to the Nightscout integration config * Add tests for nightscout config changes * Apply suggestions from code review Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/nightscout/__init__.py * Run translations script to fix en.json Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> Co-authored-by: Chris Talkington <chris@talkingtontech.com>
This commit is contained in:
parent
6028953eca
commit
3fba4274f5
9 changed files with 63 additions and 18 deletions
|
@ -7,7 +7,7 @@ from aiohttp import ClientError
|
||||||
from py_nightscout import Api as NightscoutAPI
|
from py_nightscout import Api as NightscoutAPI
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_URL
|
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
@ -30,8 +30,9 @@ async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Set up Nightscout from a config entry."""
|
"""Set up Nightscout from a config entry."""
|
||||||
server_url = entry.data[CONF_URL]
|
server_url = entry.data[CONF_URL]
|
||||||
|
api_key = entry.data.get(CONF_API_KEY)
|
||||||
session = async_get_clientsession(hass)
|
session = async_get_clientsession(hass)
|
||||||
api = NightscoutAPI(server_url, session=session)
|
api = NightscoutAPI(server_url, session=session, api_secret=api_key)
|
||||||
try:
|
try:
|
||||||
status = await api.get_server_status()
|
status = await api.get_server_status()
|
||||||
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||||
|
|
|
@ -2,27 +2,32 @@
|
||||||
from asyncio import TimeoutError as AsyncIOTimeoutError
|
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError, ClientResponseError
|
||||||
from py_nightscout import Api as NightscoutAPI
|
from py_nightscout import Api as NightscoutAPI
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, exceptions
|
from homeassistant import config_entries, exceptions
|
||||||
from homeassistant.const import CONF_URL
|
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||||
|
|
||||||
from .const import DOMAIN # pylint:disable=unused-import
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
from .utils import hash_from_url
|
from .utils import hash_from_url
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str})
|
DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str, vol.Optional(CONF_API_KEY): str})
|
||||||
|
|
||||||
|
|
||||||
async def _validate_input(data):
|
async def _validate_input(data):
|
||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
url = data[CONF_URL]
|
url = data[CONF_URL]
|
||||||
|
api_key = data.get(CONF_API_KEY)
|
||||||
try:
|
try:
|
||||||
api = NightscoutAPI(url)
|
api = NightscoutAPI(url, api_secret=api_key)
|
||||||
status = await api.get_server_status()
|
status = await api.get_server_status()
|
||||||
|
if status.settings.get("authDefaultRoles") == "status-only":
|
||||||
|
await api.get_sgvs()
|
||||||
|
except ClientResponseError as error:
|
||||||
|
raise InputValidationError("invalid_auth") from error
|
||||||
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||||
raise InputValidationError("cannot_connect") from error
|
raise InputValidationError("cannot_connect") from error
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nightscout",
|
"documentation": "https://www.home-assistant.io/integrations/nightscout",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"py-nightscout==1.2.1"
|
"py-nightscout==1.2.2"
|
||||||
],
|
],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@marciogranzotto"
|
"@marciogranzotto"
|
||||||
|
|
|
@ -3,12 +3,16 @@
|
||||||
"flow_title": "Nightscout",
|
"flow_title": "Nightscout",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
"title": "Enter your Nightscout server information.",
|
||||||
|
"description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (Optional): Only use if your instance is protected (auth_default_roles != readable).",
|
||||||
"data": {
|
"data": {
|
||||||
"url": "URL"
|
"url": "[%key:common::config_flow::data::url%]",
|
||||||
|
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Device is already configured"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Failed to connect",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"unknown": "Unexpected error"
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"flow_title": "Nightscout",
|
"flow_title": "Nightscout",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"url": "URL"
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
}
|
"url": "[%key:common::config_flow::data::url%]"
|
||||||
|
},
|
||||||
|
"description": "- URL: the address of your nightscout instance. I.e.: https://myhomeassistant.duckdns.org:5423\n- API Key (Optional): Only use if your instance is protected (auth_default_roles != readable).",
|
||||||
|
"title": "Enter your Nightscout server information."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1183,7 +1183,7 @@ py-cpuinfo==7.0.0
|
||||||
py-melissa-climate==2.1.4
|
py-melissa-climate==2.1.4
|
||||||
|
|
||||||
# homeassistant.components.nightscout
|
# homeassistant.components.nightscout
|
||||||
py-nightscout==1.2.1
|
py-nightscout==1.2.2
|
||||||
|
|
||||||
# homeassistant.components.schluter
|
# homeassistant.components.schluter
|
||||||
py-schluter==0.1.7
|
py-schluter==0.1.7
|
||||||
|
|
|
@ -573,7 +573,7 @@ py-canary==0.5.0
|
||||||
py-melissa-climate==2.1.4
|
py-melissa-climate==2.1.4
|
||||||
|
|
||||||
# homeassistant.components.nightscout
|
# homeassistant.components.nightscout
|
||||||
py-nightscout==1.2.1
|
py-nightscout==1.2.2
|
||||||
|
|
||||||
# homeassistant.components.seventeentrack
|
# homeassistant.components.seventeentrack
|
||||||
py17track==2.2.2
|
py17track==2.2.2
|
||||||
|
|
|
@ -22,6 +22,11 @@ SERVER_STATUS = ServerStatus.new_from_json_dict(
|
||||||
'{"status":"ok","name":"nightscout","version":"13.0.1","serverTime":"2020-08-05T18:14:02.032Z","serverTimeEpoch":1596651242032,"apiEnabled":true,"careportalEnabled":true,"boluscalcEnabled":true,"settings":{},"extendedSettings":{},"authorized":null}'
|
'{"status":"ok","name":"nightscout","version":"13.0.1","serverTime":"2020-08-05T18:14:02.032Z","serverTimeEpoch":1596651242032,"apiEnabled":true,"careportalEnabled":true,"boluscalcEnabled":true,"settings":{},"extendedSettings":{},"authorized":null}'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
SERVER_STATUS_STATUS_ONLY = ServerStatus.new_from_json_dict(
|
||||||
|
json.loads(
|
||||||
|
'{"status":"ok","name":"nightscout","version":"14.0.4","serverTime":"2020-09-25T21:03:59.315Z","serverTimeEpoch":1601067839315,"apiEnabled":true,"careportalEnabled":true,"boluscalcEnabled":true,"settings":{"units":"mg/dl","timeFormat":12,"nightMode":false,"editMode":true,"showRawbg":"never","customTitle":"Nightscout","theme":"default","alarmUrgentHigh":true,"alarmUrgentHighMins":[30,60,90,120],"alarmHigh":true,"alarmHighMins":[30,60,90,120],"alarmLow":true,"alarmLowMins":[15,30,45,60],"alarmUrgentLow":true,"alarmUrgentLowMins":[15,30,45],"alarmUrgentMins":[30,60,90,120],"alarmWarnMins":[30,60,90,120],"alarmTimeagoWarn":true,"alarmTimeagoWarnMins":15,"alarmTimeagoUrgent":true,"alarmTimeagoUrgentMins":30,"alarmPumpBatteryLow":false,"language":"en","scaleY":"log","showPlugins":"dbsize delta direction upbat","showForecast":"ar2","focusHours":3,"heartbeat":60,"baseURL":"","authDefaultRoles":"status-only","thresholds":{"bgHigh":260,"bgTargetTop":180,"bgTargetBottom":80,"bgLow":55},"insecureUseHttp":true,"secureHstsHeader":false,"secureHstsHeaderIncludeSubdomains":false,"secureHstsHeaderPreload":false,"secureCsp":false,"deNormalizeDates":false,"showClockDelta":false,"showClockLastTime":false,"bolusRenderOver":1,"frameUrl1":"","frameUrl2":"","frameUrl3":"","frameUrl4":"","frameUrl5":"","frameUrl6":"","frameUrl7":"","frameUrl8":"","frameName1":"","frameName2":"","frameName3":"","frameName4":"","frameName5":"","frameName6":"","frameName7":"","frameName8":"","DEFAULT_FEATURES":["bgnow","delta","direction","timeago","devicestatus","upbat","errorcodes","profile","dbsize"],"alarmTypes":["predict"],"enable":["careportal","boluscalc","food","bwp","cage","sage","iage","iob","cob","basal","ar2","rawbg","pushover","bgi","pump","openaps","treatmentnotify","bgnow","delta","direction","timeago","devicestatus","upbat","errorcodes","profile","dbsize","ar2"]},"extendedSettings":{"devicestatus":{"advanced":true,"days":1}},"authorized":null}'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def init_integration(hass) -> MockConfigEntry:
|
async def init_integration(hass) -> MockConfigEntry:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""Test the Nightscout config flow."""
|
"""Test the Nightscout config flow."""
|
||||||
from aiohttp import ClientConnectionError
|
from aiohttp import ClientConnectionError, ClientResponseError
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow, setup
|
from homeassistant import config_entries, data_entry_flow, setup
|
||||||
from homeassistant.components.nightscout.const import DOMAIN
|
from homeassistant.components.nightscout.const import DOMAIN
|
||||||
|
@ -8,7 +8,11 @@ from homeassistant.const import CONF_URL
|
||||||
|
|
||||||
from tests.async_mock import patch
|
from tests.async_mock import patch
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.components.nightscout import GLUCOSE_READINGS, SERVER_STATUS
|
from tests.components.nightscout import (
|
||||||
|
GLUCOSE_READINGS,
|
||||||
|
SERVER_STATUS,
|
||||||
|
SERVER_STATUS_STATUS_ONLY,
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG = {CONF_URL: "https://some.url:1234"}
|
CONFIG = {CONF_URL: "https://some.url:1234"}
|
||||||
|
|
||||||
|
@ -55,6 +59,28 @@ async def test_user_form_cannot_connect(hass):
|
||||||
assert result2["errors"] == {"base": "cannot_connect"}
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form_api_key_required(hass):
|
||||||
|
"""Test we handle an unauthorized error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nightscout.NightscoutAPI.get_server_status",
|
||||||
|
return_value=SERVER_STATUS_STATUS_ONLY,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.nightscout.NightscoutAPI.get_sgvs",
|
||||||
|
side_effect=ClientResponseError(None, None, status=401),
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_URL: "https://some.url:1234"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
async def test_user_form_unexpected_exception(hass):
|
async def test_user_form_unexpected_exception(hass):
|
||||||
"""Test we handle unexpected exception."""
|
"""Test we handle unexpected exception."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
|
Loading…
Add table
Reference in a new issue