Add Nightscout integration (#38615)
* Implement NightScout sensor integration * Add tests for NightScout integration * Fix Nightscout captalization * Change quality scale for Nightscout * Trigger actions * Add missing tests * Fix stale comments * Fix Nightscout manufacturer * Add entry type service * Change host to URL on nightscout config flow * Add ConfigEntryNotReady exception to nighscout init * Remote platform_schema from nightscout sensor * Update homeassistant/components/nightscout/config_flow.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> Co-authored-by: Chris Talkington <chris@talkingtontech.com>
This commit is contained in:
parent
96d48c309f
commit
761067559d
27 changed files with 759 additions and 0 deletions
|
@ -272,6 +272,7 @@ homeassistant/components/netdata/* @fabaff
|
|||
homeassistant/components/nexia/* @ryannazaretian @bdraco
|
||||
homeassistant/components/nextbus/* @vividboarder
|
||||
homeassistant/components/nextcloud/* @meichthys
|
||||
homeassistant/components/nightscout/* @marciogranzotto
|
||||
homeassistant/components/nilu/* @hfurubotten
|
||||
homeassistant/components/nissan_leaf/* @filcole
|
||||
homeassistant/components/nmbs/* @thibmaek
|
||||
|
|
73
homeassistant/components/nightscout/__init__.py
Normal file
73
homeassistant/components/nightscout/__init__.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
"""The Nightscout integration."""
|
||||
import asyncio
|
||||
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError
|
||||
from py_nightscout import Api as NightscoutAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import SLOW_UPDATE_WARNING
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = ["sensor"]
|
||||
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Nightscout component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Nightscout from a config entry."""
|
||||
server_url = entry.data[CONF_URL]
|
||||
|
||||
api = NightscoutAPI(server_url)
|
||||
try:
|
||||
status = await api.get_server_status()
|
||||
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = api
|
||||
|
||||
device_registry = await dr.async_get_registry(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, server_url)},
|
||||
manufacturer="Nightscout Foundation",
|
||||
name=status.name,
|
||||
sw_version=status.version,
|
||||
entry_type="service",
|
||||
)
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
58
homeassistant/components/nightscout/config_flow.py
Normal file
58
homeassistant/components/nightscout/config_flow.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
"""Config flow for Nightscout integration."""
|
||||
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError
|
||||
from py_nightscout import Api as NightscoutAPI
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, exceptions
|
||||
from homeassistant.const import CONF_URL
|
||||
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str})
|
||||
|
||||
|
||||
async def _validate_input(data):
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
try:
|
||||
api = NightscoutAPI(data[CONF_URL])
|
||||
status = await api.get_server_status()
|
||||
except (ClientError, AsyncIOTimeoutError, OSError):
|
||||
raise CannotConnect
|
||||
|
||||
# Return info to be stored in the config entry.
|
||||
return {"title": status.name}
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Nightscout."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
info = await _validate_input(user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
9
homeassistant/components/nightscout/const.py
Normal file
9
homeassistant/components/nightscout/const.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
"""Constants for the Nightscout integration."""
|
||||
|
||||
DOMAIN = "nightscout"
|
||||
|
||||
ATTR_DEVICE = "device"
|
||||
ATTR_DATE = "date"
|
||||
ATTR_SVG = "svg"
|
||||
ATTR_DELTA = "delta"
|
||||
ATTR_DIRECTION = "direction"
|
13
homeassistant/components/nightscout/manifest.json
Normal file
13
homeassistant/components/nightscout/manifest.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"domain": "nightscout",
|
||||
"name": "Nightscout",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nightscout",
|
||||
"requirements": [
|
||||
"py-nightscout==1.2.1"
|
||||
],
|
||||
"codeowners": [
|
||||
"@marciogranzotto"
|
||||
],
|
||||
"quality_scale": "platinum"
|
||||
}
|
126
homeassistant/components/nightscout/sensor.py
Normal file
126
homeassistant/components/nightscout/sensor.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
"""Support for Nightscout sensors."""
|
||||
from asyncio import TimeoutError as AsyncIOTimeoutError
|
||||
from datetime import timedelta
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Callable, List
|
||||
|
||||
from aiohttp import ClientError
|
||||
from py_nightscout import Api as NightscoutAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import ATTR_DATE, ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, ATTR_SVG, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "Blood Glucose"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up the Glucose Sensor."""
|
||||
api = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities([NightscoutSensor(api, "Blood Sugar")], True)
|
||||
|
||||
|
||||
class NightscoutSensor(Entity):
|
||||
"""Implementation of a Nightscout sensor."""
|
||||
|
||||
def __init__(self, api: NightscoutAPI, name):
|
||||
"""Initialize the Nightscout sensor."""
|
||||
self.api = api
|
||||
self._unique_id = hashlib.sha256(api.server_url.encode("utf-8")).hexdigest()
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._attributes = None
|
||||
self._unit_of_measurement = "mg/dL"
|
||||
self._icon = "mdi:cloud-question"
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if the sensor data are available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch the latest data from Nightscout REST API and update the state."""
|
||||
try:
|
||||
values = await self.api.get_sgvs()
|
||||
except (ClientError, AsyncIOTimeoutError, OSError) as error:
|
||||
_LOGGER.error("Error fetching data. Failed with %s", error)
|
||||
self._available = False
|
||||
return
|
||||
|
||||
self._available = True
|
||||
self._attributes = {}
|
||||
self._state = None
|
||||
if values:
|
||||
value = values[0]
|
||||
self._attributes = {
|
||||
ATTR_DEVICE: value.device,
|
||||
ATTR_DATE: value.date,
|
||||
ATTR_SVG: value.sgv,
|
||||
ATTR_DELTA: value.delta,
|
||||
ATTR_DIRECTION: value.direction,
|
||||
}
|
||||
self._state = value.sgv
|
||||
self._icon = self._parse_icon()
|
||||
else:
|
||||
self._available = False
|
||||
_LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
|
||||
def _parse_icon(self) -> str:
|
||||
"""Update the icon based on the direction attribute."""
|
||||
switcher = {
|
||||
"Flat": "mdi:arrow-right",
|
||||
"SingleDown": "mdi:arrow-down",
|
||||
"FortyFiveDown": "mdi:arrow-bottom-right",
|
||||
"DoubleDown": "mdi:chevron-triple-down",
|
||||
"SingleUp": "mdi:arrow-up",
|
||||
"FortyFiveUp": "mdi:arrow-top-right",
|
||||
"DoubleUp": "mdi:chevron-triple-up",
|
||||
}
|
||||
return switcher.get(self._attributes[ATTR_DIRECTION], "mdi:cloud-question")
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._attributes
|
16
homeassistant/components/nightscout/strings.json
Normal file
16
homeassistant/components/nightscout/strings.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"config": {
|
||||
"flow_title": "Nightscout",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/ca.json
Normal file
15
homeassistant/components/nightscout/translations/ca.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Ha fallat la connexi\u00f3",
|
||||
"unknown": "Error inesperat"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/de.json
Normal file
15
homeassistant/components/nightscout/translations/de.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung nicht m\u00f6glich",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/en.json
Normal file
15
homeassistant/components/nightscout/translations/en.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/es.json
Normal file
15
homeassistant/components/nightscout/translations/es.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar",
|
||||
"unknown": "Error inesperado"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/fr.json
Normal file
15
homeassistant/components/nightscout/translations/fr.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Echec de connexion",
|
||||
"unknown": "Erreur inattendue"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/it.json
Normal file
15
homeassistant/components/nightscout/translations/it.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Impossibile connettersi",
|
||||
"unknown": "Errore imprevisto"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/ko.json
Normal file
15
homeassistant/components/nightscout/translations/ko.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4",
|
||||
"unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL \uc8fc\uc18c"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
homeassistant/components/nightscout/translations/lb.json
Normal file
14
homeassistant/components/nightscout/translations/lb.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"unknown": "Onerwaarte Feeler"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/pl.json
Normal file
15
homeassistant/components/nightscout/translations/pl.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.",
|
||||
"unknown": "Nieoczekiwany b\u0142\u0105d."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/pt.json
Normal file
15
homeassistant/components/nightscout/translations/pt.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Falha na liga\u00e7\u00e3o",
|
||||
"unknown": "Erro inesperado"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/ru.json
Normal file
15
homeassistant/components/nightscout/translations/ru.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.",
|
||||
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL-\u0430\u0434\u0440\u0435\u0441"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/nightscout/translations/sl.json
Normal file
15
homeassistant/components/nightscout/translations/sl.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Povezava ni uspela",
|
||||
"unknown": "Nepri\u010dakovana napaka"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "\u9023\u7dda\u5931\u6557",
|
||||
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "\u7db2\u5740"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -114,6 +114,7 @@ FLOWS = [
|
|||
"nest",
|
||||
"netatmo",
|
||||
"nexia",
|
||||
"nightscout",
|
||||
"notion",
|
||||
"nuheat",
|
||||
"nut",
|
||||
|
|
|
@ -1161,6 +1161,9 @@ py-cpuinfo==5.0.0
|
|||
# homeassistant.components.melissa
|
||||
py-melissa-climate==2.0.0
|
||||
|
||||
# homeassistant.components.nightscout
|
||||
py-nightscout==1.2.1
|
||||
|
||||
# homeassistant.components.schluter
|
||||
py-schluter==0.1.7
|
||||
|
||||
|
|
|
@ -548,6 +548,9 @@ py-canary==0.5.0
|
|||
# homeassistant.components.melissa
|
||||
py-melissa-climate==2.0.0
|
||||
|
||||
# homeassistant.components.nightscout
|
||||
py-nightscout==1.2.1
|
||||
|
||||
# homeassistant.components.seventeentrack
|
||||
py17track==2.2.2
|
||||
|
||||
|
|
74
tests/components/nightscout/__init__.py
Normal file
74
tests/components/nightscout/__init__.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
"""Tests for the Nightscout integration."""
|
||||
import json
|
||||
|
||||
from aiohttp import ClientConnectionError
|
||||
from py_nightscout.models import SGV, ServerStatus
|
||||
|
||||
from homeassistant.components.nightscout.const import DOMAIN
|
||||
from homeassistant.const import CONF_URL
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
GLUCOSE_READINGS = [
|
||||
SGV.new_from_json_dict(
|
||||
json.loads(
|
||||
'{"_id":"5f2b01f5c3d0ac7c4090e223","device":"xDrip-LimiTTer","date":1596654066533,"dateString":"2020-08-05T19:01:06.533Z","sgv":169,"delta":-5.257,"direction":"FortyFiveDown","type":"sgv","filtered":182823.5157,"unfiltered":182823.5157,"rssi":100,"noise":1,"sysTime":"2020-08-05T19:01:06.533Z","utcOffset":-180}'
|
||||
)
|
||||
)
|
||||
]
|
||||
SERVER_STATUS = ServerStatus.new_from_json_dict(
|
||||
json.loads(
|
||||
'{"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}'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def init_integration(hass) -> MockConfigEntry:
|
||||
"""Set up the Nightscout integration in Home Assistant."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},)
|
||||
with patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_sgvs",
|
||||
return_value=GLUCOSE_READINGS,
|
||||
), patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_server_status",
|
||||
return_value=SERVER_STATUS,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
async def init_integration_unavailable(hass) -> MockConfigEntry:
|
||||
"""Set up the Nightscout integration in Home Assistant."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},)
|
||||
with patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_sgvs",
|
||||
side_effect=ClientConnectionError(),
|
||||
), patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_server_status",
|
||||
return_value=SERVER_STATUS,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
async def init_integration_empty_response(hass) -> MockConfigEntry:
|
||||
"""Set up the Nightscout integration in Home Assistant."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},)
|
||||
with patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_sgvs", return_value=[]
|
||||
), patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_server_status",
|
||||
return_value=SERVER_STATUS,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
85
tests/components/nightscout/test_config_flow.py
Normal file
85
tests/components/nightscout/test_config_flow.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
"""Test the Nightscout config flow."""
|
||||
from aiohttp import ClientConnectionError
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow, setup
|
||||
from homeassistant.components.nightscout.const import DOMAIN
|
||||
from homeassistant.const import CONF_URL
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.components.nightscout import GLUCOSE_READINGS, SERVER_STATUS
|
||||
|
||||
CONFIG = {CONF_URL: "https://some.url:1234"}
|
||||
|
||||
|
||||
async def test_form(hass):
|
||||
"""Test we get the user initiated form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_sgvs",
|
||||
return_value=GLUCOSE_READINGS,
|
||||
), patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_server_status",
|
||||
return_value=SERVER_STATUS,
|
||||
), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], CONFIG,
|
||||
)
|
||||
|
||||
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result2["title"] == SERVER_STATUS.name # pylint: disable=maybe-no-member
|
||||
assert result2["data"] == CONFIG
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_form_cannot_connect(hass):
|
||||
"""Test we handle cannot connect 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",
|
||||
side_effect=ClientConnectionError(),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_URL: "https://some.url:1234"},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_user_form_unexpected_exception(hass):
|
||||
"""Test we handle cannot connect 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",
|
||||
side_effect=Exception(),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_URL: "https://some.url:1234"},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
def _patch_async_setup():
|
||||
return patch("homeassistant.components.nightscout.async_setup", return_value=True)
|
||||
|
||||
|
||||
def _patch_async_setup_entry():
|
||||
return patch(
|
||||
"homeassistant.components.nightscout.async_setup_entry", return_value=True,
|
||||
)
|
43
tests/components/nightscout/test_init.py
Normal file
43
tests/components/nightscout/test_init.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
"""Test the Nightscout config flow."""
|
||||
from aiohttp import ClientError
|
||||
|
||||
from homeassistant.components.nightscout.const import DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
ENTRY_STATE_LOADED,
|
||||
ENTRY_STATE_NOT_LOADED,
|
||||
ENTRY_STATE_SETUP_RETRY,
|
||||
)
|
||||
from homeassistant.const import CONF_URL
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.nightscout import init_integration
|
||||
|
||||
|
||||
async def test_unload_entry(hass):
|
||||
"""Test successful unload of entry."""
|
||||
entry = await init_integration(hass)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ENTRY_STATE_NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_async_setup_raises_entry_not_ready(hass):
|
||||
"""Test that it throws ConfigEntryNotReady when exception occurs during setup."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.nightscout.NightscoutAPI.get_server_status",
|
||||
side_effect=ClientError(),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
|
60
tests/components/nightscout/test_sensor.py
Normal file
60
tests/components/nightscout/test_sensor.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
"""The sensor tests for the Nightscout platform."""
|
||||
|
||||
from homeassistant.components.nightscout.const import (
|
||||
ATTR_DATE,
|
||||
ATTR_DELTA,
|
||||
ATTR_DEVICE,
|
||||
ATTR_DIRECTION,
|
||||
ATTR_SVG,
|
||||
)
|
||||
from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE
|
||||
|
||||
from tests.components.nightscout import (
|
||||
GLUCOSE_READINGS,
|
||||
init_integration,
|
||||
init_integration_empty_response,
|
||||
init_integration_unavailable,
|
||||
)
|
||||
|
||||
|
||||
async def test_sensor_state(hass):
|
||||
"""Test sensor state data."""
|
||||
await init_integration(hass)
|
||||
|
||||
test_glucose_sensor = hass.states.get("sensor.blood_sugar")
|
||||
assert test_glucose_sensor.state == str(
|
||||
GLUCOSE_READINGS[0].sgv # pylint: disable=maybe-no-member
|
||||
)
|
||||
|
||||
|
||||
async def test_sensor_error(hass):
|
||||
"""Test sensor state data."""
|
||||
await init_integration_unavailable(hass)
|
||||
|
||||
test_glucose_sensor = hass.states.get("sensor.blood_sugar")
|
||||
assert test_glucose_sensor.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_sensor_empty_response(hass):
|
||||
"""Test sensor state data."""
|
||||
await init_integration_empty_response(hass)
|
||||
|
||||
test_glucose_sensor = hass.states.get("sensor.blood_sugar")
|
||||
assert test_glucose_sensor.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_sensor_attributes(hass):
|
||||
"""Test sensor attributes."""
|
||||
await init_integration(hass)
|
||||
|
||||
test_glucose_sensor = hass.states.get("sensor.blood_sugar")
|
||||
reading = GLUCOSE_READINGS[0]
|
||||
assert reading is not None
|
||||
|
||||
attr = test_glucose_sensor.attributes
|
||||
assert attr[ATTR_DATE] == reading.date # pylint: disable=maybe-no-member
|
||||
assert attr[ATTR_DELTA] == reading.delta # pylint: disable=maybe-no-member
|
||||
assert attr[ATTR_DEVICE] == reading.device # pylint: disable=maybe-no-member
|
||||
assert attr[ATTR_DIRECTION] == reading.direction # pylint: disable=maybe-no-member
|
||||
assert attr[ATTR_SVG] == reading.sgv # pylint: disable=maybe-no-member
|
||||
assert attr[ATTR_ICON] == "mdi:arrow-bottom-right"
|
Loading…
Add table
Reference in a new issue