diff --git a/.strict-typing b/.strict-typing index c5b4e376414..d3102572eb1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -148,6 +148,7 @@ homeassistant.components.jewish_calendar.* homeassistant.components.kaleidescape.* homeassistant.components.knx.* homeassistant.components.kraken.* +homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* diff --git a/CODEOWNERS b/CODEOWNERS index bd39fd68590..96238f61fbb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -573,6 +573,8 @@ build.json @home-assistant/supervisor /tests/components/kraken/ @eifinger /homeassistant/components/kulersky/ @emlove /tests/components/kulersky/ @emlove +/homeassistant/components/lacrosse_view/ @IceBotYT +/tests/components/lacrosse_view/ @IceBotYT /homeassistant/components/lametric/ @robbiet480 @frenck /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol diff --git a/homeassistant/components/lacrosse_view/__init__.py b/homeassistant/components/lacrosse_view/__init__.py new file mode 100644 index 00000000000..0d3147f43a5 --- /dev/null +++ b/homeassistant/components/lacrosse_view/__init__.py @@ -0,0 +1,78 @@ +"""The LaCrosse View integration.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from lacrosse_view import LaCrosse, Location, LoginError, Sensor + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LaCrosse View from a config entry.""" + + async def get_data() -> list[Sensor]: + """Get the data from the LaCrosse View.""" + now = datetime.utcnow() + + if hass.data[DOMAIN][entry.entry_id]["last_update"] < now - timedelta( + minutes=59 + ): # Get new token + hass.data[DOMAIN][entry.entry_id]["last_update"] = now + await api.login(entry.data["username"], entry.data["password"]) + + # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) + yesterday = now - timedelta(days=1) + yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) + yesterday_timestamp = datetime.timestamp(yesterday) + + return await api.get_sensors( + location=Location(id=entry.data["id"], name=entry.data["name"]), + tz=hass.config.time_zone, + start=str(int(yesterday_timestamp)), + end=str(int(datetime.timestamp(now))), + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "api": LaCrosse(async_get_clientsession(hass)), + "last_update": datetime.utcnow(), + } + api: LaCrosse = hass.data[DOMAIN][entry.entry_id]["api"] + + try: + await api.login(entry.data["username"], entry.data["password"]) + except LoginError as error: + raise ConfigEntryNotReady from error + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="LaCrosse View", + update_method=get_data, + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id]["coordinator"] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/lacrosse_view/config_flow.py b/homeassistant/components/lacrosse_view/config_flow.py new file mode 100644 index 00000000000..b5b89828e9b --- /dev/null +++ b/homeassistant/components/lacrosse_view/config_flow.py @@ -0,0 +1,128 @@ +"""Config flow for LaCrosse View integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from lacrosse_view import LaCrosse, Location, LoginError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("username"): str, + vol.Required("password"): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> list[Location]: + """Validate the user input allows us to connect.""" + + api = LaCrosse(async_get_clientsession(hass)) + + try: + await api.login(data["username"], data["password"]) + + locations = await api.get_locations() + except LoginError as error: + raise InvalidAuth from error + + if not locations: + raise NoLocations("No locations found for account {}".format(data["username"])) + + return locations + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LaCrosse View.""" + + VERSION = 1 + data: dict[str, str] = {} + locations: list[Location] = [] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except InvalidAuth: + errors["base"] = "invalid_auth" + except NoLocations: + errors["base"] = "no_locations" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.data = user_input + self.locations = info + return await self.async_step_location() + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_location( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the location step.""" + + if not user_input: + return self.async_show_form( + step_id="location", + data_schema=vol.Schema( + { + vol.Required("location"): vol.In( + {location.id: location.name for location in self.locations} + ) + } + ), + ) + + location_id = user_input["location"] + + for location in self.locations: + if location.id == location_id: + location_name = location.name + + await self.async_set_unique_id(location_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=location_name, + data={ + "id": location_id, + "name": location_name, + "username": self.data["username"], + "password": self.data["password"], + }, + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class NoLocations(HomeAssistantError): + """Error to indicate there are no locations.""" diff --git a/homeassistant/components/lacrosse_view/const.py b/homeassistant/components/lacrosse_view/const.py new file mode 100644 index 00000000000..cae11315bc7 --- /dev/null +++ b/homeassistant/components/lacrosse_view/const.py @@ -0,0 +1,6 @@ +"""Constants for the LaCrosse View integration.""" +import logging + +DOMAIN = "lacrosse_view" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = 30 diff --git a/homeassistant/components/lacrosse_view/manifest.json b/homeassistant/components/lacrosse_view/manifest.json new file mode 100644 index 00000000000..64f40267c8a --- /dev/null +++ b/homeassistant/components/lacrosse_view/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "lacrosse_view", + "name": "LaCrosse View", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/lacrosse_view", + "requirements": ["lacrosse-view==0.0.9"], + "codeowners": ["@IceBotYT"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py new file mode 100644 index 00000000000..8ccbe0514b4 --- /dev/null +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -0,0 +1,136 @@ +"""Sensor component for LaCrosse View.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from re import sub + +from lacrosse_view import Sensor + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, LOGGER + + +@dataclass +class LaCrosseSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[..., float] + + +@dataclass +class LaCrosseSensorEntityDescription( + SensorEntityDescription, LaCrosseSensorEntityDescriptionMixin +): + """Description for LaCrosse View sensor.""" + + +PARALLEL_UPDATES = 0 +ICON_LIST = { + "Temperature": "mdi:thermometer", + "Humidity": "mdi:water-percent", + "HeatIndex": "mdi:thermometer", + "WindSpeed": "mdi:weather-windy", + "Rain": "mdi:water", +} +UNIT_LIST = { + "degrees_celsius": "°C", + "degrees_fahrenheit": "°F", + "relative_humidity": "%", + "kilometers_per_hour": "km/h", + "inches": "in", +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaCrosse View from a config entry.""" + coordinator: DataUpdateCoordinator[list[Sensor]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinator"] + sensors: list[Sensor] = coordinator.data + + sensor_list = [] + for i, sensor in enumerate(sensors): + if not sensor.permissions.get("read"): + LOGGER.warning( + "No permission to read sensor %s, are you sure you're signed into the right account?", + sensor.name, + ) + continue + for field in sensor.sensor_field_names: + sensor_list.append( + LaCrosseViewSensor( + coordinator=coordinator, + description=LaCrosseSensorEntityDescription( + key=str(i), + device_class="temperature" if field == "Temperature" else None, + # The regex is to convert CamelCase to Human Case + # e.g. "RelativeHumidity" -> "Relative Humidity" + name=f"{sensor.name} {sub(r'(? None: + """Initialize.""" + super().__init__(coordinator) + sensor = self.coordinator.data[int(description.key)] + + self.entity_description = description + self._attr_unique_id = f"{sensor.location.id}-{description.key}-{field}" + self._attr_name = f"{sensor.location.name} {description.name}" + self._attr_icon = ICON_LIST.get(field, "mdi:thermometer") + self._attr_device_info = { + "identifiers": {(DOMAIN, sensor.sensor_id)}, + "name": sensor.name.split(" ")[0], + "manufacturer": "LaCrosse Technology", + "model": sensor.model, + "via_device": (DOMAIN, sensor.location.id), + } + + @property + def native_value(self) -> float: + """Return the sensor value.""" + return self.entity_description.value_fn() diff --git a/homeassistant/components/lacrosse_view/strings.json b/homeassistant/components/lacrosse_view/strings.json new file mode 100644 index 00000000000..76f1971518a --- /dev/null +++ b/homeassistant/components/lacrosse_view/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "no_locations": "No locations found" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json new file mode 100644 index 00000000000..c972d6d12f9 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + "no_locations": "No locations found" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + }, + "location": { + "data": { + "location": "Location" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 28d0ad6b44b..6d92f7cf7e7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -191,6 +191,7 @@ FLOWS = { "kostal_plenticore", "kraken", "kulersky", + "lacrosse_view", "launch_library", "laundrify", "lg_soundbar", diff --git a/mypy.ini b/mypy.ini index 37765023f74..af039c74de3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1351,6 +1351,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lacrosse_view.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lametric.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index ea3f2dd3e6c..bdbff5adabd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -943,6 +943,9 @@ kostal_plenticore==0.2.0 # homeassistant.components.kraken krakenex==2.1.0 +# homeassistant.components.lacrosse_view +lacrosse-view==0.0.9 + # homeassistant.components.eufy lakeside==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f19f813747..3dc23a1e07d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -684,6 +684,9 @@ kostal_plenticore==0.2.0 # homeassistant.components.kraken krakenex==2.1.0 +# homeassistant.components.lacrosse_view +lacrosse-view==0.0.9 + # homeassistant.components.laundrify laundrify_aio==1.1.2 diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py new file mode 100644 index 00000000000..ea01e7a72e3 --- /dev/null +++ b/tests/components/lacrosse_view/__init__.py @@ -0,0 +1,32 @@ +"""Tests for the LaCrosse View integration.""" + +from lacrosse_view import Location, Sensor + +MOCK_ENTRY_DATA = { + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", +} +TEST_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_NO_PERMISSION_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + permissions={"read": False}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py new file mode 100644 index 00000000000..82178f2801b --- /dev/null +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -0,0 +1,163 @@ +"""Test the LaCrosse View config flow.""" +from unittest.mock import patch + +from lacrosse_view import Location, LoginError + +from homeassistant import config_entries +from homeassistant.components.lacrosse_view.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "location" + assert result2["errors"] is None + + with patch( + "homeassistant.components.lacrosse_view.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "location": "1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "Test" + assert result3["data"] == { + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_auth_false(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "lacrosse_view.LaCrosse.login", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_login_first(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_locations", side_effect=LoginError + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_no_locations(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_locations"} + + +async def test_form_unexpected_error(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lacrosse_view.config_flow.validate_input", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py new file mode 100644 index 00000000000..a719536f737 --- /dev/null +++ b/tests/components/lacrosse_view/test_init.py @@ -0,0 +1,102 @@ +"""Test the LaCrosse View initialization.""" +from datetime import datetime, timedelta +from unittest.mock import patch + +from freezegun import freeze_time +from lacrosse_view import HTTPError, LoginError + +from homeassistant.components.lacrosse_view.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_ENTRY_DATA, TEST_SENSOR + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test the unload entry.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + assert entries[0].state == ConfigEntryState.NOT_LOADED + + +async def test_login_error(hass: HomeAssistant) -> None: + """Test login error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError("Test")): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.SETUP_RETRY + + +async def test_http_error(hass: HomeAssistant) -> None: + """Test http error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", side_effect=HTTPError + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.SETUP_RETRY + + +async def test_new_token(hass: HomeAssistant) -> None: + """Test new token.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + login.assert_called_once() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + one_hour_after = datetime.utcnow() + timedelta(hours=1) + + with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ), freeze_time(one_hour_after): + async_fire_time_changed(hass, one_hour_after) + await hass.async_block_till_done() + + login.assert_called_once() diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py new file mode 100644 index 00000000000..57197662cc9 --- /dev/null +++ b/tests/components/lacrosse_view/test_sensor.py @@ -0,0 +1,49 @@ +"""Test the LaCrosse View sensors.""" +from unittest.mock import patch + +from homeassistant.components.lacrosse_view import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_ENTRY_DATA, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR + +from tests.common import MockConfigEntry + + +async def test_entities_added(hass: HomeAssistant) -> None: + """Test the entities are added.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR] + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_test_temperature") + + +async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: + """Test if it raises a warning when there is no permission to read the sensor.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_NO_PERMISSION_SENSOR] + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_test_temperature") is None + assert "No permission to read sensor" in caplog.text