diff --git a/.coveragerc b/.coveragerc index 590f69961e2..ee8e165c9b6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1243,7 +1243,10 @@ omit = homeassistant/components/stream/fmp4utils.py homeassistant/components/stream/hls.py homeassistant/components/stream/worker.py - homeassistant/components/streamlabswater/* + homeassistant/components/streamlabswater/__init__.py + homeassistant/components/streamlabswater/binary_sensor.py + homeassistant/components/streamlabswater/coordinator.py + homeassistant/components/streamlabswater/sensor.py homeassistant/components/suez_water/__init__.py homeassistant/components/suez_water/sensor.py homeassistant/components/supervisord/sensor.py diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index c09f6040fed..986b5de8049 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -1,28 +1,31 @@ """Support for Streamlabs Water Monitor devices.""" -import logging -from streamlabswater import streamlabswater +from streamlabswater.streamlabswater import StreamlabsClient import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, Platform -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import discovery +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, +) +from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType -DOMAIN = "streamlabswater" - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN +from .coordinator import StreamlabsCoordinator ATTR_AWAY_MODE = "away_mode" SERVICE_SET_AWAY_MODE = "set_away_mode" AWAY_MODE_AWAY = "away" AWAY_MODE_HOME = "home" -PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR] - CONF_LOCATION_ID = "location_id" +ISSUE_PLACEHOLDER = {"url": "/config/integrations/dashboard/add?domain=streamlabswater"} CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -36,52 +39,77 @@ CONFIG_SCHEMA = vol.Schema( ) SET_AWAY_MODE_SCHEMA = vol.Schema( - {vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME])} + { + vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME]), + vol.Optional(CONF_LOCATION_ID): cv.string, + } ) +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] -def setup(hass: HomeAssistant, config: ConfigType) -> bool: + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the streamlabs water integration.""" - conf = config[DOMAIN] - api_key = conf.get(CONF_API_KEY) - location_id = conf.get(CONF_LOCATION_ID) + if DOMAIN not in config: + return True - client = streamlabswater.StreamlabsClient(api_key) - locations = client.get_locations().get("locations") - - if locations is None: - _LOGGER.error("Unable to retrieve locations. Verify API key") - return False - - if location_id is None: - location = locations[0] - location_id = location["locationId"] - _LOGGER.info( - "Streamlabs Water Monitor auto-detected location_id=%s", location_id + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_API_KEY: config[DOMAIN][CONF_API_KEY]}, + ) + if ( + result["type"] == FlowResultType.CREATE_ENTRY + or result["reason"] == "already_configured" + ): + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "StreamLabs", + }, ) else: - location = next( - (loc for loc in locations if location_id == loc["locationId"]), None + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_import_issue_${result['reason']}", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_placeholders=ISSUE_PLACEHOLDER, ) - if location is None: - _LOGGER.error("Supplied location_id is invalid") - return False + return True - location_name = location["name"] - hass.data[DOMAIN] = { - "client": client, - "location_id": location_id, - "location_name": location_name, - } +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up StreamLabs from a config entry.""" - for platform in PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) + api_key = entry.data[CONF_API_KEY] + client = StreamlabsClient(api_key) + coordinator = StreamlabsCoordinator(hass, client) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def set_away_mode(service: ServiceCall) -> None: """Set the StreamLabsWater Away Mode.""" away_mode = service.data.get(ATTR_AWAY_MODE) + location_id = ( + service.data.get(CONF_LOCATION_ID) or list(coordinator.data.values())[0] + ) client.update_location(location_id, away_mode) hass.services.register( @@ -89,3 +117,11 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: ) 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/streamlabswater/binary_sensor.py b/homeassistant/components/streamlabswater/binary_sensor.py index 4a974077592..d0ca500ded4 100644 --- a/homeassistant/components/streamlabswater/binary_sensor.py +++ b/homeassistant/components/streamlabswater/binary_sensor.py @@ -1,84 +1,54 @@ """Support for Streamlabs Water Monitor Away Mode.""" from __future__ import annotations -from datetime import timedelta - -from streamlabswater.streamlabswater import StreamlabsClient - from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN as STREAMLABSWATER_DOMAIN +from . import StreamlabsCoordinator +from .const import DOMAIN +from .coordinator import StreamlabsData -DEPENDS = ["streamlabswater"] - -MIN_TIME_BETWEEN_LOCATION_UPDATES = timedelta(seconds=60) - -ATTR_LOCATION_ID = "location_id" NAME_AWAY_MODE = "Water Away Mode" -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the StreamLabsWater mode sensor.""" - client = hass.data[STREAMLABSWATER_DOMAIN]["client"] - location_id = hass.data[STREAMLABSWATER_DOMAIN]["location_id"] - location_name = hass.data[STREAMLABSWATER_DOMAIN]["location_name"] + """Set up Streamlabs water binary sensor from a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] - streamlabs_location_data = StreamlabsLocationData(location_id, client) - streamlabs_location_data.update() + entities = [] - add_devices([StreamlabsAwayMode(location_name, streamlabs_location_data)]) + for location_id in coordinator.data: + entities.append(StreamlabsAwayMode(coordinator, location_id)) + + async_add_entities(entities) -class StreamlabsLocationData: - """Track and query location data.""" - - def __init__(self, location_id: str, client: StreamlabsClient) -> None: - """Initialize the location data.""" - self._location_id = location_id - self._client = client - self._is_away = None - - @Throttle(MIN_TIME_BETWEEN_LOCATION_UPDATES) - def update(self) -> None: - """Query and store location data.""" - location = self._client.get_location(self._location_id) - self._is_away = location["homeAway"] == "away" - - def is_away(self) -> bool | None: - """Return whether away more is enabled.""" - return self._is_away - - -class StreamlabsAwayMode(BinarySensorEntity): +class StreamlabsAwayMode(CoordinatorEntity[StreamlabsCoordinator], BinarySensorEntity): """Monitor the away mode state.""" - def __init__( - self, location_name: str, streamlabs_location_data: StreamlabsLocationData - ) -> None: + def __init__(self, coordinator: StreamlabsCoordinator, location_id: str) -> None: """Initialize the away mode device.""" - self._location_name = location_name - self._streamlabs_location_data = streamlabs_location_data - self._is_away = None + super().__init__(coordinator) + self._location_id = location_id + + @property + def location_data(self) -> StreamlabsData: + """Returns the data object.""" + return self.coordinator.data[self._location_id] @property def name(self) -> str: """Return the name for away mode.""" - return f"{self._location_name} {NAME_AWAY_MODE}" + return f"{self.location_data.name} {NAME_AWAY_MODE}" @property - def is_on(self) -> bool | None: + def is_on(self) -> bool: """Return if away mode is on.""" - return self._streamlabs_location_data.is_away() - - def update(self) -> None: - """Retrieve the latest location data and away mode state.""" - self._streamlabs_location_data.update() + return self.location_data.is_away diff --git a/homeassistant/components/streamlabswater/config_flow.py b/homeassistant/components/streamlabswater/config_flow.py new file mode 100644 index 00000000000..5cede037d5a --- /dev/null +++ b/homeassistant/components/streamlabswater/config_flow.py @@ -0,0 +1,75 @@ +"""Config flow for StreamLabs integration.""" +from __future__ import annotations + +from typing import Any + +from streamlabswater.streamlabswater import StreamlabsClient +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN, LOGGER + + +async def validate_input(hass: HomeAssistant, api_key: str) -> None: + """Validate the user input allows us to connect.""" + client = StreamlabsClient(api_key) + response = await hass.async_add_executor_job(client.get_locations) + locations = response.get("locations") + + if locations is None: + raise CannotConnect + + +class StreamlabsConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for StreamLabs.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + self._async_abort_entries_match(user_input) + try: + await validate_input(self.hass, user_input[CONF_API_KEY]) + 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="Streamlabs", data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } + ), + errors=errors, + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Import the yaml config.""" + self._async_abort_entries_match(user_input) + try: + await validate_input(self.hass, user_input[CONF_API_KEY]) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + return self.async_create_entry(title="Streamlabs", data=user_input) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/streamlabswater/const.py b/homeassistant/components/streamlabswater/const.py new file mode 100644 index 00000000000..ee407d376d4 --- /dev/null +++ b/homeassistant/components/streamlabswater/const.py @@ -0,0 +1,6 @@ +"""Constants for the StreamLabs integration.""" +import logging + +DOMAIN = "streamlabswater" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/streamlabswater/coordinator.py b/homeassistant/components/streamlabswater/coordinator.py new file mode 100644 index 00000000000..a11eced5a6e --- /dev/null +++ b/homeassistant/components/streamlabswater/coordinator.py @@ -0,0 +1,57 @@ +"""Coordinator for Streamlabs water integration.""" +from dataclasses import dataclass +from datetime import timedelta + +from streamlabswater.streamlabswater import StreamlabsClient + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import LOGGER + + +@dataclass(slots=True) +class StreamlabsData: + """Class to hold Streamlabs data.""" + + is_away: bool + name: str + daily_usage: float + monthly_usage: float + yearly_usage: float + + +class StreamlabsCoordinator(DataUpdateCoordinator[dict[str, StreamlabsData]]): + """Coordinator for Streamlabs.""" + + def __init__( + self, + hass: HomeAssistant, + client: StreamlabsClient, + ) -> None: + """Coordinator for Streamlabs.""" + super().__init__( + hass, + LOGGER, + name="Streamlabs", + update_interval=timedelta(seconds=60), + ) + self.client = client + + async def _async_update_data(self) -> dict[str, StreamlabsData]: + return await self.hass.async_add_executor_job(self._update_data) + + def _update_data(self) -> dict[str, StreamlabsData]: + locations = self.client.get_locations() + res = {} + for location in locations: + location_id = location["locationId"] + water_usage = self.client.get_water_usage_summary(location_id) + res[location_id] = StreamlabsData( + is_away=location["homeAway"] == "away", + name=location["name"], + daily_usage=round(water_usage["today"], 1), + monthly_usage=round(water_usage["thisMonth"], 1), + yearly_usage=round(water_usage["thisYear"], 1), + ) + return res diff --git a/homeassistant/components/streamlabswater/manifest.json b/homeassistant/components/streamlabswater/manifest.json index fae19ca3e7a..ec076bd52ec 100644 --- a/homeassistant/components/streamlabswater/manifest.json +++ b/homeassistant/components/streamlabswater/manifest.json @@ -2,6 +2,7 @@ "domain": "streamlabswater", "name": "StreamLabs", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/streamlabswater", "iot_class": "cloud_polling", "loggers": ["streamlabswater"], diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index 42e551c5c11..0b249b7c4e5 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -1,111 +1,69 @@ """Support for Streamlabs Water Monitor Usage.""" from __future__ import annotations -from datetime import timedelta - -from streamlabswater.streamlabswater import StreamlabsClient - from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN as STREAMLABSWATER_DOMAIN - -DEPENDENCIES = ["streamlabswater"] - -WATER_ICON = "mdi:water" -MIN_TIME_BETWEEN_USAGE_UPDATES = timedelta(seconds=60) +from . import StreamlabsCoordinator +from .const import DOMAIN +from .coordinator import StreamlabsData NAME_DAILY_USAGE = "Daily Water" NAME_MONTHLY_USAGE = "Monthly Water" NAME_YEARLY_USAGE = "Yearly Water" -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_devices: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up water usage sensors.""" - client = hass.data[STREAMLABSWATER_DOMAIN]["client"] - location_id = hass.data[STREAMLABSWATER_DOMAIN]["location_id"] - location_name = hass.data[STREAMLABSWATER_DOMAIN]["location_name"] + """Set up Streamlabs water sensor from a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] - streamlabs_usage_data = StreamlabsUsageData(location_id, client) - streamlabs_usage_data.update() + entities = [] - add_devices( - [ - StreamLabsDailyUsage(location_name, streamlabs_usage_data), - StreamLabsMonthlyUsage(location_name, streamlabs_usage_data), - StreamLabsYearlyUsage(location_name, streamlabs_usage_data), - ] - ) + for location_id in coordinator.data.values(): + entities.extend( + [ + StreamLabsDailyUsage(coordinator, location_id), + StreamLabsMonthlyUsage(coordinator, location_id), + StreamLabsYearlyUsage(coordinator, location_id), + ] + ) + + async_add_entities(entities) -class StreamlabsUsageData: - """Track and query usage data.""" - - def __init__(self, location_id: str, client: StreamlabsClient) -> None: - """Initialize the usage data.""" - self._location_id = location_id - self._client = client - self._today = None - self._this_month = None - self._this_year = None - - @Throttle(MIN_TIME_BETWEEN_USAGE_UPDATES) - def update(self) -> None: - """Query and store usage data.""" - water_usage = self._client.get_water_usage_summary(self._location_id) - self._today = round(water_usage["today"], 1) - self._this_month = round(water_usage["thisMonth"], 1) - self._this_year = round(water_usage["thisYear"], 1) - - def get_daily_usage(self) -> float | None: - """Return the day's usage.""" - return self._today - - def get_monthly_usage(self) -> float | None: - """Return the month's usage.""" - return self._this_month - - def get_yearly_usage(self) -> float | None: - """Return the year's usage.""" - return self._this_year - - -class StreamLabsDailyUsage(SensorEntity): +class StreamLabsDailyUsage(CoordinatorEntity[StreamlabsCoordinator], SensorEntity): """Monitors the daily water usage.""" _attr_device_class = SensorDeviceClass.WATER _attr_native_unit_of_measurement = UnitOfVolume.GALLONS - def __init__( - self, location_name: str, streamlabs_usage_data: StreamlabsUsageData - ) -> None: + def __init__(self, coordinator: StreamlabsCoordinator, location_id: str) -> None: """Initialize the daily water usage device.""" - self._location_name = location_name - self._streamlabs_usage_data = streamlabs_usage_data - self._state = None + super().__init__(coordinator) + self._location_id = location_id + + @property + def location_data(self) -> StreamlabsData: + """Returns the data object.""" + return self.coordinator.data[self._location_id] @property def name(self) -> str: """Return the name for daily usage.""" - return f"{self._location_name} {NAME_DAILY_USAGE}" + return f"{self.location_data.name} {NAME_DAILY_USAGE}" @property - def native_value(self) -> float | None: + def native_value(self) -> float: """Return the current daily usage.""" - return self._streamlabs_usage_data.get_daily_usage() - - def update(self) -> None: - """Retrieve the latest daily usage.""" - self._streamlabs_usage_data.update() + return self.location_data.daily_usage class StreamLabsMonthlyUsage(StreamLabsDailyUsage): @@ -114,12 +72,12 @@ class StreamLabsMonthlyUsage(StreamLabsDailyUsage): @property def name(self) -> str: """Return the name for monthly usage.""" - return f"{self._location_name} {NAME_MONTHLY_USAGE}" + return f"{self.location_data.name} {NAME_MONTHLY_USAGE}" @property - def native_value(self) -> float | None: + def native_value(self) -> float: """Return the current monthly usage.""" - return self._streamlabs_usage_data.get_monthly_usage() + return self.location_data.monthly_usage class StreamLabsYearlyUsage(StreamLabsDailyUsage): @@ -128,9 +86,9 @@ class StreamLabsYearlyUsage(StreamLabsDailyUsage): @property def name(self) -> str: """Return the name for yearly usage.""" - return f"{self._location_name} {NAME_YEARLY_USAGE}" + return f"{self.location_data.name} {NAME_YEARLY_USAGE}" @property - def native_value(self) -> float | None: + def native_value(self) -> float: """Return the current yearly usage.""" - return self._streamlabs_usage_data.get_yearly_usage() + return self.location_data.yearly_usage diff --git a/homeassistant/components/streamlabswater/services.yaml b/homeassistant/components/streamlabswater/services.yaml index 7504a911123..cd828fd3fed 100644 --- a/homeassistant/components/streamlabswater/services.yaml +++ b/homeassistant/components/streamlabswater/services.yaml @@ -7,3 +7,6 @@ set_away_mode: options: - "away" - "home" + location_id: + selector: + text: diff --git a/homeassistant/components/streamlabswater/strings.json b/homeassistant/components/streamlabswater/strings.json index 56b35ab1044..e6b5dd7465b 100644 --- a/homeassistant/components/streamlabswater/strings.json +++ b/homeassistant/components/streamlabswater/strings.json @@ -1,4 +1,20 @@ { + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, "services": { "set_away_mode": { "name": "Set away mode", @@ -7,8 +23,22 @@ "away_mode": { "name": "Away mode", "description": "Home or away." + }, + "location_id": { + "name": "Location ID", + "description": "The location ID of the Streamlabs Water Monitor." } } } + }, + "issues": { + "deprecated_yaml_import_issue_cannot_connect": { + "title": "The Streamlabs water YAML configuration import failed", + "description": "Configuring Streamlabs water using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Streamlabs water works and restart Home Assistant to try again or remove the Streamlabs water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + }, + "deprecated_yaml_import_issue_unknown": { + "title": "The Streamlabs water YAML configuration import failed", + "description": "Configuring Streamlabs water using YAML is being removed but there was an unknown error when trying to import the YAML configuration.\n\nEnsure the imported configuration is correct and remove the Streamlabs water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + } } } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d2674e128ce..7da240ac266 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -476,6 +476,7 @@ FLOWS = { "steamist", "stookalert", "stookwijzer", + "streamlabswater", "subaru", "suez_water", "sun", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d8ba63322ca..3a1e154facb 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5585,7 +5585,7 @@ "streamlabswater": { "name": "StreamLabs", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "subaru": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5007ed4262e..83df72a45c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1930,6 +1930,9 @@ stookalert==0.1.4 # homeassistant.components.stookwijzer stookwijzer==1.3.0 +# homeassistant.components.streamlabswater +streamlabswater==1.0.1 + # homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke diff --git a/tests/components/streamlabswater/__init__.py b/tests/components/streamlabswater/__init__.py new file mode 100644 index 00000000000..16b2e5f0974 --- /dev/null +++ b/tests/components/streamlabswater/__init__.py @@ -0,0 +1 @@ +"""Tests for the StreamLabs integration.""" diff --git a/tests/components/streamlabswater/conftest.py b/tests/components/streamlabswater/conftest.py new file mode 100644 index 00000000000..f871332e5f6 --- /dev/null +++ b/tests/components/streamlabswater/conftest.py @@ -0,0 +1,14 @@ +"""Common fixtures for the StreamLabs tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.streamlabswater.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/streamlabswater/test_config_flow.py b/tests/components/streamlabswater/test_config_flow.py new file mode 100644 index 00000000000..68f671d3b8c --- /dev/null +++ b/tests/components/streamlabswater/test_config_flow.py @@ -0,0 +1,193 @@ +"""Test the StreamLabs config flow.""" +from unittest.mock import AsyncMock, patch + +from homeassistant import config_entries +from homeassistant.components.streamlabswater.const import DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "abc"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Streamlabs" + assert result["data"] == {CONF_API_KEY: "abc"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """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.streamlabswater.config_flow.StreamlabsClient.get_locations", + return_value={}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "abc"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "abc"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Streamlabs" + assert result["data"] == {CONF_API_KEY: "abc"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.streamlabswater.config_flow.StreamlabsClient.get_locations", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "abc"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} + + with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "abc"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Streamlabs" + assert result["data"] == {CONF_API_KEY: "abc"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_entry_already_exists(hass: HomeAssistant) -> None: + """Test we handle if the entry already exists.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_API_KEY: "abc"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.streamlabswater.config_flow.StreamlabsClient.get_locations", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "abc"}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test import flow.""" + with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_API_KEY: "abc"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Streamlabs" + assert result["data"] == {CONF_API_KEY: "abc"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_cannot_connect( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: + """Test we handle cannot connect error.""" + with patch( + "homeassistant.components.streamlabswater.config_flow.StreamlabsClient.get_locations", + return_value={}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_API_KEY: "abc"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_import_unknown(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we handle unknown error.""" + with patch( + "homeassistant.components.streamlabswater.config_flow.StreamlabsClient.get_locations", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_API_KEY: "abc"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "unknown" + + +async def test_import_entry_already_exists(hass: HomeAssistant) -> None: + """Test we handle if the entry already exists.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_API_KEY: "abc"}, + ) + entry.add_to_hass(hass) + with patch("homeassistant.components.streamlabswater.config_flow.StreamlabsClient"): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_API_KEY: "abc"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured"