diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 69d79d1cecb5d5..68bb200504c497 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -5,14 +5,14 @@ from py_nightscout import Api as NightscoutAPI from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_URL +from homeassistant.const import CONF_API_KEY, CONF_UNIT_OF_MEASUREMENT, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import SLOW_UPDATE_WARNING -from .const import DOMAIN +from .const import DOMAIN, MG_DL PLATFORMS = ["sensor"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 @@ -29,6 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (ClientError, AsyncIOTimeoutError, OSError) as error: raise ConfigEntryNotReady from error + if not entry.options: + hass.config_entries.async_update_entry( + entry, options={CONF_UNIT_OF_MEASUREMENT: MG_DL} + ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index 1f3f62835bc338..0559d5ab814e1c 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -7,9 +7,10 @@ import voluptuous as vol from homeassistant import config_entries, exceptions -from homeassistant.const import CONF_API_KEY, CONF_URL +from homeassistant.const import CONF_API_KEY, CONF_UNIT_OF_MEASUREMENT, CONF_URL +from homeassistant.core import callback -from .const import DOMAIN +from .const import DOMAIN, MG_DL, MMOL_L from .utils import hash_from_url _LOGGER = logging.getLogger(__name__) @@ -62,6 +63,37 @@ async def async_step_user(self, user_input=None): 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 NightscoutOptionsFlowHandler(config_entry) + + +class NightscoutOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Nightscout.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """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) + class InputValidationError(exceptions.HomeAssistantError): """Error to indicate we cannot proceed due to invalid input.""" diff --git a/homeassistant/components/nightscout/const.py b/homeassistant/components/nightscout/const.py index 7e47f7ff49d4a2..33944f423a8c63 100644 --- a/homeassistant/components/nightscout/const.py +++ b/homeassistant/components/nightscout/const.py @@ -2,6 +2,10 @@ DOMAIN = "nightscout" +MMOL_L = "mmol/L" +MG_DL = "mg/dL" + ATTR_DEVICE = "device" +ATTR_SGV = "sgv" ATTR_DELTA = "delta" ATTR_DIRECTION = "direction" diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index 1b37fa8da7cadd..356ad2061a59f4 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -10,17 +10,18 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DATE +from homeassistant.const import ATTR_DATE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN +from .const import ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN, MMOL_L SCAN_INTERVAL = timedelta(minutes=1) _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Blood Glucose" +MMOL_CONVERSION_FACTOR = 18 async def async_setup_entry( @@ -30,20 +31,20 @@ async def async_setup_entry( ) -> None: """Set up the Glucose Sensor.""" api = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NightscoutSensor(api, "Blood Sugar", entry.unique_id)], True) + async_add_entities([NightscoutSensor(api, "Blood Sugar", entry)], True) class NightscoutSensor(SensorEntity): """Implementation of a Nightscout sensor.""" - def __init__(self, api: NightscoutAPI, name, unique_id): + def __init__(self, api: NightscoutAPI, name, entry): """Initialize the Nightscout sensor.""" self.api = api - self._unique_id = unique_id + self._unique_id = entry.unique_id self._name = name self._state = None self._attributes = None - self._unit_of_measurement = "mg/dL" + self._unit_of_measurement = entry.options[CONF_UNIT_OF_MEASUREMENT] self._icon = "mdi:cloud-question" self._available = False @@ -70,7 +71,7 @@ def available(self): @property def native_value(self): """Return the state of the device.""" - return self._state + return self._get_bgl(self._state) @property def icon(self): @@ -94,7 +95,7 @@ async def async_update(self): self._attributes = { ATTR_DEVICE: value.device, ATTR_DATE: value.date, - ATTR_DELTA: value.delta, + ATTR_DELTA: self._get_bgl(value.delta), ATTR_DIRECTION: value.direction, } self._state = value.sgv @@ -103,6 +104,11 @@ async def async_update(self): self._available = False _LOGGER.warning("Empty reply found when expecting JSON data") + def _get_bgl(self, value) -> float: + if self._unit_of_measurement == MMOL_L: + return round((value / MMOL_CONVERSION_FACTOR), 1) + return value + def _parse_icon(self) -> str: """Update the icon based on the direction attribute.""" switcher = { diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json index 709788c581830a..1390cc9699f7a1 100644 --- a/homeassistant/components/nightscout/strings.json +++ b/homeassistant/components/nightscout/strings.json @@ -18,5 +18,14 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unit of measurement" + } + } + } } } diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json index d8b4c441283ec7..c7a7a1d6a00380 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/translations/en.json @@ -19,5 +19,14 @@ "title": "Enter your Nightscout server information." } } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unit of measurement" + } + } + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/nightscout/translations/pt-BR.json b/homeassistant/components/nightscout/translations/pt-BR.json index 68dc0756725ba6..c57560922cdb78 100644 --- a/homeassistant/components/nightscout/translations/pt-BR.json +++ b/homeassistant/components/nightscout/translations/pt-BR.json @@ -14,5 +14,14 @@ } } } - } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unidade de medida" + } + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/pt.json b/homeassistant/components/nightscout/translations/pt.json index 093b7775829ff3..c879e89670ce0d 100644 --- a/homeassistant/components/nightscout/translations/pt.json +++ b/homeassistant/components/nightscout/translations/pt.json @@ -16,5 +16,14 @@ } } } - } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unidade de medida" + } + } + } + } } \ No newline at end of file diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py index 6c1a34ebe41e11..ee05fd7176c016 100644 --- a/tests/components/nightscout/__init__.py +++ b/tests/components/nightscout/__init__.py @@ -5,8 +5,13 @@ 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 homeassistant.components.nightscout.const import ( + ATTR_DELTA, + ATTR_SGV, + DOMAIN, + MG_DL, +) +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_URL from tests.common import MockConfigEntry @@ -17,6 +22,9 @@ ) ) ] + +CONVERTED_MMOL_VALUES = {ATTR_SGV: 9.4, ATTR_DELTA: -0.3} + 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}' @@ -29,11 +37,16 @@ ) -async def init_integration(hass) -> MockConfigEntry: +async def init_integration(hass, unit_of_measurement=MG_DL) -> MockConfigEntry: """Set up the Nightscout integration in Home Assistant.""" + options = {} + if unit_of_measurement: + options[CONF_UNIT_OF_MEASUREMENT] = unit_of_measurement + entry = MockConfigEntry( domain=DOMAIN, data={CONF_URL: "https://some.url:1234"}, + options=options, ) with patch( "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 9be3c95ef4236b..42bf299b3e03cc 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -4,9 +4,9 @@ from aiohttp import ClientConnectionError, ClientResponseError from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.nightscout.const import DOMAIN +from homeassistant.components.nightscout.const import DOMAIN, MG_DL, MMOL_L from homeassistant.components.nightscout.utils import hash_from_url -from homeassistant.const import CONF_URL +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_URL from tests.common import MockConfigEntry from tests.components.nightscout import ( @@ -115,6 +115,54 @@ async def test_user_form_duplicate(hass): assert result["reason"] == "already_configured" +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, + } + + def _patch_async_setup_entry(): return patch( "homeassistant.components.nightscout.async_setup_entry", diff --git a/tests/components/nightscout/test_sensor.py b/tests/components/nightscout/test_sensor.py index 5e73c75d93c619..71d6bee0eb0279 100644 --- a/tests/components/nightscout/test_sensor.py +++ b/tests/components/nightscout/test_sensor.py @@ -4,10 +4,13 @@ ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, + ATTR_SGV, + MMOL_L, ) from homeassistant.const import ATTR_DATE, ATTR_ICON, STATE_UNAVAILABLE from tests.components.nightscout import ( + CONVERTED_MMOL_VALUES, GLUCOSE_READINGS, init_integration, init_integration_empty_response, @@ -25,6 +28,14 @@ async def test_sensor_state(hass): ) +async def test_sensor_state_options_changed(hass): + """Test sensor state data with options changed.""" + await init_integration(hass, MMOL_L) + + test_glucose_sensor = hass.states.get("sensor.blood_sugar") + assert test_glucose_sensor.state == str(CONVERTED_MMOL_VALUES[ATTR_SGV]) + + async def test_sensor_error(hass): """Test sensor state data.""" await init_integration_unavailable(hass) @@ -55,3 +66,19 @@ async def test_sensor_attributes(hass): 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_ICON] == "mdi:arrow-bottom-right" + + +async def test_sensor_attributes_options_changed(hass): + """Test sensor attributes.""" + await init_integration(hass, MMOL_L) + + 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] == CONVERTED_MMOL_VALUES[ATTR_DELTA] + 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_ICON] == "mdi:arrow-bottom-right"