diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 6bf1a7f1929e7e..951e625f1d5955 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/manifest.json b/homeassistant/components/nightscout/manifest.json index 49cb077dc7956c..4d310493582a1b 100644 --- a/homeassistant/components/nightscout/manifest.json +++ b/homeassistant/components/nightscout/manifest.json @@ -3,7 +3,7 @@ "name": "Nightscout", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nightscout", - "requirements": ["py-nightscout==1.2.2"], + "requirements": ["py-nightscout==1.3.2"], "codeowners": ["@marciogranzotto"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index 1b37fa8da7cadd..fba0c55755aaab 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 @@ -94,10 +95,14 @@ async def async_update(self): self._attributes = { ATTR_DEVICE: value.device, ATTR_DATE: value.date, - ATTR_DELTA: value.delta, ATTR_DIRECTION: value.direction, } - self._state = value.sgv + if self._unit_of_measurement == MMOL_L: + self._state = value.sgv_mmol + self._attributes[ATTR_DELTA] = value.delta_mmol + else: + self._state = value.sgv + self._attributes[ATTR_DELTA] = value.delta self._icon = self._parse_icon() else: self._available = False diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json index b3b99485587ddd..da6b7cb6a58bb5 100644 --- a/homeassistant/components/nightscout/strings.json +++ b/homeassistant/components/nightscout/strings.json @@ -3,7 +3,7 @@ "step": { "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).", + "description": "- URL: the address of your nightscout instance. e.g.: https://myhomeassistant.duckdns.org:5423\n- API Key/Access Token (optional): Only use if your instance is protected (auth_default_roles != readable).\nIt is recommended NOT to use your API_SECRET but instead generate an access token. See https://nightscout.github.io/nightscout/security/#create-authentication-tokens-for-users for details.\nA 'subject' (person/device) with the 'readable' permission is sufficient for this integration.", "data": { "url": "[%key:common::config_flow::data::url%]", "api_key": "[%key:common::config_flow::data::api_key%]" @@ -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 baec475fc2d5e4..cc1a90ed0dd025 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/translations/en.json @@ -15,9 +15,18 @@ "api_key": "API Key", "url": "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).", + "description": "- URL: the address of your nightscout instance. e.g.: https://myhomeassistant.duckdns.org:5423\n- API Key/Access Token (optional): Only use if your instance is protected (auth_default_roles != readable).\nIt is recommended NOT to use your API_SECRET but instead generate an access token. See https://nightscout.github.io/nightscout/security/#create-authentication-tokens-for-users for details.\nA 'subject' (person/device) with the 'readable' permission is sufficient for this integration.", "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/requirements_all.txt b/requirements_all.txt index 02db16d8ee7ad7..ae69a320283f10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1293,7 +1293,7 @@ py-cpuinfo==8.0.0 py-melissa-climate==2.1.4 # homeassistant.components.nightscout -py-nightscout==1.2.2 +py-nightscout==1.3.2 # homeassistant.components.schluter py-schluter==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be6f6dca086c77..da2fd5584a1a93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -774,7 +774,7 @@ py-canary==0.5.1 py-melissa-climate==2.1.4 # homeassistant.components.nightscout -py-nightscout==1.2.2 +py-nightscout==1.3.2 # homeassistant.components.synology_dsm py-synologydsm-api==1.0.4 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 d7a54ba28fbd9d..03ba27ba10fc88 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -5,9 +5,9 @@ from aiohttp import ClientConnectionError, ClientResponseError from homeassistant import config_entries, data_entry_flow -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 ( @@ -116,6 +116,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"