From bcabf6da91e08330416e0e62096abb67f341fe72 Mon Sep 17 00:00:00 2001 From: Gage Benne Date: Wed, 1 Jul 2020 20:14:54 -0400 Subject: [PATCH] Add Dexcom Integration (#33852) * Initial commit for Dexcom integration * Dexcom config flow testing * Clarify errors during setup * Resolve minor test issues * Update sensor availability, resolve linting issues * Add sensor tests * Remove title due to 0.109, add abort * >94.97% codecov/patch * Move .translations/ to translations/ * Add constants for servers and unit of measurements * Bump pydexcom version * Updated domain schema, Dexcom creation * Support for different units of measurement * Update tests * Remove empty items from manifest Co-authored-by: Martin Hjelmare * Raise UpdateFailed if fetching new session fails * Switch everything over to required * Simplify state information * Simplify async_on_remove * Pydexcom package now handles fetching new session * Only allow config flow * Remove ternary operator * Bump version, pydexcom handling session refresh * Using common strings * Import from test.async_mock * Shorten variable names * Resolve tests after removing yaml support * Return false if credentials are invalid * Available seems to handle if data is empty * Now using option flow, remove handling import * Add fixture for JSON returned from API * Overhaul testing * Revise update options * Bump pydexcom version * Combat listener repetition * Undo update listener using callback * Change sensor availability to use last_update_success * Update sensor availability and tests * Rename test Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + homeassistant/components/dexcom/__init__.py | 100 +++++++++++++ .../components/dexcom/config_flow.py | 95 +++++++++++++ homeassistant/components/dexcom/const.py | 30 ++++ homeassistant/components/dexcom/manifest.json | 10 ++ homeassistant/components/dexcom/sensor.py | 133 ++++++++++++++++++ homeassistant/components/dexcom/strings.json | 30 ++++ .../components/dexcom/translations/en.json | 32 +++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/dexcom/__init__.py | 42 ++++++ tests/components/dexcom/test_config_flow.py | 130 +++++++++++++++++ tests/components/dexcom/test_init.py | 61 ++++++++ tests/components/dexcom/test_sensor.py | 114 +++++++++++++++ tests/fixtures/dexcom_data.json | 7 + 16 files changed, 792 insertions(+) create mode 100644 homeassistant/components/dexcom/__init__.py create mode 100644 homeassistant/components/dexcom/config_flow.py create mode 100644 homeassistant/components/dexcom/const.py create mode 100644 homeassistant/components/dexcom/manifest.json create mode 100644 homeassistant/components/dexcom/sensor.py create mode 100644 homeassistant/components/dexcom/strings.json create mode 100644 homeassistant/components/dexcom/translations/en.json create mode 100644 tests/components/dexcom/__init__.py create mode 100644 tests/components/dexcom/test_config_flow.py create mode 100644 tests/components/dexcom/test_init.py create mode 100644 tests/components/dexcom/test_sensor.py create mode 100644 tests/fixtures/dexcom_data.json diff --git a/CODEOWNERS b/CODEOWNERS index a2f9668b330..9ea7de5c2c1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,7 @@ homeassistant/components/denonavr/* @scarface-4711 @starkillerOG homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun +homeassistant/components/dexcom/* @gagebenne homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py new file mode 100644 index 00000000000..6c4f8c071a1 --- /dev/null +++ b/homeassistant/components/dexcom/__init__.py @@ -0,0 +1,100 @@ +"""The Dexcom integration.""" +import asyncio +from datetime import timedelta +import logging + +from pydexcom import AccountError, Dexcom, SessionError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_SERVER, + COORDINATOR, + DOMAIN, + MG_DL, + PLATFORMS, + SERVER_OUS, + UNDO_UPDATE_LISTENER, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=180) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up configured Dexcom.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Dexcom from a config entry.""" + try: + dexcom = await hass.async_add_executor_job( + Dexcom, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_SERVER] == SERVER_OUS, + ) + except AccountError: + return False + except SessionError: + raise ConfigEntryNotReady + + if not entry.options: + hass.config_entries.async_update_entry( + entry, options={CONF_UNIT_OF_MEASUREMENT: MG_DL} + ) + + async def async_update_data(): + try: + return await hass.async_add_executor_job(dexcom.get_current_glucose_reading) + except SessionError as error: + raise UpdateFailed(error) + + hass.data[DOMAIN][entry.entry_id] = { + COORDINATOR: DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ), + UNDO_UPDATE_LISTENER: entry.add_update_listener(update_listener), + } + + await hass.data[DOMAIN][entry.entry_id][COORDINATOR].async_refresh() + + 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): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +async def update_listener(hass, entry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py new file mode 100644 index 00000000000..e2ce9e49186 --- /dev/null +++ b/homeassistant/components/dexcom/config_flow.py @@ -0,0 +1,95 @@ +"""Config flow for Dexcom integration.""" +import logging + +from pydexcom import AccountError, Dexcom, SessionError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME +from homeassistant.core import callback + +from .const import ( # pylint:disable=unused-import + CONF_SERVER, + DOMAIN, + MG_DL, + MMOL_L, + SERVER_OUS, + SERVER_US, +) + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_SERVER): vol.In({SERVER_US, SERVER_OUS}), + } +) + + +class DexcomConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Dexcom.""" + + 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: + await self.hass.async_add_executor_job( + Dexcom, + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + user_input[CONF_SERVER] == SERVER_OUS, + ) + except SessionError: + errors["base"] = "session_error" + except AccountError: + errors["base"] = "account_error" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return DexcomOptionsFlowHandler(config_entry) + + +class DexcomOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Dexcom.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_UNIT_OF_MEASUREMENT, + default=self.config_entry.options.get( + CONF_UNIT_OF_MEASUREMENT, MG_DL + ), + ): vol.In({MG_DL, MMOL_L}), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/dexcom/const.py b/homeassistant/components/dexcom/const.py new file mode 100644 index 00000000000..40b7e32df6c --- /dev/null +++ b/homeassistant/components/dexcom/const.py @@ -0,0 +1,30 @@ +"""Constants for the Dexcom integration.""" + +DOMAIN = "dexcom" +PLATFORMS = ["sensor"] + +GLUCOSE_VALUE_ICON = "mdi:diabetes" + +GLUCOSE_TREND_ICON = [ + "mdi:help", + "mdi:arrow-up-thick", + "mdi:arrow-up", + "mdi:arrow-top-right", + "mdi:arrow-right", + "mdi:arrow-bottom-right", + "mdi:arrow-down", + "mdi:arrow-down-thick", + "mdi:help", + "mdi:alert-circle-outline", +] + +MMOL_L = "mmol/L" +MG_DL = "mg/dL" + +CONF_SERVER = "server" + +SERVER_OUS = "EU" +SERVER_US = "US" + +COORDINATOR = "coordinator" +UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json new file mode 100644 index 00000000000..3afe225e91b --- /dev/null +++ b/homeassistant/components/dexcom/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "dexcom", + "name": "Dexcom", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/dexcom", + "requirements": ["pydexcom==0.2.0"], + "codeowners": [ + "@gagebenne" + ] +} diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py new file mode 100644 index 00000000000..ac85e63b598 --- /dev/null +++ b/homeassistant/components/dexcom/sensor.py @@ -0,0 +1,133 @@ +"""Support for Dexcom sensors.""" +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME +from homeassistant.helpers.entity import Entity + +from .const import COORDINATOR, DOMAIN, GLUCOSE_TREND_ICON, GLUCOSE_VALUE_ICON, MG_DL + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Dexcom sensors.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + username = config_entry.data[CONF_USERNAME] + unit_of_measurement = config_entry.options[CONF_UNIT_OF_MEASUREMENT] + sensors = [] + sensors.append(DexcomGlucoseTrendSensor(coordinator, username)) + sensors.append(DexcomGlucoseValueSensor(coordinator, username, unit_of_measurement)) + async_add_entities(sensors, False) + + +class DexcomGlucoseValueSensor(Entity): + """Representation of a Dexcom glucose value sensor.""" + + def __init__(self, coordinator, username, unit_of_measurement): + """Initialize the sensor.""" + self._state = None + self._unit_of_measurement = unit_of_measurement + self._attribute_unit_of_measurement = ( + "mg_dl" if unit_of_measurement == MG_DL else "mmol_l" + ) + self._coordinator = coordinator + self._name = f"{DOMAIN}_{username}_glucose_value" + self._unique_id = f"{username}-value" + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the icon for the frontend.""" + return GLUCOSE_VALUE_ICON + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the device.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the sensor.""" + if self._coordinator.data: + return getattr(self._coordinator.data, self._attribute_unit_of_measurement) + return None + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + @property + def unique_id(self): + """Device unique id.""" + return self._unique_id + + async def async_update(self): + """Get the latest state of the sensor.""" + await self._coordinator.async_request_refresh() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + + +class DexcomGlucoseTrendSensor(Entity): + """Representation of a Dexcom glucose trend sensor.""" + + def __init__(self, coordinator, username): + """Initialize the sensor.""" + self._state = None + self._coordinator = coordinator + self._name = f"{DOMAIN}_{username}_glucose_trend" + self._unique_id = f"{username}-trend" + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the icon for the frontend.""" + if self._coordinator.data: + return GLUCOSE_TREND_ICON[self._coordinator.data.trend] + return GLUCOSE_TREND_ICON[0] + + @property + def state(self): + """Return the state of the sensor.""" + if self._coordinator.data: + return self._coordinator.data.trend_description + return None + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + @property + def unique_id(self): + """Device unique id.""" + return self._unique_id + + async def async_update(self): + """Get the latest state of the sensor.""" + await self._coordinator.async_request_refresh() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) diff --git a/homeassistant/components/dexcom/strings.json b/homeassistant/components/dexcom/strings.json new file mode 100644 index 00000000000..7b9932ec4de --- /dev/null +++ b/homeassistant/components/dexcom/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "title": "Setup Dexcom integration", + "description": "Enter Dexcom Share credentials", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "server": "Server" + } + } + }, + "error": { + "session_error": "[%key:common::config_flow::error::cannot_connect%]", + "account_error": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unit of measurement" + } + } + } + } +} diff --git a/homeassistant/components/dexcom/translations/en.json b/homeassistant/components/dexcom/translations/en.json new file mode 100644 index 00000000000..3430a65131b --- /dev/null +++ b/homeassistant/components/dexcom/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "account_error": "[%key:common::config_flow::error::invalid_auth%]", + "session_error": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "server": "Server", + "username": "[%key:common::config_flow::data::username%]" + }, + "description": "Enter Dexcom Share credentials", + "title": "Setup Dexcom integration" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unit of measurement" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index fa95014ee7a..d2afc1f3630 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -34,6 +34,7 @@ FLOWS = [ "deconz", "denonavr", "devolo_home_control", + "dexcom", "dialogflow", "directv", "doorbird", diff --git a/requirements_all.txt b/requirements_all.txt index 2037092769a..004f053c845 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1290,6 +1290,9 @@ pydeconz==71 # homeassistant.components.delijn pydelijn==0.6.0 +# homeassistant.components.dexcom +pydexcom==0.2.0 + # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fc85b0fdf8..fe3a7272b3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -584,6 +584,9 @@ pydaikin==2.2.0 # homeassistant.components.deconz pydeconz==71 +# homeassistant.components.dexcom +pydexcom==0.2.0 + # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/dexcom/__init__.py b/tests/components/dexcom/__init__.py new file mode 100644 index 00000000000..47f6238c7c1 --- /dev/null +++ b/tests/components/dexcom/__init__.py @@ -0,0 +1,42 @@ +"""Tests for the Dexcom integration.""" + +import json + +from pydexcom import GlucoseReading + +from homeassistant.components.dexcom.const import CONF_SERVER, DOMAIN, SERVER_US +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry, load_fixture + +CONFIG = { + CONF_USERNAME: "test_username", + CONF_PASSWORD: "test_password", + CONF_SERVER: SERVER_US, +} + +GLUCOSE_READING = GlucoseReading(json.loads(load_fixture("dexcom_data.json"))) + + +async def init_integration(hass) -> MockConfigEntry: + """Set up the Dexcom integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="test_username", + unique_id="test_username", + data=CONFIG, + options=None, + ) + with patch( + "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", + return_value=GLUCOSE_READING, + ), patch( + "homeassistant.components.dexcom.Dexcom.create_session", + return_value="test_session_id", + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py new file mode 100644 index 00000000000..b244751a811 --- /dev/null +++ b/tests/components/dexcom/test_config_flow.py @@ -0,0 +1,130 @@ +"""Test the Dexcom config flow.""" +from pydexcom import AccountError, SessionError + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.dexcom.const import DOMAIN, MG_DL, MMOL_L +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.dexcom import CONFIG + + +async def test_form(hass): + """Test we get the 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.dexcom.config_flow.Dexcom.create_session", + return_value="test_session_id", + ), patch( + "homeassistant.components.dexcom.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.dexcom.async_setup_entry", return_value=True, + ) 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"] == CONFIG[CONF_USERNAME] + 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_form_account_error(hass): + """Test we handle account error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.dexcom.config_flow.Dexcom", side_effect=AccountError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "account_error"} + + +async def test_form_session_error(hass): + """Test we handle session error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.dexcom.config_flow.Dexcom", side_effect=SessionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "session_error"} + + +async def test_form_unknown_error(hass): + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.dexcom.config_flow.Dexcom", side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_option_flow_default(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=DOMAIN, data=CONFIG, options=None,) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + CONF_UNIT_OF_MEASUREMENT: MG_DL, + } + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry( + domain=DOMAIN, data=CONFIG, options={CONF_UNIT_OF_MEASUREMENT: MG_DL}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_UNIT_OF_MEASUREMENT: MMOL_L, + } diff --git a/tests/components/dexcom/test_init.py b/tests/components/dexcom/test_init.py new file mode 100644 index 00000000000..393aa9cfe98 --- /dev/null +++ b/tests/components/dexcom/test_init.py @@ -0,0 +1,61 @@ +"""Test the Dexcom config flow.""" +from pydexcom import AccountError, SessionError + +from homeassistant.components.dexcom.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.dexcom import CONFIG, init_integration + + +async def test_setup_entry_account_error(hass): + """Test entry setup failed due to account error.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="test_username", + unique_id="test_username", + data=CONFIG, + options=None, + ) + with patch( + "homeassistant.components.dexcom.Dexcom", side_effect=AccountError, + ): + entry.add_to_hass(hass) + result = await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert result is False + + +async def test_setup_entry_session_error(hass): + """Test entry setup failed due to session error.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="test_username", + unique_id="test_username", + data=CONFIG, + options=None, + ) + with patch( + "homeassistant.components.dexcom.Dexcom", side_effect=SessionError, + ): + entry.add_to_hass(hass) + result = await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert result is False + + +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) diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py new file mode 100644 index 00000000000..de45e65155f --- /dev/null +++ b/tests/components/dexcom/test_sensor.py @@ -0,0 +1,114 @@ +"""The sensor tests for the griddy platform.""" + +from pydexcom import SessionError + +from homeassistant.components.dexcom.const import MMOL_L +from homeassistant.const import ( + CONF_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) + +from tests.async_mock import patch +from tests.components.dexcom import GLUCOSE_READING, init_integration + + +async def test_sensors(hass): + """Test we get sensor data.""" + await init_integration(hass) + + test_username_glucose_value = hass.states.get( + "sensor.dexcom_test_username_glucose_value" + ) + assert test_username_glucose_value.state == str(GLUCOSE_READING.value) + test_username_glucose_trend = hass.states.get( + "sensor.dexcom_test_username_glucose_trend" + ) + assert test_username_glucose_trend.state == GLUCOSE_READING.trend_description + + +async def test_sensors_unknown(hass): + """Test we handle sensor state unknown.""" + await init_integration(hass) + + with patch( + "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", + return_value=None, + ): + await hass.helpers.entity_component.async_update_entity( + "sensor.dexcom_test_username_glucose_value" + ) + await hass.helpers.entity_component.async_update_entity( + "sensor.dexcom_test_username_glucose_trend" + ) + + test_username_glucose_value = hass.states.get( + "sensor.dexcom_test_username_glucose_value" + ) + assert test_username_glucose_value.state == STATE_UNKNOWN + test_username_glucose_trend = hass.states.get( + "sensor.dexcom_test_username_glucose_trend" + ) + assert test_username_glucose_trend.state == STATE_UNKNOWN + + +async def test_sensors_update_failed(hass): + """Test we handle sensor update failed.""" + await init_integration(hass) + + with patch( + "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", + side_effect=SessionError, + ): + await hass.helpers.entity_component.async_update_entity( + "sensor.dexcom_test_username_glucose_value" + ) + await hass.helpers.entity_component.async_update_entity( + "sensor.dexcom_test_username_glucose_trend" + ) + + test_username_glucose_value = hass.states.get( + "sensor.dexcom_test_username_glucose_value" + ) + assert test_username_glucose_value.state == STATE_UNAVAILABLE + test_username_glucose_trend = hass.states.get( + "sensor.dexcom_test_username_glucose_trend" + ) + assert test_username_glucose_trend.state == STATE_UNAVAILABLE + + +async def test_sensors_options_changed(hass): + """Test we handle sensor unavailable.""" + entry = await init_integration(hass) + + test_username_glucose_value = hass.states.get( + "sensor.dexcom_test_username_glucose_value" + ) + assert test_username_glucose_value.state == str(GLUCOSE_READING.value) + test_username_glucose_trend = hass.states.get( + "sensor.dexcom_test_username_glucose_trend" + ) + assert test_username_glucose_trend.state == GLUCOSE_READING.trend_description + + with patch( + "homeassistant.components.dexcom.Dexcom.get_current_glucose_reading", + return_value=GLUCOSE_READING, + ), patch( + "homeassistant.components.dexcom.Dexcom.create_session", + return_value="test_session_id", + ): + hass.config_entries.async_update_entry( + entry=entry, options={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, + ) + await hass.async_block_till_done() + + assert entry.options == {CONF_UNIT_OF_MEASUREMENT: MMOL_L} + + test_username_glucose_value = hass.states.get( + "sensor.dexcom_test_username_glucose_value" + ) + assert test_username_glucose_value.state == str(GLUCOSE_READING.mmol_l) + test_username_glucose_trend = hass.states.get( + "sensor.dexcom_test_username_glucose_trend" + ) + assert test_username_glucose_trend.state == GLUCOSE_READING.trend_description diff --git a/tests/fixtures/dexcom_data.json b/tests/fixtures/dexcom_data.json new file mode 100644 index 00000000000..d1ae571f203 --- /dev/null +++ b/tests/fixtures/dexcom_data.json @@ -0,0 +1,7 @@ +{ + "DT": "/Date(1587165223000+0000)/", + "ST": "/Date(1587179623000)/", + "Trend": 4, + "Value": 110, + "WT": "/Date(1587179623000)/" +}